Shadow DOM 101

Dominic Cooney
Dominic Cooney

Einführung

Web Components ist eine Reihe moderner 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, Shadow World

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

Ihre Seite sieht 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.

Inhalte von der Präsentation trennen

Sehen wir uns nun an, wie Sie mit Shadow DOM Inhalt von Darstellung 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ürden Sie 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 ist uns wahrscheinlich nur Folgendes wichtig:

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

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

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

Anschließend 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>

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>

Das zeigt, dass wir mithilfe von Shadow DOM die Darstellungsdetails des Namenstags aus dem 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 Name, der gerendert wird, 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 Namen könnten dann nicht mehr synchron sein.

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 ein Einfügepunkt in der Darstellung des Widgets erstellt. Über diesen Einfügepunkt werden Inhalte aus dem Schattenhost ausgewählt, die an dieser Stelle 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';

Das war es 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 im Shadow DOM. Sie werden vom Browser automatisch synchronisiert, wenn etwas gerendert werden muss.

Schritt 3: Gewinn erzielen

Durch die Trennung von Inhalt und Darstellung können wir den Code, der den Inhalt manipuliert, vereinfachen. Im Beispiel für das Name-Tag muss dieser Code nur mit einer einfachen Struktur mit einer <div> statt mehreren umgehen.

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

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

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

Der Code für die Einrichtung der Schatten-Stammdatei bleibt unverändert. Was genau in den Schatten-Stamm aufgenommen 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>

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“), aber zuerst auf Japanisch (vor „と申します“). Diese Unterscheidung ist semantisch bedeutungslos, wenn es um die Aktualisierung des angezeigten Namens geht. Der Code zum Aktualisieren des Namens muss also nicht über dieses Detail informiert sein.

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 mit folgendem Inhalt:

<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 <div class="email">-Element wird sowohl mit dem <content select="div">- als auch dem <content select=".email">-Element abgeglichen. Wie oft und in welchen Farben wird die E-Mail-Adresse von Bob angezeigt?

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 in der Reihenfolge zugestellt, in der sie adressiert sind (d. h. das select-Attribut). Wenn Inhalte eingeladen werden, nehmen sie die Einladung immer an (wer würde das nicht tun?). Wenn eine weitere Einladung an diese Adresse gesendet wird, ist niemand zu Hause und die Person kommt nicht zu Ihrer 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 ein Element zu keiner Gruppe eingeladen ist, wird es überhaupt nicht gerendert. Das ist mit dem Text „Hallo Welt“ im allerersten 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"> eingeben, wird ein praktisches Pop-up-Kalenderfenster geöffnet. 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>

sondern ein Shadow-DOM erstellen, das mithilfe einer Tabelle einen schlanken 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 „startDate“ und „endDate“. Wenn der Nutzer das Formular einreicht, werden die Werte aus diesen Eingabeelementen 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 dann 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 Shadow DOM-Einstiegskurs bestanden

Das sind die Grundlagen von Shadow DOM. Sie haben den Shadow DOM-Einstiegskurs bestanden! Mit Shadow DOM können Sie noch mehr tun. Sie können beispielsweise mehrere Schatten auf einem Schattenhost oder verschachtelte Schatten für die Kapselung verwenden oder Ihre Seite mithilfe von modellgetriebenen Ansichten (Model-Driven Views, MDV) und Shadow DOM entwerfen. Webkomponenten sind mehr als nur Shadow DOM.

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