Part one of this series discussed the why; part two introduced our “legacy” application and got its basic functionality hammered out with surprisingly little hoopla; now we can add a little somethin’ extra to make it magical. ✨
Our latest deploy is missing some key features that were part of the spec:
- The “Reset” button doesn’t work, and
- We’re not saving the state of the modal anywhere locally so that it persists across page reloads.
So in Part Two, I lied: we’re not turning this guy into a component just yet! Let’s knock out the rest of our basic functionality first.
Resetting the product modal
The first thing we’ll need to do is save the initial, default state of the form in our application somewhere. But guess what! With a single slight modification, this is almost automatic.
Data as a method
There are a couple things to understand in order for this to make sense:
- Every Vue instance has several default properties that you can access, most of which are prefixed by a
$
. You can see all of them in the docs. The one we’re interested in is called$options
.$options
has adata
property that references whatever is in yourdata
object when the Vue instance is created. - The
data
property of the instance can also be a method that returns an object. In fact, if we’re working with components (instead of instances—more on that later), that’s often preferable, as returning a fresh object means each of our Vue components can have its own isolated state. (data
, like all JavaScript objects, is passed by reference, which means multiple instances of the same component would share the samedata
object. That’s almost definitely not what you want. More info in the docs.)So, we need
data
to be a method that returns a fresh object, so that$options.data()
(as a method) will always return the original data we set. (If that doesn’t make any sense to you at all, play around with DevTools or mess with an example on jsfiddle—that’s what I had to do!)
Step one, then, is to convert the data
object into a method:
data: function () {
return {
text: 'oh, hello',
size: {
height: 240,
width: 320
},
colorScheme: {
Flowers: {
background: '#76B041',
text: '#FFC914'
},
Rivers: {
background: '#698F3F',
text: '#804E49'
},
Sand: {
background: '#D58936',
text: '#A44200'
},
Snow: {
background: '#F8FFF4',
text: '#474350'
},
Storm: {
background: '#031926',
text: '#7CA5B8'
},
'Custom...': {
background: '#868e96',
text: '#f8f9fa'
}
},
maxTextLength: 140,
maxDimension: 600,
minDimension: 50,
selectedColorScheme: '',
url: 'https://placehold.it/320x240/868e96/f8f9fa?text=' + encodeURIComponent('Oh, hello')
}
},
See that? Almost the exact same code, but instead of having a reference to a plain object at the value of the data
key, data
is a function that returns a new object. That way, every new Vue instance will have its own, fresh object at the data
key, rather than a single, shared object.
Replacing data with data
Now we can create a new method on our instance that replaces the instance’s current data with the original data.
Object.assign()
doesn’t work in IE. We’ve still got good ole jQuery hanging around, so let’s use jQuery.extend()
instead.
methods: {
// ...
reset: function () {
$.extend(true, this.$data, this.$options.data.call(this))
}
}
What’s going on here? Let’s break it down:
-
jQuery.extend
is a super useful method that takes two objects and smashes ’em together. With the first argument, we’re telling jQuery we want to do this recursively—that is, we want to update even deeply-nested data in our object. -
this.$data
is a reference to our currentdata
object. - Since our
data
object is now a method that returns a fresh object, we cancall
it! This executes thecall
ed method with our currentthis
context passed in, which returns a new object. Any properties returned bythis.$options.data.call(this)
will overwritethis.$data
, effectively resetting our data to its original state. Neat!
(Side note: jQuery gets a lot of hate in development circles, but it’s still so useful.)
Listen up
Finally, let’s tie that method to our template:
<button type="button" class="btn btn-secondary" v-on:click="reset">Reset</button>
The v-on
directive sets a listener. You could also shorten it to @
, if you’re feelin’ frisky:
<button type="button" class="btn btn-secondary" @click="reset">Reset</button>
Me, I like the shorter syntax.
With just a few lines of code, we’ve managed to build a function that resets our whole modal. Nooice. See the deploy here (source).
Saving state
Alright, so how do we save the state of our Vue instance so that the state is remembered if the user closes their window?
I’ve used localStorage
for this in the past, and it works well, but you could use cookies if you wanted a nightmare. These days there are tons of storage options in modern browsers, but localStorage
has fairly wide support, so that’s what we’ll use here.
Caution! localStorage
only stores strings. We want to store our data
, which is a plain ol’ JavaScript object—not a string. So we have to JSON.stringify
our data
object to store it, and then JSON.parse
the string to turn it back into an object that we can use. These are blocking operations. If you’re dealing with lots of data or a slow processor, parse
-ing and stringify
-ing objects can cause jank. All this is to say, this is not an ideal solution. You could, for instance, store the user’s data in a session on the server, and update it onchange
instead…but I’m trying to stick to client-side code here. Point is, this technique comes with some trade-offs that must be considered for your use case.
Using localstorage
Caveats aside, let’s create a couple new methods on our Vue instance to help us save and restore our localstorage:
methods: {
// ...
restoreFromLocalStorage: function () {},
saveToLocalStorage: function () {},
}
First, lets’s build out our saveToLocalStorage
method.
localStorage
is a simple key/value store, so we need to come up with a key and then stringify
and store the data:
saveToLocalStorage: function () {
var data = JSON.stringify(this.$data)
window.localStorage.setItem('vueforlegacy', data)
},
Restoring from localStorage
is a similarly straightforward operation. Now that we know how to extend/overwrite our data
object (remember our reset
method?), we can use the same technique here:
restoreFromLocalStorage: function () {
var data = JSON.parse(window.localStorage.getItem('vueforlegacy'))
if (data) {
$.extend(true, this.$data, data)
}
},
Lifecycle hooks
Now we have our methods, but they’re just sittin’ there looking dumb. How do we use them? It would be really nice if there were some way to save our data to localStorage
any time it changed. It would be even cooler if we could load that data when our Vue instance is ready to go.
Enter lifecycle hooks. Every Vue instance goes through a lifecycle; each hook is called in sequence, which we can leverage here.
Specifically, we can use the beforeMount
hook (called just before our Vue instance replaces the DOM element specified in el
) to set up the data:
// Lifecycle hooks are set directly on the Vue options object,
// not inside `methods`, `data`, or `computed`
beforeMount: function () {
this.restoreFromLocalStorage();
},
…and we can use the updated
hook to update localStorage
any time our data
object changes:
updated: function () {
this.saveToLocalStorage();
}
And that wraps up the initial functionality! You can now refresh the page (or leave and come back) and your modifications to the product will be just how you left ’em. (Source.)
Till next time
In the next post, we’ll actually start turning this into a component, and in the process, we’ll build a simple reactive shopping cart.
If you’ve followed along so far, you are truly amazing. I’d love to hear what you think. If you spot any mistakes or have other comments/questions, Tweet me!