We’ve all been there. You’ve just spend 10 minutes of your precious time filling out a ridiculously long form, you get to the final input and accidentally click somewhere… you’re not exactly sure what happened, that last backspace key?
You look at the top of your browser, it’s loading. You frantically reach for your mouse in an attempt to click the stop button, but it’s too late. You yell furiously at your computer in futile despair as all your hard work is gone. This can be very frustrating and very annoying, and could be causing you to lose customers. If eight years in a marketing company taught me anything, it’s that your forms are precious, and your customer engagement and retention is fragile.
In this tutorial, we’ll learn a solution for this as we implement a simple way to refresh-proof our precious Vue forms by leveraging the power of the localStorage API.
Setting up a demo form
Before implementing this solution, we need to set up a simple demo form. Our form will have a couple of basic input
elements, one with a type of email
, a select
element, a textarea
, and finally a checkbox
. We’ll then bind each input to the local state inside our data()
by using v-model
.
You’ll see that regardless of the type of input, the solution we’re building will be able to handle them all.
Now our form looks like this:
📃DemoForm.vue
<template>
<form>
<label for="firstName">First Name</label>
<input type="text" id="firstName" v-model="form.firstName" />
<label for="lastName">Last Name</label>
<input type="text" id="lastName" v-model="form.lastName" />
<label for="email">Email</label>
<input type="email" id="email" v-model="form.email" />
<label for="framework">Favorite Framework</label>
<select id="framework" v-model="form.framework">
<option value="vue">Vue</option>
<option value="stillvue">Vue!</option>
<option value="vuevuevue">Vue :D</option>
<option value="okfine">Other</option>
</select>
<label for="extras">Describe why you love Vue</label>
<textarea id="extras" v-model="form.extras" rows="5"></textarea>
<label>
<input type="checkbox" v-model="form.spam" />
I want all the spams
</label>
<button type="submit">Submit</button>
</form>
<template>
<script>
export default {
data () {
return {
form: {
firstName: '',
lastName: '',
email: '',
framework: 'vue',
extras: '',
spam: false
}
}
}
}
</script>
Notice that we’re setting up our internal data to be stored inside a form object. This approach makes it much easier for later, when we store this state into local storage, especially for components that have additional internal data aside from the form bindings. Separating that data would become hard to do programmatically, or tedious to hardcode.
Alright! Now we have a basic form, connected to local state. Now let’s begin making it refresh-proof.
Deconstructing v-model
Remember, our goal is to keep the data that our user input into the form safe in case the browser gets refreshed somehow. We’ll accomplish this by storing a copy of that data into the localStorage of our browser.
However, to be able to keep track of all the changes that our user is making, we have to deconstruct the v-model
statement into attribute binding and events. Remember that v-model
is a two-way binding shorthand. It makes it so that we can very easily create the connection between the input
prop and event in the component, and our own internal data in the parent. However, there are cases like this one where we will want to have more control in this process, in order to do additional work at the same time as these values are getting set.
So let’s get started. First, we’ll create a catch-all method that updates our form input state. Let’s call it updateForm
.
📃DemoForm.vue
methods: {
updateForm (input, value) {
this.form[input] = value
}
}
This new method will take in two parameters:
- The first one (
input
) will be the name of the input’s local state property. For example:'firstName'
or'extras'
. Note that these are strings. - The second parameter will be the value that needs to be updated when the user types or performs an action. As we’ll see in a moment, we will catch this value from the different events that our inputs throw.
Before we proceed, take a quick look at the syntax of how we’re setting the value of our property inside of this.form
. In case you didn’t know, whenever you want to set a property dynamically in an object (meaning you want the property’s actual name to be variable), you can access it using array notation
. In the above example, if input
were equal to 'firstName'
for example, we would actually be setting the value of this.form.firstName
.
Next, let’s update our form elements to not use v-model for two-way binding to our local state. Instead, we’ll use our new updateForm
method for updating values.
Since the first three elements in our form are input
elements, that means we’re going to bind to the input event. (We could also be using change
here, but I want to make sure that we capture every keystroke just in case.)
📃DemoForm.vue
<label for="firstName">First Name</label>
<input
type="text"
id="firstName"
@input="updateForm('firstName', $event.target.value)"
:value="form.firstName" />
<label for="lastName">Last Name</label>
<input
type="text"
id="lastName"
@input="updateForm('lastName', $event.target.value)"
:value="form.lastName" />
<label for="email">Email</label>
<input
type="email"
id="email"
@input="updateForm('email', $event.target.value)"
:value="form.email" />
Remember that when we are passing the event’s value into a method, we need to do it manually and use $event.target.value
to get only the value of the triggered event.
If you go ahead and test this in your browser, you should be getting the exact same behavior as before, regarding the binding of the form data to the state. Check your Vue dev tools!
The Select Element
Let’s move on to the <select>
element. You may be tempted to bind :value
to the <select>
element, but this is actually a very common mistake. <select>
HTML elements do not have a value attribute. So instead, we have to listen for change
events on our <select>
element and set the selected attribute on its options
. (When using v-model
, Vue does this magically for us in the background, so this behavior is usually abstracted away from us.)
📃DemoForm.vue
<label for="framework">Favorite Framework</label>
<select id="framework" @change="updateForm('framework', $event.target.value)">
<option value="vue" :selected="form.framework === 'vue'">Vue</option>
<option value="stillvue" :selected="form.framework === 'stillvue'">Vue!</option>
<option value="vuevuevue" :selected="form.framework === 'vuevuevue'">Vue :D</option>
<option value="okfine" :selected="form.framework === 'okfine'">Other</option>
</select>
The TextArea Element
Moving along to the <textarea>
, we’ll use the same approach as the <input>
elements, binding to :value
and listening for input events.
📃DemoForm.vue
<label for="extras">Describe why you love Vue</label>
<textarea
id="extras"
@input="updateForm('extras', $event.target.value)"
:value="form.extras"
rows="5"></textarea>
Finally, the <checkbox>
element has a small twist. Here, we need to bind to the :checked
attribute, and listen for the change
event. We’ll use the event’s checked
property, not the value
like we have before. So: $event.target.checked
.
📃DemoForm.vue
<input
type="checkbox"
:checked="form.spam"
@change="updateForm('spam', $event.target.checked)" />
I want all the spams
</label>
Once again, you can go back to your browser to verify that everything still works. The only thing we have done so far is make sure that we can control the two-way data binding between our elements and our internal state. Now, we can intercept the binding in the updateForm
method and make sure that every time something is updated, we store the form’s data into localStorage.
Saving form data into localStorage
Our first step is to create two helper methods, one for opening the localStorage of our form, and another for saving it. Because localStorage expects store strings, that means we are dealing with state that is an object. So we’ll have to transform that state to JSON-encoded strings and back.
📃DemoForm.vue
openStorage () {
return JSON.parse(localStorage.getItem('form'))
},
saveStorage (form) {
localStorage.setItem('form', JSON.stringify(form))
},
As you would expect, the getItem
method from localStorage looks for an item in the storage and returns it if found. The setItem
sets the value for the key that is passed, overwriting or creating a new entry as needed.
Next, we need to adjust our updateForm
method to use these helpers so that we can keep an updated copy of our state in localStorage every time a form value is updated.
📃DemoForm.vue
updateForm (input, value) {
this.form[input] = value
let storedForm = this.openStorage() // extract stored form
if (!storedForm) storedForm = {} // if none exists, default to empty object
storedForm[input] = value // store new value
this.saveStorage(storedForm) // save changes into localStorage
}
We are extracting our stored form with our new openStorage
method, and checking if it has already been set before. If it’s brand new, we default it to an empty object.
Next, inside of our storedForm
, we store the input → value pair that we just got from the input. Finally, we save it back to localStorage. Our helper methods take care of all the JSON-parsing for us.
Go ahead and open your browser and type into some of the form fields, then open your developer tools and go into storage. Under localStorage, you should now see a form row that contains a serialized JSON string with all your data!
Here’s an output example:
{"firstName":"Marina","lastName":"Mosti","email":"test@test.com","framework":"stillvue","extras":"COMMUNITY!!!!","spam":true}
Loading the stored form on refresh
This is pretty cool and everything, but how do we actually make our form refresh-proof with this new stored state? Thankfully the solution is very straight forward.
First, we need to hook into the created
lifecycle method of the component that has the form. In here, we are going to extract the stored state, and if there is anything in there, we will use it as a starter value for our internal state.
📃DemoForm.vue
created () {
const storedForm = this.openStorage()
if (storedForm) {
this.form = {
...this.form,
...storedForm
}
}
}
Notice that when we are setting the new form state with this.form = {}
we are using the spread operator because we want to keep the original data structure, plus the default values that we are setting on it. If you need a refresher, I have an article on the JavaScript spread operator.
To test that this is working, you could try turning the spam checkbox to true as default. You will see that even though the localStorage does not have this property (unless you have set it previously) it will keep its true state.
Go back into the browser, make some changes into your form and reload the browser. You should be able to reload and browse away from this page as much as you want.
Magic! (or something)
Wrapping up
We’ve learned a very powerful and simple tool to empower our Vue forms. There are still a few considerations and tweaks that you may want to take in a production-ready application, though.
Clearing out the stored form once the user submits it (just in case they ever browse back they can start fresh) may be a good idea, but this will be dependent on the type of UX you’re going for.
Another good idea could be to keep a timestamp of the last time the localStorage for the form was updated, and perhaps if the user visits later than a day or so, we will delete it and let them start over. We need to be considerate of people that may be sharing computers.
This same approach can also be used in conjunction with Vuex, but instead of tapping into v-model
, you would store the global state of the form using Vuex mutations.
Finally, keep in mind that data stored in localStorage is NOT PRIVATE. I cannot stress this enough: be careful not to store credit card information, passwords, etc!
If you would like to check out the code for this article, you can clone the repository with the example.
Thanks for reading and share with me your refresh-proofing experiences on twitter at: @marinamosti