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:
| Operation | HTTP-Methode | Vue.js Action | Was passiert? |
|---|---|---|---|
| Create | POST | Form → API | Neue Daten erstellen |
| Read | GET | API → Liste | Daten anzeigen |
| Update | PUT/PATCH | Edit → API | Daten ändern |
| Delete | DELETE | Button → API | Daten 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:
- View Mode: Task anzeigen (normal)
- 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?
| Feature | v-if | v-show |
|---|---|---|
| DOM | Element wird entfernt/hinzugefügt | Element bleibt, nur CSS hidden |
| Performance (initial) | Besser (nicht gerendert) | Schlechter (immer gerendert) |
| Performance (toggle) | Schlechter (neu rendern) | Besser (nur CSS) |
| Wann nutzen? | Selten sichtbar, komplex | Hä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:
- Try/Catch um API-Calls
- Bei Fehler: Rollback der Änderung
- User informieren (Toast, Banner)
- 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):
- Conditional Rendering: https://vuejs.org/guide/essentials/conditional.html
- Teleport: https://vuejs.org/guide/built-ins/teleport.html
- Transition: https://vuejs.org/guide/built-ins/transition.html
🔧 Tools & Extensions für CRUD
Für diese Session MUST-HAVE:
| Tool | Zweck | Warum wichtig? |
|---|---|---|
| Vue DevTools | State & Events debuggen | Sieh Änderungen in Echtzeit! |
| Volar | VS Code Extension | Auto-Complete für v-if, v-show |
| Thunder Client | API-Testing | Falls 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

