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 :menuselection:`Developer Tools --> DOM`. Beispiele: .. code-block:: javascript 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): .. code-block:: javascript 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. .. code-block:: javascript let headLine = document.querySelector("h1"); // [element].innerHTML => liefert den Content Text inklusive Markups console.log(headLine.innerHTML); // überschreiben vom Text Content headLine.innerHTML = "Hallo Welt"; console.log(headLine.innerHTML); .. hint:: Die ursprüngliche HTML Seite wird durch Änderung **nicht** verändert. Die Änderung erfährt nur das DOM. .. code-block:: javascript // hinzufügen von Text headLine.innerHTML = "Achtung: " + headLine.innerHTML; headLine.innerHTML += "!!!"; // hinten anfügen // gesamter Content wird geschrieben headLine.innerHTML += " mit Zusatz unsichtbar"; Den gerenderten Content (so wie User es sieht -> Ohne Markups, ohne doppelte Leerzeichen oder ausgeblendete Inhalte) wird über ``.innerText`` sichtbar: .. code-block:: javascript headLine.innerHTML += " mit Zusatz unsichtbar"; console.log(headLine.innerHTML); // Achtung: Hallo Welt!!! mit Zusatz unsichtbar console.log(headLine.innerText); // "Achtung: Hallo Welt!!! mit Zusatz" Über ``.textContent`` werden doppelte Leerzeichen ausgegeben, aber keine Markups oder ausgeblendete Inhalte. .. code-block:: javascript headLine.innerHTML += " mit Zusatz unsichtbar"; console.log(headLine.textContent); // Achtung: Hallo Welt!!! mit Zusatz unsichtbar .. TODO: // .innerText = gerenderter Content (keine ausgeblendeten Elemente, keine Markups -> Users) // .textContent = AM PERFORMANTESTEN: so wie im Inspektor (mit doppelten Leerzeichen, mit ausgeblendeten Inhalte, aber keine Markups) .. 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. .. code-block:: javascript 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: .. code-block:: javascript // 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) .. code-block:: javascript // 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. .. code-block:: javascript /* 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) .. code-block:: javascript 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): .. code-block:: javascript 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: .. code-block:: javascript // 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): .. code-block:: javascript // Attribut entfernen myProgress.removeAttribute("id"); console.log(myProgress.getAttribute("id")); // null Über ``.hasAttribute()`` prüfen ob ein Element ein bestimmtes Attribut hat: .. code-block:: javascript // 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: .. code-block:: javascript 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 .. code-block:: javascript // 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: .. code-block:: javascript 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): .. code-block:: javascript 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. .. code-block:: javascript 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. .. code-block:: javascript 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 ``````````````````````````````` .. TODO: EventListener für DOMContentLoaded event dokumentieren 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``). .. code-block:: javascript 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. .. code-block:: javascript let inputField = document.querySelector("#eingabe"); // DOM-Event-Handler window.onload = function() { console.log("DOM und alle verknüpften Ressourcen fertig geladen"); };