Saving modified DOM state in a form with the back button

A common problem encountered with building forms in JS is that when leaving a page and coming back, form state is preserved… sort of. If you have a fixed form with no dynamic behavior, everything will be fine. However, if you have something like this (please excuse my barebones JS and markup, this is to simplify the demo):

<script type="text/javascript">
function addRow() {	
	const newRow = document.createElement('ul');
	const newInput = document.createElement('input');
	newInput.type = 'text';
	newInput.name = 'input' + document.getElementById('inputs').childNodes.length;
	newRow.appendChild(newInput);
	inputs.appendChild(newRow);
}
</script>

<input type="text" name="static" />

<ul id="inputs">
</ul>

<button onclick="addRow();">Add Row</button>

…then if you add some rows, leave the page, and then come back, the static input will still have the value you typed in, but the rows you added are gone.

Why do these rows disappear? The reason is because they were added with JS after the page was created. Any DOM manipulation that happens after a page is created will be lost when you leave the page and then come back with the back button.

So, now, what can we do about it? The answer is simple. We can use a simplified version of the memento design pattern. The fundamental idea of the memento pattern is that we store the state in some form that can be retrieved later. There are three important questions to use the memento design pattern effectively, which will all be taken care of in the next paragraphs: 1. How do we convert the state into a storeable object?, 2. Where do we store the state?, and 3. How do we restore the state?

Let’s take these one at a time:

1. How do we convert the state into a storeable object? To store the state effectively, we need a format to store it in and a way to trigger storing it. JavaScript conveniently provides JSON, which is all we need in this case. Since all we have is a list of strings, in this example we can just store it as a JSON-encoded array of strings. For example, if we have two inputs with “Value 1” and “Value 2”, we can just store ["Value 1", "Value 2"].
Now that we have a format to store it in, we need a way to trigger storing it. Thankfully there is a window event, onbeforeunload, that we can leverage. Basically the event fires when the user leaves the page but before actually leaving. In this case, we can add the following JS to generate JS of the state:

window.onbeforeunload = function() {
	const inputWrapper = document.getElementById('inputs');
	const values = [...inputWrapper.querySelectorAll('input')].map(input => input.value)
	const inputListMemento = JSON.stringify(values);
	console.log(inputListMemento);
	
	// TODO: save this somewhere we can see again when reloading the page
}

Now, if we leave the page after entering some data, we will see an array with all the input values logged to the console (make sure to set console logging to persist between pages). Now, this brings us to the next step to solve that TODO:

2. Where do we store the state? At the beginning of the post I explained that input values in inputs present when the page was first loaded will persist. Thankfully, this also includes input values dynamically populated, even to hidden inputs. We can leverage this to store the state. All we have to do is add a hidden input and store the value there.

<script type="text/javascript">
function addRow() {	
	const newRow = document.createElement('ul');
	const newInput = document.createElement('input');
	newInput.type = 'text';
	newInput.name = 'input' + document.getElementById('inputs').childNodes.length;
	newRow.appendChild(newInput);
	inputs.appendChild(newRow);
}

window.onbeforeunload = function() {
	const inputWrapper = document.getElementById('inputs');
	const values = [...inputWrapper.querySelectorAll('input')].map(input => input.value)
	const inputListMemento = JSON.stringify(values);
	
	document.getElementById('inputListMementoHolder').value = inputListMemento;
}
</script>

<input type="text" name="static" />
<input type="hidden" id="inputListMementoHolder" />

<ul id="inputs">
</ul>

<button onclick="addRow();">Add Row</button>

Then if we leave the page and come back, we can verify in your console that the hidden input you just created does indeed store the previous value of the inputs in the list by running the following code:
document.getElementById('inputListMementoHolder').value
Now that we have the value stored in the memento, we finally need to restore it, which brings us to the final part.

3. How do we restore the state? This we can do at page load by reading the value of the input. JS thankfully provides the window.onload event we can use. First, however, we need to modify our addRow function to take an argument for the value:

function addRow(value = '') {	
	const newRow = document.createElement('ul');
	const newInput = document.createElement('input');
	newInput.type = 'text';
	newInput.name = 'input' + document.getElementById('inputs').childNodes.length;
	newInput.value = value;
	newRow.appendChild(newInput);
	inputs.appendChild(newRow);
}

Now, we can reload the state when reloading the page by adding the event to read the memento:

window.onload = function() {
	const inputListMemento = document.getElementById('inputListMementoHolder').value;
	if (inputListMemento) {
		inputListItems = JSON.parse(inputListMemento);
		inputListItems.forEach(item => addRow(item));
	}
}

With this, you should see that when leaving the page and coming back via the back button, we do not lose any data from our list input. For a recap, here’s the final piece of code:

<script type="text/javascript">
function addRow(value = '') {	
	const newRow = document.createElement('ul');
	const newInput = document.createElement('input');
	newInput.type = 'text';
	newInput.name = 'input' + document.getElementById('inputs').childNodes.length;
	newInput.value = value;
	newRow.appendChild(newInput);
	inputs.appendChild(newRow);
}

window.onbeforeunload = function() {
	const inputWrapper = document.getElementById('inputs');
	const values = [...inputWrapper.querySelectorAll('input')].map(input => input.value)
	const inputListMemento = JSON.stringify(values);
	
	document.getElementById('inputListMementoHolder').value = inputListMemento;
}

window.onload = function() {
	const inputListMemento = document.getElementById('inputListMementoHolder').value;
	if (inputListMemento) {
		inputListItems = JSON.parse(inputListMemento);
		inputListItems.forEach(item => addRow(item));
	}
}
</script>

<input type="text" name="static" />
<input type="hidden" id="inputListMementoHolder" />

<ul id="inputs">
</ul>

<button onclick="addRow();">Add Row</button>

Now we can preserve the state of a dynamic form even when leaving the page and returning later.