mitmario.dev

Abschluss: eine ganze Anwendung

JavaScript Im Browser 5 Min Lesezeit 3 BeispieleLektion 6 von 7

In den letzten Abschnitten ist einiges entstanden: ein Kassenzettel, ein Einmaleins, ein Werkzeugkasten, eine Bestenliste, eine Stoppuhr, eine Galerie, eine Einstellungsleiste. Jedes Stück für sich, jedes zu genau einem Thema.

Diese Lektion sammelt sie wieder ein. Die Aufgabe danach ist die längste des Kurses, und sie bringt nichts Neues mit. Alles darin kennst du.

Die Reihenfolge, in der man baut

Es gibt eine, und sie ist fast immer dieselbe.

Erst die Daten. Woher kommen sie, wie sehen sie aus? Schreib dir ein Beispielobjekt hin, bevor du irgendetwas anderes tust. Alles Weitere hängt daran.

Dann die Logik. Filtern, sortieren, rechnen, prüfen. Reine Funktionen, die Werte hinein und Werte heraus reichen, und die du mit ein paar Beispielen abklopfen kannst, ohne dass es eine Seite gibt.

Dann die Darstellung. Eine Funktion, die aus dem aktuellen Stand die Ansicht baut.

Zuletzt die Bedienung. Klicks, Eingaben, Formulare. Sie ändern den Stand und stoßen das Neuzeichnen an, mehr tun sie nicht.

Mit dem Aussehen anzufangen ist verlockend, weil man sofort etwas sieht. Es rächt sich trotzdem: Man baut Elemente für Daten, die es noch nicht gibt, und stellt beim Anschließen fest, dass die echten Daten anders aussehen. Dann sieht man zwar immer noch etwas, aber es stimmt nicht.

Ein Zustand in der Mitte

Ein Zustand, eine Zeichenfunktion
const AUFGABEN = [
  { titel: "Einkaufen", offen: true },
  { titel: "Steuer", offen: true },
  { titel: "Rasen mähen", offen: false },
  { titel: "Rechnung schreiben", offen: false },
];

// Der Zustand: alles, was die Ansicht bestimmt, an einer Stelle.
const zustand = { suche: "", nurOffen: false };

const liste = document.getElementById("liste");
const zaehler = document.getElementById("zaehler");

// Eine Funktion rechnet aus dem Zustand die sichtbare Liste.
function sichtbar() {
  return AUFGABEN.filter((aufgabe) => {
    const passtZurSuche = aufgabe.titel
      .toLowerCase()
      .includes(zustand.suche.toLowerCase());
    const passtZumFilter = !zustand.nurOffen || aufgabe.offen;
    return passtZurSuche && passtZumFilter;
  });
}

// Eine Funktion zeichnet alles neu. Immer alles.
function zeichnen() {
  const treffer = sichtbar();
  liste.textContent = "";

  for (const aufgabe of treffer) {
    const eintrag = document.createElement("li");
    eintrag.className = aufgabe.offen ? "eintrag" : "eintrag erledigt";
    eintrag.textContent = aufgabe.titel;
    liste.append(eintrag);
  }

  zaehler.textContent = `${treffer.length} von ${AUFGABEN.length} sichtbar`;
}

// Die Ereignisse ändern nur den Zustand und zeichnen neu.
document.getElementById("suche").addEventListener("input", (ereignis) => {
  zustand.suche = ereignis.target.value;
  zeichnen();
});

document.getElementById("nur-offen").addEventListener("click", () => {
  zustand.nurOffen = !zustand.nurOffen;
  zeichnen();
});

zeichnen();

Das ist das Muster, um das es geht, und es besteht aus drei Teilen.

Ein Objekt hält alles, was die Ansicht bestimmt. Hier sind es zwei Werte, ein Suchtext und ein Schalter. In größeren Anwendungen sind es zwanzig, und das Prinzip bleibt.

Eine Funktion zeichnet die Ansicht aus diesem Objekt. Immer die ganze Ansicht, nicht nur den geänderten Teil. Das klingt verschwenderisch und ist bei dieser Größenordnung völlig unproblematisch.

Die Ereignisse ändern nur den Zustand und rufen dann zeichnen(). Kein Handler fasst ein Element direkt an.

Der Gewinn ist, dass es keine zweite Wahrheit gibt. Was auf dem Bildschirm steht, ist immer das Ergebnis des Zustands. Du kannst nie in eine Lage geraten, in der die Liste gefiltert ist und der Zähler etwas anderes behauptet.

Warum nicht ohne

Dieselbe Anwendung ohne Zustand
// Kein Zustand. Jeder Handler fasst direkt die Elemente an.
const eintraege = document.querySelectorAll(".eintrag");
const zaehler = document.getElementById("zaehler");

document.getElementById("suche").addEventListener("input", (ereignis) => {
  const suche = ereignis.target.value.toLowerCase();
  let sichtbare = 0;

  for (const eintrag of eintraege) {
    const passt = eintrag.textContent.toLowerCase().includes(suche);
    eintrag.hidden = !passt;
    if (passt) {
      sichtbare = sichtbare + 1;
    }
  }

  zaehler.textContent = `${sichtbare} von 4 sichtbar`;
});

document.getElementById("nur-offen").addEventListener("click", () => {
  // Und hier fängt es an: dieser Handler weiß nichts von der
  // Suche. Tipp erst etwas ein und drück dann den Knopf.
  for (const eintrag of eintraege) {
    eintrag.hidden = eintrag.dataset.offen === "false";
  }
});

Probier das zweite Beispiel aus, es ist kaputt und sieht nicht so aus. Tipp „re” in die Suche. Jetzt steht genau eine Zeile da, „Rechnung schreiben”, und der Zähler sagt „1 von 4 sichtbar”. So weit ist alles in Ordnung.

Drück nun auf „Nur offene”. Drei Sachen stimmen ab diesem Moment nicht mehr zusammen. Im Suchfeld steht weiterhin „re”. Auf dem Bildschirm stehen „Einkaufen” und „Steuer”, und in keinem von beiden kommt „re” vor; die einzige Zeile, die zur Suche passte, ist verschwunden. Und der Zähler sagt immer noch „1 von 4 sichtbar”, während zwei dastehen.

Keines davon ist ein Rechenfehler. Jede der beiden Funktionen rechnet für sich richtig.

Der Grund ist banal: Der zweite Handler weiß nichts vom ersten. Beide fassen dieselben Elemente an und überschreiben sich gegenseitig.

Reparieren ließe sich das, indem der zweite Handler die Suche mitliest, und der erste den Filter. Bei zwei Bedienelementen sind das zwei Stellen, bei fünf sind es zwanzig. Das ist der Punkt, an dem ein zentraler Zustand aufhört, Theorie zu sein.

Die vier Zustände

Die vier Zustände einer Liste
const liste = document.getElementById("liste");
const status = document.getElementById("status");

async function laden(adresse, suche) {
  // Zustand 1: es läuft gerade.
  status.textContent = "Lade ...";
  liste.textContent = "";

  try {
    const antwort = await fetch(adresse);
    if (!antwort.ok) {
      throw new Error(`Status ${antwort.status}`);
    }

    const kurse = await antwort.json();
    const treffer = kurse.filter((kurs) =>
      kurs.titel.toLowerCase().includes(suche)
    );

    if (treffer.length === 0) {
      // Zustand 2: geladen, aber nichts dabei.
      status.textContent = "Keine Kurse gefunden.";
      return;
    }

    // Zustand 3: Daten da.
    status.textContent = "";
    for (const kurs of treffer) {
      const eintrag = document.createElement("li");
      eintrag.textContent = `${kurs.titel} (${kurs.stunden} h)`;
      liste.append(eintrag);
    }
  } catch (fehler) {
    // Zustand 4: schiefgegangen.
    status.textContent = "Konnte nicht geladen werden.";
    console.error(fehler);
  }
}

document.getElementById("laden").addEventListener("click", () => {
  laden("/lernen/api/katalog.json", "");
});

document.getElementById("kaputt").addEventListener("click", () => {
  laden("/lernen/api/gibtesnicht.json", "");
});

document.getElementById("leer-laden").addEventListener("click", () => {
  laden("/lernen/api/katalog.json", "zzz");
});

Aus Lektion 16.4, und der wichtigste Absatz für die Aufgabe. Jede Liste, die aus dem Netz kommt, hat vier mögliche Ausgänge, und drei davon werden gern vergessen.

Es lädt gerade. Der Nutzer muss sehen, dass etwas passiert.

Es ist etwas da. Der Normalfall, den alle bauen.

Es ist nichts da. Geladen, aber ohne Treffer. Eine leere Liste ohne Erklärung sieht aus wie ein Fehler.

Es ist schiefgegangen. Eine Meldung, die sagt, was los ist, und nach Möglichkeit eine Schaltfläche zum Wiederholen.

Die Abnahmeliste

Wenn du fertig bist, geh diese sechs Punkte durch. Sie sind aus Erfahrung entstanden und decken die meisten Fehler ab. Fünf von sechs kannst du hier abhaken, ohne die Lektionsansicht zu verlassen.

Steht die Console leer? Am Reiter Console steht eine Zahl, und bei einem Fehler ist sie rot. Eine rote Zeile, die niemanden stört, stört später jemanden.

Gibt es alle vier Ladezustände? Die Aufgabe hat zwei Knöpfe dafür, einen fürs Laden und einen für eine Adresse, die es nicht gibt. Den Leerzustand erreichst du mit einer Suche ohne Treffer. Im Reiter Netzwerk steht dazu jede Anfrage mit ihrem Status, auch die mit 404.

Kommst du ohne Maus durch die Seite? Klick in die Seite und geh mit Tab durch. Wo du bist, sagt document.activeElement.id in der Eingabezeile der Console.

Übersteht die Einstellung einen Neustart, sofern sie das soll? Drück auf Seite neu laden, den Kreispfeil in der Browser-Leiste. Was im Browserspeicher liegt, überlebt das, dein Zustandsobjekt nicht.

Was passiert bei leerer Eingabe? Bei einem Leerzeichen? Bei zweihundert Zeichen?

Was passiert bei kaputten Daten? Ein fehlendes Feld, ein null, ein leeres Array. Das ist der eine Punkt, für den du selbst Hand anlegen musst: Änder die Daten im Code, statt auf den Fall zu warten.

Was als Nächstes kommt

Du hast in diesem Kurs von Hand gebaut, was Frameworks abnehmen: einen Zustand, eine Funktion, die daraus die Ansicht erzeugt, und Ereignisse, die den Zustand ändern.

Genau das ist der Kern von React, Vue und Svelte. Sie nehmen dir das zeichnen() ab und rechnen selbst aus, welche Elemente sich geändert haben. Der Rest ist ihre Schreibweise.

Der Weg dorthin ist deshalb kürzer, als er aussieht. Wer das Muster verstanden hat, lernt ein Framework in ein paar Tagen. Wer es nicht verstanden hat, lernt Zauberformeln auswendig und steht beim ersten ungewöhnlichen Fall im Dunkeln.

Und bevor du dorthin gehst: Bau erst zwei oder drei kleine Sachen ohne. Nicht aus Prinzip, sondern weil dir dabei die Fragen kommen, auf die ein Framework die Antwort ist.

Zum Mitnehmen

Ein Zustand in der Mitte, eine Funktion, die daraus die Seite zeichnet, und Ereignisse, die nur den Zustand ändern. Mehr Architektur braucht eine kleine Anwendung nicht.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 3 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.