JavaScript und das DOM/DOM-Manipulation
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
Inhaltsverzeichnis
ToDo-Liste
Eine ToDo-Liste besteht aus einer anfangs (noch) leeren Liste, der man immer wieder neue Listeneinträge mit Aufgaben anhängt.
<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>
Information
Auch wenn scheinbar alles mit JavaScript geht, ist es empfehlenswert das HTML-Markup sorgfältig zu strukturieren.[1]Viele Elemente haben ein Standardverhalten, das viel Programmierarbeit erspart!
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:
<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.
<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:
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.
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.
// 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:
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.
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.
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:
<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
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 });
- Wir registrieren einen addEventListener, der überprüft, ob auf
editSettingsgeklickt wird.- Mit editButton.closest('li') wird das zugehörige Listenelement gesucht und in
selectedTaskgespeichert - Dann wird der Text mit selectedTask.querySelector('.text').textContent ausgelesen und in das Eingabefeld kopiert:
editTextInput.value = ... - Der Dialog zeigt den aktuellen Text der Aufgabe an.
- Mit editButton.closest('li') wird das zugehörige Listenelement gesucht und in
- Bei einer Eingabe im Eingabefeld passiert im DOM nichts; erst wenn …
- Der Nutzer auf Speichern klickt,
- suchen wir wieder
selectedTask.querySelector('.text')und - ersetzen seinen Inhalt (
.textContent = editTextInput.value)
- suchen wir wieder
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:
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.
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.
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
- 1 + 2 = ?Zahlenspiele
- Zufallsgenerator
- Zahlen-Raten
- Mathe-Quiz
- Web Animations (WAAPI)

- Animieren in JavaScript
- Animationen steuern
- Bildwechsler

- 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.
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:
createElement('#taskList', 'button', 'Löschen!', { id: 'delete-button', class: 'warning' });
Weblinks
- ↑ A ToDo List Heydon Pickering, 07.04.2017 (inclusive-components.com)
- ↑ A tiny helper for document.createElement