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- oder XML-Dokumenten, der Name Document Object Model bezieht sich auf das zugrundeliegende Objektmodell.[1] Ein DOM kann für alle Sprachen erstellt werden, die dem Konzept von verschachtelbaren Elementen folgen, in der Praxis findet man es aber nur für HTML und XML vor. Da XML viele Dialekte besitzt, gibt es entsprechend viele DOM-Implementierungen.

Eine Webseite liegt dem Browser zunächst als bloßer Text vor, der mit der Auszeichnungssprache HTML formatiert ist. 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 Sammlung von Objekten, die dann im Arbeitsspeicher vorgehalten werden. Diese Objekte sind als eine Datenstruktur organisiert, die man in der Informatik einen Baum nennt.

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.

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 ein Attribut handelt. Typisch für eine Baumstruktur ist, dass jeder Knoten darin genau einen Elternknoten hat (außer dem Wurzelknoten natürlich: dem Dokument selbst) und damit vom Dokumentknoten zu einem beliebigen Knoten im Baum ein eindeutig bestimmter Weg führt: der Elternpfad.

Es gibt verschiedene Knotentypen. Davon sind drei innerhalb eines gewöhnlichen HTML-Dokuments von besonderer Bedeutung:

Elementknoten
Die Elementknoten repräsentieren die HTML-Elemente im Dokument, z. B. <body>, <h1> oder <p>. Sie können weitere Kindknoten enthalten.
Textknoten
In den Textknoten findet sich der sichtbare Text einer Webseite oder eines XML-Dokuments. Sie können keine Kindknoten haben.
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.
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. ☹“
Für HTML kann man das stehen lassen. Für ein XML-Dokument, worin man unterschiedliche Schemadefinitionen und damit auch Attribute unterschiedlicher Herkunft vorfinden kann, sind die Möglichkeit einer Namespace-Angabe unverzichtbar.

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.
einfacher Node-Inspektor ansehen …

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

Der Elementbaum des DOM ist mehr als eine einfache, objektorientierte Wiedergabe des Dokuments. Zum einen kann man die Nodes auslesen und den Verknüpfungen zu Eltern- und Kindnodes folgen, so dass deren Eigenschaften von einem JavaScript-Programm verarbeitet werden können. Darüber hinaus ist es auch möglich, das DOM durch Verändern von Eigenschaftswerten und Aufruf von Node-Methoden zu manipulieren und so Nodes zu ändern, hinzuzufügen und zu entfernen.

Elementknoten ansprechen

Die Elementknoten repräsentieren die HTML-Elemente im Dokument. Sie stellen DOM-Nodes dar und verfügen daher über die Node-Schnittstelle, als Elemente stellen sie aber zusätzlich die Methoden und Eigenschaften bereit, die durch die Element-Schnittstelle beschrieben werden. Die Aufteilung in Node und Element erfolgt, weil ihre Bereitstellung für andere Node-Typen keinen Sinn ergibt.

  • id: Gibt die ID des Elements zurück oder setzt sie.
  • className: Gibt die Klasse(n) des Elements als String zurück oder setzt sie. Diese Eigenschaft kann nicht class heißen, so wie das zugehörige HTML-Attribut, weil class ein reserviertes Wort in JavaScript ist.
  • classList: Gibt die Klassen des Elements als DOMTokenList zurück, was die Abfrage und das Ändern von Klassen ohne String-Operationen ermöglicht.
  • tagName: Der Name des Tags, für das das Element erstellt wurde (in Großbuchstaben)
  • textContent: Zugriff auf den Textinhalt.

Nicht zum DOM gehörig ist die die innerHTML-Eigenschaft, sie gehört in die HTML-Spezifikation und zur HTMLElement-Schnittstelle. Diese erweitert die Element-Schnittstelle um Funktionalität, die nur für HTML- oder generische XML-Elemente von Bedeutung ist. Für SVG-Dokumente gibt es eine andere Erweiterung: SVGElement.

Um einen Elementknoten zu finden, gibt es unterschiedliche Möglichkeiten. Die grundlegendste davon ist die getElementById-Methode des Dokumentobjekts (das der Browser als globale Variable document bereitstellt.

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.

Element mit DOM-Methoden ansprechen - 1 ansehen …
<output id="info"></output>

<script>
  
  document.getElementById('info').textContent =  'Hallo Welt!';
  
</script>

Das Script ist mit Hilfe des script -Elements 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 HTMLElement-Objekt zum output-Element anhand seiner id info ermittelt. Durch eine Zuweisung an seine textContent-Eigenschaft wird es 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 Zugriff auf Elemente über ihren Tag-Namen oder über Klassen und Attribute ermöglichte. Dieses Verfahren wurde in Form der querySelector-Methode in das DOM übernommen und wird heute 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.

Element mit DOM-Methoden ansprechen - 2 ansehen …
<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.

Beachte: querySelector() spricht das erste gefundene Element an; weitere Vorkommen werden ignoriert. Mit QuerySelectorAll kann man alle vorhandenen Vorkommen ansprechen.
Empfehlung:
  • 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:

Ändern eines Textknotens - nicht falsch aber OLD SCHOOL
  const parent = document.getElementById('info');
    
  parent.childNodes.forEach(node => {
    if (node.nodeType === Node.TEXT_NODE) {
      node.nodeValue = ' [neuer Text] ';
    }
  });

Einfacher geht dies mit textContent:

aktuellen Warnhinweis ausgeben
  document.querySelector('#info').textContent = ' [neuer Text] ';
Beachte: Wenn du element.textContent verwendest, werden keine einzelnen Textknoten geändert. Stattdessen werden alle untergeordneten Knoten (einschließlich Textknoten und Kindelementen) durch einen neuen einzelnen Textknoten ersetzt
Bei 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.

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.


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

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

Nodelist mit einer klassischen Zählschleife 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:

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.


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/‌Node
    Die Node-Schnittstelle ist das zentrale Objekt des Document Object Models (DOM). Es repräsentiert einen einzelnen Knoten im Objektbaum, …


Objektübersicht

Anhang

DOM-Traversal

Wie im oben Kapitel besprochen, besteht eine Webseite aus Markup als Text, der dann vom Browser geparst wird. Das Ergebnis ist der Elementbaum von nodes (Knoten), die sich immer weiter verzweigen.

Um das nächste gleichlautende Element zu erreichen, musste früher erst durch den Elementbaum traversiert, d.h. gequert werden, um das Elternelement zu finden und dann alle Kindknoten zu durchsuchen. Neben dem gesuchten Nachbarelement mussten aber Textknoten und Whitespace aussortiert werden.

Durchklettern des Elementbaums (2011)
items[i].onblur = function(){
  this.parentNode.className = "";
  if(this.parentNode.parentNode.parentNode.nodeName=="LI"){
     this.parentNode.parentNode.parentNode.className="";
     if(this.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName=="LI"){
        this.parentNode.parentNode.parentNode.parentNode.parentNode.className="";
        if(this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName=="LI"){
           this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.className="";
        }
     }
  }
};

In diesem Wiki-Artikel aus dem Jahre 2011[3] wird eine verschachtelte ul nach allen li-Elementen durchsucht und deren Klasse .hover entfernt. (Der IE6 verstand noch kein :hover !). Für jede Hierarchieebene wurde eine parentNode-Kette angelegt.

Dies war ein Grund Frameworks wie jQuery zu entwickeln, die Entwicklern Helferfunktionen anboten, die diese Arbeit effizienter erledigten. Heute kann man das mit Vanilla-JS (= ohne weitere Frameworks) lösen:

benachbartes li ohne Klasse finden (heute)
  let currentElement = this.parentNode;

  while (currentElement) {
    currentElement = currentElement.closest('li');

    if (currentElement) {
      currentElement.className = '';
      currentElement = currentElement.parentNode;
    }

Element.closest sucht nach dem nächstgelegenen übereinstimmenden Element:


Finde das nächstgelegene Element
let button = document.querySelector('.delete-button');
let listItem = button.closest('li');


nextElementSibling findet das nächstgelegene Geschwisterelement:

Finde das nächstgelegene Geschwisterelement
let currentTask = document.querySelector('.task');
let nextTask = currentTask.nextElementSibling;

Analog dazu gibt es previousElementSibling für das vorausgegangene Geschwisterelement.


Anstelle von parentNode kann man mit closest auch die Elternelemente ansprechen:

(bekannte) Elternelemente ansprechen
let label = document.querySelector('label');
let listItem = label.closest('li'); // Cleaner than `parentNode` with checks


querySelectorAll findet alle Vorkommen eines Elements und verwendet dabei die aus CSS bekannten Selektoren!

Finde das nächstgelegene Geschwisterelement
    let taskList = document.querySelector('#taskList');
    let tasks = taskList.querySelectorAll('li');
Empfehlung: Verwende moderne Methoden wie closest, querySelector und nextElementSibling, bzw previousElementSibling. Sie sind prägnant, leistungsfähig und besser geeignet für den deklarativen Stil des modernen JavaScript.

Whitespace

DOM - Whitespace im Elementbaum
DOM - Whitespace im Elementbaum

Das Markup wird zur besseren Übersicht oft durch Zeilenumbrüche und einrückende Leerzeichen formatiert.

Dies stellt uns manchmal vor ein Problem, da folgender Code nicht wie erwartet 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 wie rechts aus.

Dieses Beispiel zeigt, die unterschiedliche Anzahl Kindknoten:

gleiches Markup - unterschiedliche Anzahl Kindknoten? ansehen …

Im Beispiel sind zwei Listen definiert. Beide unterscheiden sich nur dahingehend, dass sich zwischen den einzelnen Elementen der erste Liste keine Leerzeichen oder Zeilenumbrüche befinden. In der zweiten Liste dagegen wurden Leerzeichen und Zeilenumbrüche verwendet. Standardkonforme Browser geben für die erste Liste als Anzahl der Kindknoten 2 aus und für die zweite Liste 5. Die jeweiligen Leerzeichen und Zeilenumbrüche werden als eigene Kindknoten betrachtet.

Da man heute mit den DOM-Methoden nicht mehr alle Kindknoten untersuchen muss, sondern zielsicher Elemente herausfiltern kann, spielt dieser Aspekt heute keine Rolle mehr.

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.

Weblinks

  1. MDN: DOM
  2. WHATWG: DOM Living Standard
  3. Eine zugängliche Multilevel-Dropdown-Navigation von Beatovich
    Damals Stand der Technik (Der IE6 verstand noch kein :hover!) sieht der Code heute komisch aus