Directives
Bind attributes, show things conditionally and render lists with v-bind, v-if, v-show and v-for.
What you'll learn
When you finish, you can do these.- 1Switch an attribute such as disabled on and off with v-bind
- 2Choose between v-if and v-show to show or hide things
- 3Repeat a row for each item with v-for, keyed by its id
- 4Spot and fix a list that vanishes or mixes up typed notes
Picture a page that lists the fruit in a bowl: Mango, Banana and Guava, each marked ripe or not yet. One button hides the unripe ones, another takes the first fruit out. Writing each row by hand as an <li>, an HTML list item, stops working once the list comes from data that changes: a fruit is eaten, another ripens, and the page has to follow.
Directives do that work: attributes in a .vue file's template, its HTML part, whose names start with v-. They repeat an element for each item in a list, show it only while a condition holds, or set an attribute from your data, and keep doing it as the data changes. The Code tab holds the fruit page as one such file, App.vue; by the end you can read every directive in it. You need one idea from the Reactivity basics lesson: a ref, a box that holds a value and tells Vue when it changes.
v-bind: attributes from your state
v-bind keeps an attribute in step with your state, the values that change while someone uses the page. In the lesson Your first component, :href="url" took a link's address from a value. That colon is short for v-bind: and works on any attribute: Vue sets the attribute to the result of the quoted expression, and again whenever the result changes.
The fruit page's state lives in the script block of App.vue: a ref called fruits holds the three fruits, and a function, removeFirst, takes the first one out.
<script setup lang="ts">
import { ref } from 'vue'
const fruits = ref([
{ id: 1, name: 'Mango', ripe: true },
{ id: 2, name: 'Banana', ripe: false },
{ id: 3, name: 'Guava', ripe: true },
])
function removeFirst() {
fruits.value.shift()
}
</script>
In script the array is fruits.value, and shift() removes its first item. The template leaves .value out, because Vue opens the box for you there.
The Remove first button calls removeFirst on each click (that is what @click does) and binds disabled to whether the bowl is empty:
<button :disabled="fruits.length === 0" @click="removeFirst">Remove first</button>
For a true-or-false attribute like disabled, Vue adds it when the value is true and removes it entirely when the value is false. Click Remove first three times: the list empties, Vue adds disabled, and the button greys out.
v-if and v-show: show or hide
v-if and v-show both show an element only while a condition holds. v-if adds the element to the page when its expression is true and removes it when it is false. v-else shows its element when the v-if right before it was false.
In the fruit list, fruit stands for the fruit of the current row (the section on v-for shows where it comes from), and its ripe flag picks one of two labels:
<strong v-if="fruit.ripe">ripe</strong>
<em v-else>not yet</em>
Mango and Guava are marked ripe, Banana not yet.
v-show looks the same but keeps the element on the page: Vue sets its CSS display to none while the expression is false.
v-ifFalse: the element is removed from the page, and anything inside it, such as typed text, is thrown away. True again: it is built from scratch.
v-showFalse: the element stays and gets display: none. True again: Vue removes that style. The element is built once, even if it starts hidden.
So choose by how the element is used. A tip that opens and closes often fits v-show: it is built once and only its style changes. A section that is rarely shown fits v-if: nothing is built until the condition is true.
The fruit page's Tip button flips showTip, a true-or-false ref that starts false, and the tip uses v-show:
<button @click="showTip = !showTip">Tip</button>
<p v-show="showTip">Ripe fruit gives a little when you press it.</p>
Click Tip and the tip appears. Click again and it hides, though the paragraph stays on the page with display: none.
v-for: one template, many items
v-for repeats an element once for each item in a list. v-for="fruit in fruits" gives one row per fruit, with fruit holding the current item. Because fruits is a ref, adding or removing a fruit adds or removes a row.
(fruit, index) in fruits also gives the position, counted from 0. A number works too: v-for="n in 3" counts from 1, so it renders 1, 2 and 3, handy for page numbers.
Give every v-for a :key, a value unique to each item, usually its id. Vue uses the key like a name tag: when the list changes, it keeps the rows whose key is still there and removes only the row whose key is gone. Without a stable key it matches rows by position, which goes wrong as soon as a row holds something of its own, such as typed text.
In App.vue, the loop and the key sit on an inner <template> tag, a wrapper Vue does not render: the page shows only what is inside it.
The <li> inside tests fruit.ripe || showUnripe, where || means or: ripe fruit always shows, unripe fruit only while showUnripe is true. That ref starts true, and the Hide unripe button flips it. Each row ends with a Note box, labelled for screen readers:
<template v-for="fruit in fruits" :key="fruit.id">
<li v-if="fruit.ripe || showUnripe">
{{ fruit.name }}
<strong v-if="fruit.ripe">ripe</strong>
<em v-else>not yet</em>
<input :aria-label="'Note for ' + fruit.name" placeholder="Note" />
</li>
</template>
Three rows appear, each with its label and an empty Note box. Click Hide unripe and Banana's row goes.
What goes wrong
The first mistake is putting v-for and v-if on the same element. It reads like "repeat for every fruit, and keep the ones that pass the test". In Vue 3, v-if runs first, before fruit exists. The list vanishes, the preview shows the error Cannot read properties of undefined (reading 'ripe'), and the browser console also lists Property "fruit" was accessed during render but is not defined on instance.
<li v-for="fruit in fruits" v-if="fruit.ripe || showUnripe" :key="fruit.id">
{{ fruit.name }}
</li>
<template v-for="fruit in fruits" :key="fruit.id">
<li v-if="fruit.ripe || showUnripe">{{ fruit.name }}</li>
</template>
Move the loop to a <template> tag around the element, and keep the condition on the element.
The second mistake is using the index, each row's position counted from 0, as the key. Type "sweet" in Mango's note and click Remove first: Mango's row disappears, yet "sweet" is now next to Banana.
<template v-for="(fruit, index) in fruits" :key="index">
<template v-for="fruit in fruits" :key="fruit.id">
After the delete, Banana has index 0, the key Mango had. Vue sees that key 0 is still there, keeps that row with its input and typed text, and puts Banana's name in it. The row that goes is the last one. With ids as keys, the row keyed 1 leaves together with Mango.
What to remember
:attr="expression"is short forv-bind:attrand keeps an attribute in step with your state. A true-or-false attribute is removed while its value is false.v-ifadds and removes the element;v-showkeeps it and togglesdisplay: none. Preferv-showfor things that flip often.v-forrepeats an element for each item, andn in 3counts 1, 2, 3.- Key every
v-forby a unique id from the data, never by index when rows hold inputs or other state of their own. - Never put
v-ifandv-foron one element: loop on a<template>and test inside it.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Add
{ id: 4, name: 'Papaya', ripe: false }tofruits. A fourth row appears, marked "not yet". Click Hide unripe: Banana and Papaya disappear together. - Under the list, add
<p v-if="fruits.length === 0">The bowl is empty.</p>followed by<p v-else>{{ fruits.length }} left</p>. Click Remove first until the list is gone: the line counts down, then says "The bowl is empty.", and Remove first is disabled. - Change the loop to
(fruit, index) in fruitswith:key="index". Type a note next to Mango, then click Remove first: your note now sits next to Banana. Put:key="fruit.id"back and repeat: the note leaves with Mango.