Fundamentals
Lists and why the key matters
You have a list and you want one element per item. v-for does that, and
:key is what separates a list that behaves from a list that loses the focus of a
field every time someone filters it.
The basics
<div v-data="{ products: [
{ id: 1, name: 'Mug', price: 39 },
{ id: 2, name: 'Shirt', price: 89 }
] }">
<ul>
<li v-for="p in products" :key="p.id">{ p.name }: $ { p.price }</li>
</ul>
<button @click="products.push({ id: Date.now(), name: 'New', price: 10 })">
Add
</button>
</div>
Every accepted form
v-for is not limited to arrays. in and of are equivalent.
<div v-data="{
users: [{ id: 1, name: 'Ann' }, { id: 2, name: 'Bob' }],
settings: { theme: 'dark', language: 'en-US' }
}">
<p>With an index:</p>
<ul><li v-for="(user, i) in users" :key="user.id">{ i }: { user.name }</li></ul>
<p>Over an object:</p>
<ul><li v-for="(value, key) in settings">{ key } = { value }</li></ul>
<p>Over a number:</p>
<ul><li v-for="n in 3">Item { n }</li></ul>
<p>Over a string:</p>
<ul><li v-for="letter in 'abc'">{ letter }</li></ul>
</div>
Arrays, numbers, strings, objects, Map and Set all work. Over a number,
counting starts at 1: v-for="n in 3" gives 1, 2 and 3.
To repeat a group of elements with no wrapper, <template> works here too:
<dl>
<template v-for="p in products" :key="p.id">
<dt>{ p.name }</dt>
<dd>{ p.price }</dd>
</template>
</dl>
Why the key matters
:key tells Voodoo how to recognize the same item across two updates. With no key,
the library only knows the position: if the list is reordered or filtered, it reuses the element
at position 1 for whatever the new item at position 1 happens to be.
In practice that means fields losing focus, text being edited disappearing and the scroll jumping. Compare the two lists below: type into one of the fields and then click to shuffle.
<div v-data="{ tasks: [
{ id: 1, text: 'Buy coffee' },
{ id: 2, text: 'Answer email' },
{ id: 3, text: 'Pay the bill' }
] }">
<button @click="tasks.reverse()">Reverse the order</button>
<p><b>With :key</b></p>
<div v-for="t in tasks" :key="t.id"><input v-model="t.text"></div>
<p><b>Without :key</b></p>
<div v-for="t in tasks"><input v-model="t.text"></div>
</div>
Whenever the list can be reordered, filtered or have items removed from the middle, put a
:key on it. The key has to be stable and unique within the list: the
id from the database works; the position index does not, because it changes along with
the order, which is exactly the problem the key was supposed to solve.
Filtering and sorting
There is no filter built into the directive: the v-for expression accepts any
expression that returns a collection, so filter, slice and
sort handle it, with the caveat that sort mutates the original array,
so it is worth copying first.
<div v-data="{
search: '',
products: [
{ id: 1, name: 'Mug', price: 39 },
{ id: 2, name: 'Shirt', price: 89 },
{ id: 3, name: 'Notebook', price: 25 }
]
}">
<input v-model="search" placeholder="Filter...">
<ul>
<li v-for="p in products.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))"
:key="p.id">
{ p.name }: $ { p.price }
</li>
</ul>
<p v-if="!products.some(p => p.name.toLowerCase().includes(search.toLowerCase()))">
Nothing found.
</p>
</div>
When the expression gets too long for the attribute, move it into a function on the
v-data. The result is the same and the HTML is readable again.
v-for and v-if do not share an element
Both take control of that node, so the library has no way of deciding which one wins. Put the
condition on a child, or wrap the repetition in a <template>.
<!-- do not do this -->
<li v-for="n in list" v-if="n % 2 === 0">{ n }</li>
<!-- do this -->
<li v-for="n in list">
<span v-if="n % 2 === 0">{ n }</span>
</li>
<!-- or filter in the expression itself -->
<li v-for="n in list.filter(n => n % 2 === 0)">{ n }</li>
What exists inside each item
Each iteration creates a scope of its own. Inside it you have the item variable, the index when
declared, and everything the outer scopes offer. The magic variables stay available, with
$el pointing at the element of that iteration.
<div v-data="{ label: 'Product', items: ['Mug', 'Shirt'] }">
<ul>
<li v-for="(item, i) in items">
{ label } { i + 1 }: { item }
<button @click="items.splice(i, 1)">remove</button>
</li>
</ul>
<p v-if="!items.length">The list is empty.</p>
</div>
The limit worth knowing about
v-for renders every item in the source: there is no virtualization. For lists of a
few hundred rows that is irrelevant. For tens of thousands updating at once, paginate, use
infinite scrolling with v-infinite-scroll, or pick a tool that virtualizes. The
performance page measures the real cost with
numbers.