EMZETT.
Login

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

BausteinWo
Arrays und Objekteaufgaben ist ein Array von Objekten
filter, find, pushAufgaben auswählen, suchen, hinzufügen
DOM erzeugencreateElement, append, replaceChildren
Events und Delegationein Klick-Listener für Checkbox und Löschen
Formularsubmit mit preventDefault, trim, Validierung
JSON und localStorageSpeichern und sicheres Laden mit try/catch
Barrierefreiheitaria-label, aria-pressed, role="status", echte Buttons
SicherheittextContent 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 offen

Reine Funktionen wie diese kannst du mit node --test leicht prüfen.

Ideen zum Weiterbauen

  1. Aufgaben per Doppelklick bearbeiten
  2. Fälligkeitsdatum mit <input type="date"> und Sortierung
  3. Drag and Drop zum Umsortieren (draggable, Ereignisse dragstart, drop)
  4. Alle erledigten Aufgaben auf einmal löschen
  5. Daten über fetch mit einem Server abgleichen
  6. Dunkles Design über prefers-color-scheme
  7. 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/catch beim 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

Weiter im Kurs

Zurück: Sauberer Code und häufige Fallen

Weiter: Nachschlagen: Spickzettel

Alle Kapitel: JavaScript im Überblick