Fortgeschritten352025-01-31

Angular Todo App - Komplettes Projekt von A bis Z

Baue eine vollständige Todo-Anwendung mit Angular: Components, Services, Routing, Forms, HTTP und alle Best Practices in einem echten Projekt.

#angular#project#todo-app#crud#best-practices#real-world

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:

  1. Backend Integration:

    • REST API mit Node.js/Express
    • HttpClient statt LocalStorage
    • Error Handling & Retry Logic
  2. Advanced Features:

    • Drag & Drop für Todo-Sortierung
    • Tags/Categories für Todos
    • Dark Mode Toggle
    • Export als PDF/CSV
    • Todo-Statistiken Dashboard
  3. Authentication:

    • Login/Register Pages
    • Auth Guards für Protected Routes
    • JWT Token Management
  4. Testing:

    • Unit Tests (Jasmine/Karma)
    • E2E Tests (Cypress)
    • Test Coverage > 80%
  5. 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! 💪

AngularLektion 10 von 10
100% abgeschlossen
Lektion abgeschlossen!

Gut gemacht! 🎉

Du hast "Angular Todo App - Komplettes Projekt von A bis Z" abgeschlossen

Artikel bewerten

0.0 (0 Bewertungen)

Bitte einloggen um zu bewerten