Zum Inhalt
wannacode

Eine Liste aus Daten

Kurz gesagt: Rendern aus Daten heißt: Eine Funktion render() baut die Liste auf der Seite aus einem Array, damit jede Stelle dasselbe zeigt.

Problem

Der Warenkorb lügt

Eine Käuferin legt Brot und Milch hinein. Die Zeilen im Warenkorb stimmen, die Summe stimmt, und oben steht „Warenkorb (0)“. Klick auf beide Buttons „Hinzufügen“ und sieh dir den Zähler an.

HTML, CSS, JS

Die Konsole ist leer. Jede Stelle der Seite wird in add von einer eigenen Codezeile aktualisiert, und für den Zähler gibt es keine solche Zeile. Der Browser weiß nicht, dass die drei Stellen dasselbe zeigen. Deshalb bemerkt niemand den Fehler, außer der Käuferin.

Die Idee

Von einem Array zu einer Liste

Die Produkte kommen als Daten: ein Array von Objekten mit Name und Preis. Die Seite muss sie als Listeneinträge zeigen. Der Weg ist bei jeder Liste derselbe:

function productItem(product) {
const li = document.createElement('li');
li.textContent = `${product.name}, ${money(product.price)}`;
return li;
}
list.replaceChildren(...products.map(productItem));
  • document.createElement('li') erzeugt ein neues Element. Es ist noch nicht auf der Seite.
  • textContent setzt den Text. Der Name des Produkts ist Text, nicht HTML, also wird kein Zeichen darin zu einem Tag.
  • products.map(productItem) macht aus jedem Produkt ein <li>: ein Array von Elementen.
  • list.replaceChildren(...) entfernt die alten Kinder der Liste und setzt die neuen ein. ... verteilt das Array auf einzelne Argumente.

Das heißt eine Liste aus Daten rendern: Die Daten leben in JavaScript, und die Seite wird von einer Funktion daraus gebaut.

Sieh es dir an

Der Katalog

HTML, CSS, JS

Ändere im Array den Preis des Brots und klick auf „Ausführen“. Das HTML fasst du nicht an: Die Liste folgt den Daten. Füge dem Array ein fünftes Produkt hinzu, und es erscheint als fünfte Zeile.

Probier es aus

Nur das, was auf Lager ist

Zeige in der Liste nur die Produkte, die auf Lager sind (stock ist größer als 0). Schreibe unter die Liste, in #count, wie viele es sind: „Verfügbar: 3 von 5“.

HTML, CSS, JS

Die Idee

Ein Datensatz, drei Stellen

Der Warenkorb ist ein Array cart. Die Seite zeigt es dreimal: die Anzahl oben, die Zeilen und die Summe. Jedes Mal, wenn sich cart ändert, müssen sich die drei Stellen gemeinsam ändern.

Von Hand heißt das: Jede Funktion, die cart ändert, fasst jede Stelle an, die es zeigt. add fasst die Zeilen und die Summe an. remove fasst die Zeilen, die Summe und den Zähler an. Morgen kommt eine vierte Stelle dazu, sagen wir „Gratis-Lieferung ab 30 €“, und jede Funktion muss daran denken.

Wird eine Stelle vergessen, laufen die Daten und der Bildschirm auseinander: cart enthält zwei Produkte, und die Seite sagt null. Es gibt keinen Fehler. Es gibt eine Seite, die etwas Falsches zeigt.

Sieh es dir an

Sieh zu, wie sie auseinanderlaufen

const cart = [];
function addByHand(product) {
cart.push(product);
cartList.append(cartItem(product));
total.textContent = `Gesamt: ${money(sum())}`;
}
function render() {
badge.textContent = `Warenkorb (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Gesamt: ${money(sum())}`;
}
function add(product) {
cart.push(product);
render();
}
Schritt für Schritt, in Worten
  1. Links stehen die Daten: das Array cart. Rechts sind die drei Stellen auf der Seite, die es zeigen: der Zähler, die Zeilen und die Summe.
  2. addByHand(Brot). Zuerst ändern sich die Daten: Das Array hat ein Produkt.
  3. Dann fasst die Funktion den Bildschirm von Hand an: eine neue Zeile und eine neue Summe. Jede Stelle ist eine eigene Codezeile.
  4. Für den Zähler hat niemand eine Zeile geschrieben. Die Daten sagen 1, der Zähler sagt 0. In der Konsole steht kein Fehler: Die Seite zeigt einfach etwas Falsches.
  5. Noch ein Produkt, wieder von Hand. Der Abstand wächst: 2 gegen 0.
  6. Jetzt add(): Sie ändert nur die Daten. Für einen Moment hinkt der ganze Bildschirm hinterher, weil noch nichts gezeichnet wurde.
  7. render() zeichnet alle drei Stellen neu aus cart: den Zähler, jede Zeile und die Summe. Es interessiert sich nicht dafür, was vorher auf dem Bildschirm stand.
  8. Die Regel: Jede Änderung ist zuerst die Daten, dann render(). Der Bildschirm folgt aus den Daten, und eine vergessene Stelle ist unmöglich.

addByHand fasst zwei von drei Stellen an. render fasst alle drei an, jedes Mal, aus den Daten. Es fragt nicht, was vorher auf dem Bildschirm stand, deshalb kann es sich davon nicht in die Irre führen lassen.

Die Idee

Ein einziges render()

Die Regel ist kurz: Die Funktionen, die die Daten ändern, fassen den Bildschirm nicht an. Sie ändern die Daten und rufen render() auf. Nur render() schreibt auf die Seite, und es schreibt alles aus den Daten:

function render() {
badge.textContent = `Warenkorb (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Gesamt: ${money(sum())}`;
}

So ist jede Stelle einmal beschrieben. Eine neue Stelle ist eine Zeile in render(), und sie funktioniert für add, für remove und für jede künftige Funktion. Die Zeilen werden jedes Mal ganz neu gezeichnet: replaceChildren entfernt die alten und setzt neue aus cart.map(cartItem) ein.

Probier es aus

Ein einziges render()

add und remove fassen den Bildschirm von Hand an, und jede vergisst eine Stelle. Schreibe render() so, dass es den Zähler, die Zeilen und die Summe aus cart zeichnet, und lass add und remove nur cart ändern und render() aufrufen.

HTML, CSS, JS

Probier es aus

Finde den Fehler

Der Zähler ist immer ein Produkt hinterher: Nach dem ersten Klick sagt er 0, nach dem zweiten 1.

Wähle die Zeile mit dem Fehler.

Die Idee

Der Preis von „alles neu“

render() weiß nicht, was sich geändert hat, also ersetzt es alles. Meistens siehst du das nicht. Manchmal doch. Im Warenkorb unten hat jede Zeile ein Mengenfeld, und jede Änderung des Felds ruft render() auf. Die Käuferin will 15 Milch: Sie löscht die 2, tippt 1 und 5.

HTML, CSS, JS

Nach der Ziffer 1 entfernt render() das alte Feld und setzt ein neues mit dem Wert 1 ein. Der Fokus war auf dem alten Feld und ist weg, also geht die 5 ins Leere. Dasselbe passiert mit dem Cursor in einem Textfeld, mit der Scrollposition in einer langen Liste und mit einem geöffneten Dropdown. Und bei 1000 Zeilen baut der Browser bei jedem Klick 1000 neue Elemente.

Beide Wege sind schlecht: Von Hand vergisst du Stellen, und „alles neu“ verliert den Fokus und ist bei einer großen Liste langsam. Die Antwort ist ein dritter Weg: Du schreibst eine Funktion, die sagt, wie der Bildschirm für diese Daten aussieht, wie render(). Etwas anderes vergleicht das neue Ergebnis mit dem alten und ändert auf der Seite nur den Unterschied: den Text des Zählers, eine Zeile, nichts am Feld. Das machen React und Vue, und es ist das nächste Modul: Komponenten.

Genauer, für Neugierige

innerHTML statt createElement. list.innerHTML = products.map((p) => `<li>${p.name}</li>`).join('') ist kürzer, aber der Name des Produkts kommt als HTML hinein. Ein Name mit <img onerror=…> wird zu Code auf der Seite (XSS). textContent und createElement behalten Text als Text.

replaceChildren. Ohne Argumente leert es das Element: list.replaceChildren(). Davor schrieb man list.innerHTML = '' und dann append.

Die Listener. Jeder neue Button bekommt in productItem einen neuen Listener. Die alten Buttons verschwinden zusammen mit ihren Listenern. Ein anderer Weg ist die Delegation: ein Listener auf dem <ul> und event.target.closest('li'), wobei das Element selbst sagt, welches Produkt es ist (data-id).

hidden. element.hidden = true ist wie das Attribut hidden in HTML: Das Element wird nicht angezeigt. Gibt eine CSS-Regel dem Element ein display, gewinnt die Regel gegen hidden, und das Element erscheint wieder.

Den Fokus von Hand behalten. Du kannst dir merken, welches Feld den Fokus hatte und wo der Cursor stand, und beides nach render() zurücksetzen. Das funktioniert, ist aber eine Stelle mehr, die man vergessen kann. Die Bibliotheken machen das für dich, weil sie das Feld gar nicht erst ersetzen.

Übung

Der Warenkorb

Baue den Katalog und den Warenkorb aus den Daten. Jedes Produkt ist eine Zeile mit einem Button „Hinzufügen“ (der Name für einen Screenreader: „Hinzufügen Weißbrot“). Jede Zeile im Warenkorb hat einen Button „Entfernen“ („Entfernen Weißbrot“). Der Zähler oben ist „Warenkorb (2)“, die Summe ist „Gesamt: 10,00 €“, und „Der Warenkorb ist leer“ ist nur sichtbar, wenn der Warenkorb leer ist (die Property hidden).

HTML, CSS, JS

Test

Teste dich

1. Der Warenkorb zeigt die Anzahl der Produkte an drei Stellen. Was ist der häufigste Fehler, wenn du von Hand aktualisierst?
2. Was macht list.replaceChildren(...items)?
3. Wohin in add gehört render()?
4. Warum verliert „alles neu zeichnen“ den Fokus in einem Feld innerhalb der Liste?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·