Kurz: Zum Abschluss baust du eine komplette kleine Anwendung, in der fast alles aus dem Kurs steckt: DOM, Events, Formulare, Arrays, JSON und localStorage. Du kannst den Code direkt ausführen und verändern.
Teil des Kurses JavaScript
Kapitel 39 von 40 im Kurs JavaScript (Abschnitt „Projekte und Nachschlagen“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.
Zum Abschluss baust du eine komplette kleine Anwendung, in der fast alles aus dem Kurs steckt: DOM, Events, Formulare, Arrays, JSON und localStorage. Du kannst den Code direkt ausführen und verändern.
Die Anforderungen
- Aufgaben hinzufügen (Enter oder Button)
- Aufgaben abhaken und löschen
- Filter: alle, offen, erledigt
- Zähler der offenen Aufgaben
- Daten bleiben nach dem Neuladen erhalten
Der komplette Code
todo.html
<style>
body { font-family: system-ui, sans-serif; max-width: 420px; margin: 1rem auto; padding: 0 1rem; }
form { display: flex; gap: .5rem; }
input[type=text] { flex: 1; padding: .5rem; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: .5rem; padding: .4rem 0; border-bottom: 1px solid #ddd; }
li.fertig span { text-decoration: line-through; color: #888; }
li span { flex: 1; }
.filter button[aria-pressed=true] { font-weight: bold; text-decoration: underline; }
</style>
<h1>Aufgaben</h1>
<form id="neu">
<label class="visually-hidden" for="text" style="position:absolute;left:-9999px">Neue Aufgabe</label>
<input type="text" id="text" placeholder="Was ist zu tun?" required maxlength="80">
<button>Hinzufügen</button>
</form>
<div class="filter" role="group" aria-label="Filter">
<button data-filter="alle" aria-pressed="true">Alle</button>
<button data-filter="offen" aria-pressed="false">Offen</button>
<button data-filter="fertig" aria-pressed="false">Erledigt</button>
</div>
<ul id="liste"></ul>
<p id="zaehler" role="status"></p>
<script>
const SCHLUESSEL = "todo-demo";
let aufgaben = lade();
let filter = "alle";
function lade() {
try {
const daten = JSON.parse(localStorage.getItem(SCHLUESSEL));
return Array.isArray(daten) ? daten.filter(a => a && typeof a.text === "string") : [];
} catch { return []; }
}
function speichere() {
try { localStorage.setItem(SCHLUESSEL, JSON.stringify(aufgaben)); } catch {}
}
function sichtbar() {
if (filter === "offen") return aufgaben.filter(a => !a.fertig);
if (filter === "fertig") return aufgaben.filter(a => a.fertig);
return aufgaben;
}
function zeichne() {
const liste = document.getElementById("liste");
liste.replaceChildren(); // Liste leeren
for (const a of sichtbar()) {
const li = document.createElement("li");
li.classList.toggle("fertig", a.fertig);
li.dataset.id = a.id;
const box = document.createElement("input");
box.type = "checkbox";
box.checked = a.fertig;
box.setAttribute("aria-label", "Erledigt: " + a.text);
const text = document.createElement("span");
text.textContent = a.text; // textContent: sicher gegen XSS
const weg = document.createElement("button");
weg.textContent = "×";
weg.setAttribute("aria-label", "Löschen: " + a.text);
li.append(box, text, weg);
liste.append(li);
}
const offen = aufgaben.filter(a => !a.fertig).length;
document.getElementById("zaehler").textContent =
offen === 1 ? "1 Aufgabe offen" : `${offen} Aufgaben offen`;
}
document.getElementById("neu").addEventListener("submit", (e) => {
e.preventDefault();
const feld = document.getElementById("text");
const text = feld.value.trim();
if (!text) return;
aufgaben.push({ id: Date.now(), text, fertig: false });
feld.value = "";
speichere(); zeichne();
});
// Event Delegation: ein Listener für alle Einträge
document.getElementById("liste").addEventListener("click", (e) => {
const li = e.target.closest("li");
if (!li) return;
const id = Number(li.dataset.id);
if (e.target.matches("input[type=checkbox]")) {
const a = aufgaben.find(x => x.id === id);
a.fertig = e.target.checked;
} else if (e.target.matches("button")) {
aufgaben = aufgaben.filter(x => x.id !== id);
} else return;
speichere(); zeichne();
});
document.querySelector(".filter").addEventListener("click", (e) => {
const knopf = e.target.closest("button[data-filter]");
if (!knopf) return;
filter = knopf.dataset.filter;
for (const b of document.querySelectorAll(".filter button")) {
b.setAttribute("aria-pressed", String(b === knopf));
}
zeichne();
});
zeichne();
</script>Was du hier wiedererkennst
| Baustein | Wo |
|---|---|
| Arrays und Objekte | aufgaben ist ein Array von Objekten |
filter, find, push | Aufgaben auswählen, suchen, hinzufügen |
| DOM erzeugen | createElement, append, replaceChildren |
| Events und Delegation | ein Klick-Listener für Checkbox und Löschen |
| Formular | submit mit preventDefault, trim, Validierung |
JSON und localStorage | Speichern und sicheres Laden mit try/catch |
| Barrierefreiheit | aria-label, aria-pressed, role="status", echte Buttons |
| Sicherheit | textContent statt innerHTML |
Die Logik vom Rest trennen
Der Kern lässt sich ohne Browser testen. Trenne Daten und Darstellung:
function neueAufgabe(liste, text) {
const t = text.trim();
if (!t) return liste;
return [...liste, { id: liste.length + 1, text: t, fertig: false }];
}
const umschalten = (liste, id) =>
liste.map(a => (a.id === id ? { ...a, fertig: !a.fertig } : a));
const offen = liste => liste.filter(a => !a.fertig).length;
let l = [];
l = neueAufgabe(l, "Milch kaufen");
l = neueAufgabe(l, " "); // wird ignoriert
l = neueAufgabe(l, "Kurs abschließen");
l = umschalten(l, 1);
console.log(l);
console.log(offen(l), "offen");Ausgabe:
[
{ id: 1, text: 'Milch kaufen', fertig: true },
{ id: 2, text: 'Kurs abschließen', fertig: false }
]
1 offenReine Funktionen wie diese kannst du mit node --test leicht prüfen.
Ideen zum Weiterbauen
- Aufgaben per Doppelklick bearbeiten
- Fälligkeitsdatum mit
<input type="date">und Sortierung - Drag and Drop zum Umsortieren (
draggable, Ereignissedragstart,drop) - Alle erledigten Aufgaben auf einmal löschen
- Daten über
fetchmit einem Server abgleichen - Dunkles Design über
prefers-color-scheme - Tastaturkürzel (zum Beispiel
/fokussiert das Eingabefeld)
Merke
- Ein kleines Projekt verbindet Daten, Darstellung und Ereignisse
- Zustand (Array) ändern, danach neu zeichnen: ein einfaches, robustes Muster
- Event Delegation,
textContent,try/catchbeim Speichern und ARIA-Attribute gehören zur Grundausstattung - Trenne reine Logik von DOM-Code, damit du sie testen kannst
Übungsaufgabe
Erweitere die App um eine Schaltfläche “Erledigte löschen” und einen Zähler der erledigten Aufgaben.
Quiz zur Selbstkontrolle
Warum wird die Liste nach jeder Änderung neu gezeichnet?
- Der Zustand (Array) ist die einzige Wahrheit, die Anzeige folgt ihm (richtig)
- Weil das DOM sonst abstürzt
- Weil localStorage es verlangt
- Damit die Seite langsamer wird
Wozu dient Event Delegation hier?
- Ein Listener bedient alle (auch neue) Einträge (richtig)
- Sie verhindert Klicks
- Sie speichert Daten
- Sie prüft das Formular
Warum textContent statt innerHTML für den Aufgabentext?
- Damit eingeschleuster HTML-Code nicht ausgeführt wird (XSS) (richtig)
- Weil es kürzer ist
- Weil innerHTML nicht existiert
- Damit Text fett wird
Weiter im Kurs
Zurück: Sauberer Code und häufige Fallen
Weiter: Nachschlagen: Spickzettel
Alle Kapitel: JavaScript im Überblick