Listen-Rendering
Dynamische Listen mit v-for – von einfachen Arrays bis zu reaktiven Todo-Listen
v-for Grundlagen
Wenn du eine Liste von Elementen im Template rendern willst, ist v-for dein bester Freund. Die Direktive iteriert über ein Array und erzeugt für jedes Element einen DOM-Knoten – vergleichbar mit .each in Ruby, nur direkt im HTML.
Die einfachste Form lautet v-for="item in items". Brauchst du zusätzlich den Index, nutze die Klammer-Syntax: (item, index) in items.
<script setup>
import { ref } from 'vue'
const fruits = ref(['Äpfel', 'Birnen', 'Kirschen', 'Trauben'])
</script>
<template>
<h3>Obstliste</h3>
<!-- Einfach: nur das Element -->
<ul>
<li v-for="fruit in fruits" :key="fruit">
{{ fruit }}
</li>
</ul>
<!-- Mit Index -->
<ul>
<li v-for="(fruit, index) in fruits" :key="fruit">
{{ index + 1 }}. {{ fruit }}
</li>
</ul>
</template>Das :key Attribut
Vue nutzt einen virtuellen DOM und einen effizienten Patching-Algorithmus, um nur die tatsächlich geänderten Elemente im echten DOM zu aktualisieren. Damit Vue einzelne Listeneinträge eindeutig zuordnen kann, braucht jedes Element einen stabilen, einzigartigen :key.
Ohne :key verwendet Vue eine „In-Place-Patch"-Strategie: Elemente werden anhand ihrer Position aktualisiert, nicht anhand ihrer Identität. Das führt zu subtilen Bugs – besonders wenn Listeneinträge eigenen State haben (z. B. Eingabefelder oder Animationen).
Was passiert ohne :key?
Ohne :key recycelt Vue DOM-Elemente nach Position. Fügst du ein Element am Anfang ein, glaubt Vue, alle Elemente hätten sich geändert, und patcht die gesamte Liste. Formulareingaben, Fokus-Zustände und CSS-Transitionen gehen dabei verloren. Verwende immer :key mit einer stabilen ID.
<script setup>
import { ref } from 'vue'
const tasks = ref([
{ id: 1, title: 'Einkaufen', done: false },
{ id: 2, title: 'Kochen', done: true },
{ id: 3, title: 'Abwaschen', done: false },
])
</script>
<template>
<!-- ✅ Richtig: stabile ID als key -->
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" :checked="task.done" />
{{ task.title }}
</li>
</ul>
<!-- ❌ Falsch: Index als key bei dynamischen Listen -->
<!--
<li v-for="(task, index) in tasks" :key="index">
Problematisch, wenn sich die Reihenfolge ändert!
</li>
-->
</template>Objekte iterieren
v-for funktioniert nicht nur mit Arrays, sondern auch mit Objekten. Die Reihenfolge der Iteration entspricht Object.keys(). Du kannst bis zu drei Parameter destrukturieren: (value, key, index).
<script setup>
import { ref } from 'vue'
const user = ref({
name: 'Anna',
email: 'anna@example.com',
rolle: 'Admin',
standort: 'Berlin',
})
</script>
<template>
<dl>
<div v-for="(value, key, index) in user" :key="key" class="flex gap-2">
<dt class="font-bold">{{ index + 1 }}. {{ key }}:</dt>
<dd>{{ value }}</dd>
</div>
</dl>
</template>v-for mit Bereichen
Du kannst v-for auch mit einem Integer-Bereich nutzen. v-for="n in 10" iteriert von 1 bis 10 (inklusive) – praktisch für Pagination, Sternbewertungen oder Platzhalter-Skelette.
<template>
<!-- Sternbewertung: n läuft von 1 bis 5 -->
<div class="flex gap-1">
<span v-for="n in 5" :key="n" class="text-xl">
{{ n <= 3 ? '★' : '☆' }}
</span>
</div>
<!-- Pagination -->
<nav>
<button v-for="page in totalPages" :key="page">
{{ page }}
</button>
</nav>
</template>v-for und v-if
Eine häufige Falle: v-for und v-if auf demselben Element kombinieren. In Vue 3 hat v-if eine höhere Priorität als v-for. Das bedeutet, v-if wird zuerst ausgewertet und hat keinen Zugriff auf die Loop-Variable – dein Template bricht mit einem Fehler ab.
Niemals v-for und v-if kombinieren!
In Vue 3 wird v-if vor v-for ausgewertet. Die Loop-Variable existiert im v-if-Scope noch nicht. Nutze stattdessen ein <template v-for>-Wrapper oder filtere die Daten vorher mit einer Computed Property.
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, text: 'Vue lernen', done: false },
{ id: 2, text: 'App bauen', done: false },
{ id: 3, text: 'Deployen', done: true },
])
// ✅ Beste Lösung: vorher filtern
const activeTodos = computed(() =>
todos.value.filter((t) => !t.done)
)
</script>
<template>
<!-- ✅ Variante 1: Computed Property (bevorzugt) -->
<ul>
<li v-for="todo in activeTodos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
<!-- ✅ Variante 2: template-Wrapper -->
<ul>
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">
{{ todo.text }}
</li>
</template>
</ul>
<!-- ❌ FALSCH: v-for und v-if auf demselben Element -->
<!--
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
{{ todo.text }}
</li>
-->
</template>Array-Mutationsmethoden
Vue erkennt automatisch, wenn du ein reaktives Array mit einer der folgenden Methoden veränderst:
push()– Element(e) am Ende hinzufügenpop()– Letztes Element entfernenshift()– Erstes Element entfernenunshift()– Element(e) am Anfang hinzufügensplice()– Elemente einfügen, entfernen oder ersetzensort()– Array sortierenreverse()– Reihenfolge umkehren
Du kannst ein Array auch komplett ersetzen, indem du der Ref einen neuen Wert zuweist. Vue erkennt den Unterschied und aktualisiert nur die betroffenen DOM-Elemente.
<script setup>
import { ref } from 'vue'
const namen = ref(['Alice', 'Bob', 'Charlie'])
function beispiele() {
// Mutation – Vue erkennt die Änderung automatisch
namen.value.push('Diana') // Am Ende hinzufügen
namen.value.unshift('Zara') // Am Anfang hinzufügen
namen.value.splice(1, 1) // Element bei Index 1 entfernen
namen.value.sort() // Alphabetisch sortieren
namen.value.reverse() // Reihenfolge umkehren
// Array komplett ersetzen (ebenfalls reaktiv)
namen.value = namen.value.filter((n) => n !== 'Bob')
}
</script>
<template>
<ul>
<li v-for="name in namen" :key="name">{{ name }}</li>
</ul>
<button @click="beispiele">Mutationen ausführen</button>
</template>Filtern und Sortieren mit Computed
Oft willst du eine gefilterte oder sortierte Ansicht einer Liste anzeigen, ohne das Original-Array zu verändern. Dafür sind Computed Properties perfekt: Sie werden automatisch neu berechnet, wenn sich die zugrunde liegenden Daten ändern – genau wie ein scope in ActiveRecord, nur im Frontend.
<script setup>
import { ref, computed } from 'vue'
const produkte = ref([
{ id: 1, name: 'Laptop', preis: 999, kategorie: 'Elektronik' },
{ id: 2, name: 'T-Shirt', preis: 25, kategorie: 'Kleidung' },
{ id: 3, name: 'Kopfhörer', preis: 150, kategorie: 'Elektronik' },
{ id: 4, name: 'Jeans', preis: 80, kategorie: 'Kleidung' },
{ id: 5, name: 'Tastatur', preis: 120, kategorie: 'Elektronik' },
])
const suchbegriff = ref('')
const sortierung = ref<'name' | 'preis'>('name')
// Gefiltert und sortiert – ohne das Original zu verändern
const gefilterteProdukte = computed(() => {
const suche = suchbegriff.value.toLowerCase()
return produkte.value
.filter((p) => p.name.toLowerCase().includes(suche))
.sort((a, b) => {
if (sortierung.value === 'preis') return a.preis - b.preis
return a.name.localeCompare(b.name)
})
})
</script>
<template>
<input v-model="suchbegriff" placeholder="Suchen …" />
<select v-model="sortierung">
<option value="name">Nach Name</option>
<option value="preis">Nach Preis</option>
</select>
<ul>
<li v-for="produkt in gefilterteProdukte" :key="produkt.id">
{{ produkt.name }} – {{ produkt.preis }} €
</li>
</ul>
</template>Rails-Vergleich
In Rails iterierst du mit .each in ERB-Templates. In Vue übernimmt v-for dieselbe Aufgabe, läuft aber vollständig im Browser. Der große Vorteil: Änderungen an der Liste aktualisieren sofort das DOM – kein Seiten-Reload nötig.
ERB vs. Vue: Listen-Rendering
In Rails serverst du die gesamte Liste beim Page-Load. Neue Einträge erfordern einen Request (oder Turbo Streams). In Vue lebt die Liste reaktiv im Browser – Hinzufügen und Entfernen passiert instant ohne Netzwerk-Roundtrip.
<%# Rails: _tasks.html.erb %>
<ul>
<% @tasks.each_with_index do |task, index| %>
<li>
<%= index + 1 %>. <%= task.title %>
<% if task.done? %>
<span class="badge">✓</span>
<% end %>
</li>
<% end %>
</ul>
<%# Neuen Eintrag hinzufügen erfordert einen Request: %>
<%= form_with model: Task.new do |f| %>
<%= f.text_field :title %>
<%= f.submit "Hinzufügen" %>
<% end %>Das gleiche Ergebnis in Vue:
<script setup>
import { ref } from 'vue'
const tasks = ref([
{ id: 1, title: 'Einkaufen', done: false },
{ id: 2, title: 'Kochen', done: true },
{ id: 3, title: 'Abwaschen', done: false },
])
const newTitle = ref('')
function addTask() {
const title = newTitle.value.trim()
if (!title) return
tasks.value.push({
id: Date.now(),
title,
done: false,
})
newTitle.value = ''
}
</script>
<template>
<ul>
<li v-for="(task, index) in tasks" :key="task.id">
{{ index + 1 }}. {{ task.title }}
<span v-if="task.done" class="badge">✓</span>
</li>
</ul>
<!-- Kein Request nötig – alles passiert instant im Browser -->
<form @submit.prevent="addTask">
<input v-model="newTitle" placeholder="Neue Aufgabe …" />
<button type="submit">Hinzufügen</button>
</form>
</template>Interaktive Demo
Probiere es selbst aus! Gib einen Namen ein, füge Einträge hinzu und entferne sie wieder. Beobachte, wie die Computed Property itemCount automatisch aktualisiert wird.
3 Einträge in der Liste
- Tomaten
- Mozzarella
- Basilikum