Vue.js Simple Tuts: Components

Tag: Vue.js 2.*

A couple of weeks ago I did the first of these "Vue.js Simple Tuts" where we walked through creating a simple toggle. You'll see all the code repeated here, but if you'd like to start "at the beginning", go ahead and have a quick look at Vue.js Simple Tuts: Toggling

When we left off we have the following code:

<!-- index.html --> <div id="app"> <button @click='toggle()'>Open/Close</button> <span v-show="isOpen">Toggle info</span> </div> <!-- app.js --> new Vue({ el: '#app', data: { isOpen: false }, methods:{ toggle: function(){ this.isOpen = !this.isOpen } } });

That's a nice little toggle set. So nice, in fact, that I'd like to have three of them on my page. So go ahead - copy/paste the button and span two more times, and have a look.

Oh. When I click one button, they all open and close. That's not what I want. Obviously, the problem is they all use the variable `isOpen` and there's no way for the Vue object to know which span to apply the function to.

In jQuery this is easy - we could go up and down the DOM with a series of `parent().children('span')` -type code, or add matching numbered ids like `data-id-1`. This is because jQuery uses the DOM as its container. In Vue.js, we need to always talk to other components via the Vue class. Let's try it out.

We want the two elements (botton and span) to work together as a single set of reusable, encapsulated code - called a Component in javascript frameworks (which I'm sure you've figured out by now). Let's open our app.js file and build that.

<!-- app.js --> <!-- put this ABOVE your new Vue() declaration --> Vue.component('toggle-component', { template: "<button @click='toggle()'>Open/Close</button><span v-show=\"isOpen\">Toggle info</span>" });

This is globally registering the component, which is perfectly fine for today's lesson. We are creating our own elements, just like a custom "div" or "span", as we'll see below:

<!-- index.html --> <div id="app"> <button @click='toggle()'>Open/Close</button> <span v-show="isOpen">Toggle info</span> </div> <toggle-component></toggle-component>

Turn on your Firebug or Dev Tools and refresh the page. You see two buttons, but a red warning message:

[Vue warn]: Component template should contain exactly one root element:

What's it trying to tell us? This is actually common across all javascript frameworks that I've worked with - I saw it first with React.js. Remember when we were thinking about how we'd do it with jQuery, and we said, "Okay, parent()...". Well, we don't have a parent here. We don't have a single element acting as a container for this component - we have "multiple roots". In other words, our Vue instances doesn't know if it should talk to the "button" or the "span". The easy fix for this is just to add a "div" wrapper like so:

Vue.component('toggle-component', { template: "<div><button @click='toggle()'>Open/Close</button><span v-show=\"isOpen\">Toggle info</span></div>" });

refresh and... a new error! Progress!

[Vue warn]: Property or method "isOpen" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.

We ran into this in the earlier tutorial where we built the toggle logic itself. In that context, the variable `isOpen` wasn't declared, and so we added the `data` variable. Here it is the same thing; within our component scope, `isOpen` is not declared. Let's fix that:

Vue.component('toggle-component', { template: "<div><button @click='toggle()'>Open/Close</button><span v-show=\"isOpen\">Toggle info</span></div>", data: { isOpen: false } });

GAHH! More errors!

[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

This is a little strange until you realize why it has to be. The key is in the second part of the message - "a per-instance value". In short, this is how we can use multiple instances of `toggle-component` and have them each have their own state. Read the wonderful example from the docs to understnd better with a code example: https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function

Vue.component('toggle-component', { template: "<div><button @click='toggle()'>Open/Close</button><span v-show=\"isOpen\">Toggle info</span></div>", data: function () { return { isOpen: false } } });

If you refresh now, no more error mesages! Until you click the button, that is. But this time the message should be quite clear - we haven't defined `toggle()` on our component.

Vue.component('toggle-component', { template: "<div><button @click='toggle()'>Open/Close</button><span v-show=\"isOpen\">Toggle info</span></div>", data: function () { return { isOpen: false } }, methods:{ toggle: function(){ this.isOpen = !this.isOpen } } });

Go ahead and add another `toggle-component` under all the existing code, and check. We have three toggle buttons, all of which work independently! I've intentionally left the original code there just so you can see that the component is, in fact, encapsulated and uses separate scope from the rest of the page.

That's just the tip of of the iceberg, but a nice place to stop and digest what we've learned before understanding props and template data and all the other things we can use to really make these powerful.

Hope that helped!