Vue template directives: v-bind, v-on, v-if, v-for, and v-model
Master Vue template syntax to display data, handle events, render conditions and lists, and create two-way bindings for form inputs.
Templates are enhanced HTML
A Vue template is valid HTML extended with special syntax. The most basic form is text interpolation with double curly braces:
<p>Author: {{ author.name }}</p>
<p>Read {{ readCount.toLocaleString('en-US') }} times</p>
Inside an interpolation you can write a single JavaScript expression, but not statements like if or for. Interpolations are always escaped, so they are safe from malicious HTML. Avoid v-html for user-generated content because it can open the door to XSS.
Special attributes prefixed with v- are called directives. Here are the ones you will use most often.
v-bind: binding attributes
v-bind connects an HTML attribute to your data. Its shorthand is a colon.
<img :src="article.image" :alt="article.title">
<a :href="`/articles/${article.slug}`">Read more</a>
<button :disabled="isSaving">Save</button>
Since Vue 3.4, when the attribute name matches the variable name, you can write just :src as shorthand for :src="src".
Dynamic classes and styles
class and style accept objects and arrays:
<li
:class="{ active: item.id === activeId, done: item.done }"
:style="{ opacity: item.done ? 0.5 : 1 }"
>
{{ item.title }}
</li>
The active class is added only when its condition is true. Static and dynamic classes can be combined on the same element.
v-on: handling events
v-on listens for DOM events. Its shorthand is @.
<button type="button" @click="remove(item.id)">Delete</button>
<input @keyup.enter="submit">
<form @submit.prevent="save">...</form>
Modifiers make code shorter:
| Modifier | Effect |
|---|---|
.prevent |
calls event.preventDefault() |
.stop |
calls event.stopPropagation() |
.once |
the handler runs only once |
.self |
only if the event originated from the element itself |
.enter, .esc |
keyboard key filters |
If you need the native event object along with other arguments, use the special $event variable: @click="select(item, $event)".
v-if, v-else-if, v-else, and v-show
Conditional rendering decides whether an element is displayed.
<p v-if="status === 'loading'">Loading data...</p>
<p v-else-if="status === 'error'">Failed to load. Please try again.</p>
<ArticleList v-else :articles="articles" />
v-else-if and v-else must come directly after the v-if element. To group several elements without adding an extra wrapper to the DOM, put v-if on a <template> tag.
v-show also hides elements, but in a different way:
v-ifactually creates and destroys elements. Toggling is more expensive, but nothing is rendered when the initial condition is false.v-showalways renders the element and only togglesdisplay: none. It suits elements that are toggled often, such as dropdowns.
v-for: rendering lists
v-for repeats an element for each item in an array:
<ul>
<li v-for="(article, index) in articles" :key="article.id">
{{ index + 1 }}. {{ article.title }}
</li>
</ul>
The :key attribute is essential. Vue uses keys to identify each item when the list is reordered, extended, or shortened. Use a unique, stable value such as an id. Avoid using index as the key when the order can change, because state inside items (for example input values) may get mixed up.
v-for also works with objects and number ranges:
<span v-for="(value, key) in profile" :key="key">{{ key }}: {{ value }}</span>
<span v-for="n in 5" :key="n">⭐</span>
Don't combine v-if and v-for on the same element. In Vue 3, v-if is evaluated first, so the v-for variable is not yet available. Filter the data with computed instead:
const publishedArticles = computed(() => articles.value.filter((a) => a.published))
v-model: two-way binding
v-model keeps an input's value in sync with state. Vue automatically picks the right property and event for each input type.
<script setup>
import { ref } from 'vue'
const name = ref('')
const age = ref(null)
const agreed = ref(false)
const interests = ref([])
const level = ref('beginner')
</script>
<template>
<input v-model.trim="name" placeholder="Name">
<input v-model.number="age" type="number">
<label><input v-model="agreed" type="checkbox"> I agree</label>
<label><input v-model="interests" type="checkbox" value="vue"> Vue</label>
<label><input v-model="interests" type="checkbox" value="laravel"> Laravel</label>
<select v-model="level">
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
</select>
</template>
v-model modifiers:
.trimremoves leading and trailing whitespace..numbercasts the input to a number..lazysyncs the value on thechangeevent instead of on every keystroke.
Case study: a simple to-do list
The directives above are enough to build a small app:
<script setup>
import { computed, ref } from 'vue'
const newTask = ref('')
const tasks = ref([])
let nextId = 1
const remaining = computed(() => tasks.value.filter((t) => !t.done).length)
function add() {
if (!newTask.value) {
return
}
tasks.value.push({ id: nextId++, title: newTask.value, done: false })
newTask.value = ''
}
function remove(id) {
tasks.value = tasks.value.filter((t) => t.id !== id)
}
</script>
<template>
<form @submit.prevent="add">
<input v-model.trim="newTask" placeholder="New task">
<button>Add</button>
</form>
<p v-if="tasks.length === 0">No tasks yet.</p>
<ul v-else>
<li v-for="task in tasks" :key="task.id" :class="{ done: task.done }">
<input v-model="task.done" type="checkbox">
{{ task.title }}
<button type="button" @click="remove(task.id)">Delete</button>
</li>
</ul>
<p>{{ remaining }} tasks remaining.</p>
</template>
Exercise
Add "All", "Active", and "Completed" filters to the to-do list above, using one ref for the active filter and one computed for the displayed list.
