Modern JS in Legacy Projects, Part Three

Saving/resetting data made easy

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:

  1. 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 a data property that references whatever is in your data object when the Vue instance is created.
  2. 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 same data 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 current data object.
  • Since our data object is now a method that returns a fresh object, we can call it! This executes the called method with our current this context passed in, which returns a new object. Any properties returned by this.$options.data.call(this) will overwrite this.$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!