Vue.js Simple Tuts: Dynamic Components

Components aren’t much good if they are just hard-coded swatches of copy/paste code inserted all over your page. Their power lies in making them flexible.

If you’ve been following along, last time we turned our simple toggle into a nice little reusable Component. Here’s a tidied up version you can use as a starting point:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Dynamic Components</title>
</head>
<body>

<div id="app">
<toggle-component></toggle-component>
<toggle-component></toggle-component>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<script src='app.js'></script>
</body>
</html>

and the app.js file:

// app.js
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
}
}
});

new Vue({
el: '#app',
});

You have two instances of your `toggle-component`, but both of them have the same hard-coded content (“Toggle info”). You don’t want all of your components saying that! Fix that.

Where will you make that change? If you look in your ‘Vue.component’ definition, you realize that this is the same for both `toggle-component` instances, so this must be like your function. Each time it is called, it does some stuff and renders a certain html to the page. Obviously, then, you want to pass the content in from outside the component. Then you’ll capture that and pass it into your template to render. Ok — concept down, let’s do it in practice!

In your template replace the hard-coded string with a variable called “content”. Notice the {{ }} to hold it:

template: "<div><button @click='toggle()'>Open/Close</button>" +
"<span v-show=\"isOpen\">{{ content }}</span>" +
"</div>",

Great! Now on the components, tell it the value to pass in:

<toggle-component content="Top one"></toggle-component>
<toggle-component content="Bottom one"></toggle-component>

Now, refresh your page and…wait! It’s just blank. No error. No nothing…

The problem is, you can’t just pass data from outside directly in to your template. Instead, use something called props. Props are used whenever you want to pass something into the child. In this case, think of the ‘toggle-component’ elements as the “outside” layers, passing contents into their own guts. Maybe that just made it more confusing. Sorry.

Just above ‘template’, add your props:

props: ['content'],

Why is it called “props”? I believe it is short for “properties”. All front-end developers are secretly jealous they are too young to have used Hungarian Notation, and are trying to make up for it (but doing it wrong). Anyway, that’s a tangent…

Alright! You have dynamic content in your components! Now, I know what you’re thinking — “Jeff, I’d really love to know how many times each of these buttons is being clicked”. Of course you would. Who wouldn’t?

Think about how you’ll do that. You already have an “onClick” function, toggle(), so that seems like a good place to increment a counter.

methods:{
toggle: function(){
this.isOpen = !this.isOpen;
this.clicked += 1;
}
}

Display it as part of the string that shows when the span toggles open, like so:

template: "<div>
<button @click='toggle()'>Open/Close</button>
<span v-show=\"isOpen\">{{ content }} clicked {{ clicked }} times.</span>
</div>",

Great! Refresh the page and…white. Blank. Nothing.

What are you missing? Ah! You haven’t defined ‘clicked’ anywhere yet. No problem — just imitate what you did for ‘content’:

<toggle-component content="Top one" clicked="0"></toggle-component>
<toggle-component content="Bottom one" clicked="0"></toggle-component>

and in your props:

props: ['content', 'clicked'],

Refresh, toggle the button, and… it stays at 0. Hmmmm….

You’re probably in front of your computer shouting, “Not like that, you #$%&!” but it wasn’t so obvious to me at first. What you are doing with this way of setting things up is to always pass a 0 into the template, every time. You don’t want to do that — instead, you want to work with ‘clicked’ internally.

Remove ‘clicked’ from props, and instead use:

data: function () {
return {
isOpen: false,
clicked: 0
}
},

This initializes the variable ‘clicked’ and sets it to 0 to start. This is why in your toggle() function you are referring to it as this.clicked, and it works just like your other variable ‘isOpen’. Now when you click, it increments, each button separately.

Things will, of course, get more complicated, but I think if you play around with that code a bit and make sure you understand the difference it will greatly help your understanding of Vue.js.

A last thing to notice — you see you forgot to remove your ‘clicked’ attribute:

<toggle-component content="Top one" clicked="0"></toggle-component>
<toggle-component content="Bottom one" clicked="0"></toggle-component>

and yet everything is still working? This is something that can make mistakes really hard to debug; you are essentially just passing in a value and ignoring it, so everything still works — but does nothing. Don’t let that catch you!

Hope that helps! Happy to respond to questions, and thankful for Recommends and Tweets if you think it will help others!