Shadow DOM 101

Dominic Cooney
Dominic Cooney

Einleitung

Webkomponenten umfassen eine Reihe hochmoderner Standards, die:

  1. Erstellung von Widgets ermöglichen
  2. ...die zuverlässig wiederverwendet werden können,
  3. ...und die die Seiten auch dann nicht beeinträchtigt, wenn die nächste Version der Komponente interne Implementierungsdetails ändert.

Heißt das, Sie müssen entscheiden, wann Sie HTML/JavaScript und wann Sie Webkomponenten verwenden möchten? Nein! HTML und JavaScript können interaktiv werden. Widgets sind interaktive visuelle Elemente. Es ist sinnvoll, bei der Entwicklung eines Widgets auf Ihre HTML- und JavaScript-Kenntnisse zurückzugreifen. Die Standards für Webkomponenten wurden entwickelt, um Ihnen dabei zu helfen.

Es gibt jedoch ein grundlegendes Problem, das die Nutzung von aus HTML und JavaScript erstellten Widgets erschwert: Der DOM-Baum innerhalb eines Widgets wird nicht vom Rest der Seite gekapselt. Diese fehlende Datenkapselung bedeutet, dass Ihr Dokument-Stylesheet möglicherweise versehentlich auf Teile im Widget angewendet wird. Ihr JavaScript ändert unter Umständen Teile im Widget versehentlich. Ihre IDs können sich mit den IDs innerhalb des Widgets überschneiden usw.

Web Components bestehen aus drei Teilen:

  1. Vorlagen
  2. Schatten-DOM
  3. Benutzerdefinierte Elemente

Mit Shadow DOM lässt sich das Problem mit der DOM-Baumkapselung beheben. Die vier Teile von Webkomponenten sind aufeinander abgestimmt. Sie können aber auch auswählen, welche Teile der Webkomponenten verwendet werden sollen. In dieser Anleitung erfahren Sie, wie Sie Shadow DOM verwenden.

Hallo Shadow World

Mit Shadow DOM kann Elementen eine neue Art von Knoten zugeordnet werden. Diese neue Art von Knoten wird als Schattenstamm bezeichnet. Ein Element, dem eine Schattenwurzel zugeordnet ist, wird als Schattenhost bezeichnet. Der Inhalt eines Schattenhosts wird nicht gerendert. Stattdessen wird der Inhalt des Schattenstamms gerendert.

Angenommen, Sie verwenden Markup wie dieses:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

dann anstelle von

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

wie Ihre Seite aussieht,

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Wenn JavaScript auf der Seite nach der textContent der Schaltfläche fragt, wird nicht nur „こんんちBonus!“ empfangen, sondern „Hello, world!“, weil die DOM-Unterstruktur unter der Schattenwurzel eingekapselt ist.

Inhalte von Präsentationen trennen

Als Nächstes sehen wir uns an, wie wir mithilfe von Shadow DOM Inhalte von Präsentationen trennen. Nehmen wir an, wir haben dieses Name-Tag:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Hier ist das Markup. Das ist der Grund, den Sie heute schreiben würden. Es verwendet kein Shadow-DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Da im DOM-Baum keine Kapselung vorhanden ist, wird die gesamte Struktur des Namens-Tags für das Dokument freigegeben. Wenn andere Elemente auf der Seite versehentlich die gleichen Klassennamen für Stile oder Skripterstellung verwenden, hat dies eine schlechte Zeit.

Wir können schlechte Zeiten vermeiden.

Schritt 1: Präsentationsdetails ausblenden

Semantisch ist für uns wahrscheinlich nur Folgendes wichtig:

  • Es ist ein Name-Tag.
  • Der Name ist „Bernd“.

Zuerst schreiben wir Markups, die der eigentlichen Semantik, die wir wollen, näher kommen:

<div id="nameTag">Bob</div>

Dann fügen wir alle für die Präsentation verwendeten Stile und Divs in ein <template>-Element ein:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

Jetzt wird nur „Bob“ gerendert. Da wir die DOM-Präsentationselemente innerhalb eines <template>-Elements verschoben haben, werden sie nicht gerendert. Sie können aber über JavaScript aufgerufen werden. Das machen wir jetzt, um die Schattenwurzel zu füllen:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Nachdem Sie nun einen Schattenstamm eingerichtet haben, wird das Name-Tag erneut gerendert. Wenn Sie mit der rechten Maustaste auf das Name-Tag klicken und das Element prüfen, sehen Sie, dass es semantisches Markup ist:

<div id="nameTag">Bob</div>

Dies zeigt, dass wir mithilfe von Shadow DOM die Darstellungsdetails des Name-Tags aus dem Dokument ausgeblendet haben. Die Präsentationsdetails sind in das Shadow-DOM gekapselt.

Schritt 2: Inhalte von der Präsentation trennen

Unser Name-Tag blendet jetzt Präsentationsdetails auf der Seite aus, trennt jedoch nicht die Präsentation vom Inhalt, denn obwohl sich der Inhalt (der Name „Bob“) auf der Seite befindet, wird der gerenderte Name dennoch in den Schattenstamm kopiert. Wenn wir den Namen des Namens-Tags ändern möchten, müssten wir dies an zwei Stellen tun, wodurch die beiden dann möglicherweise nicht mehr synchron sind.

HTML-Elemente sind kombiniert. Sie können beispielsweise eine Schaltfläche in eine Tabelle einfügen. Hier ist die Komposition erforderlich: Das Name-Tag muss aus dem roten Hintergrund, dem Text „Hi!“ und dem Inhalt des Name-Tags bestehen.

Als Komponentenautor legen Sie mit einem neuen Element namens <content> fest, wie die Komposition mit Ihrem Widget funktioniert. Dadurch wird ein Einfügepunkt in der Präsentation des Widgets erstellt, und dieser wählt Inhalte vom Schattenhost aus, die an dieser Stelle angezeigt werden sollen.

Wenn wir das Markup im Shadow DOM wie folgt ändern:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Wenn das Name-Tag gerendert wird, wird der Inhalt des Schattenhosts an die Stelle projiziert, an der das Element <content> erscheint.

Die Struktur des Dokuments ist nun einfacher, da sich der Name nur an einer Stelle befindet: dem Dokument. Falls der Name des Nutzers auf deiner Seite geändert werden muss, schreibst du einfach:

document.querySelector('#nameTag').textContent = 'Shellie';

Das war's auch schon. Das Rendering des Name-Tags wird automatisch vom Browser aktualisiert, da der Inhalt des Name-Tags mit <content> projektiert wird.

<div id="ex2b">

Jetzt haben wir eine Trennung von Inhalt und Präsentation erreicht. Der Inhalt befindet sich im Dokument; die Präsentation befindet sich im Shadow-DOM. Sie werden automatisch vom Browser synchronisiert, wenn es an der Zeit ist, etwas zu rendern.

Schritt 3: Gewinn

Durch das Trennen von Inhalt und Darstellung können wir den Code vereinfachen, mit dem der Inhalt verändert wird. Im Name-Tag-Beispiel muss sich dieser Code nur mit einer einfachen Struktur auseinandersetzen, die statt mehrerer <div> enthalten ist.

Wenn wir nun unsere Präsentation ändern, muss der Code nicht mehr geändert werden.

Angenommen, wir möchten unser Namens-Tag lokalisieren. Es ist immer noch ein Name-Tag, sodass der semantische Inhalt im Dokument nicht geändert wird:

<div id="nameTag">Bob</div>

Der Code für die Einrichtung des Schattenstamms bleibt gleich. Nur das, was in die Schattenwurzel gelegt wird, ändert sich:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Dies ist eine erhebliche Verbesserung gegenüber der heutigen Situation im Web, da Ihr Code zur Namensaktualisierung von der Struktur der Komponente abhängen kann, die einfach und einheitlich ist. Der Code zur Namensaktualisierung muss die für das Rendering verwendete Struktur nicht kennen. Wenn wir uns ansehen, was gerendert wird, erscheint der Name als Zweites auf Englisch (nach "Hallo! Mein Name ist“), aber zuerst auf Japanisch (vor "東申んちす"). Diese Unterscheidung ist semantisch nicht bedeutungslos, was die Aktualisierung des angezeigten Namens betrifft, sodass der Code für die Namensaktualisierung keine Informationen über dieses Detail haben muss.

Extra-Punkte: Erweiterte Projektion

Im obigen Beispiel werden mit dem Element <content> alle Inhalte des Schattenhosts ausgewählt. Mit dem Attribut select können Sie steuern, was ein Inhaltselement projiziert. Sie können auch mehrere Inhaltselemente verwenden.

Beispiel: Sie haben ein Dokument, das Folgendes enthält:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

und einen Schattenstamm, der CSS-Selektoren verwendet, um bestimmte Inhalte auszuwählen:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

Das Element <div class="email"> stimmt sowohl mit dem Element <content select="div"> als auch mit dem Element <content select=".email"> überein. Wie oft erscheint Bobs E-Mail-Adresse und in welchen Farben?

Die Antwort ist, dass Bodos E-Mail-Adresse einmal angezeigt wird und gelb ist.

Der Grund dafür ist, dass das Erstellen des Baums dessen, was auf dem Bildschirm gerendert wird, wie eine riesige Party ist, wie man es von Shadow DOM kennt. Das Inhaltselement ist die Einladung, über die Inhalte aus dem Dokument an die Backstage-Shadow DOM-Rendering-Party gesendet werden können. Diese Einladungen werden der Reihe nach zugestellt. Wer eine Einladung erhält, hängt davon ab, an wen sie adressiert ist (d. h. über das Attribut select). Eingeladene Inhalte nehmen die Einladung immer an (wer würde das nicht tun?!) und geht los. Wenn eine Einladung noch einmal an diese Adresse gesendet wird, ist niemand zu Hause und kommt auch nicht zu Ihrer Party.

Im Beispiel oben entspricht <div class="email"> sowohl dem Selektor div als auch dem Selektor .email. Da das Inhaltselement mit dem Selektor div jedoch weiter oben im Dokument steht, wird <div class="email"> an die gelbe Partei weitergeleitet und niemand kann zur blauen Partei zugreifen. (Das könnte sein, warum es so blau ist, obwohl das Elend Gesellschaft liebt, also Sie es nie wissen.)

Wenn etwas an keine Partys eingeladen wird, wird es überhaupt nicht gerendert. Genau das ist mit dem Text „Hello World“ im ersten Beispiel passiert. Dies ist nützlich, wenn Sie ein gänzlich anderes Rendering erzielen möchten: Schreiben Sie das semantische Modell im Dokument, auf das Skripts auf der Seite zugreifen können. Blenden Sie es jedoch zu Renderingzwecken aus und verbinden Sie es mithilfe von JavaScript mit einem ganz anderen Rendering-Modell im Shadow DOM.

HTML verfügt beispielsweise über eine praktische Datumsauswahl. Wenn Sie „<input type="date">“ schreiben, sehen Sie einen praktischen Pop-up-Kalender. Aber was ist, wenn der Nutzer eine Reihe von Terminen für seinen Urlaub auf der Wüste-Insel auswählen soll (wie gesagt... mit Hängematten aus Red Vines). So richten Sie Ihr Dokument ein:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

aber erstellen Sie Shadow DOM, das eine Tabelle verwendet, um einen eleganten Kalender zu erstellen, in dem der Datumsbereich usw. hervorgehoben wird. Wenn der Nutzer auf die Tage im Kalender klickt, aktualisiert die Komponente den Status in den Eingaben für startDate und endDate. Wenn der Nutzer das Formular sendet, werden die Werte dieser Eingabeelemente gesendet.

Warum habe ich Labels in das Dokument eingefügt, wenn sie nicht gerendert werden? Wenn ein Nutzer das Formular mit einem Browser aufruft, der Shadow DOM nicht unterstützt, kann das Formular zwar verwendet werden, ist aber nicht so schön. Dem Nutzer wird etwa Folgendes angezeigt:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Sie haben Shadow DOM 101 bestanden

Das sind die Grundlagen von Shadow DOM – Sie übergeben Shadow DOM 101! Mit Shadow DOM haben Sie mehr Möglichkeiten. Sie können beispielsweise mehrere Schatten auf einem Schattenhost verwenden, verschachtelte Schatten zur Kapselung verwenden oder Ihre Seite mithilfe von modellgesteuerten Ansichten (MDV) und Shadow DOM erstellen. Und Webkomponenten sind mehr als nur Schatten-DOM.

Wir erläutern diese in späteren Posts.