JavaScript-Eventing im Detail

preventDefault und stopPropagation: Wann welche Methode verwendet wird und was genau jede Methode?

Event.stopPropagation() und Event.preventDefault()

Die Verarbeitung von JavaScript-Ereignissen ist oft unkompliziert. Dies gilt insbesondere für eine einfache (relativ flache) HTML-Struktur. Wenn Ereignisse sich durch eine Hierarchie von Elementen bewegen (oder sich verbreiten), geht die Sache jedoch etwas komplizierter. In der Regel suchen Entwickler nach stopPropagation() und/oder preventDefault(), um ihr Problem zu lösen. Falls du schon einmal gedacht hast: „Ich versuche einfach preventDefault(). Wenn das nicht funktioniert, versuche ich stopPropagation(). Wenn das nicht funktioniert, probiere ich beides aus. Dann ist dieser Artikel genau richtig für dich. Ich erkläre Ihnen genau, was die einzelnen Methoden bewirken und wann Sie welche verwenden sollten. Außerdem stelle ich Ihnen eine Reihe von Arbeitsbeispielen vor, die Sie erkunden können. Mein Ziel ist es, diese Verwirrung ein für alle Mal zu beenden.

Bevor wir uns jedoch etwas genauer ansehen, möchte ich kurz auf die beiden Arten der Ereignisverarbeitung eingehen, die in JavaScript möglich sind. Das ist in allen modernen Browsern möglich: Internet Explorer vor Version 9 hat die Ereigniserfassung überhaupt nicht unterstützt.

Vielseitigkeitsstile (Aufnahme und Bubbling)

Alle modernen Browser unterstützen die Ereigniserfassung, aber sie wird von Entwicklern nur sehr selten verwendet. Interessanterweise war dies die einzige Form von Eventing, die ursprünglich von Netscape unterstützt wurde. Der größte Konkurrent von Netscape, Microsoft Internet Explorer, unterstützte die Ereigniserfassung überhaupt nicht, sondern unterstützte lediglich einen anderen Ereignisstil namens Event-Bubbling. Bei der Gründung des W3C erkannte das Unternehmen seine Vorteile für beide Ereignistypen und erklärte über einen dritten Parameter für die addEventListener-Methode, dass Browser beide unterstützen sollten. Ursprünglich war dieser Parameter nur ein boolescher Wert, aber alle modernen Browser unterstützen ein options-Objekt als dritten Parameter, den Sie u. a. verwenden können, um anzugeben, ob die Ereigniserfassung verwendet werden soll oder nicht:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

Das Objekt options und seine capture-Eigenschaft sind optional. Wenn einer der beiden Werte weggelassen wird, ist der Standardwert für capture false. Das bedeutet, dass Ereignis-Bubbling verwendet wird.

Ereigniserfassung

Was bedeutet es, wenn Ihr Event-Handler in der Erfassungsphase wartet? Um dies zu verstehen, müssen wir wissen, wie Ereignisse entstehen und wie sie sich bewegen. Folgendes gilt für alle Ereignisse, selbst wenn Sie als Entwickler das Ereignis nicht nutzen, sich nicht dafür interessieren oder nicht darüber nachdenken.

Alle Ereignisse beginnen am Fenster und durchlaufen zuerst die Erfassungsphase. Das bedeutet, dass beim Senden eines Ereignisses das Fenster gestartet und zuerst in Richtung seines Zielelements „nach unten“ bewegt wird. Das passiert auch dann, wenn du nur in der Bubbling-Phase zuhören. Betrachten Sie das folgende Beispiel-Markup und JavaScript:

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('#C was clicked');
  },
  true,
);

Wenn ein Nutzer auf das Element #C klickt, wird ein am window initiiertes Ereignis ausgelöst. Dieses Ereignis wird dann so durch seine Nachfolger weitergegeben:

window => document => <html> => <body> => usw., bis das Ziel erreicht ist.

Dabei spielt es keine Rolle, ob ein Klickereignis für das Element window, document oder <html> oder <body> oder ein anderes Element auf dem Weg zu seinem Ziel nicht auf ein Klickereignis wartet. Ein Ereignis tritt weiterhin bei window auf und beginnt seine Journey wie gerade beschrieben.

In unserem Beispiel wird das Klickereignis dann über alle Elemente zwischen window und #C vom window zum Zielelement (in diesem Fall #C) weitergeleitet. Das ist ein wichtiges Wort, da es direkt mit der Funktionsweise der stopPropagation()-Methode verknüpft ist und weiter unten in diesem Dokument erläutert wird.

Das bedeutet, dass das Klickereignis um window beginnt und der Browser die folgenden Fragen stellt:

„Wartet etwas auf ein Klickereignis für window in der Erfassungsphase?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst. In unserem Beispiel ist nichts, also werden keine Handler ausgelöst.

Als Nächstes wird das Ereignis an die document weitergeleitet. Der Browser fragt Sie dann: „Wartet etwas in der Erfassungsphase auf ein Klickereignis auf dem document?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst.

Als Nächstes wird das Ereignis zum <html>-Element weitergeleitet. Der Browser fragt Sie dann: „Wartet etwas in der Erfassungsphase auf einen Klick auf das <html>-Element?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst.

Als Nächstes wird das Ereignis zum <body>-Element weitergeleitet. Der Browser fragt Sie dann: „Wartet etwas in der Erfassungsphase auf ein Klickereignis auf das <body>-Element?“ In diesem Fall werden die entsprechenden Event-Handler ausgelöst.

Als Nächstes wird das Ereignis auf das #A-Element weitergeleitet. Auch hier wird vom Browser wieder die folgende Meldung angezeigt: „Wenn in der Erfassungsphase auf ein Klickereignis für #A wartet, werden die entsprechenden Event-Handler ausgelöst.

Als Nächstes wird das Ereignis weitergeleitet und auf das #B-Element übertragen. Dabei wird die gleiche Frage gestellt.

Schließlich erreicht das Ereignis sein Ziel und der Browser fragt Sie: „Wartet etwas in der Erfassungsphase auf ein Klickereignis des #C-Elements?“ Die Antwort ist diesmal „Ja!“ Dieser kurze Zeitraum, in dem das Ereignis am Ziel erreicht ist, wird als „Zielphase“ bezeichnet. An dieser Stelle wird der Event-Handler ausgelöst und der Browser gibt console.log „#C was clicks“ (#C wurde angeklickt) aus und dann sind wir fertig. Falsch! Wir sind noch nicht fertig. Der Prozess geht weiter, aber jetzt geht er in die Bubbling-Phase über.

Event-Bubbling

Der Browser fragt Sie dann:

„Wartet etwas auf ein Klickereignis auf #C in der Bubbling-Phase?“ Passe hier genau auf. Es ist möglich, sowohl in der Erfassungsphase als auch in der Bubbling-Phase auf Klicks (oder andere Ereignistypen) zu warten. Wenn Sie Event-Handler in beiden Phasen verkabelt hätten (z.B. durch zweimaliges Aufrufen von .addEventListener(), einmal mit capture = true und einmal mit capture = false), dann würden beide Event-Handler für dasselbe Element ausgelöst werden. Aber es ist auch wichtig zu beachten, dass sie in verschiedenen Phasen ausgelöst werden (eine in der Erfassungsphase und eine in der Bubbling-Phase).

Als Nächstes wird das Ereignis zum übergeordneten Element #B weitergeleitet (häufiger als „Bubble“ bezeichnet, weil es den Anschein nach oben hat, dass es sich im DOM-Baum „nach oben“ bewegt) und der Browser fragt: „Wartet etwas in der Bubbling-Phase auf Klickereignisse von #B?“ In unserem Beispiel gibt es nichts, also werden keine Handler ausgelöst.

Als Nächstes wird das Ereignis im Infofeld #A angezeigt und der Browser fragt Sie: „Wartet etwas auf Klickereignisse auf #A in der Bubbling-Phase?“

Als Nächstes wird das Ereignis im Infofeld <body> angezeigt: „Wartet etwas auf Klickereignisse für das Element <body> in der Bubbling-Phase?“

Als Nächstes das Element <html>: „Wartet etwas auf Klickereignisse für das Element <html> in der Bubbling-Phase?

Als Nächstes kommt das document: „Wartet etwas auf Klickereignisse für das document in der Bubbling-Phase?“

Zuletzt wird window: „Wartet etwas auf Klickereignisse für das Fenster in der Bubbling-Phase?“

Geschafft! Das war eine lange Reise und unsere Veranstaltung ist mittlerweile wahrscheinlich sehr müde, aber ob Sie es glauben oder nicht, das ist die Reise, die jedes Ereignis durchläuft! Meistens fällt dies nicht auf, da Entwickler normalerweise nur an einer Ereignisphase interessiert sind (normalerweise die Bubbling-Phase).

Es lohnt sich, einige Zeit mit der Ereigniserfassung und dem Ereignis-Bubbling herumzuspielen und einige Hinweise in der Konsole zu protokollieren, während die Handler ausgelöst werden. Es ist sehr aufschlussreich, den Weg eines Ereignisses zu sehen. Hier ist ein Beispiel, bei dem jedes Element in beiden Phasen überwacht wird.

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in capturing phase');
  },
  true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in capturing phase');
  },
  true,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in capturing phase');
  },
  true,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in capturing phase');
  },
  true,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in capturing phase');
  },
  true,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in capturing phase');
  },
  true,
);

document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in bubbling phase');
  },
  false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in bubbling phase');
  },
  false,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in bubbling phase');
  },
  false,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in bubbling phase');
  },
  false,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in bubbling phase');
  },
  false,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in bubbling phase');
  },
  false,
);

Die Konsolenausgabe hängt davon ab, auf welches Element Sie klicken. Wenn Sie auf das „tiefste“ Element im DOM-Baum (das Element #C) klicken, sehen Sie, dass jeder dieser Event-Handler ausgelöst wird. Mit ein wenig CSS-Stil, um deutlicher zu machen, welches Element zu welchem Element gehört, sehen Sie hier das #C-Element der Konsolenausgabe (ebenfalls mit einem Screenshot):

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"

In der Live-Demo unten können Sie damit interaktiv experimentieren. Klicken Sie auf das Element #C und beobachten Sie die Konsolenausgabe.

event.stopPropagation()

Wenn wir wissen, wo Ereignisse ihren Ursprung haben und wie sie sich durch das DOM in der Erfassungsphase und in der Bubbling-Phase bewegen (also propagieren), können wir uns jetzt event.stopPropagation() zuwenden.

Die Methode stopPropagation() kann für die meisten nativen DOM-Ereignisse aufgerufen werden. Ich sage „die meisten“, weil es einige gibt, bei denen der Aufruf dieser Methode nichts bewirkt (da das Ereignis nicht weitergegeben wird.) In diese Kategorie fallen Ereignisse wie focus, blur, load, scroll und einige andere. Sie können stopPropagation() aufrufen, aber es passiert nichts Interessantes, da diese Ereignisse nicht weitergegeben werden.

Aber was macht stopPropagation?

Sie tut genau das, was sie verspricht. Wenn Sie es aufrufen, wird das Ereignis ab diesem Zeitpunkt nicht mehr an Elemente weitergegeben, an die es ansonsten weitergeleitet werden würde. Dies gilt für beide Richtungen (Erfassung und Bubbling). Wenn Sie also in der Erfassungsphase irgendwo stopPropagation() aufrufen, erreicht das Ereignis nie die Ziel- oder Bubbling-Phase. Wenn Sie ihn in der Bubbling-Phase nennen, hat er bereits die Erfassungsphase durchlaufen, aber ab dem Zeitpunkt, an dem Sie ihn aufgerufen haben, „blinkt“ es auf.

Zurück zu unserem Beispiel-Markup: Was würde Ihrer Meinung nach passieren, wenn wir stopPropagation() in der Erfassungsphase des #B-Elements aufrufen würden?

Dies würde zu folgender Ausgabe führen:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"

In der Live-Demo unten können Sie damit interaktiv experimentieren. Klicken Sie in der Live-Demo auf das Element #C und sehen Sie sich die Konsolenausgabe an.

Wie wäre es, die Weitergabe bei #A in der Bubbling-Phase zu stoppen? Das würde zu folgender Ausgabe führen:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"

In der Live-Demo unten können Sie damit interaktiv experimentieren. Klicken Sie in der Live-Demo auf das Element #C und sehen Sie sich die Konsolenausgabe an.

Eines noch, nur zum Spaß. Was passiert, wenn wir stopPropagation() in der Zielphase für #C aufrufen? Die „Zielphase“ ist der Name des Zeitraums, in dem das Ereignis am Ziel erreicht ist. Dies würde zu folgender Ausgabe führen:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"

Beachten Sie, dass der Event-Handler für #C, in dem wir „click on #C in the recording period“ (Klick auf #C in der Erfassungsphase) protokollieren, noch ausgeführt wird. Der Event-Handler, in dem „click on #C in the Bubbling Phase“ (Klick auf #C in der Bubbling-Phase) protokolliert wird, wird jedoch nicht ausgeführt. Das sollte sehr sinnvoll sein. Wir haben stopPropagation() from der ersten Methode aufgerufen. Das ist der Punkt, an dem das Ereignis nicht mehr verbreitet wird.

In der Live-Demo unten können Sie damit interaktiv experimentieren. Klicken Sie in der Live-Demo auf das Element #C und sehen Sie sich die Konsolenausgabe an.

Ich empfehle Ihnen, in jeder dieser Live-Demos herumzuspielen. Klicken Sie z. B. nur auf das #A-Element oder nur auf das body-Element. Versuchen Sie vorherzusagen, was passieren wird, und beobachten Sie dann, ob Sie richtig liegen. An dieser Stelle sollten Sie in der Lage sein, ziemlich genau vorherzusagen.

event.stopImmediatePropagation()

Was ist das für eine seltsame und wenig genutzte Methode? Sie ist ähnlich wie stopPropagation, aber diese Methode wird nur verwendet, wenn mehr als ein Event-Handler mit einem einzelnen Element verbunden ist, anstatt zu verhindern, dass ein Ereignis zu Nachfolgern (Erfassung) oder Ancestors (Bubbling) übertragen wird. Da addEventListener() den Multicast-Ereignisstil unterstützt, ist es möglich, einen Event-Handler mehrmals mit einem einzelnen Element zu verbinden. In diesem Fall werden Event-Handler (in den meisten Browsern) in der Reihenfolge ausgeführt, in der sie verkabelt waren. Durch den Aufruf von stopImmediatePropagation() wird verhindert, dass nachfolgende Handler ausgelöst werden. Dazu ein Beispiel:

<html>
  <body>
    <div id="A">I am the #A element</div>
  </body>
</html>
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run first!');
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run second!');
    e.stopImmediatePropagation();
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
  },
  false,
);

Das obige Beispiel führt zu folgender Konsolenausgabe:

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

Der dritte Event-Handler wird nie ausgeführt, da der zweite Event-Handler e.stopImmediatePropagation() aufruft. Wenn wir stattdessen e.stopPropagation() aufgerufen hätten, würde der dritte Handler weiterhin ausgeführt werden.

event.preventDefault()

Wenn stopPropagation() verhindert, dass sich ein Ereignis nach unten (aufnehmen) oder nach oben (Blubbling) bewegt, was tut preventDefault() dann? Das hört sich an, als würde es etwas Ähnliches machen. Stimmt das?

Nein. Die beiden werden zwar oft verwechselt, haben aber nicht viel miteinander zu tun. Wenn du preventDefault() siehst, füge in deinem Kopf das Wort „Aktion“ hinzu. Denken Sie: „die Standardaktion verhindern“.

Was ist die Standardaktion, die Sie stellen könnten? Leider ist die Antwort darauf nicht ganz so eindeutig, da sie stark von der betreffenden Kombination aus Element und Ereignis abhängt. Manchmal gibt es auch gar keine Standardaktion.

Beginnen wir mit einem sehr einfachen Beispiel. Was erwarten Sie, wenn Sie auf einen Link auf einer Webseite klicken? Offensichtlich erwarten Sie, dass der Browser zu der in diesem Link angegebenen URL navigiert. In diesem Fall ist das Element ein Anchor-Tag und das Ereignis ein Klickereignis. Bei dieser Kombination (<a> + click) wird als „Standardaktion“ der href des Links aufgerufen. Wie können Sie verhindern, dass der Browser diese Standardaktion ausführt? Angenommen, Sie möchten verhindern, dass der Browser die URL aufruft, die durch das Attribut href des <a>-Elements angegeben ist. Genau das tut preventDefault() für dich. Betrachten Sie dieses Beispiel:

<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
  'click',
  function (e) {
    e.preventDefault();
    console.log('Maybe we should just play some of their music right here instead?');
  },
  false,
);

In der Live-Demo unten können Sie damit interaktiv experimentieren. Klicken Sie auf den Link The Avett Brothers und beobachten Sie die Konsolenausgabe und die Tatsache, dass Sie nicht zur Avett Brothers-Website weitergeleitet werden.

Normalerweise wird durch Klicken auf den Link „The Avett Brothers“ www.theavettbrothers.com aufgerufen. In diesem Fall haben wir jedoch einen Klick-Event-Handler mit dem <a>-Element verknüpft und angegeben, dass die Standardaktion verhindert werden soll. Wenn ein Nutzer also auf diesen Link klickt, wird er nicht weitergeleitet. Stattdessen meldet die Konsole einfach: „Vielleicht sollten wir stattdessen einfach ein bisschen Musik hier spielen?“.

Mit welchen anderen Element-/Ereignis-Kombinationen können Sie die Standardaktion verhindern? Ich kann sie nicht alle aufführen, und manchmal muss man einfach experimentieren, um es zu sehen. Hier noch einmal einige Beispiele:

  • Das <form>-Element + das „submit“-Ereignis: preventDefault() für diese Kombination verhindert, dass ein Formular gesendet wird. Dies ist nützlich, wenn Sie eine Validierung durchführen möchten und bei einem Fehler ein Fehler auftritt, können Sie „preventDefault“ bedingt aufrufen, um das Senden des Formulars zu stoppen.

  • <a>-Element + „click“-Ereignis: preventDefault() für diese Kombination verhindert, dass der Browser die im href-Attribut des <a>-Elements angegebene URL aufruft.

  • document + „mousewheel“-Ereignis: preventDefault() für diese Kombination verhindert das Scrollen mit dem Mausrad. Das Scrollen mit der Tastatur funktioniert jedoch weiterhin.
    ↜ Dazu muss addEventListener() mit { passive: false } aufgerufen werden.

  • document + Ereignis vom Typ „keydown“: preventDefault() für diese Kombination ist tödlich. Sie rendert die Seite weitgehend nutzlos und verhindert das Scrollen, die Tabulatortaste und die Tastaturhervorhebung.

  • document- + „mousedown“-Ereignis: preventDefault() für diese Kombination verhindert das Hervorheben von Text mit der Maus und jede andere „Standardaktion“, die mit einer Maus nach unten aufgerufen werden würde.

  • <input>-Element + „keypress“-Ereignis: preventDefault() für diese Kombination verhindert, dass vom Nutzer eingegebene Zeichen das Eingabeelement erreichen. Das sollte aber nicht unbedingt ein triftiger Grund dafür sein.

  • document + Ereignis vom Typ „contextmenu“: preventDefault() für diese Kombination verhindert, dass das Kontextmenü des nativen Browsers angezeigt wird, wenn ein Nutzer mit der rechten Maustaste darauf drückt oder es lange drückt (oder auf eine andere Art und Weise, in der ein Kontextmenü angezeigt wird).

Diese Liste ist auf keinen Fall vollständig, aber ich hoffe, dass sie Ihnen eine gute Vorstellung davon vermittelt, wie preventDefault() verwendet werden kann.

Ein lustiger Witz?

Was passiert, wenn Sie in der Erfassungsphase stopPropagation() und preventDefault() auswählen und beim Dokument beginnen? Es entsteht ein Hochdruck! Mit dem folgenden Code-Snippet wird jede Webseite nahezu unbrauchbar:

function preventEverything(e) {
  e.preventDefault();
  e.stopPropagation();
  e.stopImmediatePropagation();
}

document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });

Ich weiß nicht, warum Sie das jemals machen wollen (außer vielleicht, um jemandem einen Witz zu spielen), aber es ist nützlich, darüber nachzudenken, was hier passiert, und herauszufinden, warum dies die Situation schafft, die es schafft.

Alle Ereignisse beginnen um window. In diesem Snippet werden also alle click-, keydown-, mousedown-, contextmenu- und mousewheel-Ereignisse beendet, sodass sie keine Elemente abrufen, die möglicherweise auf sie warten. Außerdem rufen wir stopImmediatePropagation auf, damit Handler, die nach diesem Dokument mit dem Dokument verbunden sind, ebenfalls blockiert werden.

Beachte, dass stopPropagation() und stopImmediatePropagation() nicht (zumindest nicht überwiegend) dazu führen, dass die Seite unbrauchbar wird. Sie verhindern lediglich, dass Ereignisse dorthin gelangen, wo sie andernfalls hin wären.

Wir nennen aber auch preventDefault(), was die standardmäßige Aktion verhindert. Alle Standardaktionen wie Scrollen mit dem Mausrad, Scrollen auf der Tastatur, Hervorheben oder Tippen mit der Tabulatortaste, Klicken auf Links, Anzeige des Kontextmenüs usw. werden verhindert, wodurch die Seite recht unbrauchbar bleibt.

Livedemos

In der eingebetteten Demo unten können Sie sich alle Beispiele aus diesem Artikel noch einmal an einem Ort ansehen.

Danksagungen

Hero-Image von Tom Wilson auf Unsplash