Rendering a list of Vue components
At this point we've got a fully working component; we're now ready to add multiple ToDoItem
components to our app. In this article we'll look at adding a set of todo item data to our App.vue
component, which we'll then loop through and display inside ToDoItem
components using the v-for
directive.
Prerequisites: |
Familiarity with the core HTML, CSS, and JavaScript languages, knowledge of the terminal/command line. Vue components are written as a combination of JavaScript objects that manage the app's data and an HTML-based template syntax that maps to the underlying DOM structure. For installation, and to use some of the more advanced features of Vue (like Single File Components or render functions), you'll need a terminal with node + npm installed. |
---|---|
Objective: | To learn how to loop through an array of data and render it in multiple components. |
Rendering lists with v-for
To be an effective to-do list, we need to be able to render multiple to-do items. To do that, Vue has a special directive, v-for
. This is a built-in Vue directive that lets us include a loop inside of our template, repeating the rendering of a template feature for each item in an array. We'll use this to iterate through an array of to-do items and display them in our app in separate ToDoItem
components.
Adding some data to render
First we need to get an array of to-do items. To do that, we'll add a data
property to the App.vue
component object, containing a ToDoItems
field whose value is an array of todo items. While we'll eventually add a mechanism to add new todo items, we can start with some mock to do items. Each to-do item will be represented by an object with a label
and a done
property.
Add a few sample to-do items, along the lines of those seen below. This way you have some data available for rendering using v-for
.
export default {
name: "app",
components: {
ToDoItem,
},
data() {
return {
ToDoItems: [
{ label: "Learn Vue", done: false },
{ label: "Create a Vue project with the CLI", done: true },
{ label: "Have fun", done: true },
{ label: "Create a to-do list", done: false },
],
};
},
};
Now that we have a list of items, we can use the v-for
directive to display them. Directives are applied to elements like other attributes. In case of v-for
, you use a special syntax similar to a for...in
loop in JavaScript — v-for="item in items"
— where items
is the array you want to iterate over, and item
is a reference to the current element in the array.
v-for
attaches to the element you want to repeat, and renders that element and its children. In this case, we want to display an <li>
element for every to-do item inside our ToDoItems
array. Then we want to pass the data from each to-do item to a ToDoItem
component.
Key attribute
Before we do that, there's one other piece of syntax to know about that is used with v-for
, the key
attribute. To help Vue optimize rendering the elements in the list, it tries to patch list elements so it's not recreating them every time the list changes. However, Vue needs help. To make sure it is re-using list elements appropriately, it needs a unique "key" on the same element that you attach v-for
to.
To make sure that Vue can accurately compare the key
attributes, they need to be string or numeric values. While it would be great to use the name field, this field will eventually be controlled by user input, which means we can't guarantee that the names would be unique. We can use nanoid()
, however, as we did in the previous article.
-
Import
nanoid
into yourApp
component in the same way you did with yourToDoItem
component, usingjsimport { nanoid } from "nanoid";
-
Next, add an
id
field to each element in yourToDoItems
array, and assign each of them a value of"todo-" + nanoid()
.The
<script>
element inApp.vue
should now have the following contents:jsimport ToDoItem from "./components/ToDoItem.vue"; import { nanoid } from "nanoid"; export default { name: "app", components: { ToDoItem, }, data() { return { ToDoItems: [ { id: "todo-" + nanoid(), label: "Learn Vue", done: false }, { id: "todo-" + nanoid(), label: "Create a Vue project with the CLI", done: true, }, { id: "todo-" + nanoid(), label: "Have fun", done: true }, { id: "todo-" + nanoid(), label: "Create a to-do list", done: false, }, ], }; }, };
-
Now, add the
v-for
directive andkey
attribute to the<li>
element in yourApp.vue
template, like so:html<ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item label="My ToDo Item" :done="true"></to-do-item> </li> </ul>
When you make this change, every JavaScript expression between the
<li>
tags will have access to theitem
value in addition to the other component attributes. This means we can pass the fields of our item objects to ourToDoItem
component — just remember to use thev-bind
syntax. This is really useful, as we want our todo items to display theirlabel
properties as their label, not a static label of "My Todo Item". In addition, we want their checked status to reflect theirdone
properties, not always be set todone="true"
. -
Update the
label="My ToDo Item"
attribute to:label="item.label"
, and the:done="true"
attribute to:done="item.done"
, as seen in context below:html<ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done"></to-do-item> </li> </ul>
Now when you look at your running app, it'll show the todo items with their proper names, and if you inspect the source code you'll see that the inputs all have unique id
s, taken from the object in the App
component.
Chance for a slight refactor
There's one little bit of refactoring we can do here. Instead of generating the id
for the checkboxes inside your ToDoItem
component, we can turn the id
into a prop. While this isn't strictly necessary, it makes it easier for us to manage since we already need to create a unique id
for each todo item anyway.
- Add a new prop to your
ToDoItem
component —id
. - Make it required, and make its type a
String
. - To prevent name collisions, remove the
id
field from yourdata
attribute. - You are no longer using
nanoid
, so you need to remove theimport { nanoid } from 'nanoid';
line, otherwise your app will throw an error.
The <script>
contents in your ToDoItem
component should now look something like this:
export default {
props: {
label: { required: true, type: String },
done: { default: false, type: Boolean },
id: { required: true, type: String },
},
data() {
return {
isDone: this.done,
};
},
};
Now, over in your App.vue
component, pass item.id
as a prop to the ToDoItem
component. Your App.vue
template should now look like this:
<template>
<div id="app">
<h1>My To-Do List</h1>
<ul>
<li v-for="item in ToDoItems" :key="item.id">
<to-do-item
:label="item.label"
:done="item.done"
:id="item.id"></to-do-item>
</li>
</ul>
</div>
</template>
When you look at your rendered site, it should look the same, but our refactor now means that our id
is being taken from the data inside App.vue
and passed into ToDoItem
as a prop, just like everything else, so things are now more logical and consistent.
Summary
And that brings us to the end of another article. We now have sample data in place, and a loop that takes each bit of data and renders it inside a ToDoItem
in our app.
What we really need next is the ability to allow our users to enter their own todo items into the app, and for that we'll need a text <input>
, an event to fire when the data is submitted, a method to fire upon submission to add the data and rerender the list, and a model to control the data. We'll get to these in the next article.