Shadow DOM 101

Dominic Cooney
Dominic Cooney

Einführung

Web Components ist eine Reihe innovativer Standards, die:

  1. Ermöglichen, Widgets zu erstellen
  2. …die zuverlässig wiederverwendet werden können
  3. …und die Seiten nicht brechen, wenn sich in der nächsten Version der Komponente interne Implementierungsdetails ändern.

Heißt das, dass Sie entscheiden müssen, wann Sie HTML/JavaScript und wann Sie Webkomponenten verwenden? Nein! Mit HTML und JavaScript können Sie visuelle Elemente interaktiv gestalten. Widgets sind interaktive visuelle Elemente. Es ist sinnvoll, Ihre HTML- und JavaScript-Kenntnisse bei der Entwicklung eines Widgets zu nutzen. Die Web Components-Standards sollen Ihnen dabei helfen.

Es gibt jedoch ein grundlegendes Problem, das die Verwendung von Widgets aus HTML und JavaScript erschwert: Der DOM-Baum in einem Widget ist nicht vom Rest der Seite getrennt. Diese fehlende Kapselung bedeutet, dass Ihr Dokument-Stylesheet möglicherweise versehentlich auf Teile im Widget angewendet wird, Ihr JavaScript möglicherweise versehentlich Teile im Widget ändert, Ihre IDs sich möglicherweise mit IDs im Widget überschneiden usw.

Web Components besteht aus drei Teilen:

  1. Vorlagen
  2. Shadow DOM
  3. Benutzerdefinierte Elemente

Shadow DOM behebt das Problem der DOM-Baumkapselung. Die vier Teile von Web-Komponenten sind für die Zusammenarbeit konzipiert. Sie können jedoch auch auswählen, welche Teile von Web-Komponenten Sie verwenden möchten. In dieser Anleitung erfahren Sie, wie Sie Shadow DOM verwenden.

Hallo, Schattenwelt

Mit Shadow DOM können Elementen eine neue Art von Knoten zugeordnet werden. Diese neue Art von Knoten wird als Schattenknoten bezeichnet. Ein Element, dem ein Schatten-Stamm zugeordnet ist, wird als Schatten-Host bezeichnet. Der Inhalt eines Schattenhosts wird nicht gerendert, sondern stattdessen der Inhalt des Schatten-Stammverzeichnisses.

Angenommen, Sie haben ein Markup wie dieses:

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

dann statt der

<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>

sieht Ihre Seite so aus:

<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 fragt, was die textContent der Schaltfläche ist, wird nicht „こんにちは、影の世界!“, sondern „Hallo Welt!“ zurückgegeben, da der DOM-Unterbaum unter dem Shadow Root gekapselt ist.

Trennen von Inhalten von Präsentationen

Sehen wir uns nun an, wie Sie mit Shadow DOM Inhalte von der Präsentation trennen. Angenommen, wir haben dieses Namensschild:

<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 würdet ihr heute schreiben. Es wird kein Shadow DOM verwendet:

<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 der DOM-Baum nicht gekapselt ist, ist die gesamte Struktur des Namenstags für das Dokument sichtbar. Wenn andere Elemente auf der Seite versehentlich dieselben Klassennamen für das Styling oder Scripting verwenden, wird es schwierig.

So können wir unnötige Probleme vermeiden.

Schritt 1: Präsentationsdetails ausblenden

Semantisch interessiert uns wahrscheinlich nur Folgendes:

  • Es ist ein Namensschild.
  • Der Name ist „Max“.

Zuerst schreiben wir Markup, das der gewünschten Semantik näher kommt:

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

Dann fügen wir alle für die Präsentation verwendeten Stile und div-Elemente 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>

Zu diesem Zeitpunkt wird nur „Max“ gerendert. Da wir die Präsentations-DOM-Elemente in ein <template>-Element verschoben haben, werden sie nicht gerendert, aber es kann über JavaScript darauf zugegriffen werden. Das tun wir jetzt, um die Schatten-Stammstruktur 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 wir einen Schattenknoten eingerichtet haben, wird das Name-Tag noch einmal gerendert. Wenn Sie mit der rechten Maustaste auf das Name-Tag klicken und das Element untersuchen, sehen Sie, dass es sich um ein schönes semantisches Markup handelt:

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

Dies zeigt, dass wir mithilfe von Shadow DOM die Darstellungsdetails des Namens-Tags im Dokument ausgeblendet haben. Die Präsentationsdetails sind im Shadow-DOM gekapselt.

Schritt 2: Inhalte von der Präsentation trennen

Unser Name-Tag blendet jetzt die Präsentationsdetails von der Seite aus, trennt aber nicht wirklich die Präsentation vom Inhalt. Der Inhalt (der Name „Max“) ist zwar auf der Seite, aber der gerenderte Name ist derjenige, den wir in den Schattenknoten kopiert haben. Wenn wir den Namen auf dem Namensschild ändern möchten, müssten wir das an zwei Stellen tun. Die Änderungen könnten dann nicht synchron erfolgen.

HTML-Elemente sind kompositionell – Sie können beispielsweise eine Schaltfläche in eine Tabelle einfügen. Hier kommt die Komposition ins Spiel: Das Namensschild muss eine Komposition aus dem roten Hintergrund, dem Text „Hallo!“ und dem Inhalt des Namensschilds sein.

Als Komponentenautor legen Sie mit einem neuen Element namens <content> fest, wie die Zusammensetzung mit Ihrem Widget funktioniert. Dadurch wird in der Präsentation des Widgets eine Einfügungsstelle erstellt und Inhalte vom Schattenhost ausgewählt, die an diesem Punkt präsentiert werden sollen.

Wenn wir das Markup im Shadow-DOM in Folgendes ä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 <content>-Element erscheint.

Jetzt ist die Struktur des Dokuments einfacher, da der Name nur an einer Stelle steht: im Dokument. Wenn der Name des Nutzers auf Ihrer Seite aktualisiert werden muss, schreiben Sie einfach Folgendes:

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

Und das wars auch schon. Das Rendern des Namen-Tags wird vom Browser automatisch aktualisiert, da wir den Inhalt des Namen-Tags mit <content> projektieren.

<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 vom Browser automatisch synchronisiert, wenn etwas gerendert werden muss.

Schritt 3: Gewinn erzielen

Durch die Trennung von Inhalt und Präsentation können wir den Code vereinfachen, mit dem der Inhalt verändert wird. Im Name-Tag-Beispiel muss dieser Code nur mit einer einfachen Struktur zu tun haben, die statt mehrerer nur ein <div> enthält.

Wenn wir die Präsentation ändern, müssen wir den Code nicht ändern.

Angenommen, wir möchten unser Namens-Tag lokalisieren. Es handelt sich weiterhin um ein Name-Tag, sodass sich der semantische Inhalt im Dokument nicht ändert:

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

Der Einrichtungscode des Shadow-Stamms bleibt unverändert. Was im Schatten steht, ä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>

Das ist eine große Verbesserung gegenüber der aktuellen Situation im Web, da der Code zum Aktualisieren des Namens von der Struktur der Komponente abhängen kann, die einfach und konsistent ist. Der Code zum Aktualisieren des Namens muss die Struktur, die für das Rendering verwendet wird, nicht kennen. Wenn wir uns ansehen, was gerendert wird, wird der Name auf Englisch an zweiter Stelle angezeigt (nach „Hallo! Mein Name ist“), jedoch zuerst auf Japanisch (vor „名申まん“). Diese Unterscheidung ist semantisch bedeutungslos, was die Aktualisierung des angezeigten Namens angeht, sodass der Namensaktualisierungscode diese Details nicht kennen muss.

Zusatzaufgabe: Erweiterte Projektion

Im obigen Beispiel werden mit dem <content>-Element alle Inhalte aus dem Schattenhost ausgewählt. Mit dem select-Attribut kannst du festlegen, was ein Inhaltselement projiziert. Sie können auch mehrere Inhaltselemente verwenden.

Angenommen, 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 eine Schatten-Stammstruktur, in der bestimmte Inhalte mithilfe von CSS-Selektoren ausgewählt werden:

<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 und in welchen Farben ist Bobs E-Mail-Adresse zu sehen?

Die Antwort lautet: Die E-Mail-Adresse von Bob wird einmal angezeigt und ist gelb.

Der Grund dafür ist, dass das Erstellen des Baums dessen, was tatsächlich auf dem Bildschirm gerendert wird, wie eine riesige Party ist. Das Inhaltselement ist die Einladung, die Inhalte aus dem Dokument in die Backstage-Shadow-DOM-Rendering-Party einlädt. Diese Einladungen werden der Reihe nach zugestellt. Wer eine Einladung erhält, hängt davon ab, an wen sie gerichtet ist (d. h. über das Attribut select). Wenn Inhalte eingeladen werden, nehmen sie die Einladung immer an (wer würde das nicht tun?). Wenn dann wieder eine Einladung an diese Adresse gesendet wird, ist niemand zu Hause und kommt auch nicht an Ihre Party.

Im Beispiel oben stimmt <div class="email"> sowohl mit dem Selektor div als auch mit dem Selektor .email überein. Da das Inhaltselement mit dem Selektor div jedoch früher im Dokument steht, wird <div class="email"> zur gelben Party eingeladen und niemand zur blauen. Vielleicht ist das der Grund, warum es so blau ist. Aber wer weiß, vielleicht ist es auch einfach nur ein Mitleidsverein.

Wenn etwas für keine Parteien eingeladen wird, wird es überhaupt nicht gerendert. Genau das ist mit dem Text „Hello, world“ im ersten Beispiel passiert. Das ist nützlich, wenn Sie ein radikal anderes Rendering erzielen möchten: Schreiben Sie das semantische Modell in das Dokument, auf das Scripts auf der Seite zugreifen können, verbergen Sie es jedoch zu Rendering-Zwecken und verbinden Sie es mit einem ganz anderen Rendering-Modell im Shadow-DOM mit JavaScript.

HTML bietet beispielsweise eine praktische Datumsauswahl. Wenn Sie <input type="date"> schreiben, erhalten Sie einen übersichtlichen Pop-up-Kalender. Aber was ist, wenn Sie dem Nutzer die Möglichkeit geben möchten, einen Zeitraum für seinen Nachtisch auf einer Insel mit Hängematten aus roten Trauben auszuwählen? 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 ein Shadow DOM, das anhand einer Tabelle einen schnellen Kalender erstellt, in dem der Zeitraum hervorgehoben wird usw. 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 aufgenommen, wenn sie nicht gerendert werden? Der Grund dafür ist, dass das Formular auch in einem Browser funktioniert, der Shadow DOM nicht unterstützt. Es sieht dann nur nicht so schön aus. Der Nutzer sieht etwa Folgendes:

<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 den Einstieg in Shadow DOM erfolgreich absolviert

Das sind die Grundlagen von Shadow DOM. Sie haben den Shadow DOM-Einstiegskurs bestanden! Mit Shadow DOM können Sie mehr Möglichkeiten haben, beispielsweise mehrere Schatten auf einem Shadow-Host, verschachtelte Schatten für die Kapselung oder die Erstellung Ihrer Seite mit Model-Driven Views (MDV) und Shadow DOM. Und Webkomponenten sind mehr als nur Shadow DOM.

Diese werden in späteren Beiträgen erläutert.