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()
- ✅ Teil 5 : CRUD Operations Complete – Edit & Delete, Conditional Rendering
Heute: Teil 6 fokussiert auf Component Communication – Wie deine Components miteinander „reden“!
Neu in der Serie? Du kannst hier einsteigen, aber die ersten fünf Teile geben dir das vollständige Fundament.
⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden
Dein Problem: Deine App wächst, aber deine Components können nicht miteinander kommunizieren! Du kopierst Code, alles ist in einer Datei, und es wird unübersichtlich! 🍝
Die Lösung: Props und Emits – das Kommunikations-System von Vue.js!
Heute lernst du:
- ✅ Props (Daten von Parent zu Child übergeben)
- ✅ Emits (Events von Child zu Parent senden)
- ✅ v-model auf eigenen Components
- ✅ Props Validation (Typen & Required)
- ✅ Best Practice: Smart vs Dumb Components
- ✅ Provide/Inject (für tiefe Hierarchien – Bonus!)
Dein größter Gewinn: Deine App wird modular, wiederverwendbar und wartbar! Keine Spaghetti mehr! 🎉
Zeit-Investment: 60-75 Minuten | Schwierigkeit: Mittel (wichtiges Konzept!)
👋 Nova: „Meine App ist ein Spaghetti-Monster!“
Hi Leute! Nova hier – und ich hab ein Problem! 😰
Letzte Woche haben wir CRUD fertig gemacht. Edit, Delete, alles funktioniert.
ABER…
Meine App.vue hat jetzt 500 Zeilen! Alles ist in einer Datei:
- Task-Liste
- Task-Form
- Task-Card
- Filter-Buttons
- Stats-Dashboard
- Loading-States
- Error-Handling
Es ist… ein Monster. 🍝
<!-- App.vue - 500+ Zeilen Horror -->
<template>
<div>
<!-- Task Form hier... -->
<!-- Filter hier... -->
<!-- Stats hier... -->
<!-- Task Liste hier... -->
<!-- Jeder Task hier... -->
<!-- Modals hier... -->
<!-- ALLES HIER! 😱 -->
</div>
</template>
<script setup>
// 300 Zeilen Script...
// 50 refs...
// 30 functions...
// Chaos!
</script>
Nova: „KAT! Mein Code ist unlesbar! Ich finde nichts mehr!“ 😭
Kat: [schaut auf Nova’s Screen, zuckt zusammen] „Oh wow. Das ist… ambitioniert.“
Nova: „Ich wollte alles aufteilen, aber dann wusste ich nicht wie die Teile miteinander reden sollen!“
Kat: „Das ist genau das richtige Problem zur richtigen Zeit. Zeit für Props und Emits!“
Nova: „Props und… was?!“
Kat: „Setz dich. Wir machen aus deinem Spaghetti-Monster eine saubere Architektur.“ 🏗️
🏗️ Kat: Component-Architektur verstehen
Hey! Kat hier! 👋
Nova’s Problem ist klassisch. Jeder macht das am Anfang.
Die Lösung: Deine App in kleine, spezialisierte Components aufteilen!
Das Ziel: Component-Baum
App.vue
├── TaskForm.vue (Formulare)
├── TaskFilters.vue (Filter-Buttons)
├── TaskStats.vue (Statistiken)
└── TaskList.vue (Liste)
└── TaskCard.vue (Einzelne Task)
└── TaskActions.vue (Edit/Delete Buttons)
Aber hier kommt das Problem:
Wie „redet“ TaskCard mit App.vue? Wie weiß TaskForm, dass es eine neue Task erstellen soll?
Die Antwort:
- Props → Daten fließen RUNTER (Parent → Child)
- Emits → Events fließen HOCH (Child → Parent)
Nova: „Also wie… ein Wasserfall?“
Kat: „Genau! Props down, Events up. Wie Wasser, das runterfällt und Dampf, der aufsteigt!“
⬇️ Props – Daten von Parent zu Child
Was sind Props?
Props sind wie Funktions-Parameter für Components!
<!-- Parent: App.vue --> <template> <TaskCard :task="myTask" /> </template>
<!-- Child: TaskCard.vue -->
<script setup>
const props = defineProps({
task: Object
});
</script>
<template>
<div>{{ props.task.title }}</div>
</template>
Nova: „Oh! Das ist wie eine Funktion aufrufen und einen Parameter übergeben!“
Kat: „Exactly! <TaskCard :task="myTask" /> ist wie TaskCard(myTask) in Java!“
Praxis: TaskCard mit Props
Lass uns Nova’s Spaghetti aufräumen:
Vorher (alles in App.vue):
<!-- App.vue - CHAOS! -->
<template>
<div v-for="task in tasks" :key="task.id">
<div class="bg-white p-4 rounded shadow">
<h3>{{ task.title }}</h3>
<p>{{ task.description }}</p>
<span v-if="task.completed">✅</span>
<span v-else>⏳</span>
<button @click="toggleTask(task.id)">Toggle</button>
<button @click="deleteTask(task.id)">Delete</button>
</div>
</div>
</template>
Nachher (saubere Components):
<!-- App.vue - CLEAN! -->
<template>
<TaskCard
v-for="task in tasks"
:key="task.id"
:task="task"
/>
</template>
<!-- TaskCard.vue - Eigene Component! -->
<template>
<div class="bg-white p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow">
<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>
<span
class="text-2xl"
:class="task.completed ? 'text-green-500' : 'text-yellow-500'"
>
{{ task.completed ? '✅' : '⏳' }}
</span>
</div>
<div class="mt-4 flex gap-2">
<button
class="px-3 py-1 bg-blue-100 text-blue-700 rounded hover:bg-blue-200"
>
Toggle
</button>
<button
class="px-3 py-1 bg-red-100 text-red-700 rounded hover:bg-red-200"
>
Delete
</button>
</div>
</div>
</template>
<script setup>
defineProps({
task: {
type: Object,
required: true
}
});
</script>
Nova: „Oh wow, das ist SO viel übersichtlicher!“
Kat: „Ja! TaskCard kümmert sich NUR um die Darstellung EINER Task. Single Responsibility!“
✅ Props Validation – Typen & Required
Warum Validation?
Props ohne Typen sind wie Java ohne Typen. Chaos!
<script setup>
// ❌ SCHLECHT - Keine Validation
defineProps(['task', 'showDelete']);
// ✅ GUT - Mit Validation!
defineProps({
task: {
type: Object,
required: true
},
showDelete: {
type: Boolean,
default: true
}
});
</script>
Alle Prop-Typen
<script setup>
defineProps({
// Primitive Types
title: String,
count: Number,
isActive: Boolean,
// Object & Array
task: Object,
tasks: Array,
// Function
onSubmit: Function,
// Mit Optionen
priority: {
type: String,
required: true,
validator: (value) => ['low', 'medium', 'high'].includes(value)
},
// Mit Default
showActions: {
type: Boolean,
default: true
},
// Object mit Default (ACHTUNG: Factory Function!)
config: {
type: Object,
default: () => ({ theme: 'light', lang: 'de' })
},
// Mehrere Typen erlaubt
id: [String, Number]
});
</script>
Nova’s Struggle: Props sind READ-ONLY!
Nova: „Ich hab versucht, die Task in TaskCard zu ändern…“
<!-- ❌ FEHLER! -->
<script setup>
const props = defineProps({ task: Object });
function toggleComplete() {
props.task.completed = !props.task.completed; // ❌ VERBOTEN!
}
</script>
Kat: „Props sind READ-ONLY! Du kannst sie nicht ändern!“
Nova: „Aber… wie ändere ich dann was?!“
Kat: „Du schickst ein EVENT nach oben! Das bringt uns zu… Emits!“
⬆️ Emits – Events von Child zu Parent
Das Problem
TaskCard will eine Task als „completed“ markieren. Aber die Daten leben in App.vue!
Lösung: TaskCard sagt App.vue: „Hey, toggle diese Task!“
defineEmits() – Events definieren
<!-- TaskCard.vue -->
<template>
<div class="task-card">
<h3>{{ task.title }}</h3>
<button @click="$emit('toggle', task.id)">Toggle</button>
<button @click="$emit('delete', task.id)">Delete</button>
</div>
</template>
<script setup>
defineProps({
task: {
type: Object,
required: true
}
});
defineEmits(['toggle', 'delete']);
</script>
<!-- App.vue -->
<template>
<TaskCard
v-for="task in tasks"
:key="task.id"
:task="task"
@toggle="handleToggle"
@delete="handleDelete"
/>
</template>
<script setup>
import { ref } from 'vue';
import TaskCard from './components/TaskCard.vue';
const tasks = ref([
{ id: 1, title: 'Vue lernen', completed: false },
{ id: 2, title: 'Props verstehen', completed: true }
]);
function handleToggle(taskId) {
const task = tasks.value.find(t => t.id === taskId);
if (task) {
task.completed = !task.completed;
}
}
function handleDelete(taskId) {
tasks.value = tasks.value.filter(t => t.id !== taskId);
}
</script>
Nova: „OH! Das ist wie Events in JavaScript! Oder wie Pub/Sub in Java!“
Kat: „Genau! Child published ein Event, Parent subscribed darauf!“
Emit mit emit() Funktion (Alternative)
<script setup>
const props = defineProps({
task: Object
});
const emit = defineEmits(['toggle', 'delete', 'edit']);
function handleToggle() {
emit('toggle', props.task.id);
}
function handleDelete() {
emit('delete', props.task.id);
}
function handleEdit(newData) {
emit('edit', { id: props.task.id, ...newData });
}
</script>
<template>
<div>
<button @click="handleToggle">Toggle</button>
<button @click="handleDelete">Delete</button>
<button @click="handleEdit({ title: 'Neuer Titel' })">Edit</button>
</div>
</template>
Emit Validation (TypeScript-Style)
<script setup>
const emit = defineEmits({
// Einfach: Nur Name
toggle: null,
// Mit Validation
delete: (id) => {
if (typeof id !== 'number') {
console.warn('Delete erwartet eine Number!');
return false;
}
return true;
},
// Mit Payload-Struktur
edit: (payload) => {
return payload.id && payload.title;
}
});
</script>
🔄 v-model auf Custom Components
Das Problem
Du hast ein eigenes Input-Component und willst v-model nutzen:
<!-- ❌ Das funktioniert nicht automatisch! --> <CustomInput v-model="searchQuery" />
Die Lösung: modelValue + update:modelValue
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
class="border rounded px-3 py-2 w-full"
/>
</template>
<script setup>
defineProps({
modelValue: {
type: String,
default: ''
}
});
defineEmits(['update:modelValue']);
</script>
<!-- App.vue -->
<template>
<!-- Jetzt funktioniert v-model! ✅ -->
<CustomInput v-model="searchQuery" />
<p>Du suchst: {{ searchQuery }}</p>
</template>
<script setup>
import { ref } from 'vue';
import CustomInput from './components/CustomInput.vue';
const searchQuery = ref('');
</script>
Nova: „Wait… modelValue und update:modelValue? Das ist wie v-model unter der Haube!“
Kat: „Exactly! v-model ist nur Syntactic Sugar für :modelValue + @update:modelValue!“
Praxis: SearchInput Component
<!-- SearchInput.vue -->
<template>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
🔍
</span>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
type="text"
:placeholder="placeholder"
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
v-if="modelValue"
@click="$emit('update:modelValue', '')"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
✕
</button>
</div>
</template>
<script setup>
defineProps({
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: 'Suchen...'
}
});
defineEmits(['update:modelValue']);
</script>
<!-- Verwendung in App.vue -->
<template>
<SearchInput
v-model="searchQuery"
placeholder="Tasks durchsuchen..."
/>
<TaskCard
v-for="task in filteredTasks"
:key="task.id"
:task="task"
/>
</template>
<script setup>
import { ref, computed } from 'vue';
const searchQuery = ref('');
const tasks = ref([...]);
const filteredTasks = computed(() => {
if (!searchQuery.value) return tasks.value;
return tasks.value.filter(t =>
t.title.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
</script>
🎯 Best Practice: Smart vs Dumb Components
Kat’s Architektur-Geheimnis
Kat: „Es gibt zwei Arten von Components…“
Dumb Components (Presentational)
- Bekommen NUR Props
- Senden NUR Events
- Haben KEINEN eigenen State (außer UI-State)
- Sind WIEDERVERWENDBAR
<!-- TaskCard.vue - DUMB Component -->
<template>
<div class="card">
<h3>{{ task.title }}</h3>
<button @click="$emit('toggle')">Toggle</button>
</div>
</template>
<script setup>
defineProps({ task: Object });
defineEmits(['toggle', 'delete']);
// Kein eigener State! Kein API-Call! Nur Darstellung!
</script>
Smart Components (Container)
- Managen State
- Machen API-Calls
- Koordinieren Child-Components
- Business-Logik
<!-- TaskManager.vue - SMART Component -->
<template>
<div>
<TaskFilters v-model="filter" />
<TaskList
:tasks="filteredTasks"
@toggle="handleToggle"
@delete="handleDelete"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
const tasks = ref([]);
const filter = ref('all');
// API-Calls hier!
onMounted(async () => {
const response = await fetch('/api/tasks');
tasks.value = await response.json();
});
// Business-Logik hier!
const filteredTasks = computed(() => {
if (filter.value === 'all') return tasks.value;
if (filter.value === 'open') return tasks.value.filter(t => !t.completed);
return tasks.value.filter(t => t.completed);
});
async function handleToggle(taskId) {
// API-Update...
await fetch(`/api/tasks/${taskId}/toggle`, { method: 'PATCH' });
// Local Update...
const task = tasks.value.find(t => t.id === taskId);
task.completed = !task.completed;
}
</script>
Nova: „Also Dumb Components sind wie React’s ‚Presentational Components‘?“
Kat: „Genau! Und Smart Components sind die ‚Container Components‘. Das Pattern funktioniert in jedem Framework!“
🏗️ Praxis: Task-Manager aufräumen!
Lass uns Nova’s Spaghetti-Monster in saubere Components aufteilen:
Projekt-Struktur
src/ ├── App.vue (Smart: Haupt-Container) ├── components/ │ ├── TaskForm.vue (Dumb: Formular) │ ├── TaskFilters.vue (Dumb: Filter-Buttons) │ ├── TaskStats.vue (Dumb: Statistiken) │ ├── TaskList.vue (Dumb: Liste) │ ├── TaskCard.vue (Dumb: Einzelne Task) │ └── ui/ │ ├── SearchInput.vue (Dumb: Suchfeld) │ └── Button.vue (Dumb: Button)
TaskForm.vue
<!-- src/components/TaskForm.vue -->
<template>
<form
@submit.prevent="handleSubmit"
class="bg-white rounded-lg shadow-md p-6 mb-6"
>
<h2 class="text-xl font-bold text-gray-800 mb-4">
➕ Neue Task erstellen
</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"
required
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Was möchtest du erledigen?"
/>
</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 border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Details zur Task..."
/>
</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 border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500"
>
<option value="low">🟢 Niedrig</option>
<option value="medium">🟡 Mittel</option>
<option value="high">🔴 Hoch</option>
</select>
</div>
<button
type="submit"
:disabled="!form.title.trim()"
class="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white font-medium px-6 py-3 rounded-lg transition-colors"
>
Task hinzufügen
</button>
</div>
</form>
</template>
<script setup>
import { reactive } from 'vue';
const emit = defineEmits(['submit']);
const form = reactive({
title: '',
description: '',
priority: 'medium'
});
function handleSubmit() {
emit('submit', { ...form });
// Form zurücksetzen
form.title = '';
form.description = '';
form.priority = 'medium';
}
</script>
TaskFilters.vue
<!-- src/components/TaskFilters.vue -->
<template>
<div class="flex flex-wrap gap-2 mb-6">
<button
v-for="option in filterOptions"
:key="option.value"
@click="$emit('update:modelValue', option.value)"
:class="[
'px-4 py-2 rounded-lg font-medium transition-colors',
modelValue === option.value
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
>
{{ option.label }}
<span
v-if="option.count !== undefined"
class="ml-1 text-sm opacity-75"
>
({{ option.count }})
</span>
</button>
</div>
</template>
<script setup>
defineProps({
modelValue: {
type: String,
default: 'all'
},
filterOptions: {
type: Array,
default: () => [
{ value: 'all', label: 'Alle' },
{ value: 'open', label: 'Offen' },
{ value: 'completed', label: 'Erledigt' }
]
}
});
defineEmits(['update:modelValue']);
</script>
TaskStats.vue
<!-- src/components/TaskStats.vue -->
<template>
<div class="grid grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-lg shadow p-4 text-center">
<div class="text-3xl font-bold text-gray-800">{{ total }}</div>
<div class="text-sm text-gray-500">Gesamt</div>
</div>
<div class="bg-white rounded-lg shadow p-4 text-center">
<div class="text-3xl font-bold text-green-600">{{ completed }}</div>
<div class="text-sm text-gray-500">Erledigt</div>
</div>
<div class="bg-white rounded-lg shadow p-4 text-center">
<div class="text-3xl font-bold text-yellow-600">{{ open }}</div>
<div class="text-sm text-gray-500">Offen</div>
</div>
</div>
<!-- Progress Bar -->
<div class="mb-6">
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Fortschritt</span>
<span>{{ percentage }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div
class="bg-green-500 h-3 rounded-full transition-all duration-300"
:style="{ width: percentage + '%' }"
/>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
total: {
type: Number,
required: true
},
completed: {
type: Number,
required: true
},
open: {
type: Number,
required: true
}
});
const percentage = computed(() => {
if (props.total === 0) return 0;
return Math.round((props.completed / props.total) * 100);
});
</script>
TaskCard.vue
<!-- src/components/TaskCard.vue -->
<template>
<div
class="bg-white rounded-lg shadow-md p-4 hover:shadow-lg transition-shadow"
:class="{ 'opacity-60': task.completed }"
>
<div class="flex items-start gap-4">
<!-- Checkbox -->
<button
@click="$emit('toggle', task.id)"
class="mt-1 text-2xl hover:scale-110 transition-transform"
>
{{ task.completed ? '✅' : '⬜' }}
</button>
<!-- Content -->
<div class="flex-1">
<h3
class="text-lg font-semibold"
:class="task.completed ? 'text-gray-400 line-through' : 'text-gray-800'"
>
{{ task.title }}
</h3>
<p
v-if="task.description"
class="text-gray-600 mt-1 text-sm"
>
{{ task.description }}
</p>
<div class="flex items-center gap-2 mt-2">
<!-- Priority Badge -->
<span
class="px-2 py-0.5 rounded text-xs font-medium"
:class="priorityClasses"
>
{{ priorityLabel }}
</span>
<!-- Created Date -->
<span class="text-xs text-gray-400">
{{ formattedDate }}
</span>
</div>
</div>
<!-- Actions -->
<div class="flex gap-2">
<button
@click="$emit('edit', task)"
class="p-2 text-blue-500 hover:bg-blue-50 rounded"
title="Bearbeiten"
>
✏️
</button>
<button
@click="$emit('delete', task.id)"
class="p-2 text-red-500 hover:bg-red-50 rounded"
title="Löschen"
>
🗑️
</button>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
task: {
type: Object,
required: true
}
});
defineEmits(['toggle', 'edit', 'delete']);
const priorityClasses = computed(() => {
const classes = {
high: 'bg-red-100 text-red-700',
medium: 'bg-yellow-100 text-yellow-700',
low: 'bg-green-100 text-green-700'
};
return classes[props.task.priority] || classes.medium;
});
const priorityLabel = computed(() => {
const labels = {
high: '🔴 Hoch',
medium: '🟡 Mittel',
low: '🟢 Niedrig'
};
return labels[props.task.priority] || labels.medium;
});
const formattedDate = computed(() => {
if (!props.task.createdAt) return '';
return new Date(props.task.createdAt).toLocaleDateString('de-DE');
});
</script>
TaskList.vue
<!-- src/components/TaskList.vue -->
<template>
<div>
<!-- Empty State -->
<div
v-if="tasks.length === 0"
class="text-center py-12 bg-white rounded-lg shadow"
>
<div class="text-6xl mb-4">📋</div>
<h3 class="text-xl font-semibold text-gray-700">Keine Tasks</h3>
<p class="text-gray-500 mt-1">
{{ emptyMessage }}
</p>
</div>
<!-- Task Cards -->
<div v-else class="space-y-4">
<TaskCard
v-for="task in tasks"
:key="task.id"
:task="task"
@toggle="$emit('toggle', $event)"
@edit="$emit('edit', $event)"
@delete="$emit('delete', $event)"
/>
</div>
</div>
</template>
<script setup>
import TaskCard from './TaskCard.vue';
defineProps({
tasks: {
type: Array,
required: true
},
emptyMessage: {
type: String,
default: 'Erstelle deine erste Task!'
}
});
defineEmits(['toggle', 'edit', 'delete']);
</script>
App.vue (Smart Component)
<!-- src/App.vue -->
<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">Organisiere deinen Tag mit Vue.js!</p>
</header>
<!-- Task Form -->
<TaskForm @submit="addTask" />
<!-- Stats -->
<TaskStats
:total="stats.total"
:completed="stats.completed"
:open="stats.open"
/>
<!-- Search -->
<div class="mb-4">
<SearchInput
v-model="searchQuery"
placeholder="Tasks durchsuchen..."
/>
</div>
<!-- Filters -->
<TaskFilters
v-model="currentFilter"
:filter-options="filterOptions"
/>
<!-- Task List -->
<TaskList
:tasks="displayedTasks"
:empty-message="emptyMessage"
@toggle="toggleTask"
@edit="startEdit"
@delete="deleteTask"
/>
<!-- Edit Modal -->
<div
v-if="editingTask"
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4"
>
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<h2 class="text-xl font-bold mb-4">Task bearbeiten</h2>
<input
v-model="editingTask.title"
class="w-full border rounded px-3 py-2 mb-4"
/>
<textarea
v-model="editingTask.description"
class="w-full border rounded px-3 py-2 mb-4"
rows="3"
/>
<div class="flex gap-2">
<button
@click="saveEdit"
class="flex-1 bg-blue-500 text-white py-2 rounded"
>
Speichern
</button>
<button
@click="cancelEdit"
class="flex-1 bg-gray-200 py-2 rounded"
>
Abbrechen
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import TaskForm from './components/TaskForm.vue';
import TaskFilters from './components/TaskFilters.vue';
import TaskStats from './components/TaskStats.vue';
import TaskList from './components/TaskList.vue';
import SearchInput from './components/SearchInput.vue';
// State
const tasks = ref([]);
const searchQuery = ref('');
const currentFilter = ref('all');
const editingTask = ref(null);
// Initial Tasks laden
onMounted(() => {
const saved = localStorage.getItem('tasks');
if (saved) {
tasks.value = JSON.parse(saved);
} else {
// Demo-Tasks
tasks.value = [
{ id: 1, title: 'Vue.js lernen', description: 'Props & Emits verstehen', priority: 'high', completed: false, createdAt: new Date().toISOString() },
{ id: 2, title: 'Task-Manager bauen', description: 'Components aufteilen', priority: 'medium', completed: true, createdAt: new Date().toISOString() },
{ id: 3, title: 'Kaffee trinken', description: '', priority: 'low', completed: false, createdAt: new Date().toISOString() }
];
}
});
// Auto-Save
watch(tasks, (newTasks) => {
localStorage.setItem('tasks', 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 filterOptions = computed(() => [
{ value: 'all', label: 'Alle', count: stats.value.total },
{ value: 'open', label: 'Offen', count: stats.value.open },
{ value: 'completed', label: 'Erledigt', count: stats.value.completed }
]);
const filteredTasks = computed(() => {
let result = tasks.value;
// Filter by status
if (currentFilter.value === 'open') {
result = result.filter(t => !t.completed);
} else if (currentFilter.value === 'completed') {
result = result.filter(t => t.completed);
}
// Filter by search
if (searchQuery.value.trim()) {
const query = searchQuery.value.toLowerCase();
result = result.filter(t =>
t.title.toLowerCase().includes(query) ||
t.description?.toLowerCase().includes(query)
);
}
return result;
});
const displayedTasks = computed(() => {
// Sort: open first, then by priority, then by date
return [...filteredTasks.value].sort((a, b) => {
if (a.completed !== b.completed) return a.completed ? 1 : -1;
const priorityOrder = { high: 0, medium: 1, low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
});
const emptyMessage = computed(() => {
if (searchQuery.value) return `Keine Ergebnisse für "${searchQuery.value}"`;
if (currentFilter.value === 'open') return 'Alle Tasks erledigt! 🎉';
if (currentFilter.value === 'completed') return 'Noch keine erledigten Tasks.';
return 'Erstelle deine erste Task!';
});
// Actions
function addTask(taskData) {
tasks.value.push({
id: Date.now(),
...taskData,
completed: false,
createdAt: new Date().toISOString()
});
}
function toggleTask(taskId) {
const task = tasks.value.find(t => t.id === taskId);
if (task) {
task.completed = !task.completed;
}
}
function deleteTask(taskId) {
tasks.value = tasks.value.filter(t => t.id !== taskId);
}
function startEdit(task) {
editingTask.value = { ...task };
}
function saveEdit() {
const index = tasks.value.findIndex(t => t.id === editingTask.value.id);
if (index !== -1) {
tasks.value[index] = { ...editingTask.value };
}
editingTask.value = null;
}
function cancelEdit() {
editingTask.value = null;
}
</script>
🎁 BONUS: Provide/Inject für tiefe Hierarchien
Das Problem
Was wenn du Daten 5 Ebenen tief brauchst?
App.vue
└── Layout.vue
└── MainContent.vue
└── TaskSection.vue
└── TaskList.vue
└── TaskCard.vue ← Braucht "currentUser"!
Mit Props: Du müsstest currentUser durch ALLE Ebenen durchreichen! 😱
Die Lösung: Provide/Inject
<!-- App.vue (Provider) -->
<script setup>
import { ref, provide } from 'vue';
const currentUser = ref({ name: 'Nova', role: 'developer' });
const theme = ref('light');
// Provide macht Daten für ALLE Kinder verfügbar!
provide('currentUser', currentUser);
provide('theme', theme);
</script>
<!-- TaskCard.vue (Irgendwo tief unten) -->
<script setup>
import { inject } from 'vue';
// Inject holt die Daten - egal wie tief!
const currentUser = inject('currentUser');
const theme = inject('theme', 'light'); // Mit Fallback
</script>
<template>
<div :class="theme === 'dark' ? 'bg-gray-800' : 'bg-white'">
<p>Erstellt von: {{ currentUser.name }}</p>
</div>
</template>
Nova: „Das ist wie Dependency Injection in Spring!“
Kat: „Exactly! Aber nutze es sparsam. Für die meisten Fälle sind Props + Emits besser!“
Wann Provide/Inject?
✅ Gut für:
- Theme/Dark Mode
- User-Daten (Auth)
- Konfiguration
- i18n (Übersetzungen)
❌ Schlecht für:
- Business-Daten (Tasks, Orders, etc.)
- Alles was sich oft ändert
- Wenn Props reichen würden
❓ FAQ (Häufige Fragen)
Frage 1: Kann ich Props ändern?
Antwort: NEIN! Props sind read-only. Wenn du was ändern willst, emit ein Event nach oben und lass den Parent die Änderung machen!
Frage 2: Wie viele Props sind zu viele?
Antwort: Faustregel: Mehr als 5-6 Props? Überlege ob du:
- Ein Object übergeben solltest statt einzelner Props
- Die Component aufteilen solltest
- Slot nutzen könntest
<!-- ❌ Zu viele Props --> <TaskCard :title="task.title" :description="task.description" :priority="task.priority" :completed="task.completed" :createdAt="task.createdAt" :assignee="task.assignee" /> <!-- ✅ Ein Object --> <TaskCard :task="task" />
Frage 3: v-model oder @update?
Antwort: v-model ist für Two-Way-Binding (Inputs, Forms). Einzelne Events für Actions (click, submit, delete). v-model = :value + @input in einem!
Frage 4: defineProps vs props Option?
Antwort: defineProps() ist der moderne Weg mit <script setup>. Die alte props: [...] Syntax funktioniert noch, aber nur in der Options API!
Frage 5: Emit Naming Convention?
Antwort:
- Verben im Präsens:
submit,update,delete - Oder
update:propNamefür v-model - Kebab-case im Template:
@update-user - camelCase in defineEmits:
updateUser
Frage 6: Smart vs Dumb – Wo ziehe ich die Grenze?
Antwort:
- Dumb: Kann ich diese Component in einem anderen Projekt wiederverwenden? Dann dumb!
- Smart: Hat sie Business-Logik? Macht sie API-Calls? Dann smart!
- Meist: Wenige Smart Components, viele Dumb Components!
Frage 7: Wie kommuniziert ihr im Team bei Konflikten?
Antwort: Gute Frage! Props down, Events up – das funktioniert bei Components. Aber bei Menschen? Da ist Kommunikation manchmal komplizierter. 😅 Manchmal wünschte ich, es gäbe ein emit('frustration') das der andere auch wirklich empfängt. Real talk: Die echten Kommunikations-Struggles finden sich nicht in Code… sondern eher in unseren „private logs“. Manche Gespräche sind schwieriger als jede Architektur-Entscheidung.
Frage 8: Wann Provide/Inject vs Props?
Antwort:
- Props: Default! 1-3 Ebenen tief = immer Props
- Provide/Inject: Nur für globale Dinge (Theme, Auth, Config)
- State Management (Pinia): Wenn mehrere unabhängige Components dieselben Daten brauchen
Frage 9: Kann ich Events an Grandparents senden?
Antwort: Nicht direkt! Du musst das Event durch jede Ebene durchreichen. Oder: Nutze Pinia für komplexe Kommunikation (kommt in Teil 7!).
Frage 10: Funktioniert das auch mit TypeScript?
Antwort: JA! Sogar besser! Mit TypeScript bekommst du volle Type-Safety:
<script setup lang="ts">
interface Task {
id: number;
title: string;
completed: boolean;
}
defineProps<{
task: Task;
showActions?: boolean;
}>();
defineEmits<{
(e: 'toggle', id: number): void;
(e: 'delete', id: number): void;
}>();
</script>
📖 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
- Teil 6 : Component Communication – Du bist hier! ✨
🔜 Kommende Teile:
- 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
🔧 Tools & Extensions für Components
Für diese Session MUST-HAVE:
| Tool | Zweck | Warum wichtig? |
|---|---|---|
| Vue DevTools | Component-Hierarchie visualisieren! | Sieh Props, Events, State in Echtzeit |
| Volar | VS Code Vue-Support | Auto-Complete für defineProps, defineEmits |
| Vue Component Generator | Schnell Components erstellen | VS Code Extension |
Vue DevTools für Component Communication:
# So nutzt du Vue DevTools für Props/Emits 1. F12 → Vue Tab öffnen 2. Component-Tree betrachten (links) 3. Component anklicken 4. "Props" Tab → Sieh alle Props und Werte 5. "Events" Tab → Sieh emitted Events in Echtzeit! 6. "Timeline" Tab → Verfolge den Event-Flow!
Kat’s Pro-Tipp: „Im Timeline-Tab siehst du: User klickt → Child emittet → Parent reagiert. Das hilft ENORM beim Debuggen von Event-Problemen!“
💬 Real Talk: Component-Architektur
Java Fleet Küche, 13:15 Uhr. Nova sitzt mit ihrem Laptop am Tisch, Kat holt sich einen Kaffee. Kofi kommt mit seinem Mittagessen vorbei.
Nova: „Kofi! Rate mal – ich hab heute meinen Code von 500 auf 150 Zeilen reduziert!“
Kofi: [setzt sich] „Wie das? Hast du Features gelöscht?“
Nova: „Nein! Components! Ich hab alles in kleine Teile aufgeteilt!“
Kat: [setzt sich dazu] „Props down, Events up. Der Klassiker.“
Kofi: „Ah, du hast den ‚Spaghetti-zu-Ravioli‘-Moment erlebt?“
Nova: „Den WAS?!“
Kofi: [grinst] „Spaghetti-Code: Alles ist verbunden, chaotisch. Ravioli-Code: Kleine Pakete, jedes mit eigenem Inhalt, aber sie können kombiniert werden.“
Kat: „Das ist… eine interessante Metapher.“
Nova: „Ich hab nur nicht verstanden, warum Props read-only sind. Warum kann ich nicht einfach ändern?“
Kat: „Stell dir vor: Parent gibt dir ein Buch. Du schreibst rein – aber Parent weiß das nicht. Chaos!“
Kofi: „Das ist wie im echten Leben. Du kannst nicht einfach die Meinung anderer ändern. Du kannst nur sagen, was DU denkst, und hoffen, dass sie zuhören.“
Nova: „Okay, DAS ist deep für Component-Architektur.“
Kat: [schmunzelt] „Kofi hat manchmal Momente.“
Kofi: „Backend-Entwickler philosophieren halt. Wir sehen die Datenbank. Wir wissen, dass alles verbunden ist.“
Nova: „Aber mit Emits… ich schicke eine Nachricht, und der andere MUSS reagieren, oder?“
Kat: „Muss nicht. Kann ignorieren. Wie im echten Leben.“
[Kurze Pause]
Kofi: „Manchmal wünschte ich, menschliche Kommunikation wäre so klar. emit('ich-brauche-hilfe') – und jemand fängt es auf.“
Kat: [nickt langsam] „Ja… manchmal ist emit einfacher als reden.“
Nova: „Ihr seid heute beide komisch philosophisch.“
Kat: „Sorry. Lange Woche. Aber hey – dein Code ist jetzt clean! Das ist ein Win!“
Nova: „Stimmt! Und nächste Woche: State Management! Da wird’s bestimmt noch komplizierter…“
Kofi: „Spoiler: Pinia ist eigentlich ziemlich chill.“
🎁 Bonus: Component Communication Cheat Sheet
<!-- ===== PROPS (Parent → Child) ===== -->
<!-- Parent -->
<TaskCard :task="myTask" :show-actions="true" />
<!-- Child -->
<script setup>
const props = defineProps({
task: { type: Object, required: true },
showActions: { type: Boolean, default: true }
});
</script>
<!-- ===== EMITS (Child → Parent) ===== -->
<!-- Child -->
<script setup>
const emit = defineEmits(['toggle', 'delete']);
// Option 1: im Template
// <button @click="$emit('toggle', task.id)">Toggle</button>
// Option 2: im Script
emit('toggle', props.task.id);
</script>
<!-- Parent -->
<TaskCard
:task="task"
@toggle="handleToggle"
@delete="handleDelete"
/>
<!-- ===== v-model auf Custom Components ===== -->
<!-- Child (CustomInput.vue) -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps({ modelValue: String });
defineEmits(['update:modelValue']);
</script>
<!-- Parent -->
<CustomInput v-model="searchQuery" />
<!-- ===== PROVIDE/INJECT ===== -->
<!-- Provider (App.vue) -->
<script setup>
import { provide, ref } from 'vue';
const user = ref({ name: 'Nova' });
provide('currentUser', user);
</script>
<!-- Consumer (irgendwo tief unten) -->
<script setup>
import { inject } from 'vue';
const user = inject('currentUser');
</script>
🎨 Challenge für die Community!
Kat’s Herausforderung:
„Du verstehst jetzt Component Communication! Zeit für eine Challenge:
Level 1 – Basic Refactoring:
- 📦 Teile deine App in mindestens 5 Components
- ⬇️ Nutze Props für Daten-Weitergabe
- ⬆️ Nutze Emits für Events
- 🔄 Implementiere v-model auf einem eigenen Input
Level 2 – Reusable Components:
- 🎨 Erstelle eine wiederverwendbare Button-Component
- 📝 Erstelle eine Card-Component mit Slots
- 🔍 Erstelle eine SearchInput-Component mit v-model
Level 3 – Advanced Patterns:
- 🌳 Nutze Provide/Inject für Theme-Switching
- 🎭 Implementiere Dark Mode mit Inject
- 🧩 Erstelle eine komplexe Component mit Props + Emits + Slots
Teile dein Ergebnis:
- Screenshot deines Component-Trees (Vue DevTools!)
- GitHub-Repo verlinken
- Erkläre deine Component-Architektur
Die saubersten Architekturen featuren wir! 🎉“
💬 Das war Teil 6 der Vue.js für Anfänger Serie!
Nova & Kat: „Von Spaghetti zu Clean Architecture! 🍝➡️🏗️“
Du hast heute etwas FUNDAMENTALES gelernt! Component Communication ist das Herzstück jeder Vue.js App!
Was du geschafft hast:
✅ Props verstanden (Daten runter!) ✅ Emits verstanden (Events hoch!) ✅ v-model auf Custom Components ✅ Props Validation gelernt ✅ Smart vs Dumb Components verstanden ✅ Provide/Inject kennengelernt (Bonus!) ✅ Deinen Code modular aufgeteilt
Real talk: Das war ein Game-Changer! Deine Apps werden nie wieder Spaghetti sein. Props und Emits sind wie… die Grammatik von Vue.js. Ohne sie kannst du nicht richtig „sprechen“. Mit ihnen? Du kannst komplexe Apps bauen die wartbar sind! 💪
Hast du es nachgebaut? Teile deine Component-Architektur!
Fragen? Schreib uns:
- Nova: nova.trent@java-developer.online
- Kat: katharina.schmidt@java-developer.online
Nächster Teil: In 1 Woche! State Management mit Composables & Pinia! 🚀
Keep coding, keep composing! 🎵
Nova & Kat – Von Chaos zu Struktur, gemeinsam! 💚
Tags: #VueJS #Components #Props #Emits #ComponentCommunication #Frontend #WebDevelopment #Architektur #JavaScript
© 2025 Java Fleet Systems Consulting | java-developer.online

