JavaScript und das DOM/Was ist das DOM
Das DOM ist eigentlich eine Schnittstelle zwischen JavaScript und HTML-Dokumenten, der Name Document Object Model bezieht sich auf das zugrundeliegende Objektmodell.[1]
Eine Webseite liegt dem Browser zunächst als bloßer Text, der mit der Auszeichnungssprache HTML formatiert ist, vor. Noch während der Browser den Code über das Netz empfängt, verarbeitet er ihn Stück für Stück. Diese Aufgabe übernimmt der sogenannte Parser (englisch parse = einen Satz in seine grammatikalischen Einzelteile zerlegen).
Der Parser überführt den HTML-Code in eine Objektstruktur, die dann im Arbeitsspeicher vorgehalten wird. Diese Objektstruktur besteht aus verschachtelten Knoten, die in einer Baumstruktur angeordnet sind.
Dieses Tutorial zeigt, wie du mit JavaScript nun beliebige Elemente im DOM ansprechen und diese verändern, entfernen und mit Anwenderereignissen verknüpfen kannst. Wie in der Info-Box angegeben, solltest du bereits erste Erfahrungen mit JavaScript gemacht haben.
Inhaltsverzeichnis
DOM als Baumstruktur
Das DOM ist also die Webseite, deren Elemente in einer verzweigten Objektstruktur angeordnet sind.
Der Browser nutzt für alle weiteren Operationen diese Objektstruktur, nicht den HTML-Quellcode, an dem der Webautor üblicherweise arbeitet. Insbesondere CSS und JavaScript beziehen sich nicht auf den HTML-Code als Text, sondern auf den entsprechenden Elementbaum im Speicher.
Die einzelnen Bestandteile einer solchen Baumstruktur werden als Knoten bezeichnet. Das zentrale Objekt des DOM ist deshalb das Node-Objekt (node = Knoten). Jedes Objekt im DOM-Baum ist ein Node, egal ob es sich um ein Element, einen Text, einen Kommentar oder einen Attributknoten handelt.
Es gibt verschiedene Knotentypen. Innerhalb eines gewöhnlichen HTML-Dokuments gibt es auf jeden Fall drei wichtige Knotentypen:
- Die Elementknoten repräsentieren die HTML-Elemente im Dokument, z. B. <body>, <h1> oder <p>. Sie können weitere Kindknoten enthalten.
- In den Textknoten findet sich der sichtbare Text einer Webseite oder eines XML-Dokuments. Sie können keine Kindknoten haben.
- Attributknoten wurden bei der Konzipierung des DOM von Node abgeleitet, stellen aber keine allgemeinen Kind-Knoten im DOM-Baum dar. Nur Elemente können Attribute besitzen, deswegen kann man mit Element.attributes ermitteln, welche(s) Attribut(e) dieses Elements enthält. Man kann Attribute einfach als Name-Wert Paare behandeln, die einem Element zugeordnet sind.
Die Living Standard Spezifikation des Document Object Model[2] enthält einen bezeichnenden Satz zu Attributen:
„Note: If designed today they would just have a name and value. ☹“
Einfacher Node-Inpektor
Das Node-Objekt hat verschiedene Eigenschaften, um diese Knoten zu untersuchen und zu verändern:
- nodeType: Gibt an, um welchen Knoten es sich handelt (z. B. Element, Text, Kommentar).
- nodeName: Gibt den Namen des Knotens zurück (z. B. "DIV" für ein <div>-Element oder "#text" für einen Textknoten).
- childNodes: Gibt eine NodeList mit allen direkten Kindknoten zurück, egal ob Text oder Elemente.
Die Webseite enthält mehrere Elemente. Bei einem Klick auf ein Element werden nodeName, nodeType, sowie eventuelle childNodes ermittelt und im unteren div ausgegeben.
DOM-Methoden
Dieser Elementbaum ist jedoch nicht statisch, sondern kann durch DOM-Manipulation ausgelesen, verändert und erweitert werden.
Ausgehend von einem ermittelten Elementknoten lässt sich dann schnell auf dessen Attribute und Inhalt zugreifen.
Elementknoten ansprechen
Diese
Elementknoten repräsentieren die HTML-Elemente im Dokument und können über die Methoden des Node
-Objekts, aber auch des Element-Objekts angesprochen werden. Element
erweitert Node
um zusätzliche Funktionalitäten, die speziell für HTML- oder XML-Elemente gedacht sind:
- id: Gibt die ID des Elements zurück oder setzt sie.
- className: Gibt die Klasse(n) des Elements zurück oder setzt sie.
- innerHTML: Zugriff auf den HTML-Inhalt.
Daneben gibt es weitere spezialisierte Typen, die von Node erben, wie HTMLElement und HTMLorSVGElement
.
Ein solch ermitteltes Element kann nun „bearbeitet“ werden.
Erinnerst du dich noch an unser "Hallo Welt"-Beispiel? Die Nachteile einer Textausgabe mittels alert
sind vor allem, dass JavaScript die Ausführung weiterer Interaktionen unterbricht, bis der Benutzer die Meldung löscht, sowie die mangelnde Gestaltungsmöglichkeit des Ausgabefensters. Wenn man die Nachricht ins DOM schreibt, kann man den Ausgabetext beliebig mit CSS gestalten.
<output id="info"></output>
<script>
'use strict';
document.getElementById('info').textContent = 'Hallo Welt!';
</script>
Das Script ist nun in eine Webseite eingebunden. Teil dieser Webseite ist ein (noch) leeres output-Element mit der id info
.
Anstelle der Ausgabe mit alert
wird mit document.getElementById
das output-Element über seine id info
angesprochen und mit textContent mit dem auszugebenden Text gefüllt.
Dies war die früher übliche Methode, mit JavaScript auf einzelne Elemente zuzugreifen. Da man nicht genau wusste, welche Elemente später als Zugriffspunkte benötigt würden, wurden sicherheitshalber viele id-Attribute vergeben, die das HTML-Markup (womöglich noch in Verbindung mit präsentationsbezogenen Klassennamen ) unnötig aufblähten.
Da diese Art des Elementzugriffs eigentlich umständlich war, entwickelten Frameworks wie jQuery eine Methode, die den direkten Zugriff auf Elemente über ihren Tag-Namen oder über Klassen und Attribute ermöglichte. Diese querySelector-Methode wurde in natives JavaScript übernommen und wird von allen Browsern verstanden.
querySelector()
Im Folgenden verwenden wir die vielseitigere querySelector()-Methode. Gegenüber document.getElementById()
, das nur Elemente anspricht, die ein eindeutiges, unverwechselbares id-Universalattribut besitzen, sucht die querySelector-Methode nach den im CSS üblichen Selektoren wie Elemente, Klassen und IDs, aber auch ARIA-Attributen, wie wir später sehen werden. Sie benötigt als Parameter einen „normalen“ CSS-Selektor.
<output></output>
<script>
'use strict';
let text = 'Hallo Welt!';
document.querySelector('output').textContent = text;
</script>
Das (noch) leere output-Element - für das wir keine id mehr benötigen - wird über die querySelector()-Methode angesprochen und mit textContent mit dem auszugebenden Text gefüllt.
- Versuche semantisches Markup zu verwenden.
- Verwende die passenden Elemente und nutze deren browsereigene Funktionalität!
- Setze ids und Klassennamen nur sparsam ein.
Textknoten ansprechen
Die meisten Elemente wie Überschriften, Absätze, Links und Buttons enthalten neben möglichen Kind-Elementen noch Textknoten mit dem eigentlichen Inhalt der Webseite.
Mit den oben vorgestellten Eigenschaften könnte man nun die Kindknoten eines Elements überprüfen, ob sie vom Typ 3 TEXT_NODE
sind und dann den Wert auslesen oder ändern:
const parent = document.getElementById('info');
parent.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
node.nodeValue = ' [neuer Text] ';
}
});
Einfacher geht dies mit textContent:
document.querySelector('#info').textContent = ' [neuer Text] ';
element.textContent
verwendest, werden keine einzelnen Textknoten geändert. Stattdessen werden alle untergeordneten Knoten (einschließlich Textknoten und Kindelementen) durch einen neuen einzelnen Textknoten ersetztBei sorgfältiger Vorplanung (Textausgabe nur in
<p>
oder <output>
) spielt dies in der Praxis aber keine Rolle.
Des Weiteren existiert mit innerHTML eine weitere Methode, mit der nicht nur Text, sondern sogar weitere HTML-Kindelemente eingefügt werden können.
document.querySelector('#info').innerHTML = '<strong>' + text + '</strong>';
In diesem Beispiel wird die Variable text
von zwei Zeichenketten umschlossen, in denen sich das Markup für ein strong-Element befindet. Wenn der Code ausgeführt wird, ändert sich nicht nur der Textknoten, sondern es wird ein strong-Element in den Elementbaum eingefügt.
textContent
oder innerText
, wenn du reine Textinhalte verändern willst. Dadurch, dass keine HTML-Elemente erzeugt werden müssen, ist dies performanter. Darüber hinaus verhindert man so potentielle XSS-Angriffe.→ JavaScript/Tutorials/Cross Site Scripting
Eine potenziell sehr gefährliche XSS-Lücke besteht nicht: Die HTML-Spezifikation verlangt, dass ein <Script>
-Element, das per innerHTML ins DOM gebracht wird, nicht ausgeführt wird.
Es gibt aber subtilere Möglichkeiten, die nicht verhindert werden:
<p id="daten"></p>
<script>
let elem = document.getElementById("daten");
elem.innerHTML = `<img src="no.image.png" onerror="alert('Gotcha!')">`;
</script>
Sobald der Server mit HTTP-Status 404 antwortet, wird der alert-Aufruf ausgeführt.
- Texteigenschaften mit JS
- textContent vs innerText und innerHTML
mehrere Elemente gleichzeitig ansprechen
Die querySelector()
-Methode gibt das erste Element, auf den der Selektor zutrifft, zurück - alle anderen werden ignoriert. Um mehrere Elemente zu selektieren, kann man die querySelectorAll()-Methode verwenden.
document.querySelectorAll('article img') // alle img-Elemente innerhalb von article
document.querySelectorAll('h2, h3') // alle h2 + h3 Überschriften
document.querySelectorAll('input[type="number"]') //alle inputs mit type="number"
document.querySelectorAll('tr:nth-child(odd)') // alle Tabellenreihen mit ungeradem Index
Die querySelectorAll()-Methode gibt nun eine statische NodeList zurück. Diese NodeList ist eine Sammlung mehrerer Knoten des DOM. Eine statische NodeList ändert sich nicht mehr, auch wenn das DOM nach ihrer Ermittlung verändert wird.
Die herkömmliche Lösung ist, eine NodeList mit einer Zählschleife zu durchlaufen:
function changeClasses() {
const elements = document.querySelectorAll('article > .beispiel');
for (let index = 0; index < elements.length; index++) {
elements[index].classList.add('geändert');
}
}
Einfacher ist es mit der auch von Arrays bekannten forEach-Methode oder einer for...of-Schleife:
document.getElementById('changer').addEventListener('click', changeClasses);
function changeClasses() {
const elements = document.querySelectorAll('article > .beispiel');
for (let element of elements)
element.classList.add('geändert');
});
}
Im Beispiel werden bei click auf den Button alle Elemente mit der Klasse .beispiel
, die direkte Kindelemente eines <article>
sind, ermittelt. Die NodeList mit dem Suchergebnis wird in elements
gespeichert. NodeList-Objekte sind iterierbar, deshalb kann man die for...of-Schleife nutzen, um die gefundenen Elemente nacheinander zu verarbeiten. Im Schleifenrumpf erhalten die Suchtreffer mit classList.add eine weitere Klasse geändert
.
Im Beispiel wurde mit einem addEventListener auf einen Klick - bzw. ein durch eine Person ausgelöstes Ereignis gewartet. Dies führt uns zum nächsten Kapitel, in dem unsere Webseite wirklich interaktiv wird.
Im dritten Kapitel gibt es dann weitere Beispiele, wie man das Aussehen von Elementen mit classList ändern kann.
Siehe auch
- Eventhandling
Ereignisse verarbeiten- auf Events reagieren
- Events weitergeben
- Standardverhalten unterdrücken
- DOM-Traversal
den Elementbaum hoch und runterklettern
- Whitespace
- JavaScript/DOM/NodeDie Node-Schnittstelle ist das zentrale Objekt des Document Object Models (DOM). Es repräsentiert einen einzelnen Knoten im Objektbaum, …
Objektübersicht
- EventTarget
- Node
- Document (forms, elements)
- DocumentFragment
- Element (abstrakt)
- Attr (Attribut-Node)
- CharacterData (abstrakt)
- Text
- Comment (Kommentare)
- AbortSignal
- Event
- Range
- Selection
- XML/Regeln/Baumstruktur
- Primitiver Website-Builder (Neue Knoten erzeugen und in Baumstruktur einhängen)