Modul 2: Vue Grundlagen

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.

BasicList.vue
<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.

KeyExample.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 },
])
</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).

ObjectIteration.vue
<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.

RangeExample.vue
<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.

TemplateWrapper.vue
<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ügen
  • pop() – Letztes Element entfernen
  • shift() – Erstes Element entfernen
  • unshift() – Element(e) am Anfang hinzufügen
  • splice() – Elemente einfügen, entfernen oder ersetzen
  • sort() – Array sortieren
  • reverse() – 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.

ArrayMutations.vue
<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.

ComputedFilter.vue
<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.

_tasks.html.erb
<%# 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:

TaskList.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.

Dynamische Einkaufsliste
Interaktiv

3 Einträge in der Liste

  • Tomaten
  • Mozzarella
  • Basilikum