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.
Konsole
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.textContentsetzt 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
Konsole
Ä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“.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Brot und Milch</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<main>
<h1>Brot und Milch</h1>
<ul id="products"></ul>
<p id="count"></p>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
li { margin: 4px 0; }
button { font: inherit; margin-left: 8px; }
script.jsconst products = [
{ id: 1, name: 'Weißbrot', price: 1.2, stock: 40 },
{ id: 4, name: 'Banitsa mit Käse', price: 2.5, stock: 0 },
{ id: 5, name: 'Frischmilch', price: 1.6, stock: 35 },
{ id: 13, name: 'Gurken', price: 2.8, stock: 0 },
{ id: 10, name: 'Äpfel', price: 2.1, stock: 80 },
];
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
function productItem(product) {
const li = document.createElement('li');
li.textContent = `${product.name}, ${money(product.price)}`;
return li;
}
const list = document.querySelector('#products');
const count = document.querySelector('#count');
const inStock = products.filter((p) => p.stock > 0);
list.replaceChildren(...inStock.map(productItem));
count.textContent = `Verfügbar: ${inStock.length} von ${products.length}`;
products.filter((p) => p.stock > 0) ist ein neues Array nur mit den Produkten auf Lager. Die Liste wird daraus gemacht, und die Längen der beiden Arrays liefern die beiden Zahlen. Das Array products bleibt ganz: Der Filter ändert es nicht.
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
- 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.
- addByHand(Brot). Zuerst ändern sich die Daten: Das Array hat ein Produkt.
- Dann fasst die Funktion den Bildschirm von Hand an: eine neue Zeile und eine neue Summe. Jede Stelle ist eine eigene Codezeile.
- 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.
- Noch ein Produkt, wieder von Hand. Der Abstand wächst: 2 gegen 0.
- Jetzt add(): Sie ändert nur die Daten. Für einen Moment hinkt der ganze Bildschirm hinterher, weil noch nichts gezeichnet wurde.
- 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.
- 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.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Warenkorb</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<header>
<a href="/">Der Shop</a>
<a href="/cart" id="badge">Warenkorb (0)</a>
</header>
<main>
<h1>Brot und Milch</h1>
<ul id="products"></ul>
<h2>Dein Warenkorb</h2>
<ul id="cart"></ul>
<p id="total">Gesamt: 0,00 €</p>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
li { margin: 4px 0; }
button { font: inherit; margin-left: 8px; }
script.jsconst products = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 10, name: 'Äpfel', price: 2.1 },
];
const cart = [];
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const badge = document.querySelector('#badge');
const cartList = document.querySelector('#cart');
const total = document.querySelector('#total');
const sum = () => cart.reduce((s, p) => s + p.price, 0);
function productItem(product) {
const li = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Hinzufügen';
button.setAttribute('aria-label', `Hinzufügen ${product.name}`);
button.addEventListener('click', () => add(product));
li.append(`${product.name}, ${money(product.price)}`, button);
return li;
}
document.querySelector('#products').replaceChildren(...products.map(productItem));
function cartItem(product, index) {
const li = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Entfernen';
button.setAttribute('aria-label', `Entfernen ${product.name}`);
button.addEventListener('click', () => remove(index));
li.append(product.name, button);
return li;
}
function add(product) {
cart.push(product);
render();
}
function remove(index) {
cart.splice(index, 1);
render();
}
function render() {
badge.textContent = `Warenkorb (${cart.length})`;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Gesamt: ${money(sum())}`;
}
add macht cart.push(product) und render(). remove macht cart.splice(index, 1) und render(). render() schreibt den Zähler, ersetzt alle Zeilen mit replaceChildren und schreibt die Summe. Werden die Zeilen mit append hinzugefügt, ohne die alten zu entfernen, ergeben drei Klicks sechs Zeilen: 1 + 2 + 3.
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.
render() steht in Zeile 2, vor cart.push(product). Es zeichnet die Daten so, wie sie in diesem Moment sind: ohne das neue Produkt. Erst ändern sich die Daten, dann zeichnest du.
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.
Konsole
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).
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Warenkorb</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<header>
<a href="/">Der Shop</a>
<a href="/cart" id="badge">Warenkorb (0)</a>
</header>
<main>
<h1>Brot und Milch</h1>
<ul id="products"></ul>
<h2>Dein Warenkorb</h2>
<p id="empty">Der Warenkorb ist leer</p>
<ul id="cart"></ul>
<p id="total">Gesamt: 0,00 €</p>
</main>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; }
header { display: flex; justify-content: space-between; background: #f3efe6; padding: 8px 16px; }
main { padding: 0 16px; }
li { margin: 4px 0; }
button { font: inherit; margin-left: 8px; }
script.jsconst products = [
{ id: 1, name: 'Weißbrot', price: 1.2 },
{ id: 5, name: 'Frischmilch', price: 1.6 },
{ id: 7, name: 'Weißkäse', price: 7.9 },
{ id: 10, name: 'Äpfel', price: 2.1 },
];
const cart = [];
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const badge = document.querySelector('#badge');
const cartList = document.querySelector('#cart');
const total = document.querySelector('#total');
const empty = document.querySelector('#empty');
const sum = () => cart.reduce((s, p) => s + p.price, 0);
function productItem(product) {
const li = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Hinzufügen';
button.setAttribute('aria-label', `Hinzufügen ${product.name}`);
button.addEventListener('click', () => add(product));
li.append(`${product.name}, ${money(product.price)}`, button);
return li;
}
function cartItem(product, index) {
const li = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Entfernen';
button.setAttribute('aria-label', `Entfernen ${product.name}`);
button.addEventListener('click', () => remove(index));
li.append(product.name, button);
return li;
}
function add(product) {
cart.push(product);
render();
}
function remove(index) {
cart.splice(index, 1);
render();
}
function render() {
badge.textContent = `Warenkorb (${cart.length})`;
empty.hidden = cart.length > 0;
cartList.replaceChildren(...cart.map(cartItem));
total.textContent = `Gesamt: ${money(sum())}`;
}
document.querySelector('#products').replaceChildren(...products.map(productItem));
render();
Die Lösung hat ein einziges render(): den Zähler, empty.hidden = cart.length > 0, die Zeilen mit replaceChildren und die Summe. add und remove ändern nur cart und rufen render() auf. Unten wird render() auch einmal beim Laden aufgerufen, damit die Seite vor dem ersten Klick stimmt. Die Prüfung legt zwei Produkte hinein, entfernt sie wieder und prüft, dass die Meldung für den leeren Warenkorb zurückkommt.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Text, Links, Bilder, Tabellen · Barrierefreiheit · Das DOM: die Seite aus JavaScript · Ereignisse
- Geht weiter in: Daten aus einer API · Die erste Komponente · Props: Daten von oben · Listen und Keys
- Aufgaben zum Thema: JavaScript in der Seite