JavaScript und das DOM/DOM-Manipulation

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Wenn ein Anwender mit der Webseite interagiert, ist es oft nötig, je nach erfolgter Eingabe unterschiedliche Inhalte zur Verfügung zu stellen.

  • So könnte ein Login flexibel werden: Wenn eine Person schon registriert ist, kann sie nach dem Einloggen einfach weitermachen, für neue werden je nach Stand der Eingabe unterschiedliche Inhalte wie ein Anmeldeformular dynamisch nachgeladen.
  • Auch bei einem Bildwechsler benötigt man die Zurück und Weiter-Buttons erst in der Großansicht.
  • Bei Spielen wird das Spielfeld oft erst geladen, wenn der Benutzer die Spielanleitung oder den bisherigen Punktestand (Highscore) gelesen hat, da das Hin- und Herklicken zwischen verschiedenen Webseiten zu umständlich wäre.

Dieses Tutorial zeigt, wie man Webseiten dynamisch erweitert, indem neue Elemente erzeugt, verändert und dann wieder gelöscht werden. Als Beispiel dient eine ToDo-Liste, in der Aufgaben in einer Liste angelegt, bearbeitet und später wieder gelöscht werden.

ToDo-Liste

Eine ToDo-Liste besteht aus einer anfangs (noch) leeren Liste, der man immer wieder neue Listeneinträge mit Aufgaben anhängt.

leere Liste
<form id="controls"> 
	<!-- <label for="newtask">Neues Vorhaben</label>
	<input id="newtask"> -->
	<button type="button" id="new">Hinzufügen!</button>
</form>	
<ul id="taskList">
</ul>

Als Container verwenden wir ein form-Element. Dies ermöglicht es, das noch auskommentierte input-Element auch mit einem einzugeben.

Der Hinzufügen-Button ist mit type="button" versehen, um ein Absenden und damit Reload der Seite zu verhindern.

Die Liste (ul) enthält noch keinen Listeneintrag (li).

Elemente dynamisch erzeugen

Mit JavaScript können wir nun auf Eingaben warten, um HTML-Elemente erzeugen, sie mit Inhalt zu füllen und in das DOM der Webseite einzuhängen:

Listeneinträge dynamisch einfügen ansehen …
 1 'use strict';
 2 document.addEventListener('DOMContentLoaded', function () {
 3 
 4   let text = 'Dies ist ein neuer Listeneintrag!'; 
 5  
 6   document.querySelector('#add').addEventListener('click', createElement);
 7 
 8   function createElement(){
 9     let container = document.querySelector('#taskList'),
10         newElm = document.createElement('li');
11     newElm.textContent = text;
12     container.appendChild(newElm);
13   }
14  
15 });

Wiederholung:

Wie im letzten Kapitel besprochen, wird ein Eventlistener eingerichtet, der das DOMContentLoaded-Event überwacht und dann unser Script lädt. Ein weiterer Eventlistener belauscht den #add-Button.

Dieser Button wurde mit document.querySelector('#add') ausgewählt.

Ein Klick ruft die Funktion createElement() auf. Diese erzeugt mit createElement ein neues li-Element und weist es der Variablen newElm zu. Dem Absatz wird mit textContent eine in der Variablen text enthaltenen Zeichenkette als Textknoten an das Element hinzugefügt. Anschließend wird das so dynamisch erzeugte Element mit appendChild in das DOM eingehängt.

Wenn du es mit der Konsole untersuchst, wirst du keinen Unterschied zu normalen li-Elementen finden.

In umfangreichen Projekten müssen immer wieder Elemente erzeugt werden, oft mehrere auf einmal. Hier empfiehlt es sich eine Helferfunktion für neue Elemente bereitzustellen. Sie nutzt anstelle der festen Werte Parameter, mit denen die Funktion aufgerufen wird.

HTML-Blöcke einfügen

In unserer ToDo-Liste sollen die Einträge durch interaktive Elemente wie Buttons erweitert werden. Dies würde aber ein mehrfaches Aufrufen unserer Helferfunktion erfordern.

Hier hilft die Element.insertAdjacentHTML()-Methode, mit der ganze Blöcke HTML-Markup eingefügt werden können.

Buttons mit insertAdjacentHTML einfügen ansehen …
 
  addButton.addEventListener('click', function () {
    const task = newTaskInput.value.trim(); 
    const taskId = `todo-${taskIdCounter++}`;

    taskList.insertAdjacentHTML('beforeend', `
      <li>
        <input type="checkbox" id="${taskId}">
        <label for="${taskId}">
          <span class="text">${task}</span>
        </label>
        <button aria-label="Delete task: ${task}" class="delete-button">Löschen</button>
      </li>
    `);

Sobald auf den add-Button geklickt wird, wird der Wert von newTaskInput ausgelesen und mit trim um Leerzeichen am Anfang und Ende des Strings bereinigt.

Aus der Zählvariable taskIdCounter wird eine taskId gebildet.

Mit insertAdjacentHTML wird nun das li-Element mit seinen Kindelementen eingefügt. Die jeweiligen Textknoten werden einfach durch die Variablen gefüllt.

Alternativ könnte man ein template-Element anlegen und mit appendChild ins DOM einfügen.

Elemente dynamisch entfernen

Du kannst Elemente dynamisch erzeugen - sie aber auch dynamisch wieder entfernen. In der klassischen Vorgehensweise wurde mit der parentNode-Eigenschaft der Elternknoten gefunden und dann dessen Kind (also das gewünschte Element) mit removeChild entfernt.

Elemente entfernen (2011)
// Identifizieren des Kindknotens
var element = document.getElementById('id');

// Aufruf des Elternknotens, um dann dessen Kindknoten zu löschen
element.parentNode.removeChild(element);

Heute ist dies nicht mehr nötig, die Element.remove()-Methode entfernt ein Objekt direkt aus dem DOM:

Elemente entfernen ansehen …
  taskList.addEventListener('click', function (event) {
    if (event.target.classList.contains('delete-button')) {
      const taskItem = event.target.closest('li');
      taskItem.remove();
    }
  });

Bei einem Klick wird mit Event.target überprüft, ob einer der Lösch-Buttons (classList.contains schaut, ob die Klasse delete-buttons vorhanden ist) angeklickt wurde.

Falls ja, wird mit Element.closest('li') das nächste li gesucht und mit seinen Kindelementen gelöscht.


Beachte: Die DOM-API ist so konzipiert, dass sie sich auf die Manipulation des DOM-Baums konzentriert und nicht auf die Verwaltung von Event-Listenern oder JavaScript-Referenzen.
Das Entfernen eines DOM-Elements mit diesen Methoden entfernt nicht automatisch die Event-Listener, die mit dem Element oder seinen Nachkommen verbunden sind. Wenn Verweise darauf existieren (z. B. in einer globalen Variablen oder einem Closure), werden das Element und die zugehörigen Ereignis-Listener nicht gelöscht.
Dies kann dann zu Problemen führen, wenn ein gleichlautendes Element erneut erzeugt wird.
Empfehlung: Registriere Event-Listener immer zentral auf dem document anstelle bei einzelnen Elementen.
Entferne sie wieder mit removeEventListener, wenn sie nicht mehr benötigt werden.

Komfort-Version

Mir gefällt nicht, dass man eine bereits als erledigt gekennzeichnete Aufgabe durch erneutes Anklicken wieder aktivieren kann. Des Weiteren sollen die Aufgaben nach Wichtigkeit und (später) Termin sortiert, bzw. verschoben werden können. Auch dies kann mit JavaScript erreicht werden.

Jeder Eintrag hat nun anstelle des Löschen-Buttons einen Button, mit dem man eine Bearbeitungsfunktion aktivieren kann. Im HTML-Markup findet sich ein dialog-Element, das nun mit JavaScirpt geöffnet wird:

Dialog-Fenster öffnen und schließen ansehen …
taskList.addEventListener('click', function (event) {
  if (event.target.classList.contains('settings')) {
    editDialog.showModal();
  }
});
document.querySelector('.close-button').addEventListener('click', function () {
  editDialog.close();
});

Bei jedem Klick wird überprüft, ob das mit Event.target ermittelte Element, durch dass das Ereignis ausgelöst wurde, auch die Klasse settings enthält. Dafür wird die Methode classList.contains verwendet. Falls dies zutrifft wird der Dialog mit showModal() geöffnet, bzw. später mit einem Klick auf close-button mit close() wieder geschlossen.

Textknoten einlesen und ändern

Textknoten ändern ansehen …

Attributknoten einlesen und ändern

Ergebnisse mit Web Storage speichern

Damit unsere ToDo-Liste nach dem Schließen der Webseite nicht alle eingegeben Daten verliert, werden die Daten nun mit Web Storage im Browser des Nutzers gespeichert und sind auf diesem Gerät beim erneuten Öffnen der Webseite wieder verfügbar.

ToDo (weitere ToDos)


Anhang

Helferfunktion für neue Elemente

Mit der oben vorgestellten Funktion konnten neue li-Elemente in ul#task erzeugt werden. Für den Alltagseinsatz benötigt man aber eine Helfer-Funktion, mit der beliebige neue Elemente an beliebigen Stellen im DOM erzeugt werden.

Primitiver Website-Builder
mit Helferfunktion zum Erzeugen neuer Elemente ansehen …
 function createElement(parent, elem, content, attributes = {}) {
    const container = document.querySelector(parent);
    
    if (!container) {
      console.error(`Parent element "${parent}" not found.`);
      return;
    }

    if (typeof elem !== 'string') {
      console.error(`Invalid element type: ${elem}`);
      return;
    }

    const newElm = document.createElement(elem);
    newElm.textContent = content;

    // Apply optional attributes
    for (let [key, value] of Object.entries(attributes)) {
      newElm.setAttribute(key, value);
    }

    container.appendChild(newElm);
  }

Die Funktion createElement() wurde jetzt so erweitert, dass ihr Parameter übergeben werden können.

Mit document.querySelector(parent) wird das als Parameter angegebene Elternelement gesucht. Fall es nicht vorhanden ist, wird in der Konsole ein Fehler ausgegeben.

Eine ähnliche Abfrage, ob es das zu erzeugende Element überhaupt gibt, wäre hier mit dem option-Menü nicht nötig - die Helfer-Funktion soll aber universell einsetzbar sein!

Mit document.createElement(elem); wird dann ein leeres, neues Element erzeugt, also je nach Wert der Variablen Typ z. B. ein h1-Element oder ein p-Element. Damit wird das Element aber noch nicht angezeigt. document.createElement() erzeugt lediglich den Elementknoten, hängt ihn aber noch nicht in den Strukturbaum des Dokuments ein.

Mit newElm.textContent = content; wird der eingegebene Text als Textknoten erzeugt.

Erst jetzt wird das neue Element mit seinem Textknoten mit der Methode appendChild() ins DOM eingehängt. Anwendbar ist die Methode auf ein Knotenobjekt, das Kindknoten haben darf. Also beispielsweise Elementknoten. Als Parameter erwartet die Methode einen Knoten, der als Kindknoten eingehängt werden soll.

Mit der Anweisung container.appendChild(newElm); wird auf den zunächst leeren div-Bereich im Dokument zugegriffen. Diesem Element wird der neu erzeugte Elementknoten hinzugefügt. [2]

Falls du dem neuen Element Attribute verpassen willst, musst du diese als key-value-pairs (Schlüssel-Wert-Paare) in einem JS-Objekt notieren:

Attribut und Attributwert als Schlüssel-Wert-Paare
createElement('#taskList', 'button', 'Löschen!', { id: 'delete-button', class: 'warning' });

DOM-Traversal

DOM - Whitespace im Elementbaum
DOM - Whitespace im Elementbaum

Wie im ersten Kapitel besprochen, besteht eine vom Browser geparste Webseite aus einem Elementbaum von nodes (Knoten), die sich immer weiter verzweigen.

Wenn man - wie oben gezeigt - ein Element erzeugt, hängt man es anschließend in den Elementbaum ein. Auch beim Löschen mit removeChild muss mit der parentNode-Eigenschaft der Elternknoten gefunden und dann dessen Kind (also das gewünschte Element) wieder entfernt werden.

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

Siehe auch

  • Bildwechsler
    Carousel-icon.svg
  • Web Animations (WAAPI)
    • Animieren in JavaScript
    • Animationen steuern
  • Fullscreen-Ansicht

    (Bild mit Lightbox-Effekt)

Weblinks

  1. A Todo List Heydon Pickering, 07.04.2017 (inclusive-components.com)
  2. A tiny helper for document.createElement
  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