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
- Single Source of Truth: State-Objekt
- Pure Functions: CRUD-Operationen
- Separation of Concerns: Klare Trennung
- LocalStorage: Persistenz
- Animations: Bessere UX
- Semantic HTML: Accessibility
- 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!