DOM - Document Object Model

Browser analysiert HTML Datei und legt die Elemente als Objekte in einen Speicher, das DOM, ab -> “rendern”.

  • Es besteht aus Knoten, den sogenannten Nodes

  • Es gibt Element-Knoten, Text-Knoten und Kommentar-Knoten. Knoten können auch verschachtelt sein (z.B. Satz mit einzelnem Wort als Link)

  • Das oberste Element ist der document Knoten: dieser enthält zahlreiche Infos zum Dokument und zahlreiche Methoden um das Dokument zu bearbeiten.

Hint

Erreichbar über Developer Tools ‣ DOM.

Beispiele:

console.log(document); // gesamtes Dokument Element ausgeben
console.log(document.body); // body Element des Dokuments

// Zugriff auf beliebige Elemente (sind JS Objekte)
document.getElementById("id");  // => Element
document.getElementByTagName("tagname");  // => HTML-Collection
document.getElementByClassName("classname");  // => HTML-Collection
document.getElementsByName("name");  // => HTML-Collection

Die Methoden können auf alle Elemente angewendet werden und werden dann auf alle innenliegenden Elemente zugreifen. Mit JQuery ist jedoch eine zielgerichtetere Suche nach bestimmten Elementen möglich. Jedoch kamen mit ES6 zwei neue Selektoren hinzu, die wiederum die Anwendung von JQuery nicht mehr nötig machen.

Query-Selector

Neu seit ES6 (von JQuery inspiriert):

document.querySelector("css-syntax");  // => Element
document.querySelectorAll("css-syntax");  // => NodeList (moderne Version von HTML-Collection)

console.log(document.querySelector("h1"));  // Element: erstes Match wird zurückgegeben
console.log(document.querySelector("h2"));
console.log(document.querySelectorAll("h2"));  // NodeList: alle Matches werden zurückgegeben
console.log(document.querySelectorAll("h2")[1]);  // Element an index 1

Content-Zugriff

Zugriff auf Inhalt von Elementen, inklusive Unter-Elementen (wenn vorhanden).

Hint

Zugriffe auf das DOM sind das Nadelöhr für eine Webseite. Daher sollten Zugriffe auf das DOM möglichst minimal gehalten werden, d.h.

  • vorhanden DOM Element als Variablen ablegen

  • neue Elemente vorab anfertigen, bevor sie ins DOM geschrieben werden

Mit .innerHTML wird der Content inklusive aller Markups (Element-Tags) geliefert.

let headLine = document.querySelector("h1");

// [element].innerHTML => liefert den Content Text inklusive Markups
console.log(headLine.innerHTML);

// überschreiben vom Text Content
headLine.innerHTML = "<em>Hallo</em> Welt";
console.log(headLine.innerHTML);

Hint

Die ursprüngliche HTML Seite wird durch Änderung nicht verändert. Die Änderung erfährt nur das DOM.

// hinzufügen von Text
headLine.innerHTML = "Achtung: " + headLine.innerHTML;
headLine.innerHTML += "!!!";  // hinten anfügen

// gesamter Content wird geschrieben
headLine.innerHTML +=  " mit             Zusatz <span style=\"display:none\">unsichtbar</span>";

Den gerenderten Content (so wie User es sieht -> Ohne Markups, ohne doppelte Leerzeichen oder ausgeblendete Inhalte) wird über .innerText sichtbar:

headLine.innerHTML +=  " mit             Zusatz <span style=\"display:none\">unsichtbar</span>";
console.log(headLine.innerHTML);
// Achtung: <em>Hallo</em> Welt!!! mit             Zusatz <span style="display:none">unsichtbar</span>
console.log(headLine.innerText); // "Achtung: Hallo Welt!!! mit Zusatz"

Über .textContent werden doppelte Leerzeichen ausgegeben, aber keine Markups oder ausgeblendete Inhalte.

headLine.innerHTML +=  " mit             Zusatz <span style=\"display:none\">unsichtbar</span>";
console.log(headLine.textContent); // Achtung: Hallo Welt!!! mit             Zusatz unsichtbar

Hint

.innerText und .textContent sind performanter, als .innerHTML, aber enthalten weniger Informationen. Es kommt auf den Anwendungsfall an, welches Property ausgelesen werden soll.

NodeList

Im Gegensatz zu JQuery, welche die Ergebnis-Elemente als Liste von Objekten zurückgibt, sind auf eine NodeList diese Methoden nicht direkt verfügbar. Die NodeList enthält wiederum diese Objekte.

let listItems = document.querySelectorAll("li");
console.log(listItems);

// Anzahl der selektierten Elemente
console.log(listItems.length);

console.log(listItems.innerHTML); // undefined -> nicht verfügbar

for (let i = 0; i < listItems.length; i++) {
    console.log(listItems[i]);  // OK
    console.log(listItems[i]).innerHTML;
}

Bei nicht vorhandenen Elementen wird null zurückgegeben bzw. ein leeres NodeList Element. Hier mit einer einfachen Prüfung arbeiten:

// Element nicht vorhanden
let orderedList = document.querySelector("ol");
console.log(orderedList);  // null
console.log(orderedList.innerHTML);  // TypeError

// prüfe ob Element im DOM vorhanden (Zugriffsfehler vermeiden)
if (orderedList) {
    console.log(orderedList.innerHTML);
}

// Bei querySelectorAll
let orderedLists = document.querySelectorAll("ol");
console.log(orderedLists);  // empty NodeList

if (orderedList.length) {
    // hier nur wenn NodeList nicht leer
}

Globales vs lokales Selektieren

  • Global: über document Objekt

  • Lokal: über ein Element (innerhalb aller eingeschlossenen Elemente)

// global selektieren (über document)
let myList = document.querySelector("#secondList");
let listItems = document.querySelectorAll("li");

console.log(myList);
console.log(listItems);

// lokales selektieren (innerhalb eines Elements)
listItems = myList.querySelectorAll("li");
console.log(listItems);

DOM-Zugriff auf Class-Attribut

Das class Attribute von Elementen manipulieren. Die className Attribut ist die performanteste Methode, es gibt auch die Möglichkeit über classList (DOMTokenList, ein Array-artiges Gebilde) Klassen zu verwalten, welches weitere Methoden liefert.

/*
    Variante 1:
    [element].className => speichert Strings
    Manipulation über Zeichenkettenverarbeitung
*/
let para = document.querySelector(".absatz");
console.log(para.className);
para.className = "wichtig";  // className überschreiben
para.className += " absatz texte";  // Klassen hinzufügen

/*
    Variante 2:
    [element].classList

*/
console.log(para.classList);  // => DOMTokenList

// Anzahl der am Element anliegeneden Klassen
console.log(para.classList.length);

// Zugriff über Property Access
console.log(para.classList[1]);  // "absatz"
}

classList Methoden:

  • .add() fügt Klassen hinzu (keine doppelten erlaubt)

  • .remove() entfernt eine Klasse

  • .replace() ersetzt eine Klasse

  • .toggle() fügt eine Klasse hinzu wenn nicht vorhanden, sonst wird diese entfernt

  • .contains() prüft ob eine Klasse einem Element anhängt (alternativ auch über Property Access)

para.classList.add("hinweis");  // Klasse hinzufügen
para.classList.add("hinweis");  // Versuch doppeltes Hinzufügen wird ignoriert

para.classList.remove("texte");  // Klasse entfernen

para.classList.replace("absatz", "new-absatz");  // Klasse ersetzen
para.classList.replace("fehlt", "fehlt-immer-noch");

/*
toggle:
    wenn Klasse vorhanden -> wird entfernt
    wenn Klasse nicht vorhanden -> wird hinzugefügt
*/
para.classList.toggle("wichtig");

console.log(para.classList);

// auf Klasse prüfen
if (para.classList.contains("hinweis")) {
    console.log(para.classList.contains("hinweis"));  // true
}

// durch classList iterieren über Property Access
for (let i = 0; i < para.classList.length; i++) {
    console.log(para.classList[i]);
}

Zugriff auf Attribute von HTML-Elementen

Content-Zugriff

Über .getAttribute() wird Inhalt eines Attributs geliefert (oder null, falls Attribut nicht existiert):

let myProgress = document.querySelector("progress");


// Attribute auslesen
// [element].getAttribute(attr_name);
// Rückgabe ist immer ein String

console.log(myProgress.getAttribute("max"));  // "10"
console.log(myProgress.getAttribute("value"));  // "1"
console.log(myProgress.getAttribute("title"));  // null

Über .setAttribute() den Wert eines Attributs ändern oder neue Attribute hinzufügen:

// Attribute setzen
myProgress.setAttribute("max", 100);
myProgress.setAttribute("value", 8);
myProgress.setAttribute("id", "progress-bar");   // neues Attribut

Über .removeAttribute() ein vorhandenes Attribut entfernen. Ist das Attribut nicht vorhanden, wird der Befehl ignoriert (kein Fehler):

// Attribut entfernen
myProgress.removeAttribute("id");
console.log(myProgress.getAttribute("id")); // null

Über .hasAttribute() prüfen ob ein Element ein bestimmtes Attribut hat:

// prüfen ob eine Attribut anliegt
console.log(myProgress.hasAttribute("id"));   // true
console.log(myProgress.hasAttribute("max"));  // false

Über .attributes alle Attribute eines Elements in einer NamedNodeMap erhalten:

console.log(myProgress.attributes);   // NamedNodeMap

// etwas umständlich, aber möglich
console.log(myProgress.attributes.max);   // max="100"
console.log(myProgress.attributes.max.nodeValue);   // "100"

IDL-Zugriff

IDL … Interface Description Language

Einfacher als Content-Zugriff, allerdings ist der Zugriff nur möglich, wenn das Element als Interface definiert ist.

Vorteile:

  • einfachere Notation

  • Der Rückgabewert erfolgt im exakten Datentyp, nicht stets als String

Nachteile:

  • Funktioniert nur über ein Interface

// IDL-Zugriff
let myProgress = document.querySelector("progress");

// Attribut auslesen
// (direkter Zugriff über Punktnotation anstelle Weg über Methoden)
console.log(myProgress.max);   // 100 (number)
console.log(myProgress.value);  // 8 (number)

// Attribut setzen
myProgress.max = 8;
myProgress.value = 5;
console.log(myProgress.max);   // 8 (number)
console.log(myProgress.value);  // 5 (number)

Sonderfall: Boolesche Attribute

Attribute, welche ohne Wert das Verhalten eines Elements beeinflussen

Beispiele: async, defer, disabled, required

Über Content-Zugriff:

let myBtn = document.querySelector("button");

// mit Content-Zugriff

// boolesches Attribut setzen
// doppelte Übergabe nötig (Wert des 2. Arguments jedoch egal)
myBtn.setAttribute("disabled", "disabled");

// boolesches Attribut entfernen
myBtn.removeAttribute("disabled");

// boolesches Attribut toggeln
myBtn.toggleAttribute("disabled");  // hinzufügen
myBtn.toggleAttribute("disabled");  // entfernen

Über IDL-Zugriff (wieder einfacher):

let myBtn = document.querySelector("button");

// mit IDL-Zugriff
myBtn.disabled = true;
myBtn.disabled = false;

Interface Zugriff

Der Content-Zugriff kann nicht auf Interfaces zugreifen, dies ist nur über den IDL-Zugriff möglich.

let eingabeFeld = document.querySelector("#eingabe");

console.log(eingabeFeld.getAttribute("value"));   // "Hey"
console.log(eingabeFeld.value); // "Hey"

// Änderung über Content
eingabeFeld.setAttribute("value", "Hey, du da");
console.log(eingabeFeld.getAttribute("value"));   // "Hey, du da"
console.log(eingabeFeld.value); // "Hey, du da"

// Änderung über IDL
eingabeFeld.value = "Hoppla";
console.log(eingabeFeld.getAttribute("value"));   // "Hey, du da"  (hat nicht geklappt)
console.log(eingabeFeld.value); // "Hoppla"  (OK)

// Änderung bei Nutzer-Eingabe
eingabeFeld.onchange = function() {
    console.log(eingabeFeld.getAttribute("value"));   // NOK -> alter Wert
    console.log(eingabeFeld.value);   // OK -> neuer Wert
};

Fazit:

  • Bei Nutzereingaben muss immer über IDL gegangen werden –> bei Dokument im DOM

  • Vorherige Werte lassen sich auch über Content Zugriff geholt –> bei Dokument im Speicher

Events

Auf Vorgänge, die während der Laufzeit auftreten können (z.B. Maus- & Tastatureingaben) kann mit einem anderen Ereignis reagiert werden. Es gibt dafür zwei Möglichkeiten:

  • Event-Listener

  • Event-Handler

Important

Um zu verhindern, dass Events mehrfach registriert werden, stets mit dem Event-Handler arbeiten, nicht mit dem Event-Listener.

Event-Listener

eine Funktion wird an das Element (z.B. Button), welches eine Ereignis erfährt, registriert. Tritt das Ereignis ein, wird die Funktion ausgeführt. Es können mehrere Funktionen für das selbe Event für ein Element definiert werden, wobei die Funktionen hintereinander ausgeführt werden.

let inputField = document.querySelector("#eingabe");

function log_msg (msg) {
    console.log(msg);
}

// anonyme Funktion als ersten Event-Listener für 'blur' event registrieren
// NACHTEIL: Kann nicht de-registriert werden, da anonyme Funktion
inputField.addEventListener("blur", function() {
    console.log("Ich bin nicht mehr selektiert");
});
// anonyme Funktion zweiter Event-Lister für 'blur' event registrieren
// beide Funktionen werden nun ausgeführt beim Eintritt des Events
// NACHTEIL: Kann nicht de-registriert werden, da anonyme Funktion
inputField.addEventListener("blur", function() {
    log_msg("Selektiere mich bitte erneut");
});

function onBlurHandler() {
    console.log("Ich mach das ...");
}

function onBlurHandlerWithArgs(event) {
    console.log(event.type);
}

// benannte Funktion als dritten Event-Listener für 'blur' event registrieren
inputField.addEventListener("blur", onBlurHandler);

// de-registrieren von drittem Event-Listener
// GEHT NUR BEI BENANNTEN FUNKTIONEN!
inputField.removeEventListener("blur", onBlurHandler);

// benannte Funktion mit Argument als Event-Listener für 'blur' registrieren
inputField.addEventListener("blur", (event) => onBlurHandlerWithArgs(event));

// de-registriere Funktion mit Argumenten als Event-Listener
inputField.removeEventListener("blur", onBlurHandlerWithArgs);

Note

The blur event is fired, when the element has lost focus (e.g. user selects a different element).

DOMContentLoaded Event Listener

Feuert etwas früher, wie load event. Siehe Dokumentation.

Event-Handler

Ist eine Eigenschaft eines Elements, welche direkt einer Funktion zugewiesen werden kann. Diese wird ebenfalls beim Eintreten des Ereignisses ausgeführt. Es gibt hier nur eine Funktion. Sobald die Eigenschaft einer anderen Funktion zugewiesen wird, wird die bisherige Zuweisung überschrieben.

Note

Jeder Event-Handler beginnt mit on plus dem Event Namen und kann in den Elementen nachgeschaut werden. Event-Handler Properties sind alle im lower-Case, ohne Trennzeichen, geschrieben (z.B. onclick).

let inputField = document.querySelector("#eingabe");

function log_msg (msg) {
    console.log(msg);
}

// Event-Handler für 'blur' event zuweisen
inputField.onblur = function() {
    log_msg("Ich handle das allein");
};

// Event-Handler für 'blur' event wird überschrieben
inputField.onblur = function() {
    log_msg("Denkste! Ich bin jetzt der Chef! Hinfort!");
};

function gibMeldungDazu() {
    console.log("Das Event ist wirklich eingetreten");
}

// Ausgelagerte Funktion als Event-Handler registrieren (OHNE Klammern übergeben)
inputField.onfocus = gibMeldungDazu;
console.log(gibMeldungDazu);  // function
console.log(gibMeldungDazu());  // undefined (da kein Return-value)

// Event-Handler de-registrieren (auf 'null' setzen)
inputField.onfocus = null;

DOM-Event-Handler

Der onload Event-Handler wird erst nach abgeschlossenen Laden des DOMs und allen verknüpften Resources (CSS, Bilder, usw.) aufgerufen.

let inputField = document.querySelector("#eingabe");

// DOM-Event-Handler
window.onload = function() {
    console.log("DOM und alle verknüpften Ressourcen fertig geladen");
};