Tutorials › Teil 3: Die Seite
Kapitel 12 von 16
Etwas auf der Seite ausgeben
Fast jedes Skript in diesem Archiv baut seine Oberfläche so auf:
document.write('<p>Verwendbare Zeichen: abc...</p>');
document.write('<textarea id="eingabe"></textarea>');
document.write('<input type="button" value="Übersetzen!">');
Das hat zwanzig Jahre lang funktioniert. Trotzdem sollte man es nicht mehr schreiben, und der Grund ist nicht Geschmack.
Was document.write anrichtet
Solange der Browser die Seite noch aufbaut, schiebt document.write seinen Text an die aktuelle Stelle in den Dokumentenstrom. Das geht.
Ist die Seite fertig geladen, passiert etwas anderes. Dann öffnet document.write das Dokument neu und löscht dabei alles, was darin steht.
<p>Dieser Absatz existiert. Gleich nicht mehr.</p>
<button id="knopf">document.write nach dem Laden</button>
<script>
document.getElementById("knopf").addEventListener("click", () => {
document.write("Alles weg.");
});
</script>
Drück den Knopf. Die Seite im Ergebnisfenster ist danach leer bis auf zwei Wörter. Der Absatz, der Knopf, das Skript, alles fort.
Dazu kommt, dass document.write den Seitenaufbau blockiert. Der Browser muss anhalten und warten, weil er nicht weiß, ob gleich noch mehr Inhalt nachkommt.
Aus beiden Gründen taucht die Funktion in jeder Liste veralteter Technik auf. Chrome zeigt in der Konsole seit Jahren eine Warnung an.
Der Ersatz besteht aus zwei Schritten
Erstens: ein Element im Dokument finden. Zweitens: seinen Inhalt setzen.
<p id="ziel">Platzhalter</p>
<script>
const p = document.getElementById("ziel");
p.textContent = "Von JavaScript eingesetzt.";
</script>
Das HTML steht im HTML, das Skript füllt nur noch. Wer den Quelltext liest, sieht die Struktur der Seite, ohne den Code ausführen zu müssen. Suchmaschinen und Vorleseprogramme übrigens auch.
Elemente finden
<p id="einzeln">A</p>
<p class="viele">B</p>
<p class="viele">C</p>
<script>
zeige(document.getElementById("einzeln").textContent);
zeige(document.querySelector(".viele").textContent);
zeige(document.querySelectorAll(".viele").length);
document.querySelectorAll(".viele").forEach((el) => zeige("gefunden:", el.textContent));
</script>
getElementById sucht nach der Kennung und ist die schnellste Variante.
querySelector nimmt einen CSS-Selektor, genau denselben, den du auch im Stylesheet schreiben würdest. Es liefert den ersten Treffer oder null.
querySelectorAll liefert alle Treffer. Das Ergebnis ist kein echtes Array, hat aber forEach und lässt sich mit for...of durchgehen. Für map und filter musst du es umwandeln: [...document.querySelectorAll(".viele")].
<div class="karte" data-preis="19">Tarif A</div>
<div class="karte" data-preis="45">Tarif B</div>
<script>
const karten = [...document.querySelectorAll(".karte")];
const teuer = karten.filter((k) => Number(k.dataset.preis) > 30);
zeige("teure Karten:", teuer.map((k) => k.textContent));
</script>
dataset liest Attribute, die mit data- beginnen. Eine saubere Art, kleine Datenmengen im HTML abzulegen.
textContent oder innerHTML
Das ist die wichtigste Entscheidung in diesem Kapitel.
<p id="a"></p>
<p id="b"></p>
<script>
const text = "<b>fett</b> & <i>kursiv</i>";
document.getElementById("a").textContent = text;
document.getElementById("b").innerHTML = text;
</script>
textContent behandelt alles als Text. Spitze Klammern erscheinen als spitze Klammern.
innerHTML behandelt alles als Markup. Der Browser baut daraus Elemente.
Das klingt nach einem Komfortunterschied, ist aber ein Sicherheitsunterschied.
<p id="ziel"></p>
<script>
const vomBesucher = '<img src=x onerror="document.body.style.background=\'darkred\'">';
document.getElementById("ziel").innerHTML = vomBesucher;
zeige("Der Hintergrund hat sich geändert, ohne dass jemand geklickt hat.");
</script>
Tausch in dem Beispiel innerHTML gegen textContent und drück auf „Auf Anfang". Dann steht der Text einfach da und nichts passiert.
innerHTML ist nicht verboten. Es ist in Ordnung für Markup, das du selbst geschrieben hast und das keine fremden Bestandteile enthält.
Elemente selbst bauen
Für alles Zusammengesetzte ist der saubere Weg, echte Elemente zu erzeugen.
<ul id="liste"></ul>
<script>
const anbieter = ["Telekom", "Vodafone", "o2"];
const liste = document.getElementById("liste");
for (const name of anbieter) {
const eintrag = document.createElement("li");
eintrag.textContent = name;
liste.appendChild(eintrag);
}
</script>
createElement erzeugt ein Element, das noch nirgends hängt. Erst appendChild hängt es ein.
Attribute und Klassen setzt man vorher:
<div id="ziel"></div>
<script>
const link = document.createElement("a");
link.href = "/javascripts/spiele/kniffel/";
link.textContent = "Zum Kniffel";
link.className = "knopf";
link.setAttribute("data-herkunft", "kurs");
document.getElementById("ziel").appendChild(link);
zeige("Erzeugt:", link.outerHTML);
</script>
Viele Elemente auf einmal
Bei jedem appendChild rechnet der Browser das Layout neu. Bei drei Einträgen ist das egal. Bei tausend merkt man es.
<ul id="liste"></ul>
<script>
const start = performance.now();
const liste = document.getElementById("liste");
const sammlung = document.createDocumentFragment();
for (let i = 1; i <= 500; i++) {
const li = document.createElement("li");
li.textContent = "Eintrag " + i;
sammlung.appendChild(li);
}
liste.appendChild(sammlung);
zeige("500 Einträge in", (performance.now() - start).toFixed(1), "ms");
</script>
Ein Fragment ist ein Sammelbehälter außerhalb des Dokuments. Man füllt ihn in Ruhe und hängt ihn dann in einem Zug ein. Das Layout wird einmal gerechnet statt fünfhundertmal.
Ändern und entfernen
<p id="a" class="alt">Erster</p>
<p id="b">Zweiter</p>
<script>
const a = document.getElementById("a");
a.classList.add("neu");
a.classList.remove("alt");
a.classList.toggle("hervorgehoben");
zeige("Klassen:", a.className);
a.style.color = "darkgreen";
a.hidden = false;
document.getElementById("b").remove();
zeige("Zweiter Absatz entfernt.");
</script>
classList ist der richtige Weg, um Zustände zu schalten. Das Aussehen gehört ins Stylesheet, das Skript sagt nur, welcher Zustand gerade gilt.
Direkte Zuweisungen an style sind für Werte gedacht, die man vorher nicht kennt, etwa eine berechnete Breite. Für alles andere ist eine Klasse besser.
Ein Archivskript, übersetzt
So sieht die Ausgabe eines alten Skripts aus:
document.write('<div align="center">');
document.write('<p>Verwendbare Zeichen: abc</p>');
document.write('<textarea id="eingabe"></textarea>');
document.write('</div>');
Und so heute:
<div id="werkzeug"></div>
<script>
const box = document.getElementById("werkzeug");
const hinweis = document.createElement("p");
hinweis.textContent = "Verwendbare Zeichen: abc";
const feld = document.createElement("textarea");
feld.id = "eingabe";
feld.rows = 3;
box.append(hinweis, feld);
zeige("Aufgebaut, ohne das Dokument anzufassen.");
</script>
Der Unterschied in Zeilen ist gering. Der Unterschied im Verhalten ist groß: Das hier funktioniert auch, wenn die Seite längst geladen ist, es blockiert nichts, und die Zentrierung steht im Stylesheet statt in einem align-Attribut, das seit HTML5 nicht mehr gültig ist.
append kann mehrere Elemente auf einmal und akzeptiert auch reinen Text, appendChild nur ein einzelnes Element.
Bisher passiert alles sofort beim Laden. Interessant wird es, wenn der Besucher etwas tut.