Shadow DOM 101

Dominic Cooney
Dominic Cooney

Einführung

Web Components sind eine Reihe von innovativen Standards, die:

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

Bedeutet das, dass Sie entscheiden müssen, wann Sie HTML/JavaScript und wann Sie Webkomponenten verwenden? Nein! Mit HTML und JavaScript können Sie interaktive visuelle Inhalte erstellen. Widgets sind interaktive visuelle Elemente. Es ist sinnvoll, Ihre HTML- und JavaScript-Kenntnisse bei der Entwicklung eines Widgets zu nutzen. Die Webkomponentenstandards sollen Ihnen dabei helfen.

Es gibt jedoch ein grundlegendes Problem, das die Verwendung von Widgets aus HTML und JavaScript erschwert: Der DOM-Baum innerhalb eines Widgets ist nicht vom Rest der Seite abgekapselt. Diese fehlende Kapselung bedeutet, dass das Stylesheet Ihres Dokuments versehentlich auf Teile innerhalb des Widgets angewendet werden kann, Ihr JavaScript versehentlich Teile innerhalb des Widgets ändern kann, Ihre IDs sich mit IDs innerhalb des Widgets überschneiden können usw.

Webkomponenten bestehen aus drei Teilen:

  1. Vorlagen
  2. Shadow DOM
  3. Benutzerdefinierte Elemente

Shadow DOM löst das Problem der Kapselung des DOM-Baums. Die vier Teile von Webkomponenten sind so konzipiert, dass sie zusammenarbeiten. Sie können aber auch auswählen, welche Teile von Webkomponenten Sie verwenden möchten. In dieser Anleitung erfahren Sie, wie Sie Shadow DOM verwenden.

Hallo, Schattenwelt

Mit Shadow DOM können Elemente mit einer neuen Art von Knoten verknüpft werden. Diese neue Art von Knoten wird als Shadow-Root bezeichnet. Ein Element, dem ein Shadow-Root zugeordnet ist, wird als Shadow-Host bezeichnet. Der Inhalt eines Shadow-Hosts wird nicht gerendert, stattdessen wird der Inhalt des Shadow-Roots gerendert.

Beispiel:

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

dann wird statt

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

Trennung von Inhalt und Präsentation

Als Nächstes sehen wir uns an, wie Sie mit Shadow DOM Inhalte von der Darstellung trennen können. Angenommen, wir haben dieses Namens-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 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, wird die gesamte Struktur des Namensschilds im Dokument offengelegt. Wenn andere Elemente auf der Seite versehentlich dieselben Klassennamen für Styling oder Scripting verwenden, haben wir ein Problem.

Wir können uns eine schlechte Zeit ersparen.

Schritt 1: Präsentationsdetails ausblenden

Semantisch ist wahrscheinlich nur Folgendes wichtig:

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

Zuerst schreiben wir ein 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 „Bob“ gerendert. Da wir die darstellenden DOM-Elemente in ein <template>-Element verschoben haben, werden sie nicht gerendert, aber es kann über JavaScript auf sie zugegriffen werden. Das tun wir jetzt, um die Shadow-Root 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 ein Shadow-Root eingerichtet haben, wird das Namens-Tag wieder gerendert. Wenn Sie mit der rechten Maustaste auf das Namens-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 durch die Verwendung von Shadow DOM die Darstellungsdetails des Namensschilds aus dem Dokument ausgeblendet haben. Die Präsentationsdetails sind im Shadow DOM gekapselt.

Schritt 2: Inhalt von der Präsentation trennen

Unser Namens-Tag verbirgt jetzt die Präsentationsdetails auf der Seite, trennt aber nicht wirklich die Präsentation vom Inhalt. Denn obwohl der Inhalt (der Name „Bob“) auf der Seite ist, ist der gerenderte Name der, den wir in die Shadow-Root kopiert haben. Wenn wir den Namen auf dem Namensschild ändern möchten, müssen wir das an zwei Stellen tun, die möglicherweise nicht mehr synchron sind.

HTML-Elemente sind kompositional. Sie können beispielsweise eine Schaltfläche in eine Tabelle einfügen. Hier ist eine Komposition erforderlich: Das Namensschild muss eine Komposition aus dem roten Hintergrund, dem Text „Hi!“ und dem Inhalt auf dem Namensschild sein.

Sie als Komponentenautor definieren, wie die Komposition mit Ihrem Widget funktioniert, indem Sie ein neues Element namens <content> verwenden. Dadurch wird ein Einfügepunkt in der Darstellung des Widgets erstellt. Der Einfügepunkt wählt Inhalte aus dem Shadow-Host aus, die an diesem Punkt dargestellt werden sollen.

Wenn wir das Markup im Shadow DOM so ä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 Namens-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 Sie den Namen des Nutzers auf Ihrer Seite aktualisieren müssen, schreiben Sie einfach:

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

und das war es auch schon. Das Rendering des Namensschilds wird automatisch vom Browser aktualisiert, da wir den Inhalt des Namensschilds mit <content> an die richtige Stelle projizieren.

<div id="ex2b">

Jetzt haben wir Inhalt und Darstellung voneinander getrennt. Der Inhalt befindet sich im Dokument, die Darstellung im Shadow DOM. Sie werden vom Browser automatisch synchronisiert, wenn etwas gerendert werden muss.

Schritt 3: Profit

Durch die Trennung von Inhalt und Darstellung können wir den Code vereinfachen, der den Inhalt manipuliert. Im Beispiel mit dem Namensschild muss dieser Code nur eine einfache Struktur mit einem <div> anstelle von mehreren verarbeiten.

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

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

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

Der Einrichtungscode für die Shadow-Root bleibt unverändert. Nur was in den Shadow-Root eingefügt 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 heutigen Situation im Web, da der Code für die Namensaktualisierung von der Struktur der Komponente abhängen kann, die einfach und konsistent ist. Der Code zum Aktualisieren des Namens muss die für das Rendering verwendete Struktur nicht kennen. Wenn wir uns ansehen, was gerendert wird, erscheint der Name im Englischen an zweiter Stelle (nach „Hi! „Mein Name ist“), aber auf Japanisch zuerst (vor „と申します“). Diese Unterscheidung ist semantisch bedeutungslos, wenn es darum geht, den angezeigten Namen zu aktualisieren. Der Code für die Namensaktualisierung muss dieses Detail also nicht kennen.

Zusatzaufgabe: Erweiterte Projektion

Im obigen Beispiel werden mit dem <content>-Element alle Inhalte aus dem Shadow-Host 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 mit folgendem Inhalt:

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

und ein Shadow-Root, das 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 <div class="email">-Element wird sowohl mit dem <content select="div">- als auch mit dem <content select=".email">-Element abgeglichen. Wie oft wird Bobs E-Mail-Adresse angezeigt und in welchen Farben?

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, der tatsächlich auf dem Bildschirm gerendert wird, wie eine riesige Party ist, wie Leute, die sich mit Shadow DOM auskennen, wissen. Das Inhaltselement ist die Einladung, mit der Inhalte aus dem Dokument in die Backstage-Shadow-DOM-Rendering-Party aufgenommen werden. Diese Einladungen werden in der Reihenfolge zugestellt, in der sie adressiert sind (d. h. das Attribut select). Inhalte nehmen die Einladung immer an (wer würde das nicht?!) und schon geht es los. Wenn eine weitere Einladung an diese Adresse gesendet wird, ist niemand zu Hause und die Person kommt nicht zu Ihrer Party.

Im obigen Beispiel entspricht <div class="email"> sowohl dem Selektor div als auch dem Selektor .email. Da das Inhaltselement mit dem Selektor div jedoch früher im Dokument vorkommt, geht <div class="email"> zur gelben Party und niemand ist verfügbar, um zur blauen Party zu gehen. (Vielleicht ist er deshalb so blau, aber geteiltes Leid ist halbes Leid, also weiß man nie.)

Wenn etwas zu keiner Party eingeladen wird, wird es überhaupt nicht gerendert. Das ist mit dem Text „Hello, world“ im allerersten Beispiel passiert. Das ist nützlich, wenn Sie ein völlig anderes Rendering erzielen möchten: Schreiben Sie das semantische Modell in das Dokument, das für Skripts auf der Seite zugänglich ist, blenden Sie es aber für das Rendering aus und verbinden Sie es mit einem ganz anderen Rendering-Modell im Shadow DOM mithilfe von JavaScript.

HTML hat beispielsweise eine gute Datumsauswahl. Wenn Sie <input type="date"> eingeben, wird ein Kalender-Pop-up angezeigt. Was aber, wenn Sie dem Nutzer die Möglichkeit geben möchten, einen Zeitraum für seinen Urlaub auf einer Dessert-Insel auszuwählen (Sie wissen schon… mit Hängematten aus Red Vines)? Sie richten Ihr Dokument so 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 ein Shadow DOM erstellen, das eine Tabelle verwendet, um einen schicken Kalender zu erstellen, der den Datumsbereich und so weiter hervorhebt. 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? Wenn ein Nutzer das Formular mit einem Browser aufruft, der Shadow DOM nicht unterstützt, ist das Formular trotzdem verwendbar, nur nicht so schön. 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>

Du hast den Shadow-DOM-Grundkurs bestanden.

Das waren die Grundlagen von Shadow DOM – Sie haben Shadow DOM 101 bestanden! Mit Shadow-DOM können Sie noch mehr tun, z. B. mehrere Schatten auf einem Schattenhost oder verschachtelte Schatten für die Kapselung verwenden oder Ihre Seite mit modellgesteuerten Ansichten (MDV) und Shadow-DOM erstellen. Und Webkomponenten sind mehr als nur Shadow DOM.

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