JavaScript Fortgeschritten

Projekt: Todo-App mit JavaScript bauen

Projekt: Todo-App mit JavaScript

Zeit, alles Gelernte anzuwenden! In diesem Projekt bauen wir eine vollständige Todo-App mit:

  • Todos hinzufügen, löschen, bearbeiten
  • Als erledigt markieren
  • Filter (Alle, Aktiv, Erledigt)
  • LocalStorage Persistenz
  • Animationen
  • Clean Code Architektur

Das Endergebnis

Unsere App wird diese Features haben:

+------------------------------------------+
|  Meine Todos                             |
+------------------------------------------+
|  [________________________] [Hinzufuegen]|
+------------------------------------------+
|  [Alle] [Aktiv] [Erledigt]   3 uebrig    |
+------------------------------------------+
|  [ ] Einkaufen gehen                  X  |
|  [x] JavaScript lernen                X  |
|  [ ] Projekt fertigstellen            X  |
+------------------------------------------+
|  [Erledigte loeschen]                    |
+------------------------------------------+

Projekt-Setup

Erstelle diese Dateien:

todo-app/
├── index.html
├── style.css
└── app.js

HTML Grundgeruest

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="app">
        <header class="header">
            <h1>Meine Todos</h1>
        </header>

        <main class="main">
            <form class="todo-form" id="todo-form">
                <input
                    type="text"
                    class="todo-input"
                    id="todo-input"
                    placeholder="Was muss erledigt werden?"
                    autocomplete="off"
                >
                <button type="submit" class="add-btn">Hinzufuegen</button>
            </form>

            <div class="filters">
                <div class="filter-buttons">
                    <button class="filter-btn active" data-filter="all">Alle</button>
                    <button class="filter-btn" data-filter="active">Aktiv</button>
                    <button class="filter-btn" data-filter="completed">Erledigt</button>
                </div>
                <span class="todo-count" id="todo-count">0 Todos</span>
            </div>

            <ul class="todo-list" id="todo-list"></ul>

            <div class="actions">
                <button class="clear-btn" id="clear-completed">Erledigte loeschen</button>
            </div>
        </main>
    </div>

    <script src="app.js"></script>
</body>
</html>

CSS Styling

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

:root {
    --primary: #6366f1;
    --primary-dark: #4f46e5;
    --success: #22c55e;
    --danger: #ef4444;
    --bg: #0f172a;
    --card: #1e293b;
    --border: #334155;
    --text: #f1f5f9;
    --text-muted: #94a3b8;
}

body {
    font-family: 'Segoe UI', system-ui, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    padding: 2rem;
}

.app {
    width: 100%;
    max-width: 500px;
}

.header h1 {
    font-size: 2rem;
    text-align: center;
    margin-bottom: 2rem;
    background: linear-gradient(135deg, var(--primary), #a855f7);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.todo-form {
    display: flex;
    gap: 0.75rem;
    margin-bottom: 1.5rem;
}

.todo-input {
    flex: 1;
    padding: 0.875rem 1rem;
    background: var(--card);
    border: 2px solid var(--border);
    border-radius: 12px;
    color: var(--text);
    font-size: 1rem;
}

.todo-input:focus {
    outline: none;
    border-color: var(--primary);
}

.add-btn {
    padding: 0.875rem 1.5rem;
    background: var(--primary);
    color: white;
    border: none;
    border-radius: 12px;
    cursor: pointer;
}

.filters {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
    padding: 0.75rem;
    background: var(--card);
    border-radius: 12px;
}

.filter-btn {
    padding: 0.5rem 1rem;
    background: transparent;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
}

.filter-btn.active {
    background: var(--primary);
    color: white;
    border-radius: 8px;
}

.todo-list {
    list-style: none;
}

.todo-item {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    margin-bottom: 0.75rem;
    background: var(--card);
    border-radius: 12px;
    animation: slideIn 0.3s ease;
}

@keyframes slideIn {
    from { opacity: 0; transform: translateY(-10px); }
    to { opacity: 1; transform: translateY(0); }
}

.todo-item.completed .todo-text {
    text-decoration: line-through;
    color: var(--text-muted);
}

.todo-checkbox {
    width: 24px;
    height: 24px;
    border: 2px solid var(--border);
    border-radius: 6px;
    cursor: pointer;
}

.todo-item.completed .todo-checkbox {
    background: var(--success);
    border-color: var(--success);
}

.todo-text {
    flex: 1;
}

.delete-btn {
    padding: 0.5rem;
    background: transparent;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
    opacity: 0;
}

.todo-item:hover .delete-btn {
    opacity: 1;
}

.delete-btn:hover {
    color: var(--danger);
}

.clear-btn {
    display: block;
    margin: 1.5rem auto 0;
    padding: 0.75rem 1.5rem;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 10px;
    color: var(--text-muted);
    cursor: pointer;
}

JavaScript - Die App-Logik

Jetzt der spannende Teil! Wir bauen die App mit einer Clean Code Architektur.

// === State Management ===
const state = {
    todos: [],
    filter: 'all'
};

// === DOM Elements ===
const elements = {
    form: document.getElementById('todo-form'),
    input: document.getElementById('todo-input'),
    list: document.getElementById('todo-list'),
    count: document.getElementById('todo-count'),
    clearBtn: document.getElementById('clear-completed'),
    filterBtns: document.querySelectorAll('.filter-btn')
};

// === Utility Functions ===
function generateId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

function saveToStorage() {
    localStorage.setItem('todos', JSON.stringify(state.todos));
}

function loadFromStorage() {
    const stored = localStorage.getItem('todos');
    if (stored) {
        state.todos = JSON.parse(stored);
    }
}

// === Todo CRUD Operations ===
function addTodo(text) {
    const todo = {
        id: generateId(),
        text: text.trim(),
        completed: false,
        createdAt: new Date().toISOString()
    };

    state.todos.unshift(todo);
    saveToStorage();
    render();
    return todo;
}

function deleteTodo(id) {
    state.todos = state.todos.filter(todo => todo.id !== id);
    saveToStorage();
    render();
}

function toggleTodo(id) {
    const todo = state.todos.find(todo => todo.id === id);
    if (todo) {
        todo.completed = !todo.completed;
        saveToStorage();
        render();
    }
}

function clearCompleted() {
    state.todos = state.todos.filter(todo => !todo.completed);
    saveToStorage();
    render();
}

// === Filtering ===
function getFilteredTodos() {
    switch (state.filter) {
        case 'active':
            return state.todos.filter(todo => !todo.completed);
        case 'completed':
            return state.todos.filter(todo => todo.completed);
        default:
            return state.todos;
    }
}

function setFilter(filter) {
    state.filter = filter;
    elements.filterBtns.forEach(btn => {
        btn.classList.toggle('active', btn.dataset.filter === filter);
    });
    render();
}

// === Rendering ===
function createTodoElement(todo) {
    const li = document.createElement('li');
    li.className = 'todo-item' + (todo.completed ? ' completed' : '');
    li.dataset.id = todo.id;

    const checkbox = document.createElement('div');
    checkbox.className = 'todo-checkbox';
    checkbox.textContent = todo.completed ? '✓' : '';

    const text = document.createElement('span');
    text.className = 'todo-text';
    text.textContent = todo.text;

    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'delete-btn';
    deleteBtn.textContent = '✕';

    // Event Listeners
    checkbox.addEventListener('click', () => toggleTodo(todo.id));
    deleteBtn.addEventListener('click', () => deleteTodo(todo.id));

    li.appendChild(checkbox);
    li.appendChild(text);
    li.appendChild(deleteBtn);

    return li;
}

function renderTodoList() {
    const filteredTodos = getFilteredTodos();
    elements.list.textContent = '';

    if (filteredTodos.length === 0) {
        const empty = document.createElement('p');
        empty.textContent = 'Keine Todos vorhanden';
        empty.style.textAlign = 'center';
        empty.style.color = 'var(--text-muted)';
        empty.style.padding = '2rem';
        elements.list.appendChild(empty);
        return;
    }

    filteredTodos.forEach(todo => {
        elements.list.appendChild(createTodoElement(todo));
    });
}

function renderCount() {
    const activeCount = state.todos.filter(todo => !todo.completed).length;
    elements.count.textContent = activeCount === 1
        ? '1 Todo uebrig'
        : activeCount + ' Todos uebrig';
}

function render() {
    renderTodoList();
    renderCount();
}

// === Event Handlers ===
function handleSubmit(e) {
    e.preventDefault();
    const text = elements.input.value.trim();
    if (!text) return;

    addTodo(text);
    elements.input.value = '';
    elements.input.focus();
}

// === Initialization ===
function init() {
    loadFromStorage();

    elements.form.addEventListener('submit', handleSubmit);
    elements.clearBtn.addEventListener('click', clearCompleted);

    elements.filterBtns.forEach(btn => {
        btn.addEventListener('click', () => setFilter(btn.dataset.filter));
    });

    render();
    console.log('Todo App initialized!');
}

init();

Die App verstehen

1. State Management

const state = {
    todos: [],
    filter: 'all'
};

Der State ist die einzige Quelle der Wahrheit.

2. Unidirektionaler Datenfluss

User Action -> State aendern -> render() -> UI Update

3. Separation of Concerns

  • CRUD Functions: addTodo, deleteTodo, toggleTodo
  • Rendering: render, renderTodoList, createTodoElement
  • State: Zentralisiert
  • Events: Separate Handler

Best Practices angewendet

  1. Single Source of Truth: State-Objekt
  2. Pure Functions: CRUD-Operationen
  3. Separation of Concerns: Klare Trennung
  4. LocalStorage: Persistenz
  5. Animations: Bessere UX
  6. Semantic HTML: Accessibility
  7. Responsive Design: Mobile-first

Zusammenfassung

Du hast gelernt:

  • Komplette App-Architektur
  • State Management
  • DOM Manipulation
  • Event Handling
  • LocalStorage
  • CSS Animationen
  • Clean Code Patterns

Challenge: Erweitere die App um Kategorien, Faelligkeitsdaten und einen Dark/Light Mode Toggle!