DOM und Events
Das DOM ist die Seite, wie der Browser sie im Speicher hält, ein Baum aus Elementen. JavaScript kann diesen Baum lesen und ändern. In Teil 2 baust du so eine erste SPA von Hand, bevor Vue dir diese Arbeit abnimmt. Die Beispiele dieser Einheit sind kleine HTML-Seiten. Mit «Ausführen» erscheinen sie unter dem Code, und die Ausgabe der Konsole steht darunter.
Lernziele
Nach dieser Einheit kannst du
- Elemente mit
querySelectorfinden und neue Elemente erzeugen, - Text sicher mit
textContentsetzen und erklären, waruminnerHTMLgefährlich sein kann, - mit
addEventListenerauf Klicks und Eingaben reagieren, - mit einem einzigen Listener viele Elemente bedienen und ein Formular ohne Neuladen verarbeiten.
Elemente finden und erzeugen
Abschnitt betitelt „Elemente finden und erzeugen“document.querySelector nimmt einen CSS-Selektor und liefert das erste passende Element oder null. querySelectorAll liefert alle passenden Elemente. Mit document.createElement erzeugst du ein neues Element, das erst sichtbar wird, wenn du es mit append in den Baum einfügst.
<!doctype html><h1>Katalog</h1><ul id="catalog"></ul><script> const names = ['Bohrmaschine', 'Zelt', 'Leiter'] const list = document.querySelector('#catalog') for (const name of names) { const entry = document.createElement('li') entry.textContent = name list.append(entry) } console.log(list.children.length)</script>Die Schleife erzeugt für jeden Namen ein <li>, setzt seinen Text und hängt es an die Liste. list.children enthält danach drei Elemente.
textContent setzt reinen Text. Mit innerHTML könntest du auch HTML einsetzen, doch das ist gefährlich, sobald der Text von Mitgliedern stammt. Wer in eine Bemerkung HTML mit Script schreibt, bringt sonst fremden Code in die Seite. Diese Lücke heisst Cross-Site Scripting, kurz XSS. Die Aufgabe «Einstieg» zeigt den Unterschied.
Auf Events reagieren
Abschnitt betitelt „Auf Events reagieren“Klicks, Eingaben und andere Ereignisse heissen Events. Mit addEventListener hängst du eine Funktion an ein Element, und der Browser ruft sie bei jedem passenden Event auf.
<!doctype html><button id="reserve">Reservieren</button><p id="count">0</p><ul id="catalog"> <li data-id="1">Bohrmaschine</li> <li data-id="2">Zelt</li></ul><script> let reservations = 0 document.querySelector('#reserve').addEventListener('click', () => { reservations += 1 document.querySelector('#count').textContent = String(reservations) })
// One listener on the list handles clicks on every entry, also on entries added later document.querySelector('#catalog').addEventListener('click', (event) => { const entry = event.target.closest('li') if (entry) console.log('Gewählt', entry.dataset.id, entry.textContent) })
document.querySelector('#reserve').click() document.querySelector('li[data-id="2"]').click()</script>Der erste Listener zählt die Klicks auf den Knopf. Der zweite hängt an der ganzen Liste. Ein Event steigt vom angeklickten Element im Baum nach oben, darum bekommt die Liste auch die Klicks auf ihre Einträge. event.target ist das Element, auf das wirklich geklickt wurde, und closest('li') sucht von dort aus den nächsten Eintrag nach oben. Dieses Muster heisst Event-Delegation. Ein Listener genügt für alle Einträge, auch für solche, die erst später dazukommen.
data-id ist ein eigenes Attribut, das du im Script über dataset.id liest. Werte aus dataset sind immer Text. Am Schluss löst das Script zwei Klicks selbst aus, damit in der Ausgabe etwas steht. Klick danach selbst auf den Knopf und auf die Einträge.
Formulare
Abschnitt betitelt „Formulare“Ein Formular schickt seine Daten normalerweise an einen Server und lädt dann eine neue Seite. Eine SPA verhindert das mit event.preventDefault() und verarbeitet die Daten selbst.
<!doctype html><form id="request"> <label>Name <input name="name" required></label> <label>Tage <input name="days" type="number" min="1" max="7" value="2"></label> <button>Anfragen</button></form><p id="message"></p><script> const form = document.querySelector('#request') form.addEventListener('submit', (event) => { event.preventDefault() const data = new FormData(form) document.querySelector('#message').textContent = `${data.get('name')}, ${data.get('days')} Tage` console.log(document.querySelector('#message').textContent) }) form.elements.name.value = 'Martina' form.requestSubmit()</script>Der Listener hängt am submit-Event des Formulars, nicht am Klick auf den Knopf. So funktioniert das Absenden auch mit der Eingabetaste. FormData liest alle Felder mit einem name auf einmal, data.get('days') liefert den Wert als Text. Die Attribute required, min und max prüft der Browser selbst, bevor er das Event auslöst. requestSubmit sendet das Formular aus dem Script ab, wie es ein Klick auf den Knopf täte.
Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
querySelector liefert null, weil das Script vor dem HTML läuft. | Das Script ans Ende der Seite setzen oder type="module" verwenden, denn Module laufen erst, wenn das HTML gelesen ist. |
Text von Mitgliedern wird mit innerHTML eingesetzt. | textContent verwenden. |
| Das Formular lädt die Seite neu, und die Ausgabe verschwindet sofort. | Im submit-Listener event.preventDefault() aufrufen. |
| Jeder Eintrag einer Liste bekommt einen eigenen Listener, und neue Einträge reagieren nicht. | Einen Listener auf die Liste setzen und mit event.target.closest(...) den Eintrag finden. |
addEventListener('click', handle()) ruft die Funktion sofort auf. | Die Funktion selbst übergeben, ohne Klammern. |
Aufgaben
Abschnitt betitelt „Aufgaben“Aufgabe Einstieg
Ein Mitglied gibt als Bemerkung <img src=x onerror=alert(1)> Zelt ein. Zeig die Bemerkung in einem Absatz an, ohne dass daraus ein Element wird. Wie viele Kindelemente hat der Absatz danach?
Lösung
<!doctype html><p id="note"></p><script> const input = '<img src=x onerror=alert(1)> Zelt' // textContent shows the text as it is, innerHTML would turn it into an element document.querySelector('#note').textContent = input console.log(document.querySelector('#note').children.length)</script>Die Ausgabe ist 0. textContent zeigt die Zeichen genau so, wie sie eingegeben wurden, auch die spitzen Klammern. Mit innerHTML entstünde ein <img>-Element, dessen Bild nicht lädt, und onerror würde den Code des Mitglieds ausführen.
Aufgabe Vertiefung
Ein Klick auf einen Eintrag der Liste markiert ihn als Favorit, ein zweiter Klick hebt die Markierung wieder auf. Verwende einen einzigen Listener auf der Liste und die Klasse favorite. Gib nach jedem Klick den Namen aus und ob der Eintrag gerade Favorit ist.
Lösung
<!doctype html><ul id="catalog"> <li>Bohrmaschine</li> <li>Zelt</li></ul><script> const list = document.querySelector('#catalog') list.addEventListener('click', (event) => { const entry = event.target.closest('li') if (!entry) return entry.classList.toggle('favorite') console.log(entry.textContent, entry.classList.contains('favorite')) }) list.querySelector('li').click() list.querySelector('li').click()</script>classList.toggle setzt die Klasse, wenn sie fehlt, und entfernt sie, wenn sie da ist. classList.contains fragt den Stand ab. Das return am Anfang des Listeners fängt Klicks ab, die zwar in der Liste, aber auf keinem Eintrag landen.
Aufgabe Knobelei
Baue eine Suche. Bei jeder Eingabe im Suchfeld zeigt die Liste nur die Namen, die den Suchtext enthalten, ohne Rücksicht auf Gross- und Kleinschreibung. Schreib dafür eine Funktion render(query), die die Liste jedes Mal neu aufbaut.
Lösung
<!doctype html><input id="search" placeholder="Suchen"><ul id="catalog"></ul><script> const names = ['Bohrmaschine', 'Stichsäge', 'Zelt'] const list = document.querySelector('#catalog')
function render(query) { list.replaceChildren() for (const name of names) { if (!name.toLowerCase().includes(query.toLowerCase())) continue const entry = document.createElement('li') entry.textContent = name list.append(entry) } console.log(list.children.length) }
const search = document.querySelector('#search') search.addEventListener('input', () => render(search.value)) render('') search.value = 'säge' search.dispatchEvent(new Event('input'))</script>replaceChildren() ohne Argumente leert die Liste, danach fügt die Schleife die passenden Einträge ein. Das input-Event kommt bei jeder Änderung im Feld, nicht erst beim Verlassen. Am Schluss setzt das Script den Suchtext «säge» und löst das Event selbst aus, darum zeigt die Ausgabe zuerst 3 und dann 1. Zustand ändern und die Liste neu aufbauen, diese Arbeit nimmt dir Vue in Teil 3 ab.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.