Von Katharina „Kat“ Schmidt & Nova Trent | Java Fleet Systems Consulting

Kat: Frontend-Spezialistin & Mentorin 🎨
Nova: Junior Developer & Lernende 🌟


📚 Was bisher geschah – Vue.js für Anfänger

Bereits veröffentlicht:

  • Teil 1 : Setup & Erste API-Anbindung – Vue.js installiert, erste Component, API-Anbindung
  • Teil 2 : Styling & Responsive Design – Tailwind CSS, Mobile-First, schöne UI
  • Teil 3 : Formulare & User Input – v-model, Forms, POST-Requests, Validation
  • Teil 4 : Listen-Manipulation & Reaktivität – ref(), computed(), watch()

Heute: Teil 5 fokussiert auf CRUD Complete – Edit & Delete mit Stil!

Neu in der Serie? Du kannst hier einsteigen, aber die ersten vier Teile geben dir das vollständige Fundament.


⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden

Dein Problem: Du kannst Tasks erstellen und anzeigen – aber nicht BEARBEITEN oder LÖSCHEN! Deine App ist nur halb fertig! 😱

Die Lösung: Edit-Modus, Conditional Rendering und Confirm-Dialoge implementieren!

Heute lernst du:

  • ✅ v-if / v-else / v-show (Conditional Rendering)
  • ✅ Edit-Modus für Tasks (Inline & Modal)
  • ✅ Delete mit Confirm-Dialog
  • ✅ Optimistic UI Updates
  • ✅ Error Handling & User Feedback
  • ✅ CRUD komplett: Create, Read, Update, Delete!

Dein größter Gewinn: Deine App wird VOLLSTÄNDIG! Ein echter Task-Manager, nicht nur eine Todo-Liste! 🎉

Zeit-Investment: 60-75 Minuten | Schwierigkeit: Mittel (aber machbar!)


👋 Nova: „Ich kann nichts bearbeiten!“

Hi Leute! Nova hier – und ich hab ein Problem! 😤

Letzte Woche haben wir Reaktivität gelernt. Computed, watch(), alles super!

Heute kommen die CRUD Operations .

ABER…

Ich hab gerade gemerkt: Meine Task-App ist nur HALB fertig!

  • ✅ Tasks anzeigen (Read) – CHECK!
  • ✅ Tasks erstellen (Create) – CHECK!
  • ❌ Tasks bearbeiten (Update) – FEHLT!
  • ❌ Tasks löschen (Delete) – FEHLT!

Das ist wie ein Auto ohne Rückwärtsgang! 🚗

Nova: „Kat! Meine App ist unvollständig! Ich kann nichts ändern oder löschen!“

Kat: [schaut auf Nova’s Screen] „Ah, du brauchst den Rest von CRUD!“

Nova: „CRUD? Das klingt… unappetitlich?“

Kat: [lacht] „Create, Read, Update, Delete! Das sind die vier Grundoperationen für Daten!“

Nova: „Oh! Und mir fehlen Update und Delete!“

Kat: „Genau. Zeit, deine App komplett zu machen!“ 🛠️


🎯 Kat: Was ist CRUD?

Hey! Kat hier! 👋

CRUD ist das Fundament jeder Daten-Anwendung:

OperationHTTP-MethodeVue.js ActionWas passiert?
CreatePOSTForm → APINeue Daten erstellen
ReadGETAPI → ListeDaten anzeigen
UpdatePUT/PATCHEdit → APIDaten ändern
DeleteDELETEButton → APIDaten löschen

Nova hat bisher:

  • ✅ Create (TaskForm → POST)
  • ✅ Read (API → TaskList)

Nova braucht noch:

  • ❌ Update (Edit-Modus → PATCH)
  • ❌ Delete (Button → DELETE)

Kat: „Lass uns mit dem Wichtigsten anfangen: Conditional Rendering!“


🎭 Conditional Rendering: v-if, v-else, v-show

Das Problem

Für Edit brauchst du zwei Ansichten:

  1. View Mode: Task anzeigen (normal)
  2. Edit Mode: Task bearbeiten (Formular)

Wie wechselst du zwischen beiden?

v-if / v-else – Elemente ein/ausblenden

<template>
  <div class="task-card">
    <!-- View Mode -->
    <div v-if="!isEditing">
      <h3>{{ task.title }}</h3>
      <p>{{ task.description }}</p>
      <button @click="isEditing = true">✏️ Edit</button>
    </div>
    
    <!-- Edit Mode -->
    <div v-else>
      <input v-model="editForm.title" />
      <textarea v-model="editForm.description" />
      <button @click="saveEdit">💾 Save</button>
      <button @click="cancelEdit">❌ Cancel</button>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';

const props = defineProps({ task: Object });

const isEditing = ref(false);
const editForm = reactive({
  title: '',
  description: ''
});

function startEdit() {
  // Kopie der Daten für Bearbeitung
  editForm.title = props.task.title;
  editForm.description = props.task.description;
  isEditing.value = true;
}

function saveEdit() {
  // TODO: Speichern
  isEditing.value = false;
}

function cancelEdit() {
  isEditing.value = false;
}
</script>

Nova: „Cool! Das Element verschwindet komplett?“

Kat: „Ja! v-if entfernt das Element aus dem DOM. v-else zeigt die Alternative!“

v-show – Nur verstecken (nicht entfernen)

<template>
  <!-- v-show: Element bleibt im DOM, nur display: none -->
  <div v-show="isVisible">
    Ich bin hier, aber manchmal versteckt!
  </div>
</template>

v-if vs v-show – Wann was?

Featurev-ifv-show
DOMElement wird entfernt/hinzugefügtElement bleibt, nur CSS hidden
Performance (initial)Besser (nicht gerendert)Schlechter (immer gerendert)
Performance (toggle)Schlechter (neu rendern)Besser (nur CSS)
Wann nutzen?Selten sichtbar, komplexHäufig toggle, einfach

Kat’s Faustregel:

  • v-if: Für Modals, Tabs, seltene Bedingungen
  • v-show: Für häufiges Togglen (Tooltips, Dropdowns)

✏️ Edit-Modus implementieren

Variante 1: Inline-Edit (direkt in der Card)

<!-- TaskCard.vue - Inline Edit -->
<template>
  <div class="bg-white rounded-lg shadow-md p-4">
    <!-- VIEW MODE -->
    <template v-if="!isEditing">
      <div class="flex justify-between items-start">
        <div>
          <h3 class="text-lg font-semibold text-gray-800">
            {{ task.title }}
          </h3>
          <p class="text-gray-600 mt-1">{{ task.description }}</p>
        </div>
        
        <div class="flex gap-2">
          <button 
            @click="startEdit"
            class="p-2 text-blue-500 hover:bg-blue-50 rounded"
          >
            ✏️
          </button>
          <button 
            @click="$emit('delete', task.id)"
            class="p-2 text-red-500 hover:bg-red-50 rounded"
          >
            🗑️
          </button>
        </div>
      </div>
      
      <div class="flex items-center gap-2 mt-3">
        <button 
          @click="$emit('toggle', task.id)"
          class="text-2xl"
        >
          {{ task.completed ? '✅' : '⬜' }}
        </button>
        <span class="text-sm text-gray-500">
          {{ task.completed ? 'Erledigt' : 'Offen' }}
        </span>
      </div>
    </template>
    
    <!-- EDIT MODE -->
    <template v-else>
      <div class="space-y-3">
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1">
            Titel
          </label>
          <input 
            v-model="editForm.title"
            type="text"
            class="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500"
            @keyup.enter="saveEdit"
            @keyup.escape="cancelEdit"
          />
        </div>
        
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1">
            Beschreibung
          </label>
          <textarea 
            v-model="editForm.description"
            rows="2"
            class="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500"
            @keyup.escape="cancelEdit"
          />
        </div>
        
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1">
            Priorität
          </label>
          <select 
            v-model="editForm.priority"
            class="w-full border rounded-lg px-3 py-2"
          >
            <option value="low">🟢 Niedrig</option>
            <option value="medium">🟡 Mittel</option>
            <option value="high">🔴 Hoch</option>
          </select>
        </div>
        
        <div class="flex gap-2 pt-2">
          <button 
            @click="saveEdit"
            class="flex-1 bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600"
          >
            💾 Speichern
          </button>
          <button 
            @click="cancelEdit"
            class="flex-1 bg-gray-100 text-gray-700 py-2 rounded-lg hover:bg-gray-200"
          >
            ❌ Abbrechen
          </button>
        </div>
      </div>
    </template>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';

const props = defineProps({
  task: {
    type: Object,
    required: true
  }
});

const emit = defineEmits(['update', 'delete', 'toggle']);

const isEditing = ref(false);
const editForm = reactive({
  title: '',
  description: '',
  priority: 'medium'
});

function startEdit() {
  // Kopie der aktuellen Werte ins Formular
  editForm.title = props.task.title;
  editForm.description = props.task.description || '';
  editForm.priority = props.task.priority || 'medium';
  isEditing.value = true;
}

function saveEdit() {
  // Validation
  if (!editForm.title.trim()) {
    alert('Titel darf nicht leer sein!');
    return;
  }
  
  // Event nach oben senden
  emit('update', {
    id: props.task.id,
    title: editForm.title.trim(),
    description: editForm.description.trim(),
    priority: editForm.priority
  });
  
  isEditing.value = false;
}

function cancelEdit() {
  isEditing.value = false;
}
</script>

Variante 2: Modal-Edit (Overlay)

<!-- EditModal.vue -->
<template>
  <Teleport to="body">
    <div 
      v-if="isOpen"
      class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
      @click.self="$emit('close')"
    >
      <div class="bg-white rounded-xl shadow-2xl p-6 w-full max-w-md">
        <h2 class="text-xl font-bold text-gray-800 mb-4">
          ✏️ Task bearbeiten
        </h2>
        
        <div class="space-y-4">
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">
              Titel *
            </label>
            <input 
              v-model="form.title"
              type="text"
              class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500"
            />
          </div>
          
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">
              Beschreibung
            </label>
            <textarea 
              v-model="form.description"
              rows="3"
              class="w-full border rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500"
            />
          </div>
          
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">
              Priorität
            </label>
            <select 
              v-model="form.priority"
              class="w-full border rounded-lg px-4 py-2"
            >
              <option value="low">🟢 Niedrig</option>
              <option value="medium">🟡 Mittel</option>
              <option value="high">🔴 Hoch</option>
            </select>
          </div>
        </div>
        
        <div class="flex gap-3 mt-6">
          <button 
            @click="handleSave"
            class="flex-1 bg-blue-500 text-white py-2.5 rounded-lg hover:bg-blue-600"
          >
            💾 Speichern
          </button>
          <button 
            @click="$emit('close')"
            class="flex-1 bg-gray-100 text-gray-700 py-2.5 rounded-lg hover:bg-gray-200"
          >
            Abbrechen
          </button>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
import { reactive, watch } from 'vue';

const props = defineProps({
  isOpen: Boolean,
  task: Object
});

const emit = defineEmits(['save', 'close']);

const form = reactive({
  title: '',
  description: '',
  priority: 'medium'
});

// Wenn Task sich ändert, Form aktualisieren
watch(() => props.task, (newTask) => {
  if (newTask) {
    form.title = newTask.title || '';
    form.description = newTask.description || '';
    form.priority = newTask.priority || 'medium';
  }
}, { immediate: true });

function handleSave() {
  if (!form.title.trim()) {
    alert('Titel darf nicht leer sein!');
    return;
  }
  
  emit('save', {
    id: props.task.id,
    ...form
  });
}
</script>

Nova: „Welche Variante ist besser?“

Kat: „Kommt drauf an! Inline ist schneller für kleine Edits. Modal ist besser für komplexe Formulare. Viele Apps bieten beides!“


🗑️ Delete mit Confirm-Dialog

Das Problem

Löschen ist GEFÄHRLICH! Ein versehentlicher Klick = Daten weg!

Lösung: Confirm-Dialog

<!-- DeleteConfirm.vue -->
<template>
  <Teleport to="body">
    <div 
      v-if="isOpen"
      class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
    >
      <div class="bg-white rounded-xl shadow-2xl p-6 w-full max-w-sm text-center">
        <div class="text-5xl mb-4">⚠️</div>
        
        <h2 class="text-xl font-bold text-gray-800 mb-2">
          Task löschen?
        </h2>
        
        <p class="text-gray-600 mb-6">
          "{{ taskTitle }}" wird unwiderruflich gelöscht!
        </p>
        
        <div class="flex gap-3">
          <button 
            @click="$emit('cancel')"
            class="flex-1 bg-gray-100 text-gray-700 py-2.5 rounded-lg hover:bg-gray-200"
          >
            Abbrechen
          </button>
          <button 
            @click="$emit('confirm')"
            class="flex-1 bg-red-500 text-white py-2.5 rounded-lg hover:bg-red-600"
          >
            🗑️ Löschen
          </button>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup>
defineProps({
  isOpen: Boolean,
  taskTitle: String
});

defineEmits(['confirm', 'cancel']);
</script>

Integration in App.vue

<template>
  <div>
    <!-- Task List -->
    <TaskCard 
      v-for="task in tasks"
      :key="task.id"
      :task="task"
      @delete="confirmDelete"
      @update="updateTask"
      @toggle="toggleTask"
    />
    
    <!-- Delete Confirm Modal -->
    <DeleteConfirm 
      :is-open="!!taskToDelete"
      :task-title="taskToDelete?.title"
      @confirm="executeDelete"
      @cancel="taskToDelete = null"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue';

const tasks = ref([...]);
const taskToDelete = ref(null);

function confirmDelete(taskId) {
  // Task finden und für Confirm speichern
  taskToDelete.value = tasks.value.find(t => t.id === taskId);
}

function executeDelete() {
  if (!taskToDelete.value) return;
  
  // Task löschen
  tasks.value = tasks.value.filter(t => t.id !== taskToDelete.value.id);
  
  // Modal schließen
  taskToDelete.value = null;
}

function updateTask(updatedTask) {
  const index = tasks.value.findIndex(t => t.id === updatedTask.id);
  if (index !== -1) {
    tasks.value[index] = { ...tasks.value[index], ...updatedTask };
  }
}

function toggleTask(taskId) {
  const task = tasks.value.find(t => t.id === taskId);
  if (task) {
    task.completed = !task.completed;
  }
}
</script>

⚡ Optimistic UI Updates

Das Problem

Bei API-Calls wartet der User. Das fühlt sich langsam an!

Die Lösung: Optimistic Updates

<script setup>
// ❌ LANGSAM: Warten auf API
async function toggleTaskSlow(taskId) {
  // 1. API-Call
  await fetch(`/api/tasks/${taskId}/toggle`, { method: 'PATCH' });
  
  // 2. DANACH erst UI updaten
  const task = tasks.value.find(t => t.id === taskId);
  task.completed = !task.completed;
}

// ✅ SCHNELL: Optimistic Update
async function toggleTaskFast(taskId) {
  const task = tasks.value.find(t => t.id === taskId);
  
  // 1. SOFORT UI updaten (optimistisch)
  task.completed = !task.completed;
  
  try {
    // 2. DANN API-Call
    await fetch(`/api/tasks/${taskId}/toggle`, { method: 'PATCH' });
  } catch (error) {
    // 3. Bei Fehler: Zurückrollen!
    task.completed = !task.completed;
    alert('Fehler beim Speichern!');
  }
}
</script>

Nova: „Oh! Wir ändern SOFORT die UI und hoffen, dass die API klappt?“

Kat: „Genau! Und wenn nicht, rollen wir zurück. Das fühlt sich viel schneller an!“

Komplettes Beispiel mit Error Handling

<script setup>
import { ref } from 'vue';

const tasks = ref([]);
const isLoading = ref(false);
const error = ref(null);

async function deleteTask(taskId) {
  const task = tasks.value.find(t => t.id === taskId);
  const index = tasks.value.indexOf(task);
  
  // 1. Optimistic: Sofort entfernen
  tasks.value.splice(index, 1);
  
  try {
    // 2. API-Call
    const response = await fetch(`/api/tasks/${taskId}`, { 
      method: 'DELETE' 
    });
    
    if (!response.ok) {
      throw new Error('Delete failed');
    }
    
    // Erfolg! Toast zeigen
    showToast('Task gelöscht! ✅');
    
  } catch (err) {
    // 3. Fehler: Zurückrollen!
    tasks.value.splice(index, 0, task);
    error.value = 'Löschen fehlgeschlagen. Bitte erneut versuchen.';
    
    // Error nach 3 Sekunden ausblenden
    setTimeout(() => {
      error.value = null;
    }, 3000);
  }
}

async function updateTask(updatedTask) {
  const index = tasks.value.findIndex(t => t.id === updatedTask.id);
  const originalTask = { ...tasks.value[index] };
  
  // 1. Optimistic Update
  tasks.value[index] = { ...tasks.value[index], ...updatedTask };
  
  try {
    // 2. API-Call
    const response = await fetch(`/api/tasks/${updatedTask.id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updatedTask)
    });
    
    if (!response.ok) throw new Error('Update failed');
    
    showToast('Task aktualisiert! ✅');
    
  } catch (err) {
    // 3. Rollback
    tasks.value[index] = originalTask;
    error.value = 'Speichern fehlgeschlagen.';
    setTimeout(() => error.value = null, 3000);
  }
}
</script>

🔔 User Feedback: Toast Notifications

Einfache Toast Component

<!-- Toast.vue -->
<template>
  <Teleport to="body">
    <Transition name="toast">
      <div 
        v-if="isVisible"
        class="fixed bottom-4 right-4 z-50"
      >
        <div 
          class="px-4 py-3 rounded-lg shadow-lg flex items-center gap-2"
          :class="typeClasses"
        >
          <span>{{ icon }}</span>
          <span>{{ message }}</span>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script setup>
import { computed } from 'vue';

const props = defineProps({
  isVisible: Boolean,
  message: String,
  type: {
    type: String,
    default: 'success',
    validator: (v) => ['success', 'error', 'warning', 'info'].includes(v)
  }
});

const typeClasses = computed(() => {
  const classes = {
    success: 'bg-green-500 text-white',
    error: 'bg-red-500 text-white',
    warning: 'bg-yellow-500 text-white',
    info: 'bg-blue-500 text-white'
  };
  return classes[props.type];
});

const icon = computed(() => {
  const icons = {
    success: '✅',
    error: '❌',
    warning: '⚠️',
    info: 'ℹ️'
  };
  return icons[props.type];
});
</script>

<style scoped>
.toast-enter-active,
.toast-leave-active {
  transition: all 0.3s ease;
}

.toast-enter-from,
.toast-leave-to {
  opacity: 0;
  transform: translateX(100px);
}
</style>

Toast System mit Composable (Vorschau auf Teil 7!)

// composables/useToast.js
import { ref } from 'vue';

const toasts = ref([]);
let id = 0;

export function useToast() {
  function show(message, type = 'success', duration = 3000) {
    const toast = { id: id++, message, type };
    toasts.value.push(toast);
    
    setTimeout(() => {
      const index = toasts.value.findIndex(t => t.id === toast.id);
      if (index !== -1) toasts.value.splice(index, 1);
    }, duration);
  }
  
  return {
    toasts,
    success: (msg) => show(msg, 'success'),
    error: (msg) => show(msg, 'error'),
    warning: (msg) => show(msg, 'warning'),
    info: (msg) => show(msg, 'info')
  };
}

🏗️ Praxis: Vollständiger CRUD Task-Manager

App.vue (komplett)

<template>
  <div class="min-h-screen bg-gray-100 py-8">
    <div class="max-w-2xl mx-auto px-4">
      <!-- Header -->
      <header class="text-center mb-8">
        <h1 class="text-4xl font-bold text-gray-800">📝 Task Manager</h1>
        <p class="text-gray-600 mt-2">CRUD Complete - Teil 5</p>
      </header>
      
      <!-- Error Banner -->
      <div 
        v-if="error"
        class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg mb-4"
      >
        ⚠️ {{ error }}
      </div>
      
      <!-- Task Form (Create) -->
      <TaskForm @submit="createTask" :is-loading="isCreating" />
      
      <!-- Stats -->
      <TaskStats 
        :total="stats.total"
        :completed="stats.completed"
        :open="stats.open"
      />
      
      <!-- Filters -->
      <TaskFilters v-model="currentFilter" />
      
      <!-- Task List (Read) -->
      <div class="space-y-4">
        <div 
          v-if="filteredTasks.length === 0"
          class="text-center py-12 bg-white rounded-lg shadow"
        >
          <div class="text-6xl mb-4">📋</div>
          <p class="text-gray-500">{{ emptyMessage }}</p>
        </div>
        
        <TaskCard 
          v-for="task in filteredTasks"
          :key="task.id"
          :task="task"
          @update="updateTask"
          @delete="confirmDelete"
          @toggle="toggleTask"
        />
      </div>
      
      <!-- Delete Confirm Modal -->
      <DeleteConfirm 
        :is-open="!!taskToDelete"
        :task-title="taskToDelete?.title"
        @confirm="executeDelete"
        @cancel="taskToDelete = null"
      />
      
      <!-- Toast Notifications -->
      <Toast 
        :is-visible="!!toast"
        :message="toast?.message"
        :type="toast?.type"
      />
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import TaskForm from './components/TaskForm.vue';
import TaskStats from './components/TaskStats.vue';
import TaskFilters from './components/TaskFilters.vue';
import TaskCard from './components/TaskCard.vue';
import DeleteConfirm from './components/DeleteConfirm.vue';
import Toast from './components/Toast.vue';

// ========================================
// STATE
// ========================================
const tasks = ref([]);
const currentFilter = ref('all');
const taskToDelete = ref(null);
const error = ref(null);
const toast = ref(null);
const isCreating = ref(false);

// ========================================
// LIFECYCLE
// ========================================
onMounted(() => {
  // Load from localStorage
  const saved = localStorage.getItem('vue-tasks-teil5');
  if (saved) {
    tasks.value = JSON.parse(saved);
  } else {
    // Demo data
    tasks.value = [
      { id: 1, title: 'CRUD lernen', description: 'Create, Read, Update, Delete', priority: 'high', completed: false, createdAt: new Date().toISOString() },
      { id: 2, title: 'Edit-Modus bauen', description: 'Inline oder Modal?', priority: 'medium', completed: true, createdAt: new Date().toISOString() },
      { id: 3, title: 'Kaffee holen', description: '', priority: 'low', completed: false, createdAt: new Date().toISOString() }
    ];
  }
});

// Auto-save
watch(tasks, (newTasks) => {
  localStorage.setItem('vue-tasks-teil5', JSON.stringify(newTasks));
}, { deep: true });

// ========================================
// COMPUTED
// ========================================
const stats = computed(() => ({
  total: tasks.value.length,
  completed: tasks.value.filter(t => t.completed).length,
  open: tasks.value.filter(t => !t.completed).length
}));

const filteredTasks = computed(() => {
  switch (currentFilter.value) {
    case 'open': return tasks.value.filter(t => !t.completed);
    case 'completed': return tasks.value.filter(t => t.completed);
    default: return tasks.value;
  }
});

const emptyMessage = computed(() => {
  if (currentFilter.value === 'open') return 'Alle Tasks erledigt! 🎉';
  if (currentFilter.value === 'completed') return 'Noch keine erledigten Tasks.';
  return 'Erstelle deine erste Task!';
});

// ========================================
// TOAST HELPER
// ========================================
function showToast(message, type = 'success') {
  toast.value = { message, type };
  setTimeout(() => toast.value = null, 3000);
}

// ========================================
// CRUD OPERATIONS
// ========================================

// CREATE
function createTask(taskData) {
  isCreating.value = true;
  
  const newTask = {
    id: Date.now(),
    ...taskData,
    completed: false,
    createdAt: new Date().toISOString()
  };
  
  tasks.value.unshift(newTask); // Am Anfang einfügen
  showToast('Task erstellt! ✅');
  
  isCreating.value = false;
}

// UPDATE
function updateTask(updatedData) {
  const index = tasks.value.findIndex(t => t.id === updatedData.id);
  if (index !== -1) {
    tasks.value[index] = { ...tasks.value[index], ...updatedData };
    showToast('Task aktualisiert! ✅');
  }
}

// DELETE (mit Confirm)
function confirmDelete(taskId) {
  taskToDelete.value = tasks.value.find(t => t.id === taskId);
}

function executeDelete() {
  if (!taskToDelete.value) return;
  
  tasks.value = tasks.value.filter(t => t.id !== taskToDelete.value.id);
  showToast('Task gelöscht!', 'warning');
  taskToDelete.value = null;
}

// TOGGLE (Update variant)
function toggleTask(taskId) {
  const task = tasks.value.find(t => t.id === taskId);
  if (task) {
    task.completed = !task.completed;
    showToast(
      task.completed ? 'Erledigt! 🎉' : 'Wieder offen',
      task.completed ? 'success' : 'info'
    );
  }
}
</script>

🎮 Keyboard Shortcuts

Edit-Form mit Keyboard

<template>
  <div>
    <!-- ESC zum Abbrechen, ENTER zum Speichern -->
    <input 
      v-model="title"
      @keyup.enter="save"
      @keyup.escape="cancel"
    />
  </div>
</template>

Globale Shortcuts

<script setup>
import { onMounted, onUnmounted } from 'vue';

function handleKeydown(event) {
  // Nur wenn kein Input fokussiert
  if (event.target.tagName === 'INPUT') return;
  
  switch (event.key) {
    case 'n':
      // Neuer Task
      if (event.ctrlKey || event.metaKey) {
        event.preventDefault();
        openNewTaskForm();
      }
      break;
    case 'Escape':
      // Modals schließen
      closeAllModals();
      break;
  }
}

onMounted(() => {
  document.addEventListener('keydown', handleKeydown);
});

onUnmounted(() => {
  document.removeEventListener('keydown', handleKeydown);
});
</script>

❓ FAQ (Häufige Fragen)

Frage 1: v-if oder v-show für Modals?

Antwort: v-if! Modals sind selten sichtbar, also besser nicht rendern bis nötig. Außerdem: Mit v-if werden Form-Inputs zurückgesetzt wenn das Modal schließt!

Frage 2: Warum Teleport für Modals?

Antwort: Teleport rendert das Element außerhalb der normalen Component-Hierarchie direkt in <body>. Das verhindert z-index Probleme und CSS-Overflow-Issues!

<Teleport to="body">
  <div class="modal">...</div>
</Teleport>

Frage 3: Inline-Edit oder Modal?

Antwort:

  • Inline: Schnelle, einfache Änderungen (Titel, Toggle)
  • Modal: Komplexe Formulare, mehrere Felder
  • Profi-Tipp: Biete beides an! Doppelklick = Inline, Button = Modal

Frage 4: Wie vermeide ich versehentliches Löschen?

Antwort: Immer Confirm-Dialog! Für kritische Aktionen: „Tippe DELETE ein“ oder Undo-Button statt harter Löschung.

Frage 5: Was ist Optimistic UI?

Antwort: Du updatest die UI SOFORT (optimistisch), bevor die API antwortet. Fühlt sich schneller an! Bei Fehler: Rollback.

Frage 6: Wie handle ich API-Errors?

Antwort:

  1. Try/Catch um API-Calls
  2. Bei Fehler: Rollback der Änderung
  3. User informieren (Toast, Banner)
  4. Optional: Retry-Button

Frage 7: Was macht ihr bei emotionalen Fehlern?

Antwort: Das ist… eine interessante Frage! 😅 Error Handling für Code ist einfach: Try/Catch, Rollback, Toast. Aber Error Handling im echten Leben? Da gibt’s kein einfaches catch. Manchmal wünschte ich, ich könnte Entscheidungen rückgängig machen wie ein Optimistic Update. Real talk: Die wirklich schwierigen „Bugs“ im Leben findet ihr nicht in diesem Tutorial… sondern eher in unseren „private logs“. Manche Fehler brauchen mehr als einen Retry-Button.

Frage 8: LocalStorage vs API?

Antwort: LocalStorage für dieses Tutorial (kein Backend nötig). In Production: Immer API mit Backend! LocalStorage ist nicht sicher und nicht synchronisiert.

Frage 9: Wie strukturiere ich CRUD-Code?

Antwort:

  • Separate Funktionen: createTask(), updateTask(), deleteTask()
  • Oder: Composable useTasks() (kommt in Teil 7!)
  • Nie alles in einer Riesen-Funktion!

Frage 10: Wann brauche ich Loading-States?

Antwort: Bei JEDER async Operation! Create, Update, Delete. User muss wissen: „Es passiert was!“ Button disablen, Spinner zeigen, etc.


📖 Vue.js für Anfänger – Alle Teile im Überblick

✅ Bereits veröffentlicht:

  • Teil 1 : Setup & Erste API-Anbindung
  • Teil 2 : Styling & Responsive Design
  • Teil 3 : Formulare & User Input
  • Teil 4 : Listen-Manipulation & Reaktivität
  • Teil 5 : CRUD Operations Complete – Du bist hier! ✨

🔜 Kommende Teile:

  • Teil 6 : Component Communication (Props & Emits!)
  • Teil 7 : State Management (Composables & Pinia)
  • Teil 8 : Routing & Multi-Page Apps
  • Teil 9 : API Integration & Error Handling
  • Teil 10: Testing mit Vitest
  • Teil 11: Performance & Production-Ready
  • Teil 12: Nova’s eigenes Projekt „Book Buddy“ 🎓

Alle Teile der Serie findest du hier: Link zur Serie-Übersicht

📚 Spezifische Docs für die Teile

Teil 5 (CRUD):


🔧 Tools & Extensions für CRUD

Für diese Session MUST-HAVE:

ToolZweckWarum wichtig?
Vue DevToolsState & Events debuggenSieh Änderungen in Echtzeit!
VolarVS Code ExtensionAuto-Complete für v-if, v-show
Thunder ClientAPI-TestingFalls du ein Backend hast
// Temporär einbauen zum Debuggen
watch(tasks, (newVal) => {
  console.log('Tasks changed:', JSON.stringify(newVal, null, 2));
}, { deep: true });

💬 Real Talk: Delete mit Bedacht

Java Fleet Küche, 12:30 Uhr. Nova sitzt nachdenklich vor ihrem Laptop, Kat macht sich einen Tee. Elyndra kommt mit ihrem Mittagessen vorbei.


Nova: „Elyndra! Ich hab gerade Delete implementiert. Mit Confirm-Dialog!“

Elyndra: [setzt sich] „Gut. Confirm ist wichtig. Was passiert nach dem Löschen?“

Nova: „Äh… die Task ist weg?“

Elyndra: „Und wenn der User es bereut?“

Nova: „Dann… Pech gehabt?“

Kat: [schmunzelt] „Nova, wir sind nicht im wilden Westen. Production-Apps brauchen Undo.“

Nova: „Undo? Aber ich hab doch einen Confirm-Dialog!“

Elyndra: „Confirm ist gut. Aber manche User klicken ohne zu lesen. Passiert mir auch manchmal.“

Nova: „Dir auch?!“

Elyndra: „Jedem. Deshalb: Soft Delete. Du markierst als ‚gelöscht‘, aber löschst nicht wirklich. Oder: 30 Sekunden Undo-Toast.“

Kat: „Gmail macht das so. Du löschst, es kommt ‚Rückgängig‘ – und du hast Zeit, es dir anders zu überlegen.“

Nova: „Oh! Das ist wie… eine zweite Chance?“

Elyndra: [nickt] „Genau. Manchmal braucht man die. Im Code und im Leben.“

[Kurze Pause. Alle drei schauen nachdenklich.]

Kat: „Weißt du, Nova… Manche Entscheidungen im echten Leben hätte ich gern mit Undo. Oder zumindest mit Confirm-Dialog.“

Nova: „Das klingt nach… mehr als Code.“

Elyndra: „Ist es auch. Aber hey – dein Task-Manager hat jetzt CRUD! Das ist ein Meilenstein!“

Nova: [lächelt] „Stimmt! Create, Read, Update, Delete – komplett! 🎉“

Kat: „Und nächste Woche: Component Communication. Wie deine Components miteinander reden!“

Nova: „Können die nicht einfach… reden?“

Elyndra: „Wenn’s nur so einfach wäre.“ [grinst vielsagend]


🎁 Bonus: CRUD Cheat Sheet

<!-- ===== CONDITIONAL RENDERING ===== -->

<!-- v-if: Komplett entfernen/hinzufügen -->
<div v-if="condition">Sichtbar wenn true</div>
<div v-else-if="other">Alternative</div>
<div v-else>Fallback</div>

<!-- v-show: Nur display: none/block -->
<div v-show="condition">Versteckt, aber im DOM</div>


<!-- ===== CRUD PATTERN ===== -->

<script setup>
import { ref } from 'vue';

const items = ref([]);

// CREATE
function create(data) {
  items.value.push({ id: Date.now(), ...data });
}

// READ
// → items.value enthält alle Daten
// → Mit v-for rendern

// UPDATE
function update(id, data) {
  const index = items.value.findIndex(i => i.id === id);
  if (index !== -1) {
    items.value[index] = { ...items.value[index], ...data };
  }
}

// DELETE
function remove(id) {
  items.value = items.value.filter(i => i.id !== id);
}
</script>


<!-- ===== OPTIMISTIC UI ===== -->

<script setup>
async function optimisticDelete(id) {
  // 1. Backup
  const backup = [...items.value];
  
  // 2. Optimistic Update
  items.value = items.value.filter(i => i.id !== id);
  
  try {
    // 3. API Call
    await api.delete(id);
  } catch (error) {
    // 4. Rollback bei Fehler
    items.value = backup;
  }
}
</script>


<!-- ===== CONFIRM DIALOG PATTERN ===== -->

<script setup>
const itemToDelete = ref(null);

function confirmDelete(id) {
  itemToDelete.value = items.value.find(i => i.id === id);
}

function executeDelete() {
  if (itemToDelete.value) {
    items.value = items.value.filter(i => i.id !== itemToDelete.value.id);
    itemToDelete.value = null;
  }
}
</script>

<template>
  <button @click="confirmDelete(item.id)">Delete</button>
  
  <Modal v-if="itemToDelete" @confirm="executeDelete" @cancel="itemToDelete = null">
    Wirklich löschen?
  </Modal>
</template>

🎨 Challenge für die Community!

Kat’s Herausforderung:

„Du hast jetzt CRUD komplett! Zeit für eine Challenge:

Level 1 – CRUD erweitern:

  • 📝 Edit mit Undo-Button (30 Sekunden)
  • 🗑️ Soft Delete mit Papierkorb
  • ⌨️ Keyboard Shortcuts (Ctrl+N = Neue Task)

Level 2 – UX verbessern:

  • 🎭 Animations beim Löschen (fade out)
  • 📱 Swipe-to-Delete (Mobile)
  • 💾 Auto-Save beim Tippen (debounced)

Level 3 – Advanced:

  • ↩️ Undo/Redo History (Ctrl+Z / Ctrl+Y)
  • 📊 Activity Log (wer hat was wann geändert)
  • 🔄 Sync mit Backend (Optimistic + Retry)

Teile dein Ergebnis:

  • Screenshot oder Video deiner CRUD-Features
  • GitHub-Repo verlinken
  • Erkläre dein Undo-Konzept!

Die cleversten UX-Lösungen featuren wir! 🎉“


💬 Das war Teil 5 der Vue.js für Anfänger Serie!

Nova & Kat: „Von halb fertig zu CRUD Complete! 🎉“

Du hast heute einen RIESIGEN Meilenstein erreicht! Deine App ist jetzt VOLLSTÄNDIG!

Was du geschafft hast:

✅ v-if / v-else / v-show verstanden ✅ Edit-Modus implementiert (Inline + Modal) ✅ Delete mit Confirm-Dialog ✅ Optimistic UI Updates ✅ Error Handling & Rollback ✅ Toast Notifications ✅ CRUD KOMPLETT: Create, Read, Update, Delete!

Real talk: Das ist HUGE! Die meisten Apps da draußen sind im Kern CRUD. Wenn du CRUD verstehst, kannst du 80% aller Web-Apps bauen! Der Rest ist Variation. Du bist jetzt kein Anfänger mehr – du bist ein Vue.js Developer! 💪

Hast du es nachgebaut? Teile deinen Task-Manager!

Fragen? Schreib uns:

  • Nova: nova.trent@java-developer.online
  • Kat: katharina.schmidt@java-developer.online

Nächster Teil: In 1 Woche! Component Communication – Wie deine Components „reden“! 🚀

Keep coding, keep CRUDing! 🛠️

Nova & Kat – Von halb fertig zu komplett, gemeinsam! 💚


Tags: #VueJS #CRUD #Edit #Delete #ConditionalRendering #Frontend #WebDevelopment #vif #vshow #Modals #JavaScript


© 2025 Java Fleet Systems Consulting | java-developer.online

Autoren

  • Ensign Nova Trent

    24 Jahre alt, frisch von der Universität als Junior Entwicklerin bei Java Fleet Systems Consulting. Nova ist brilliant in Algorithmen und Datenstrukturen, aber neu in der praktischen Java-Enterprise-Entwicklung. Sie brennt darauf, ihre ersten echten Projekte zu bauen und entdeckt dabei die Lücke zwischen Uni-Theorie und Entwickler-Realität. Sie liebt Star Treck das ist der Grund warum alle Sie Ensign Nova nennen und arbeitet daraufhin das sie Ihren ersten Knopf am Kragen bekommt.

  • Katharina Schmidt

    Katharina „Kat“ Schmidt – Die Design-Denkerin

    Senior Frontend- & UI/UX-Developerin | 29 Jahre | „Design ist Kommunikation – nicht Dekoration.“

    Kat ist das Gesicht hinter dem, was Nutzer sehen, fühlen und anklicken.
    In einem Team, das tief in Backend-Architekturen, Security und Data Flows denkt, sorgt sie dafür, dass der Mensch nicht vergessen wird.
    Ihre Arbeitsweise ist wie ihr Stil: präzise, ruhig, ästhetisch – und immer auf das Wesentliche reduziert.

    Sie kam 2022 zu Java Fleet, mit einem Hintergrund in Medieninformatik und einer klaren Mission: gute Technik sichtbar machen.
    Sie übersetzt komplexe Backend-Prozesse in klare Interfaces, macht aus endlosen Formularen strukturierte Nutzerreisen – und schafft es, dass ein Button „richtig“ wirkt, ohne dass jemand erklären kann, warum.

    💻 Die Tech-Seite

    Kat beherrscht den kompletten modernen Frontend-Stack:
    React, Vue.js, Tailwind CSS, SASS, Figma, Storybook – sie baut Systeme, keine Einzelkomponenten.
    Für sie ist Design kein Add-on, sondern ein integraler Teil der Architektur.
    Wenn sie an einer Oberfläche arbeitet, denkt sie wie ein Entwickler; wenn sie mit Entwicklern spricht, denkt sie wie eine Nutzerin.

    „Design ohne Verständnis für Code ist Deko. Code ohne Verständnis für Menschen ist Selbstgespräch.“

    Kat liebt es, Barrieren zu entfernen – technische, visuelle und emotionale.
    Accessibility (WCAG 2.1), Responsiveness, Semantik – das sind für sie keine Checklisten, sondern Selbstverständlichkeiten.

    🌿 Die menschliche Seite

    Kat ist der kreative Ruhepol im Team.
    Sie arbeitet konzentriert, lacht leise, und wenn sie eine Idee erklärt, spürt man sofort: Sie hat sie nicht nur verstanden – sie hat sie durchdacht.
    In ihrem Büro stehen eine Zimmerpflanze, ein Notizblock mit Skizzen, ein Grafiktablett und eine große Tasse Tee.
    Wenn sie nachdenkt, zeichnet sie. Wenn sie zuhört, beobachtet sie.

    Sie ist die empathischste Entwicklerin im Raum – und genau das macht sie so wichtig.
    Nova nennt sie „meine UI-Großschwester“, Cassian bewundert ihre gedankliche Klarheit, und Franz-Martin sagt:

    „Kat sieht das, was wir übersehen – und sie sagt’s, ohne laut zu werden.“

    🧠 Ihre Rolle im Team

    Kat ist die Schnittstelle zwischen Technik und Mensch – zwischen Backend und Benutzer, Code und Kommunikation.
    Sie organisiert Design-System-Workshops, dokumentiert Prinzipien, und sorgt dafür, dass jedes Projekt der Java Fleet ein Gesicht bekommt.
    Dabei denkt sie nicht in Farben oder Fonts, sondern in Abläufen und Emotionen: Wie fühlt sich ein Login an? Wann entsteht Vertrauen?
    Ihr Ziel: Interfaces, die sich richtig anfühlen – weil sie logisch sind.

    ⚡ Superkraft

    Empathie in Codeform.
    Kat übersetzt komplexe Technik in nutzerfreundliche Realität – elegant, barrierefrei und ehrlich.

    💬 Motto

    „Design ist, wenn du nicht erklären musst, wie es funktioniert.“