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.
- So könnte ein Login flexibel werden: Wenn der Benutzer schon registriert ist, kann er nach dem Einloggen einfach weitermachen, für neue Nutzer 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 und später wieder gelöscht werden.
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>
<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:
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>
<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.
// 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 (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.
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
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 Termin sortiert, bzw. verschoben werden können. Auch dies kann mit JavaScript erreicht werden:
ToDo (weitere ToDos)
- Changing Content
- element.textContent
- Changing Attributes
- element.setAttribute(attribute, value)
Bildwechsler
Die oben gezeigten Methoden wollen wir nun in einem weiterführenden Anwendungsbeispiel ausprobieren: Im SELF-Forum wurde gefragt, wie man einen Bildwechsler (image-slider) programmiert, der sich mehrfach laden lässt. Deshalb wollen wir für eine Webseite mit mehreren Bildergruppen ein Script programmieren, das …
- das ausgewählte Bild in einer Großansicht zeigt
- Buttons zur Verfügung stellt, die eine weitere Auswahl durch den Nutzer ermöglichen.
- erkennt, welche Bildergruppe angeklickt wird
HTML-Markup
Da es in JavaScript nicht möglich ist, auf Unterseiten oder das Dateiverzeichnis des Servers zuzugreifen, sollten die Bilder (oder deren URLs) schon auf der Webseite notiert sein.
<section id="peru" class="gallery">
<h2>Peru 2007</h2>
<a href="peru-3.jpg">
<img src="peru-3-sm.jpg" alt="Peru 2007: Machu Picchu">
</a>
<a href="peru-4.jpg">
<img src="peru-4-sm.jpg" alt="Peru 2007: Machu Picchu - Lamas in den Ruinen">
</a>
<a href="peru-5.jpg">
<img src="peru-5-sm.jpg" alt="Peru 2007: Uros-Inseln im Titicaca-See">
</a>
...
</section>
Dieses HTML funktioniert bereits ohne JavaScript:
Das im img notierte kleine Bild wird angezeigt. Bei einem Klick darauf wird das im a-Element referenzierte große Bild geladen und im Browser angezeigt. So wird das im letzten Kapitel erwähnte Standardverhalten der HTML-Elemente genutzt.
.gallery a
bzw. .gallery img
kann jedes Element mit CSS oder JavaScript selektiert werden.Bildertausch mit JavaScript
MIt einem Script soll die im href-Attribut notierte URL des großen Bilds für eine Darstellung innerhalb der Webseite genutzt werden.
document.querySelector('.gallery').addEventListener('click',function (evt) {
evt.preventDefault();
let element = event.target;
let parentURL = element.parentNode.href;
element.src = parentURL;
element.classList.add('current');
});
Nach den Regeln des unobtrusive JavaScript wird das Script nicht mit dem HTML vermischt, sondern im head notiert.
Mit AddEventHandler wird dem angeklickten Element eine Funktion zugewiesen.
Diese Funktion …
- verhindert mit Event.preventDefault das Aufrufen des Links
- ermittelt das auslösende Element
- ermittelt mit parentNode das Elternelement des Bilds, liest die URL des href-Attributs aus und …
- setzt diese in das src-Attribut des angeklickten Bilds.
- Damit dies größer dargestellt wird, erhält es mit classList.add() eine neue Klasse.
Mit jedem geklickten Bild wird ein weiteres mit der Klasse current
(engl. für 'aktuell') auf Großansicht umgestellt, die bereits geklickten bleiben groß.
Eine solche Klasse kann man aber auch wieder entfernen:
1 let allPics = document.querySelectorAll('.gallery img');
2 console.log('Anzahl Bilder: ' + allPics.length)
3
4 document.querySelector('.gallery').addEventListener('click',function (evt) {
5 evt.preventDefault();
6 let element = event.target,
7 parentURL = element.parentNode.href;
8 // bisherige Klassenzuweisungen entfernen
9 for (let pic of allPics) {
10 pic.classList.remove('current');
11 };
12 // dem ausgewählten Bild die URL der Großansicht und die Klasse 'current' zuweisen
13 element.src = parentURL;
14 element.classList.add('current');
15 // get the number
16 let idx = index(element);
17 console.log('geklicktes Bild: ' + idx);
18 });
19
20 function index(el) {
21 for (let i=0; i < allPics.length; i++) {
22 if (allPics[i] == el) {
23 return i;
24 }
25 }
26 return -1;
27 }
Mit querySelectorAll können wir alle Elemente, die vom Selektor .gallery img
, also alle img-Elemente, die Kinder und Kindeskinder des Elements mit der Klasse gallery
sind, finden. Das Ergebnis wird als statische NodeList der Variable allPics
zugewiesen.
Vor die dynamische Großansicht schieben wir nun in Zeile 9 eine for...of-Schleife ein, die alle Bilder durchläuft und ihnen mit classList.remove() die eventuell vorhandene Klasse current
entfernt.
Störend wirkt aber, dass die thumbnails sowohl ober- als auch unterhalb der Großansicht dargestellt sind. Dies soll im nächsten Schritt verbessert werden.
Anhang
Helferfunktion für neue Elemente
Mit der oben vorgestellten Funktion konnten neue li-Elemente 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. 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.
[1]
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' });
DOM-Traversal
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.
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[2] 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:
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:
let button = document.querySelector('.delete-button');
let listItem = button.closest('li');
nextElementSibling findet 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:
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!
let taskList = document.querySelector('#taskList');
let tasks = taskList.querySelectorAll('li');
Siehe auch
- Bildwechsler
- Web Animations (WAAPI)
- Animieren in JavaScript
- Animationen steuern
- Fullscreen-Ansicht
(Bild mit Lightbox-Effekt)
Weblinks
- ↑ A tiny helper for document.createElement
- ↑ Eine zugängliche Multilevel-Dropdown-Navigation von Beatovich
Damals Stand der Technik (Der IE6 verstand noch kein:hover
!) sieht der Code heute komisch aus