Zum Inhalt
wannacode

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.

HTML, CSS, JS

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 id
const title = document.querySelector('h1'); // nach Tag
const first = document.querySelector('.product'); // nach Klasse: das erste
const links = document.querySelectorAll('nav a'); // alle Links in nav
  • querySelector geht den Baum von oben nach unten durch und gibt das erste passende Element zurück. Gibt es keins, kommt null.
  • querySelectorAll gibt alle als NodeList zurück: eine Liste mit length und forEach, aber ohne map und filter. Dafür wandelst du sie in ein Array um: [...links]. Arrays und forEach stammen 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>
  • defer sagt: 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.
  • defer wirkt nur bei einem Skript mit src. 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.

HTML, CSS, JS

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 ist
note.innerHTML = 'Preis: <strong>1,80 €</strong>'; // der Text wird als HTML gelesen
  • textContent setzt 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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. createElement macht ein neues <li>, und textContent gibt ihm Text. Es ist noch nicht im Baum, deshalb ändert sich die Seite nicht.
  6. 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.
  7. 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.

HTML, CSS, JS

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.value ist das, was jetzt im Feld steht. Das Attribut value ist der Startwert und ändert sich nicht, während die Käuferin tippt. Das Attribut liest du mit getAttribute('value').
  • button.disabled = true schaltet den Button aus. Die Property ist true oder false.
  • data-…-Attribute sind für deine eigenen Daten. Du liest sie über dataset: data-stock ist dataset.stock, und data-product-id ist dataset.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) und contains.
HTML, CSS, JS

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.

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 Seite
li.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.

HTML, CSS, JS

Test

Teste dich

1. Was gibt document.querySelector('#total') zurück, wenn es auf der Seite kein Element mit id="total" gibt?
2. Das Skript steht in <head> ohne defer und sucht einen Button im <body>. Was findet es?
3. Der Name eines Produkts kommt von einem Nutzer. Wie bringst du ihn auf die Seite?
4. Die Käuferin hat 3 in <input value="1"> getippt. Was liefern input.value und input.getAttribute('value')?
5. Welche Zeile gibt jedem .price-Element die Klasse sale?

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