Das DOM: die Seite aus JavaScript
Kurz gesagt: Das DOM ist der Baum aus Objekten, über den JavaScript die Seite liest und verändert: querySelector findet ein Element, textContent ändert seinen Text, createElement und append fügen neue hinzu.
Problem
Der Preis bleibt „—“
Preis und Lagerbestand des Produkts kommen aus JavaScript. Ein Kollege hat das Skript ganz nach oben gesetzt, „damit es früher startet“. Die Seite zeigt „—“ statt eines Preises.
Konsole
Die Konsole sagt zwei Dinge. console.log(price) hat null ausgegeben, und die nächste Zeile scheitert mit TypeError: Cannot set properties of null. Das Element <strong id="price"> steht doch im HTML. Warum findet JavaScript es nicht?
Die Idee
Das Dokument als Objekt
Im HTML-Modul hast du gesehen, dass der Browser aus dem Text einen Baum baut. JavaScript arbeitet mit demselben Baum: Für JavaScript ist die Seite ein Objekt namens document. Jedes Element im Baum ist ein Objekt mit Feldern und Methoden. Deshalb heißt der Baum DOM: Document Object Model.
Ein Element findest du mit einem Selektor, genau wie in CSS:
const price = document.querySelector('#price'); // nach idconst title = document.querySelector('h1'); // nach Tagconst first = document.querySelector('.product'); // nach Klasse: das ersteconst links = document.querySelectorAll('nav a'); // alle Links in navquerySelectorgeht den Baum von oben nach unten durch und gibt das erste passende Element zurück. Gibt es keins, kommtnull.querySelectorAllgibt alle als NodeList zurück: eine Liste mitlengthundforEach, aber ohnemapundfilter. Dafür wandelst du sie in ein Array um:[...links]. Arrays undforEachstammen aus dem JavaScript-Kurs: Arrays und Lambdas.- Die NodeList ist eine Momentaufnahme: Ein Element, das später dazukommt, ist nicht darin.
null heißt „so ein Element gibt es nicht“, nicht „ein leeres Element“. Der Fehler kommt erst danach: null hat kein textContent, und der Code scheitert in der nächsten Zeile.
Die Idee
Wann das Skript läuft
Der Parser liest das HTML von oben nach unten. Sobald er <script> erreicht, hält er an, führt das Skript aus und macht erst dann weiter. Im Problem steht das Skript vor <article>. Wenn es läuft, besteht der Baum nur aus <body>, und der Preis ist noch nicht da. Deshalb gibt querySelector null zurück.
Die Lösung: Das Skript wartet auf den Baum.
<head> <meta charset="utf-8"> <script src="script.js" defer></script></head>defersagt: Lade das Skript sofort herunter, führe es aber erst aus, wenn das ganze HTML gelesen ist. Dann ist jedes Element im Baum.- Der ältere Weg ist ein
<script>ganz unten, direkt vor</body>. Er funktioniert aus demselben Grund. deferwirkt nur bei einem Skript mitsrc. Bei einem Skript mit Code zwischen den Tags bewirkt es nichts.
Probier es aus
Den Preis reparieren
Repariere die Seite so, dass sie den Preis und den Lagerbestand zeigt. Fass script.js nicht an: Das Problem ist der Zeitpunkt, zu dem es läuft, nicht das, was es tut.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Dobrudzha-Brot</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<article>
<h1>Dobrudzha-Brot</h1>
<p>Preis: <strong id="price">—</strong></p>
<p id="stock">—</p>
<button type="button">In den Warenkorb</button>
</article>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; padding: 16px; }
button { font-size: 16px; padding: 8px 16px; }
script.jsconst price = document.querySelector('#price');
console.log(price);
price.textContent = '1,80 €';
document.querySelector('#stock').textContent = 'Auf Lager: 12 Stk.';
<script src="script.js" defer></script> im <head> wartet, bis der Baum fertig ist. Dann sind #price und #stock darin, und querySelector findet sie. Ohne defer läuft ein Skript im <head> noch früher und findet wieder null. Eine Prüfung if (price) in script.js versteckt den Fehler, aber der Preis erscheint trotzdem nicht: Sie schützt vor null, sie behebt es nicht.
Die Idee
Der Text: textContent und innerHTML
Das gefundene Element änderst du über seine Felder. Für Text gibt es zwei:
price.textContent = '1,80 €'; // der Text, wie er istnote.innerHTML = 'Preis: <strong>1,80 €</strong>'; // der Text wird als HTML gelesentextContentsetzt Text ein. Steht darin<strong>, erscheint das als Zeichen, nicht als Tag.innerHTMLübergibt den Text an den Parser, der daraus neue Elemente baut. Die alten Kinder des Elements sind danach weg.
innerHTML mit Daten von einem Nutzer (ein Name, eine Bewertung, eine Suche) ist gefährlich. Steckt darin HTML mit Code, läuft dieser Code im Browser von jedem, der die Seite öffnet: Das ist ein XSS-Angriff, um den es in der Lektion „Nicht vertrauenswürdige Eingaben“ geht. Deshalb kommt Text mit textContent hinein, und neue Elemente entstehen mit createElement (unten).
Nach der Änderung zeichnet der Browser von selbst neu. Nicht die ganze Seite, nur das, was sich geändert hat.
Sieh es dir an
So verändert sich die Seite
Links ist der Baum, rechts die Seite, die der Browser daraus zeichnet. Der Code findet den Warenkorb, ändert seinen Text und seine Klasse und hängt eine neue Zeile an die Liste.
const cart = document.querySelector('#cart');cart.textContent = 'Warenkorb: 1';cart.classList.add('full');const li = document.createElement('li');li.textContent = 'Milch';document.querySelector('ul').append(li);Schritt für Schritt, in Worten
- Der Parser hat den Baum aus dem HTML schon gebaut. Rechts ist die Seite, die der Browser daraus zeichnet. JavaScript sieht über document denselben Baum.
- querySelector('#cart') geht den Baum von oben durch und vergleicht jedes Element mit dem Selektor. Es gibt das erste passende zurück: cart zeigt jetzt auf diesen Knoten. Gibt es keins, kommt null.
- textContent ersetzt den Text des Knotens. Der Browser bemerkt die Änderung und zeichnet nur diesen Teil der Seite neu. Der Rest bleibt, wie er war.
- classList.add gibt dem Element eine Klasse. Eine CSS-Regel für .full gibt ihm eine neue Farbe, und wieder wird nur der Warenkorb neu gezeichnet.
- createElement macht ein neues <li>, und textContent gibt ihm Text. Es ist noch nicht im Baum, deshalb ändert sich die Seite nicht.
- append setzt es als letztes Kind von <ul> ein. Jetzt ist es im Baum, und die Liste auf der Seite wächst um eine Zeile.
- JavaScript zeichnet die Seite nie selbst. Es ändert den Baum, und der Browser zeichnet, was sich geändert hat. Die Seite wird nicht neu geladen.
JavaScript zeichnet nichts selbst. Es ändert den Baum, und der Browser zeichnet die Änderung. Die Seite wird nicht neu geladen, deshalb bleiben der Text in den Feldern und die Scrollposition erhalten.
Probier es aus
Nur noch 3 Stück
Zeige den Bestand in #stock an: „Nur noch 3 Stk.“. Bei weniger als 5 bekommt das Element die Klasse low (das CSS färbt sie rot). Der fette Text aus der Klasse stock muss bleiben.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Frische Milch 3 %</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<article>
<h1>Frische Milch 3 %</h1>
<p id="stock" class="stock">—</p>
</article>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; padding: 16px; }
.stock { font-weight: bold; }
.low { color: #c2452a; }
script.jsconst stock = 3;
const text = document.querySelector('#stock');
text.textContent = `Nur noch ${stock} Stk.`;
if (stock < 5) {
text.classList.add('low');
}
text.textContent = `Nur noch ${stock} Stk.` setzt den Text ein, und text.classList.add('low') fügt die Klasse neben stock hinzu. className = 'low' macht den Text auch rot, ersetzt aber alle Klassen: stock ist weg, und der Text ist nicht mehr fett. Und text.class = 'low' bewirkt nichts Sichtbares: Es ist ein neues Feld am Objekt, nicht die Klasse des Elements.
Die Idee
Attribute, Properties und Klassen
In HTML hat ein Element Attribute: value="1", disabled, data-id="7". In JavaScript hat es Properties: Felder des Objekts. Die meisten Attribute haben eine Property mit demselben Namen, aber beide sind nicht dasselbe.
input.valueist das, was jetzt im Feld steht. Das Attributvalueist der Startwert und ändert sich nicht, während die Käuferin tippt. Das Attribut liest du mitgetAttribute('value').button.disabled = trueschaltet den Button aus. Die Property isttrueoderfalse.data-…-Attribute sind für deine eigenen Daten. Du liest sie überdataset:data-stockistdataset.stock, unddata-product-idistdataset.productId. Der Wert ist immer ein String.- Mit Klassen arbeitest du über
classList:add,remove,toggle(fügt die Klasse hinzu, wenn sie fehlt, und entfernt sie, wenn sie da ist) undcontains.
Konsole
Die Konsole zeigt, dass die Property 3 ist, während das Attribut noch 1 ist. dataset.stock ist "12", ein String: Wandle ihn mit Number(...) in eine Zahl um, bevor du damit rechnest.
Probier es aus
Finde den Fehler
Der Sale soll jeden Preis rot färben, aber die Konsole meldet Cannot read properties of undefined (reading 'add').
Wähle die Zeile mit dem Fehler.
querySelectorAll gibt eine NodeList zurück, und eine NodeList hat kein classList: prices.classList ist undefined. Die Klasse kommt auf jedes Element einzeln: prices.forEach((p) => p.classList.add('sale')).
Die Idee
Neue Elemente
Ein neues Element machst du in drei Schritten: erzeugen, mit Inhalt füllen und in den Baum setzen.
const li = document.createElement('li'); // ein neues <li>, noch nicht auf der Seiteli.textContent = 'Milch';li.dataset.id = '2';document.querySelector('ul').append(li); // jetzt ist es das letzte Kind von <ul>Solange du es nicht mit append einsetzt, ist das Element nicht auf der Seite. append nimmt auch mehrere Elemente auf einmal: li.append(title, button). prepend setzt es an den Anfang, und remove() nimmt das Element aus dem Baum.
Genauer, für Neugierige
Weitere Suchwege. getElementById('price') entspricht querySelector('#price'). getElementsByClassName und getElementsByTagName geben eine „lebende“ Liste zurück: Sie ändert sich, wenn sich der Baum ändert. querySelectorAll gibt eine Momentaufnahme zurück. Nach einem append eines neuen .price hat die lebende Liste ein Element mehr, die Momentaufnahme nicht.
Suchen innerhalb eines Elements. Jedes Element hat querySelector, nicht nur document: card.querySelector('.price') sucht nur innerhalb der Karte.
setAttribute('disabled', 'false') schaltet den Button aus. Bei disabled zählt nur, ob das Attribut da ist, nicht sein Wert. Schreib also button.disabled = false oder removeAttribute('disabled').
innerHTML und <script>. Ein Skript, das du mit innerHTML einsetzt, läuft nicht. Ein Attribut wie onerror an einem Bild dagegen schon. Deshalb ist innerHTML mit fremden Daten auch ohne <script> gefährlich.
className und classList. className ist das ganze Attribut class als ein String. Wer ihm etwas zuweist, ersetzt alle Klassen auf einmal.
Übung
Die Produktkarte
In script.js gibt es ein Objekt product. Fülle die Karte damit: Der Name ist die Überschrift (#name), der Preis ist „8,90 €“ (zwei Nachkommastellen, mit Komma), und der Bestand ist „Ausverkauft“, wenn stock 0 ist. Dann bekommt die Karte die Klasse sold-out, und der Button wird ausgeschaltet. Setze auf die Karte ein data-id mit der id des Produkts. Füge für jedes Label in badges ein <li> in #badges ein.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Vitosha-Käse</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<article class="product">
<h2 id="name"></h2>
<p id="price"></p>
<p id="stock"></p>
<ul id="badges" aria-label="Etiketten"></ul>
<button type="button" id="add">In den Warenkorb</button>
</article>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; padding: 16px; }
.product { border: 1px solid #ccc; border-radius: 8px; padding: 12px 16px; max-width: 320px; }
.sold-out { opacity: 0.6; }
#badges { list-style: none; padding: 0; display: flex; gap: 8px; }
#badges li { background: #f3efe6; border-radius: 4px; padding: 2px 8px; }
button { font-size: 16px; padding: 8px 16px; }
script.jsconst product = {
id: 7,
name: 'Vitosha-Käse',
price: 8.9,
stock: 0,
badges: ['neu', 'bulgarisch'],
};
const card = document.querySelector('.product');
card.dataset.id = product.id;
document.querySelector('#name').textContent = product.name;
document.querySelector('#price').textContent = `${product.price.toFixed(2).replace('.', ',')} €`;
const stock = document.querySelector('#stock');
const add = document.querySelector('#add');
if (product.stock === 0) {
stock.textContent = 'Ausverkauft';
card.classList.add('sold-out');
add.disabled = true;
} else {
stock.textContent = `Auf Lager: ${product.stock} Stk.`;
}
const badges = document.querySelector('#badges');
product.badges.forEach((badge) => {
const li = document.createElement('li');
li.textContent = badge;
badges.append(li);
});
card.dataset.id = product.id schreibt das Attribut data-id="7". Der Preis ist `${product.price.toFixed(2).replace('.', ',')} €`: toFixed(2) liefert "8.90", und replace macht daraus das Komma. Bei stock === 0 gehören drei Dinge zusammen: der Text, die Klasse und add.disabled = true. Die Labels sind ein forEach über das Array, mit createElement('li') und append für jedes. badges.textContent = product.badges.join(', ') zeigt dieselben Wörter, aber als einen Text: Die Liste hat dann gar keine Einträge.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Das HTML-Dokument · Formulare · Selektoren und die Kaskade
- Geht weiter in: Ereignisse · Eine Liste aus Daten · Daten aus einer API · Die erste Komponente
- Aufgaben zum Thema: JavaScript in der Seite