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.

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 mit typischen CRUD-Operationen (Create, Read, Update, Delete):

  • Eine neue Aufgabe anlegen
  • Aufgaben auslesen
  • Aufgaben als erledigt markieren
  • Aufgaben löschen

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>	
<ol id="taskList">
</ol>

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 einen Reload der Seite zu verhindern.

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

Dieser soll nach einer Benutzereingabe jeweils neu erzeugt werden:

Listeneintrag
      
<li id="${taskId}">
  <span class="text">${task}</span>
  <button aria-label="Delete task: ${task}" class="delete-button">Löschen</button>
</li>

Jedes Listenelemenent li enthält als Kind-Elemente einen span mit dem Text und einen Button zum Löschen und Bearbeiten der jeweiligen Aufgabe. Innerhalb des Button befindet sich ein weiterer span, der mit visually-hidden visuell verborgen ist, aber von Assistenztechniken beachtet wird.


Alternativer Listeneintrag mit Checkbox
<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>

In einer früheren Version und im Tutorial ToDo-Liste_mit_PHP_und_SQL besteht die ToDo-Liste aus einer Checkbox mit dazugehörigem label. So kann man direkt mit HTML zwischen den Zuständen done und pending umschalten und den Wert dann auslesen, bzw. gleich mit CSS stylen. Dieser Aufbau ist aber nicht erweiterbar.

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 id="${taskId}">
        <span class="text">${task}</span>
        <button class="settings-button">
          Bearbeiten
          <span class="visually-hidden">von Aufgabe ${task}</span>
        </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. (In dieser Version bleibt das Eingabefeld noch ausgegraut, um sich auf das Erzeugen und Löschen zu konzentrieren.

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 angeklickt wurde (classList.contains schaut, ob die Klasse delete-buttons vorhanden ist).

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

Bis jetzt können wir neue Tasks (bzw. HTML-Elemente) anlegen und wieder löschen. Komfortabler wäre es, bestehende Texte auch ändern zu können und anzuzeigen, ob eine Aufgabe als erledigt (done) gilt. Des Weiteren sollen die Aufgaben nach Wichtigkeit und (später) Termin sortiert, bzw. verschoben werden können.

Auch dies kann mit JavaScript erreicht werden. Dabei wollen wir die HTML-Elemente verändern, indem wir Textknoten und Attributknoten auslesen und gegebenfalls ändern.

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 durch den Bearbeiten-Button geöffnet wird:

Dialog-Fenster öffnen und schließen ansehen …
<ol id="taskList">
	<li>
		<span class="text"></span>
		<button aria-label="Edit ${taskId}" command="show-modal" commandfor="editSettings">Bearbeiten</button>
	</li></ol>

<dialog id="editSettings" closedby="any">
  <form method="dialog">
    <label for="editText">Ändere den Text:</label><br>
    <input type="text" id="editText">
    <button type="button" class="save-button">Speichern</button>
    <p><button aria-label="Delete task: ${task}" class="delete-button">Löschen</button></p>

    <select class="status-dropdown" size="3" >
      <option value="planned">Planned</option>
      <option value="in-work">In Work</option>
      <option value="finished">Finished</option>
    </select>
  </form>
</dialog>

Das Öffnen wird durch das command-Attribut nativ durch den Browser erledigt - für ältere Browser findet sich am Ende des Scripts ein Polyfill.

Textknoten einlesen und ändern

Textknoten ändern ansehen …
 1 taskList.addEventListener('click', (event) => {
 2   const editButton = event.target.closest('[commandfor="editSettings"]');
 3   if (!editButton) return;
 4 
 5   selectedTask = editButton.closest('li');
 6 
 7   editTextInput.value =
 8     selectedTask.querySelector('.text').textContent;
 9 });
10 
11 saveButton.addEventListener('click', () => {
12   if (!selectedTask) return;
13 
14   selectedTask.querySelector('.text').textContent =
15     editTextInput.value.trim();
16 
17   editDialog.close();
18   selectedTask = null;
19 });
  1. Wir registrieren einen addEventListener, der überprüft, ob auf editSettings geklickt wird.
  2. Bei einer Eingabe im Eingabefeld passiert im DOM nichts; erst wenn …
  3. Der Nutzer auf Speichern klickt,
    • suchen wir wieder selectedTask.querySelector('.text') und
    • ersetzen seinen Inhalt (.textContent = editTextInput.value)

Attributknoten einlesen und ändern

Die verschiedenen Listeneinträge (sprich: Aufgaben) können über die im Dialog-Feld vorhandene Checkbox als erledigt gekennzeichnet werden.

Toggle

Das findet über eine Klasse statt, die von JavaScript gesetzt und dann mit CSS formatiert wird:

Klassen ändern ansehen …
      
const checkbox     = document.querySelector('.completion-status');	

checkbox.addEventListener('change', () => {
  selectedTask.classList.toggle('done', checkbox.checked);
});

Um eine Aufgabe als erledigt zu formatieren benötigen wir nur eine Klasse. Aufgaben, die noch „in Bearbeitung“ sind werden normal über das li-Element formatiert.

Ändert sich der Zustand der Checkbox, wird mit classList.toggle die Klasse done getoggelt, d.h. gesetzt oder entfernt, ohne dass wir erst überprüfen müssen, welcher Zustand vorher gesetzt war. Dies ist die optimale Vorgehensweise, um zwischen zwei Zuständen hin- und herzuschalten.

Abgehakt - Formatierung mit CSS ansehen …
      
li {
  background: lightYellow;

  &.done {
    background: #d4e3b5;;
  }	
  &.done:before {
	font-size:1.5em; 
	content: " ✓ ";
	line-height: 150%;
	padding-left: 0.25em;
	color:green;
	grid-column: 1/2;
	grid-row: 1/2;		
  }	
}

Aufgaben mit der Klasse done erhalten einen hellgrünen Hintergrund und ein Pseudoelement in Form eines Checkmarks.

3 Zustände

Wichtigere ToDos sollen einen „In Bearbeitung“-Status erhalten und dann hoffentlich bald als „Erledigt“ abgehakt werden.

Klassen auslesen und setzen ansehen …
 
statusSelect.addEventListener('change', () => {
  	if (!selectedTask) return;

  	const status = statusSelect.value; // planned | pending | done

  	selectedTask.classList.remove('planned', 'pending', 'done');

  	selectedTask.classList.add(status);
});

Das select-Menü erhält drei option-Elemente. Über einen Eventlistener wird auf Änderungen gewartet und dann der ausgewählte Wert der Variablen status zugewiesen.

Anschließend werden mit classList.remove() die drei als Parameter angegebenen Klassen entfernt und mit classList.add der aktuelle Wert neu gesetzt.

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)

Besser wäre es, die Daten serverseitig zu speichern, damit ein Nutzer mit mehreren Geräten, bzw. evtl sogar mehrere Nutzer auf die einzelnen Aufgaben zugreifen können.

Siehe auch

  • Progressive Web-App

    Unsere ToDo-Liste wird zur App!

    • Installierbar
    • Offline verfügbar
    • mehrere Benutzer und Geräte
  • Zahlenspiele
    • Zufallsgenerator
    • Zahlen-Raten
    • Mathe-Quiz
  • Web Animations (WAAPI)
    • Animieren in JavaScript
    • Animationen steuern
  • Bildwechsler
    Carousel-icon.svg
  • ToDo-Liste mit PHP und SQL
    im Model-View-Controller-Pattern (MVC)


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. Falls 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' });

Weblinks

  1. A ToDo List Heydon Pickering, 07.04.2017 (inclusive-components.com)
  2. A tiny helper for document.createElement