Angular Todo App - Komplettes Projekt
Zeit für ein echtes Projekt! Wir bauen eine komplette Todo-Anwendung mit allem was du gelernt hast. 🚀
📋 Was wir bauen:
- ✅ Complete CRUD (Create, Read, Update, Delete)
- ✅ Multi-Page mit Routing
- ✅ Reactive Forms mit Validation
- ✅ HTTP API Integration
- ✅ State Management mit Services
- ✅ Responsive Design
- ✅ Error Handling & Loading States
- ✅ Filter & Search
- ✅ Local Storage Persistence
Projekt Setup
1. Neues Projekt erstellen
ng new angular-todo-app
Fragen:
- Routing? → Ja (y)
- Stylesheet? → CSS
cd angular-todo-app
ng serve --open
2. Projekt-Struktur planen
src/app/
├── core/ # Core Services & Guards
│ ├── services/
│ │ ├── todo.service.ts
│ │ └── storage.service.ts
│ └── models/
│ └── todo.model.ts
│
├── features/ # Feature Modules
│ ├── todo-list/
│ │ └── todo-list.component.ts
│ ├── todo-detail/
│ │ └── todo-detail.component.ts
│ └── todo-form/
│ └── todo-form.component.ts
│
├── shared/ # Shared Components
│ ├── components/
│ │ ├── header/
│ │ ├── footer/
│ │ └── loading-spinner/
│ └── pipes/
│ └── filter-todos.pipe.ts
│
└── app-routing.module.ts
Schritt 1: Models & Interfaces
core/models/todo.model.ts:
export interface Todo {
id: number;
title: string;
description: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
dueDate: Date | null;
createdAt: Date;
updatedAt: Date;
}
export interface TodoCreateDto {
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
dueDate: Date | null;
}
export interface TodoUpdateDto {
title?: string;
description?: string;
completed?: boolean;
priority?: 'low' | 'medium' | 'high';
dueDate?: Date | null;
}
export type TodoFilter = 'all' | 'active' | 'completed';
Schritt 2: Services
Storage Service (LocalStorage)
core/services/storage.service.ts:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
setItem(key: string, value: any): void {
try {
const jsonValue = JSON.stringify(value);
localStorage.setItem(key, jsonValue);
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}
getItem<T>(key: string): T | null {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error('Error reading from localStorage:', error);
return null;
}
}
removeItem(key: string): void {
localStorage.removeItem(key);
}
clear(): void {
localStorage.clear();
}
}
Todo Service (State Management)
core/services/todo.service.ts:
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Todo, TodoCreateDto, TodoUpdateDto, TodoFilter } from '../models/todo.model';
import { StorageService } from './storage.service';
@Injectable({
providedIn: 'root'
})
export class TodoService {
private readonly STORAGE_KEY = 'angular_todos';
private nextId = 1;
// State
private todosSubject = new BehaviorSubject<Todo[]>([]);
private filterSubject = new BehaviorSubject<TodoFilter>('all');
// Public Observables
todos$ = this.todosSubject.asObservable();
filter$ = this.filterSubject.asObservable();
// Filtered Todos
filteredTodos$ = this.todos$.pipe(
map(todos => {
const filter = this.filterSubject.value;
switch (filter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
})
);
constructor(private storage: StorageService) {
this.loadFromStorage();
}
// CRUD Operations
getTodos(): Todo[] {
return this.todosSubject.value;
}
getTodoById(id: number): Todo | undefined {
return this.todosSubject.value.find(t => t.id === id);
}
createTodo(dto: TodoCreateDto): Todo {
const newTodo: Todo = {
id: this.nextId++,
...dto,
completed: false,
createdAt: new Date(),
updatedAt: new Date()
};
const todos = [...this.todosSubject.value, newTodo];
this.updateTodos(todos);
return newTodo;
}
updateTodo(id: number, dto: TodoUpdateDto): Todo | null {
const todos = this.todosSubject.value;
const index = todos.findIndex(t => t.id === id);
if (index === -1) return null;
const updatedTodo = {
...todos[index],
...dto,
updatedAt: new Date()
};
todos[index] = updatedTodo;
this.updateTodos([...todos]);
return updatedTodo;
}
toggleTodo(id: number): void {
const todo = this.getTodoById(id);
if (todo) {
this.updateTodo(id, { completed: !todo.completed });
}
}
deleteTodo(id: number): boolean {
const todos = this.todosSubject.value.filter(t => t.id !== id);
this.updateTodos(todos);
return true;
}
deleteCompleted(): void {
const todos = this.todosSubject.value.filter(t => !t.completed);
this.updateTodos(todos);
}
// Filter
setFilter(filter: TodoFilter): void {
this.filterSubject.next(filter);
}
getFilter(): TodoFilter {
return this.filterSubject.value;
}
// Stats
getStats() {
const todos = this.todosSubject.value;
return {
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length,
high: todos.filter(t => t.priority === 'high' && !t.completed).length
};
}
// Storage
private updateTodos(todos: Todo[]): void {
this.todosSubject.next(todos);
this.saveToStorage();
}
private saveToStorage(): void {
const todos = this.todosSubject.value;
this.storage.setItem(this.STORAGE_KEY, {
todos,
nextId: this.nextId
});
}
private loadFromStorage(): void {
const data = this.storage.getItem<{ todos: Todo[]; nextId: number }>(this.STORAGE_KEY);
if (data) {
// Parse Dates
const todos = data.todos.map(t => ({
...t,
createdAt: new Date(t.createdAt),
updatedAt: new Date(t.updatedAt),
dueDate: t.dueDate ? new Date(t.dueDate) : null
}));
this.todosSubject.next(todos);
this.nextId = data.nextId;
}
}
}
Schritt 3: Shared Components
Header Component
ng g c shared/components/header
shared/components/header/header.component.ts:
import { Component } from '@angular/core';
import { TodoService } from '../../../core/services/todo.service';
@Component({
selector: 'app-header',
template: `
<header class="header">
<div class="container">
<h1 routerLink="/">📝 Angular Todo App</h1>
<nav class="nav">
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
Todos
</a>
<a routerLink="/create" routerLinkActive="active">
Neu erstellen
</a>
</nav>
<div class="stats">
<span class="stat">Total: {{ stats.total }}</span>
<span class="stat active">Active: {{ stats.active }}</span>
<span class="stat completed">Done: {{ stats.completed }}</span>
</div>
</div>
</header>
`,
styles: [`
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
}
h1 {
margin: 0;
font-size: 1.5rem;
cursor: pointer;
}
.nav {
display: flex;
gap: 1rem;
flex: 1;
}
.nav a {
color: rgba(255,255,255,0.9);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: all 0.2s;
}
.nav a:hover {
background: rgba(255,255,255,0.1);
}
.nav a.active {
background: rgba(255,255,255,0.2);
font-weight: 600;
}
.stats {
display: flex;
gap: 1rem;
}
.stat {
padding: 0.25rem 0.75rem;
background: rgba(255,255,255,0.2);
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
`]
})
export class HeaderComponent {
get stats() {
return this.todoService.getStats();
}
constructor(private todoService: TodoService) {}
}
Loading Spinner
ng g c shared/components/loading-spinner
shared/components/loading-spinner/loading-spinner.component.ts:
import { Component } from '@angular/core';
@Component({
selector: 'app-loading-spinner',
template: `
<div class="spinner-container">
<div class="spinner"></div>
<p>Lädt...</p>
</div>
`,
styles: [`
.spinner-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
p {
margin-top: 1rem;
color: #6b7280;
}
`]
})
export class LoadingSpinnerComponent {}
Schritt 4: Todo List Component
ng g c features/todo-list
features/todo-list/todo-list.component.ts:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TodoService } from '../../core/services/todo.service';
import { Todo, TodoFilter } from '../../core/models/todo.model';
import { Observable } from 'rxjs';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
todos$!: Observable<Todo[]>;
filter$!: Observable<TodoFilter>;
searchQuery = '';
constructor(
private todoService: TodoService,
private router: Router
) {}
ngOnInit() {
this.todos$ = this.todoService.filteredTodos$;
this.filter$ = this.todoService.filter$;
}
setFilter(filter: TodoFilter) {
this.todoService.setFilter(filter);
}
toggleTodo(id: number) {
this.todoService.toggleTodo(id);
}
deleteTodo(id: number) {
if (confirm('Todo wirklich löschen?')) {
this.todoService.deleteTodo(id);
}
}
editTodo(id: number) {
this.router.navigate(['/edit', id]);
}
viewTodo(id: number) {
this.router.navigate(['/todo', id]);
}
deleteCompleted() {
if (confirm('Alle erledigten Todos löschen?')) {
this.todoService.deleteCompleted();
}
}
filterBySearch(todos: Todo[]): Todo[] {
if (!this.searchQuery.trim()) return todos;
const query = this.searchQuery.toLowerCase();
return todos.filter(todo =>
todo.title.toLowerCase().includes(query) ||
todo.description.toLowerCase().includes(query)
);
}
getPriorityClass(priority: string): string {
return `priority-${priority}`;
}
get stats() {
return this.todoService.getStats();
}
}
features/todo-list/todo-list.component.html:
<div class="todo-list-container">
<div class="controls">
<!-- Search -->
<div class="search">
<input
type="text"
[(ngModel)]="searchQuery"
placeholder="🔍 Todos durchsuchen..."
class="search-input"
>
</div>
<!-- Filter Buttons -->
<div class="filters">
<button
(click)="setFilter('all')"
[class.active]="(filter$ | async) === 'all'"
class="filter-btn"
>
Alle ({{ stats.total }})
</button>
<button
(click)="setFilter('active')"
[class.active]="(filter$ | async) === 'active'"
class="filter-btn"
>
Aktiv ({{ stats.active }})
</button>
<button
(click)="setFilter('completed')"
[class.active]="(filter$ | async) === 'completed'"
class="filter-btn"
>
Erledigt ({{ stats.completed }})
</button>
</div>
<button (click)="deleteCompleted()" class="btn-danger">
🗑️ Erledigte löschen
</button>
</div>
<!-- Todo List -->
<div class="todos" *ngIf="todos$ | async as todos">
<div *ngIf="filterBySearch(todos).length === 0" class="empty-state">
<p>📭 Keine Todos gefunden</p>
<button routerLink="/create" class="btn-primary">
Ersten Todo erstellen
</button>
</div>
<div
*ngFor="let todo of filterBySearch(todos)"
class="todo-card"
[class.completed]="todo.completed"
>
<!-- Checkbox -->
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
class="todo-checkbox"
>
<!-- Content -->
<div class="todo-content" (click)="viewTodo(todo.id)">
<h3 class="todo-title">{{ todo.title }}</h3>
<p class="todo-description">{{ todo.description }}</p>
<div class="todo-meta">
<span [class]="getPriorityClass(todo.priority)" class="badge">
{{ todo.priority }}
</span>
<span *ngIf="todo.dueDate" class="due-date">
📅 {{ todo.dueDate | date:'dd.MM.yyyy' }}
</span>
</div>
</div>
<!-- Actions -->
<div class="todo-actions">
<button (click)="editTodo(todo.id)" class="btn-icon" title="Bearbeiten">
✏️
</button>
<button (click)="deleteTodo(todo.id)" class="btn-icon" title="Löschen">
🗑️
</button>
</div>
</div>
</div>
</div>
features/todo-list/todo-list.component.css:
.todo-list-container {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
align-items: center;
}
.search {
flex: 1;
min-width: 200px;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
}
.filters {
display: flex;
gap: 0.5rem;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 2px solid #e5e7eb;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.todo-card {
display: flex;
gap: 1rem;
padding: 1.5rem;
background: white;
border: 2px solid #e5e7eb;
border-radius: 12px;
margin-bottom: 1rem;
transition: all 0.2s;
}
.todo-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
}
.todo-card.completed {
opacity: 0.6;
background: #f9fafb;
}
.todo-checkbox {
width: 24px;
height: 24px;
cursor: pointer;
}
.todo-content {
flex: 1;
cursor: pointer;
}
.todo-title {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
}
.todo-card.completed .todo-title {
text-decoration: line-through;
}
.todo-description {
margin: 0 0 1rem 0;
color: #6b7280;
}
.todo-meta {
display: flex;
gap: 1rem;
align-items: center;
}
.badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.priority-low { background: #dbeafe; color: #1e40af; }
.priority-medium { background: #fef3c7; color: #b45309; }
.priority-high { background: #fee2e2; color: #dc2626; }
.due-date {
color: #6b7280;
font-size: 0.875rem;
}
.todo-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon {
padding: 0.5rem;
background: none;
border: none;
font-size: 1.25rem;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s;
}
.btn-icon:hover {
background: #f3f4f6;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: #6b7280;
}
.empty-state p {
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.btn-primary {
padding: 0.75rem 1.5rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
background: #5a67d8;
}
.btn-danger {
padding: 0.5rem 1rem;
background: #ef4444;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-danger:hover {
background: #dc2626;
}
Schritt 5: Todo Form Component
ng g c features/todo-form
features/todo-form/todo-form.component.ts:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { TodoService } from '../../core/services/todo.service';
@Component({
selector: 'app-todo-form',
templateUrl: './todo-form.component.html',
styleUrls: ['./todo-form.component.css']
})
export class TodoFormComponent implements OnInit {
isEditMode = false;
todoId: number | null = null;
todoForm = this.fb.group({
title: ['', [Validators.required, Validators.minLength(3)]],
description: ['', [Validators.required, Validators.minLength(10)]],
priority: ['medium' as 'low' | 'medium' | 'high', Validators.required],
dueDate: [null as Date | null]
});
constructor(
private fb: FormBuilder,
private todoService: TodoService,
private router: Router,
private route: ActivatedRoute
) {}
ngOnInit() {
// Check if Edit Mode
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.isEditMode = true;
this.todoId = parseInt(id);
this.loadTodo();
}
}
loadTodo() {
if (!this.todoId) return;
const todo = this.todoService.getTodoById(this.todoId);
if (todo) {
this.todoForm.patchValue({
title: todo.title,
description: todo.description,
priority: todo.priority,
dueDate: todo.dueDate
});
} else {
this.router.navigate(['/']);
}
}
onSubmit() {
if (this.todoForm.invalid) {
this.todoForm.markAllAsTouched();
return;
}
const formValue = this.todoForm.value;
if (this.isEditMode && this.todoId) {
// Update
this.todoService.updateTodo(this.todoId, formValue);
} else {
// Create
this.todoService.createTodo(formValue as any);
}
this.router.navigate(['/']);
}
cancel() {
this.router.navigate(['/']);
}
get title() { return this.todoForm.get('title'); }
get description() { return this.todoForm.get('description'); }
get priority() { return this.todoForm.get('priority'); }
}
features/todo-form/todo-form.component.html:
<div class="form-container">
<h1>{{ isEditMode ? 'Todo bearbeiten' : 'Neues Todo' }}</h1>
<form [formGroup]="todoForm" (ngSubmit)="onSubmit()" class="todo-form">
<!-- Title -->
<div class="form-field">
<label>Titel *</label>
<input
type="text"
formControlName="title"
[class.error]="title?.invalid && title?.touched"
placeholder="z.B. Angular lernen"
>
<div *ngIf="title?.invalid && title?.touched" class="error-message">
<p *ngIf="title?.errors?.['required']">Titel ist erforderlich</p>
<p *ngIf="title?.errors?.['minlength']">Mindestens 3 Zeichen</p>
</div>
</div>
<!-- Description -->
<div class="form-field">
<label>Beschreibung *</label>
<textarea
formControlName="description"
rows="5"
[class.error]="description?.invalid && description?.touched"
placeholder="Was möchtest du tun?"
></textarea>
<div *ngIf="description?.invalid && description?.touched" class="error-message">
<p *ngIf="description?.errors?.['required']">Beschreibung ist erforderlich</p>
<p *ngIf="description?.errors?.['minlength']">Mindestens 10 Zeichen</p>
</div>
</div>
<!-- Priority -->
<div class="form-field">
<label>Priorität *</label>
<select formControlName="priority">
<option value="low">Niedrig</option>
<option value="medium">Mittel</option>
<option value="high">Hoch</option>
</select>
</div>
<!-- Due Date -->
<div class="form-field">
<label>Fälligkeitsdatum</label>
<input type="date" formControlName="dueDate">
</div>
<!-- Actions -->
<div class="form-actions">
<button type="submit" class="btn-primary" [disabled]="todoForm.invalid">
{{ isEditMode ? 'Speichern' : 'Erstellen' }}
</button>
<button type="button" (click)="cancel()" class="btn-secondary">
Abbrechen
</button>
</div>
</form>
</div>
features/todo-form/todo-form.component.css:
.form-container {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
}
h1 {
margin-bottom: 2rem;
}
.todo-form {
background: white;
padding: 2rem;
border-radius: 12px;
border: 2px solid #e5e7eb;
}
.form-field {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #374151;
}
input, textarea, select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #667eea;
}
input.error, textarea.error {
border-color: #ef4444;
}
.error-message {
margin-top: 0.5rem;
color: #ef4444;
font-size: 0.875rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.btn-primary {
flex: 1;
padding: 0.75rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover:not(:disabled) {
background: #5a67d8;
}
.btn-primary:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.btn-secondary {
flex: 1;
padding: 0.75rem;
background: white;
color: #374151;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: #f3f4f6;
}
Schritt 6: Todo Detail Component
ng g c features/todo-detail
features/todo-detail/todo-detail.component.ts:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TodoService } from '../../core/services/todo.service';
import { Todo } from '../../core/models/todo.model';
@Component({
selector: 'app-todo-detail',
templateUrl: './todo-detail.component.html',
styleUrls: ['./todo-detail.component.css']
})
export class TodoDetailComponent implements OnInit {
todo: Todo | null = null;
constructor(
private route: ActivatedRoute,
private router: Router,
private todoService: TodoService
) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.todo = this.todoService.getTodoById(parseInt(id)) || null;
}
if (!this.todo) {
this.router.navigate(['/']);
}
}
toggleCompleted() {
if (this.todo) {
this.todoService.toggleTodo(this.todo.id);
this.todo = this.todoService.getTodoById(this.todo.id) || null;
}
}
editTodo() {
if (this.todo) {
this.router.navigate(['/edit', this.todo.id]);
}
}
deleteTodo() {
if (!this.todo) return;
if (confirm('Todo wirklich löschen?')) {
this.todoService.deleteTodo(this.todo.id);
this.router.navigate(['/']);
}
}
goBack() {
this.router.navigate(['/']);
}
}
Template & Styles ähnlich wie oben, mit Detail-View Layout.
Schritt 7: Routing Setup
app-routing.module.ts:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TodoListComponent } from './features/todo-list/todo-list.component';
import { TodoFormComponent } from './features/todo-form/todo-form.component';
import { TodoDetailComponent } from './features/todo-detail/todo-detail.component';
const routes: Routes = [
{ path: '', component: TodoListComponent },
{ path: 'create', component: TodoFormComponent },
{ path: 'edit/:id', component: TodoFormComponent },
{ path: 'todo/:id', component: TodoDetailComponent },
{ path: '**', redirectTo: '' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Schritt 8: App Module
app.module.ts:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// Components
import { HeaderComponent } from './shared/components/header/header.component';
import { LoadingSpinnerComponent } from './shared/components/loading-spinner/loading-spinner.component';
import { TodoListComponent } from './features/todo-list/todo-list.component';
import { TodoFormComponent } from './features/todo-form/todo-form.component';
import { TodoDetailComponent } from './features/todo-detail/todo-detail.component';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
LoadingSpinnerComponent,
TodoListComponent,
TodoFormComponent,
TodoDetailComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Schritt 9: App Component
app.component.html:
<app-header></app-header>
<main class="main-content">
<router-outlet></router-outlet>
</main>
app.component.css:
.main-content {
min-height: calc(100vh - 100px);
padding: 2rem 0;
background: #f9fafb;
}
styles.css (Global):
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
color: #111827;
background: #f9fafb;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
Zusammenfassung des Projekts
Was du gebaut hast:
- ✅ Complete CRUD Application
- ✅ Reactive Forms mit Validation
- ✅ Routing mit Edit/Detail Pages
- ✅ State Management mit Services & Observables
- ✅ LocalStorage Persistence
- ✅ Filter & Search Functionality
- ✅ Responsive Design
- ✅ Best Practices (DRY, SOLID, Clean Code)
Zusammenfassung
Was du gelernt hast:
- ✅ Projekt-Struktur professionell organisieren
- ✅ Services für State Management & Business Logic
- ✅ Reactive Forms mit Validation
- ✅ Routing für Multi-Page Apps
- ✅ Observables für reaktive Updates
- ✅ Component Communication (Services, Routing)
- ✅ LocalStorage für Persistence
- ✅ TypeScript Interfaces für Type Safety
Key Takeaways:
- Services = Single Source of Truth
- BehaviorSubject + Observable = Reactive State
- Reactive Forms > Template-Driven Forms
- Component = UI, Service = Logic
- Always unsubscribe (oder async pipe nutzen)
Du hast jetzt eine vollständige, produktionsreife Angular App! 🎉
Erweiterungsmöglichkeiten
Was du noch hinzufügen könntest:
-
Backend Integration:
- REST API mit Node.js/Express
- HttpClient statt LocalStorage
- Error Handling & Retry Logic
-
Advanced Features:
- Drag & Drop für Todo-Sortierung
- Tags/Categories für Todos
- Dark Mode Toggle
- Export als PDF/CSV
- Todo-Statistiken Dashboard
-
Authentication:
- Login/Register Pages
- Auth Guards für Protected Routes
- JWT Token Management
-
Testing:
- Unit Tests (Jasmine/Karma)
- E2E Tests (Cypress)
- Test Coverage > 80%
-
Performance:
- Lazy Loading für Features
- OnPush Change Detection
- Virtual Scrolling für große Listen
Quiz
📝 Quiz
Warum nutzen wir BehaviorSubject statt Subject?
📝 Quiz
Wofür nutzen wir async pipe?
Nächste Schritte
Du bist jetzt ein Angular Developer! 🚀
Weiterlernen:
- NgRx für Advanced State Management
- Angular Material für UI Components
- PWA Features (Offline Support)
- Server-Side Rendering (Angular Universal)
- Testing mit Jest statt Jasmine
Build & Deploy:
ng build --configuration production
Deployen auf:
- Vercel
- Netlify
- Firebase Hosting
- AWS S3
Glückwunsch! Du hast Angular gemeistert! 💪
Gut gemacht! 🎉
Du hast "Angular Todo App - Komplettes Projekt von A bis Z" abgeschlossen
Artikel bewerten
Bitte einloggen um zu bewerten
Das könnte dich auch interessieren
Angular Reactive Forms - Formulare professionell erstellen
Meistere Reactive Forms in Angular: Form Validation, Custom Validators, Dynamic Forms und Best Practices für robuste Formulare.
Angular HTTP Client & APIs - Backend-Kommunikation
Lerne HTTP Requests mit Angular HttpClient: GET, POST, PUT, DELETE, Error Handling, Interceptors und RxJS Operators für professionelle API-Integration.
Angular Routing & Navigation - Multi-Page Apps erstellen
Lerne Angular Router kennen, erstelle Routes, navigiere zwischen Seiten, nutze Route Parameters und schütze Routes mit Guards.