JavaScript und das DOM/Was ist das DOM

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

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.

DOM als Baumstruktur

Das DOM ist also die Webseite, deren Elemente in einer verzweigten Objektstruktur angeordnet sind.

DOM - Baumstruktur einer Webseite

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.

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.
einfacher Node-Inspektor ansehen …
document.body.addEventListener('click', function (event) {
	const clickedElement = event.target;
	const nodeType = clickedElement.nodeType;
	const nodeName = clickedElement.nodeName;
	const childNodes = clickedElement.childNodes;

	const childNodesInfo = Array.from(childNodes)
		.map(node =>
			`Type: ${node.nodeType}, Name: ${node.nodeName}, Content: "${node.textContent.trim()}"`
		)
		.join('\n');
	...
});

Die Webseite enthält mehrere Elemente. Bei einem Klick auf ein Element werden nodeName, nodeType, sowie eventuelle childNodes ermittelt und im unteren div ausgegeben.

Es gibt verschiedene Knotentypen. Innerhalb eines gewöhnlichen HTML-Dokuments gibt es auf jeden Fall drei wichtige Knotentypen:

Elementknoten

Diese Knoten repräsentieren die HTML-Elemente im Dokument, z. B. <div>, <p>, <h1>. Sie können weitere Kindknoten enthalten.

Sie 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.

Attributknoten

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.

Bei der Verarbeitung eines XML Dokuments sieht die Sache etwas anders aus, dort muss man möglicherweise Namespaces behandeln. Man sollte dann aber mit Namespace-Präfixen arbeiten und den qualifizierten Namen eines Attributs als seinen ganz normalen Namen auffassen.

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. ☹“

Textknoten

In den Textknoten findet sich der sichtbare Text einer Webseite oder eines XML-Dokuments. Sie können keine Kindknoten haben.

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

Erinnerst du dich noch an unser "Hallo Welt"-Beispiel? Die Nachteile einer Textausgabe mittels alert sind vor allem die Speicherbelastung des Browsers sowie die mangelnde Gestaltungsmöglichkeit des Ausgabefensters.

Beispiel ansehen …
<output id="info"></output>


<script>
  'use strict';
  let text = 'Hallo Welt!';
  
  document.getElementById('info').innerText = text;
  
</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 innerText mit dem auszugebenden Text gefüllt.
Ein weiterer Vorteil dieser Methode ist die Möglichkeit den Ausgabetext beliebig mit CSS zu gestalten.


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.

Empfehlung:
  • Versuche semantisches Markup zu verwenden.
  • Verwende die passenden Elemente und nutze deren browsereigene Funktionalität!
  • Setze ids und Klassennamen nur sparsam ein.

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.

Im Folgenden verwenden wir die vielseitigere querySelector()-Methode. Sie benötigt als Parameter einen „normalen“ CSS-Selektor und ist daher vielseitiger als das traditionelle getElementById().

Beispiel ansehen …
<output></output>

<script>
  'use strict';
  let text = 'Hallo Welt!';
  
  document.querySelector('output').innerText = text;
  
</script>

Das (noch) leere output-Element wird über die querySelector()-Methode angesprochen und mit innerText mit dem auszugebenden Text gefüllt.


Empfehlung: 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 spricht das erste gefundene Element an. Mit QuerySelectorAll kann man alle vorhandenen Vorkommen ansprechen.


Mit diesen Methoden kann man auf das DOM (und dessen Elementknoten) einer Webseite zugreifen:

  • querySelector: gibt das erste Element zurück, das dem angegebenen CSS-Selektor entspricht
  • querySelectorAll: gibt eine Liste von Elementen zurück, die dem angegebenen CSS-Selektor entsprechen
  • classList: erlaubt das Hinzufügen, Entfernen und Toggeln von Klassen.

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.

Im oberen Beispiel wurde dieser Textknoten mit textContent verändert:

aktuellen Warnhinweis ausgeben
  document.querySelector('#warnung').textContent = hinweis;


Des Weiteren existiert mit innerHTML eine weitere Methode, mit der nicht nur Text, sondern sogar weitere HTML-Kindelemente eingefügt werden können.

innerHTML
  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.

Empfehlung: Verwende 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:

XSS mit innerHTML
<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.

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.

Beispiele für gültige Selektoren
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
Beachte, dass der Parameter ein String mit einem gültigen CSS-Selektor sein muss.

Die querySelectorAll()-Methode gibt nun eine statische NodeList zurück. Diese ist trotz aller Ähnlichkeit kein Array, kann aber in aktuellen Browsern mit einer for...of-Schleife durchlaufen werden. Darüber hinaus unterstützen die Browser auch von Arrays bekannte forEach-Methode, auch wenn die DOM-Spezifikation das nicht vorschreibt. Die mit allen Browsern kompatible Lösung ist, eine NodeList mit einer Zählschleife zu durchlaufen:

Nodelist mit einer klassischen Zählschleife durchlaufen
    const elements = document.querySelectorAll('article > .beispiel');
    for (let index = 0; index < elements.length; index++) {
      elements[index].classList.add('geändert');
    }

Einfacher ist es mit einer for...of-Schleife:

NodeList und for...of zum Ändern mehrerer Elemente ansehen …
  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.

Besonderheiten

Whitespace

Wie im oberen Abschnitt schon erwähnt, sind Webseiten Text-Dateien, die zur besseren Übersicht oft durch Zeilenumbrüche und einrückende Leerzeichen formatiert werden. Dies stellt Sie manchmal vor ein Problem, da folgender Code nicht wie oben dargestellt geparst wird:

Beispiel
<main>
  <h1>Überschrift</h1>
  <p>
    Textinhalt
    <img src="bild.jpg" alt="">
  </p>
  <!-- Hier ist ein Kommentar -->
</main>

Tatsächlich ist es aber so, dass auch der Whitespace zwischen den HTML Elementen Text darstellt und in Form von Textknoten im DOM abgelegt wird. In Wirklichkeit sieht das DOM also so aus:

DOM - Whitespace im Elementbaum
DOM - Whitespace im Elementbaum

Diese Weißräume können nützlich sein. Ein HTML wie das nachfolgende zeigt eine Leerstelle zwischen Hallo und Welt, weil sich zwischen den beiden <span>-Elementen ein Textknoten befindet, der einen Zeilenumbruch und zwei Leerstellen enthält.

Beispiel
<main>
  <span>Hallo</span>
  <span>Welt</span>
</main>

Oft wird Whitespace vom Browser aber auch tatsächlich ignoriert. Was ist zum Beispiel mit dem Whitespace-Bereich zwischen den <main>- und <span>-Tags? Wenn jegliches Whitespace als ein Leerzeichen dargestellt würde, dann müsste das Wort "Hallo" um ein Leerzeichen eingerückt angezeigt werden. Das wird es aber nicht. Grund dafür ist, dass Leerraum am Beginn und am Ende eines HTML Elements nicht dargestellt wird.

Das vorige Beispiel verwendete einen Inline-Kontext zur Darstellung (weil das die normale Anzeigeart von <span>-Elementen ist), aber wenn Sie Blockelemente wie <div> verwenden, entsteht ein Block-Kontext. Hier gelten weitere Regeln.

Beispiel
<main>
  <div>Hallo</div> Du
  <div>schöne</div>
  <div>Welt</div>
</main>

Die Ausgabe wird 4 Zeilen enthalten:

 Hallo
 Du
 schöne
 Welt

Das Whitespace zwischen den beiden letzten <div>-Elementen wird unterdrückt, weil ein Block-Kontext vorliegt.

Whitespace kann aber auch stören. Wenn Sie eine Toolbar mit vielen nebeneinanderliegenden Buttons erzeugen möchten, und jeden Button auf eine eigene Zeile schreiben, würde der Whitespace der Zeilenumbrüche jeweils als eine störende Leerstelle zwischen den Buttons angezeigt.

Um das zu lösen, könnten Sie alle Buttons nebeneinander ins HTML schreiben. Oft genug wird eine solche Toolbar auch per JavaScript erzeugt, und wenn Sie die Buttons einfach nacheinander in ein <div> einsetzen, befinden sich keine Textknoten zwischen ihnen.

Wenn Sie die Toolbar aber als HTML im Editor erstellen, ist diese Notation schwer lesbar. Früher wurde empfohlen, die schließende spitze Klammer des </button>-Tags in die Folgezeile zu schreiben, direkt vor das nächste <button>-Tag. Das ist aber fehleranfällig. Es gibt auch den Vorschlag, den Whitespace in einen HTML Kommentar einzuschließen. Das ist auch nicht schön. Die korrekte Lösung ist, den standardmäßigen Inline-Kontext durch einen anderen zu ersetzen, indem Sie das Containerelement für die Buttons als Flexbox oder Grid darstellen.

Baumreihenfolge

Der Begriff der Baumreihenfolge (tree order) der Elemente im DOM ist zuweilen von Bedeutung. Es gibt in der Informatik drei wichtige Algorithmen, um eine Baumstruktur, wie sie das DOM darstellt, zu durchlaufen. Einer davon ist die Tiefensuche (englisch pre-order traversal), bei der für jeden Knoten im Baum der Nachfolger in der Durchlaufreihenfolge so bestimmt wird, dass zunächst geschaut wird, ob er Kindelemente hat. Wenn ja, wird mit dem ersten Kindelement fortgesetzt. Erst, wenn alle Kindelemente durchlaufen sind, wird mit seinen Geschwisterelementen weitergemacht.

Siehe auch

Objektübersicht

Weblinks

  1. MDN: DOM
  2. WHATWG: DOM Living Standard