Shadow DOM v1 – Eigenständige Webkomponenten

Mit Shadow-DOM können Webentwickler kompartmentierte DOM- und CSS-Elemente für Webkomponenten erstellen.

Zusammenfassung

Dank Shadow-DOM werden die Probleme beim Erstellen von Webanwendungen beseitigt. Die Schwäche entsteht durch den globalen Charakter von HTML, CSS und JS. Im Laufe der Jahre haben wir eine exorbitante Anzahl von tools zur Umgehung dieser Probleme entwickelt. Wenn Sie beispielsweise eine neue HTML-ID bzw. HTML-Klasse verwenden, kann nicht festgestellt werden, ob diese mit einem bereits von der Seite verwendeten Namen in Konflikt steht. Es kommen immer mehr kleine Fehler hinzu, die CSS-Spezifität wird zum großen Problem (!importantsowie alle!), die Stilauswahl gerät außer Kontrolle und die Leistung kann darunter leiden. Die Liste ist noch lang.

Shadow DOM korrigiert CSS und DOM. Es werden Bereichsstile für die Webplattform eingeführt. Ohne Tools oder Namenskonventionen können Sie CSS mit Markup bündeln, Implementierungsdetails ausblenden und eigenständige Komponenten in einfachem JavaScript erstellen.

Einleitung

Shadow DOM ist einer der drei Standards für Webkomponenten: HTML-Vorlagen, Shadow DOM und Benutzerdefinierte Elemente. HTML-Importe waren früher Teil der Liste, gelten aber heute als eingestellt.

Sie müssen keine Webkomponenten entwickeln, die Shadow DOM verwenden. Aber wenn Sie dies tun, nutzen Sie die Vorteile (CSS-Bereich, DOM-Kapselung, Zusammensetzung) und erstellen wiederverwendbare benutzerdefinierte Elemente, die stabil, hoch konfigurierbar und extrem wiederverwendbar sind. Wenn Sie mit einer JS API benutzerdefinierte Elemente zum Erstellen eines neuen HTML-Codes verwenden, stellen Sie den HTML- und CSS-Code über das Schatten-DOM bereit. Die beiden APIs bilden zusammen eine Komponente mit eigenständigem HTML, CSS und JavaScript.

Shadow DOM wurde als Tool zum Erstellen komponentenbasierter Apps entwickelt. Daher bietet es Lösungen für häufige Probleme in der Webentwicklung:

  • Isoliertes DOM: Das DOM einer Komponente ist in sich geschlossen. So gibt document.querySelector() keine Knoten im Schatten-DOM der Komponente zurück.
  • Bereichsbezogener CSS-Code: Der im Schatten-DOM definierte CSS-Code ist auf diesen Bereich beschränkt. Stilregeln und Seitenstile funktionieren nicht.
  • Komposition: Entwerfen Sie eine deklarative, Markup-basierte API für Ihre Komponente.
  • Vereinfacht CSS: Ein beschränktes DOM bedeutet, dass Sie einfache CSS-Selektoren und allgemeinere ID-/Klassennamen verwenden können und sich keine Gedanken über Namenskonflikte machen müssen.
  • Produktivität: Stellen Sie sich Apps in DOM-Chunks statt auf einer großen (globalen) Seite vor.

fancy-tabs – Demo

In diesem Artikel werde ich auf eine Demokomponente (<fancy-tabs>) sowie auf Code-Snippets aus dieser Komponente verweisen. Wenn Ihr Browser die APIs unterstützt, finden Sie unten eine Live-Demo. Alternativ kannst du dir den vollständigen Quellcode auf GitHub ansehen.

Quelle auf GitHub ansehen

Was ist Shadow DOM?

Hintergrundinformationen zu DOM

HTML ist die Grundlage für das Web, weil es einfach ist, damit zu arbeiten. Durch das Deklarieren einiger Tags können Sie in Sekundenschnelle eine Seite erstellen, die sowohl ansprechend als auch strukturiert ist. HTML selbst ist jedoch nicht allzu nützlich. Für Menschen ist es leicht, eine textbasierte Sprache zu verstehen, aber Maschinen benötigen mehr. Geben Sie das Dokumentobjektmodell oder DOM ein.

Wenn der Browser eine Webseite lädt, kann er eine Menge interessante Funktionen nutzen. Dazu wandelt sie unter anderem den HTML-Code des Autors in ein Live-Dokument um. Um die Struktur der Seite zu verstehen, parst der Browser HTML (statische Textstrings) in ein Datenmodell (Objekte/Knoten). Der Browser behält die HTML-Hierarchie bei, indem er eine Baumstruktur aus diesen Knoten erstellt: das DOM. Das Tolle an DOM ist, dass es Ihre Seite live repräsentiert. Im Gegensatz zu statischem HTML-Code, den wir erstellen, enthalten die im Browser erstellten Knoten Eigenschaften, Methoden und können – vor allem – durch Programme verändert werden. Deshalb können wir DOM-Elemente direkt mit JavaScript erstellen:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

wird das folgende HTML-Markup erzeugt:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

All das ist gut und gut. Was genau ist dann Shadow DOM?

DOM... in den Schatten

Ein Shadow-DOM ist ein normales DOM mit zwei Unterschieden: 1) wie es erstellt/verwendet wird und 2) wie es sich im Verhältnis zum Rest der Seite verhält. Normalerweise erstellen Sie DOM-Knoten und hängen sie als untergeordnete Elemente eines anderen Elements an. Mit Shadow DOM erstellen Sie einen bereichsspezifischen DOM-Baum, der an das Element angehängt, aber von seinen eigentlichen untergeordneten Elementen getrennt ist. Diese auf einen Bereich reduzierte Unterstruktur wird als Schattenbaum bezeichnet. Das Element, an das es angehängt ist, ist sein Shadowhost. Alles, was Sie den Schatten hinzufügen, wird für das Hostingelement lokal gespeichert, einschließlich <style>. So erreicht das Schatten-DOM die CSS-Stilbereiche.

Shadow-DOM wird erstellt

Ein Schattenstamm ist ein Dokumentfragment, das an ein Hostelement angehängt wird. Beim Anhängen einer Schattenwurzel erhält das Element sein Schatten-DOM. Rufen Sie element.attachShadow() auf, um ein Schatten-DOM für ein Element zu erstellen:

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Ich verwende .innerHTML, um den Schattenstamm zu füllen. Sie können aber auch andere DOM APIs verwenden. Das ist das Web. Wir haben die Wahl.

Die Spezifikation definiert eine Liste von Elementen, die keinen Schattenbaum hosten können. Es gibt verschiedene Gründe, warum ein Element in der Liste enthalten sein kann:

  • Der Browser hostet bereits ein eigenes internes Schatten-DOM für das Element (<textarea>, <input>).
  • Es ist nicht sinnvoll, ein Schatten-DOM (<img>) für das Element zu hosten.

Das funktioniert beispielsweise nicht:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Shadow-DOM für ein benutzerdefiniertes Element erstellen

Das Shadow-DOM ist besonders nützlich, wenn Sie benutzerdefinierte Elemente erstellen. Verwenden Sie Shadow DOM, um den HTML-, CSS- und JS-Code eines Elements aufzuteilen und so eine „Webkomponente“ zu erzeugen.

Beispiel: Ein benutzerdefiniertes Element hängt sich das Schatten-DOM an und kapselt sein DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Ein paar interessante Dinge sind hier vor sich. Zum einen erstellt das benutzerdefinierte Element ein eigenes Schatten-DOM, wenn eine Instanz von <fancy-tabs> erstellt wird. Das ist über constructor() möglich. Zweitens werden die CSS-Regeln in <style> auf <fancy-tabs> beschränkt, da wir einen Schattenstamm erstellen.

Komposition und Flächen

Die Zusammensetzung ist eines der am wenigsten bekannten Merkmale von Shadow DOM, aber wohl auch das wichtigste.

In der Webentwicklung geht es um die Entwicklung von Anwendungen, deklarativ aus HTML. Verschiedene Bausteine (<div>s, <header>s, <form>s, <input>s) ergeben zusammen Apps. Einige dieser Tags funktionieren sogar miteinander. Die Zusammensetzung ist der Grund, warum native Elemente wie <select>, <details>, <form> und <video> so flexibel sind. Jedes dieser Tags akzeptiert bestimmte HTML-Elemente als untergeordnete Tags und macht etwas Besonderes damit. <select> kann beispielsweise <option> und <optgroup> in Drop-down- und Mehrfachauswahl-Widgets rendern. Das <details>-Element rendert <summary> als erweiterbaren Pfeil. Sogar <video> kann mit bestimmten Kindern umgehen: <source>-Elemente werden nicht gerendert, beeinflussen aber das Verhalten des Videos. Was für Magie!

Terminologie: Light DOM vs. Shadow DOM

Die Shadow-DOM-Zusammensetzung bietet eine Reihe neuer Grundlagen in der Webentwicklung. Bevor wir zu den Einzelheiten gehen, sollten wir einige Terminologie standardisieren, damit wir dieselbe Sprache sprechen.

Light DOM

Das Markup, das ein Nutzer Ihrer Komponente schreibt. Dieses DOM befindet sich außerhalb des Schatten-DOM der Komponente. Es handelt sich dabei um die tatsächlichen untergeordneten Elemente des Elements.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

Schatten-DOM

Das DOM, das ein Komponentenautor schreibt. Das Shadow-DOM ist lokal für die Komponente und definiert ihre interne Struktur, den beschränkten CSS-Code und enthält Ihre Implementierungsdetails. Sie kann auch definieren, wie das vom Nutzer Ihrer Komponente verfasste Markup gerendert werden soll.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Vereinfachter DOM-Baum

Das Ergebnis, bei dem der Browser das Light DOM des Nutzers in Ihr Schatten-DOM verteilt und so das Endprodukt rendert. Der vereinfachte Baum ist das, was Sie in den Entwicklertools sehen und was auf der Seite gerendert wird.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

Das <slot>-Element

Das Shadow-DOM setzt verschiedene DOM-Bäume mithilfe des Elements <slot> zusammen. Slots sind Platzhalter in Ihrer Komponente, die Nutzer mit ihrem eigenen Markup füllen können. Indem Sie einen oder mehrere Slots definieren, laden Sie externes Markup zum Rendern im Schatten-DOM Ihrer Komponente ein. Im Wesentlichen sagen Sie "Das Markup des Nutzers hier rendern".

Elemente dürfen die Schatten-DOM-Grenze „überqueren“, wenn ein <slot> sie einlädt. Diese Elemente werden als verteilte Knoten bezeichnet. Konzeptionell können verteilte Knoten etwas skurril wirken. Slots verschieben das DOM nicht; sie rendern es an einer anderen Stelle im Shadow DOM.

Eine Komponente kann null oder mehr Flächen in ihrem Schatten-DOM definieren. Slots können leer sein oder Fallback-Inhalte bereitstellen. Wenn der Nutzer keinen Light DOM-Inhalt bereitstellt, rendert die Anzeigenfläche den Fallback-Inhalt.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

Sie können auch benannte Slots erstellen. Benannte Slots sind spezifische Löcher im Schatten-DOM, auf die Nutzer mit ihrem Namen verweisen.

Beispiel – die Anzeigenflächen im Schatten-DOM von <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

So deklarieren Nutzer von Komponenten <fancy-tabs>:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

Der abgeflachte Baum sieht ungefähr so aus:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Unsere Komponente kann verschiedene Konfigurationen verarbeiten, der vereinfachte DOM-Baum bleibt jedoch unverändert. Wir können auch von <button> zu <h2> wechseln. Diese Komponente wurde für verschiedene Arten von Kindern entwickelt – genau wie <select>.

Stile

Es gibt viele Möglichkeiten, Webkomponenten zu gestalten. Eine Komponente, die Schatten-DOM verwendet, kann von der Hauptseite gestaltet, eigene Stile definiert oder Hooks (in Form von benutzerdefinierten CSS-Eigenschaften) bereitgestellt werden, mit denen Nutzer die Standardeinstellungen überschreiben können.

Komponentendefinierte Stile

Die nützlichste Funktion des Shadow DOM ist CSS-Bereich:

  • CSS-Selektoren von der äußeren Seite werden nicht innerhalb Ihrer Komponente angewendet.
  • In der innen definierte Stile werden nicht eingeblendet. Sie beziehen sich auf das Hostelement.

Die im Schatten-DOM verwendeten CSS-Selektoren werden lokal auf Ihre Komponente angewendet. In der Praxis bedeutet dies, dass wir wieder gängige ID-/Klassennamen verwenden können, ohne uns Gedanken über Konflikte an anderen Stellen auf der Seite machen zu müssen. Einfachere CSS-Selektoren sind eine bewährte Methode im Shadow-DOM. Sie sind auch gut für die Leistung.

Beispiel – in einem Schattenstamm definierte Stile sind lokal

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Stylesheets sind auch auf den Schattenbaum beschränkt:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Haben Sie sich schon einmal gefragt, wie das Element <select> ein Mehrfachauswahl-Widget (anstelle eines Drop-down-Menüs) rendert, wenn Sie das Attribut multiple hinzufügen:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> kann sich anhand der Attribute, die Sie deklarieren, selbst unterschiedlich gestalten. Webkomponenten können sich mithilfe der Auswahl :host auch selbst gestalten.

Beispiel: eine Komponente, die sich selbst gestaltet

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Ein Problem bei :host ist, dass Regeln auf der übergeordneten Seite spezifischer sind als die :host-Regeln, die im Element definiert sind. Das heißt, „Outdoor-Stile gewinnen“. So können Nutzer den Stil der obersten Ebene von außen überschreiben. Außerdem funktioniert :host nur in Verbindung mit einem Shadow-Stamm, sodass Sie es nicht außerhalb des Schatten-DOM verwenden können.

Mit der funktionalen Form von :host(<selector>) können Sie den Host als Ziel angeben, wenn er einer <selector> entspricht. So können mit Ihrer Komponente Verhaltensweisen gekapselt werden, die auf Nutzerinteraktionen reagieren oder auf den Host reagieren bzw. interne Knoten gestalten.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Stile basierend auf Kontext festlegen

:host-context(<selector>) entspricht der Komponente, wenn sie oder einer ihrer Ancestors mit <selector> übereinstimmt. Dies wird häufig für Themen verwendet, die auf der Umgebung einer Komponente basieren. Viele Nutzer wenden beispielsweise eine Klasse auf <html> oder <body> an, um Themen zu erstellen:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) würde <fancy-tabs> formatieren, wenn es ein Nachfolger von .darktheme ist:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() kann für Themen nützlich sein, ein noch besserer Ansatz besteht jedoch darin, Stil-Hooks mithilfe von benutzerdefinierten CSS-Eigenschaften zu erstellen.

Verteilte Knoten gestalten

::slotted(<compound-selector>) gleicht Knoten ab, die in einer <slot> verteilt sind.

Angenommen, wir haben eine Namensschild-Komponente erstellt:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

Mit dem Shadow DOM der Komponente können die <h2> und .title des Nutzers gestaltet werden:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Wie Sie sich vielleicht erinnern, verschieben <slot>s das Light DOM des Nutzers nicht. Wenn Knoten in einem <slot> verteilt sind, rendert <slot> das DOM, die Knoten bleiben jedoch am selben Standort. Stile, die vor der Verteilung angewendet wurden, werden auch nach der Verteilung angewendet. Wenn das Light DOM jedoch verteilt ist, kann es zusätzliche Stile annehmen (d. h. Stile, die durch das Shadow DOM definiert werden).

Ein weiteres, ausführlicheres Beispiel von <fancy-tabs>:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

In diesem Beispiel gibt es zwei Slots: einen benannten Slot für die Tab-Titel und einen für den Inhalt des Tabbereichs. Wählt der Nutzer einen Tab aus, wird seine Auswahl fett formatiert und das entsprechende Feld angezeigt. Dazu werden verteilte Knoten mit dem Attribut selected ausgewählt. Das JS-Element des benutzerdefinierten Elements (hier nicht gezeigt) fügt dieses Attribut zur richtigen Zeit hinzu.

Komponenten von außen gestalten

Es gibt mehrere Möglichkeiten, eine Komponente von außen zu gestalten. Am einfachsten ist es, den Tag-Namen als Selektor zu verwenden:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Externe Stile haben immer Vorrang vor Stilen, die im Schatten-DOM definiert sind. Wenn der Nutzer beispielsweise den Selektor fancy-tabs { width: 500px; } schreibt, hat dieser Vorrang vor der Regel der Komponente: :host { width: 650px;}.

Wenn Sie den Stil der Komponente selbst festlegen, geht das nur bis zum nächsten Punkt. Aber was passiert, wenn Sie das Innen einer Komponente gestalten möchten? Dazu benötigen wir benutzerdefinierte CSS-Eigenschaften.

Stil-Hooks mithilfe von benutzerdefinierten CSS-Eigenschaften erstellen

Nutzer können interne Stile anpassen, wenn der Autor der Komponente mithilfe von benutzerdefinierten CSS-Eigenschaften Stil-Hooks bereitstellt. Das Konzept ähnelt dem von <slot>. Sie erstellen „Stilplatzhalter“, die Nutzer überschreiben können.

Beispiel: Mit <fancy-tabs> können Nutzer die Hintergrundfarbe überschreiben:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

Im Schatten-DOM:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

In diesem Fall verwendet die Komponente black als Hintergrundwert, da der Nutzer ihn angegeben hat. Andernfalls wird standardmäßig #9E9E9E verwendet.

Themen für Fortgeschrittene

Geschlossene Schattenwurzeln erstellen (sollte nicht verwendet werden)

Es gibt noch eine Variante des Schatten-DOMs, den „geschlossenen“ Modus. Wenn Sie einen geschlossenen Schattenbaum erstellen, kann außerhalb von JavaScript nicht auf das interne DOM der Komponente zugegriffen werden. Das funktioniert ähnlich wie native Elemente wie <video>. JavaScript kann nicht auf das Shadow-DOM von <video> zugreifen, da es vom Browser mit einem Shadow-Stamm im geschlossenen Modus implementiert wird.

Beispiel – Erstellen eines geschlossenen Schattenbaums:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Andere APIs sind ebenfalls vom geschlossenen Modus betroffen:

  • Element.assignedSlot / TextNode.assignedSlot gibt null zurück
  • Bei Event.composedPath() für Ereignisse, die mit Elementen im Schatten-DOM verknüpft sind, wird [] zurückgegeben.

Hier ist meine Zusammenfassung, warum Sie niemals Webkomponenten mit {mode: 'closed'} erstellen sollten:

  1. Künstliches Gefühl von Sicherheit. Nichts hindert einen Angreifer daran, Element.prototype.attachShadow zu hacken.

  2. Der geschlossene Modus verhindert, dass der Code Ihres benutzerdefinierten Elements auf sein eigenes Schatten-DOM zugreift. Das ist komplett gescheitert. Du musst stattdessen eine Referenz für später speichern, wenn du Dinge wie querySelector() verwenden möchtest. Damit wird der ursprüngliche Zweck des geschlossenen Modus vollständig verfehlt.

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. Der geschlossene Modus macht Ihre Komponente für Endnutzer weniger flexibel. Wenn Sie Webkomponenten erstellen, werden Sie manchmal vergessen, eine Funktion hinzuzufügen. Eine Konfigurationsoption. Ein Anwendungsfall, den die Nutzenden möchten. Ein häufiges Beispiel ist, dass man vergisst, angemessene Stil-Hooks für interne Knoten einzufügen. Im geschlossenen Modus haben Nutzer keine Möglichkeit, Standardeinstellungen zu überschreiben und Stile anzupassen. Es ist sehr hilfreich, auf das Innere der Komponente zugreifen zu können. Letztendlich verzweigen Nutzer Ihre Komponente, suchen eine andere oder erstellen eine eigene, wenn sie nicht das tut, was sie möchten.

Mit Slots in JS arbeiten

Die Shadow DOM API bietet Dienstprogramme für die Arbeit mit Slots und verteilten Knoten. Diese sind nützlich, wenn Sie ein benutzerdefiniertes Element erstellen.

Slotchange-Ereignis

Das Ereignis slotchange wird ausgelöst, wenn sich die verteilten Knoten eines Slots ändern. Das ist beispielsweise der Fall, wenn der Nutzer untergeordnete Elemente im Light DOM hinzufügt/entfernt.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Wenn Sie andere Arten von Änderungen am Light DOM beobachten möchten, können Sie im Konstruktor des Elements einen MutationObserver einrichten.

Welche Elemente werden in einer Anzeigenfläche gerendert?

Manchmal ist es hilfreich zu wissen, welche Elemente einer Anzeigenfläche zugeordnet sind. Rufen Sie slot.assignedNodes() auf, um zu ermitteln, welche Elemente die Anzeigenfläche rendert. Mit der Option {flatten: true} wird auch der Fallback-Inhalt eines Slots zurückgegeben, wenn keine Knoten verteilt werden.

Angenommen, Ihr Shadow DOM sieht so aus:

<slot><b>fallback content</b></slot>
NutzungCallErgebnis
<my-component>Komponententext</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

Welcher Fläche wird ein Element zugewiesen?

Es ist auch möglich, die umgekehrte Frage zu beantworten. element.assignedSlot gibt an, welchen der Komponentenslots Ihr Element zugewiesen ist.

Shadow-DOM-Ereignismodell

Wenn ein Ereignis im Schatten-DOM bekannt wird, wird das Ziel angepasst, um die Kapselung des Schatten-DOMs beizubehalten. Das bedeutet, dass beim Retargeting von Ereignissen der Eindruck entsteht, dass sie von der Komponente und nicht von internen Elementen in Ihrem Shadow DOM stammen. Einige Ereignisse werden nicht einmal außerhalb des Schatten-DOM weitergegeben.

Folgende Ereignisse überschreiten die Schattengrenze:

  • Schwerpunkt-Ereignisse: blur, focus, focusin, focusout
  • Mausereignisse: click, dblclick, mousedown, mouseenter, mousemove usw.
  • Rad-Ereignisse: wheel
  • Eingabeereignisse: beforeinput, input
  • Tastaturereignisse: keydown, keyup
  • Ereignisse für Zusammensetzung: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop usw.

Tipps

Wenn der Schattenbaum geöffnet ist, wird beim Aufrufen von event.composedPath() ein Array von Knoten zurückgegeben, die das Ereignis durchlaufen hat.

Benutzerdefinierte Ereignisse verwenden

Benutzerdefinierte DOM-Ereignisse, die auf internen Knoten in einem Schattenbaum ausgelöst werden, treten nicht außerhalb der Schattengrenze auf, es sei denn, das Ereignis wird mit dem Flag composed: true erstellt:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

Ist der Wert composed: false (Standard), können Nutzer außerhalb des Schattenstamms nicht auf das Ereignis warten.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Fokus bedienen

Im Ereignismodell des Schatten-DOMs werden Ereignisse, die im Schatten-DOM ausgelöst werden, so angepasst, als würden sie aus dem Hosting-Element stammen. Angenommen, Sie klicken auf ein <input> innerhalb eines Schattenstamms:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

Das focus-Ereignis wird dann so aussehen, als käme es von <x-focus>, nicht von <input>. Entsprechend ist document.activeElement <x-focus>. Wenn die Schattenwurzel mit mode:'open' erstellt wurde (siehe geschlossener Modus), können Sie auch auf den internen Knoten zugreifen, der jetzt in den Fokus rückt:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Wenn bei der Wiedergabe mehrere Schatten-DOM-Ebenen vorhanden sind (z. B. ein benutzerdefiniertes Element in einem anderen benutzerdefinierten Element), müssen Sie die Schattenwurzeln rekursiv bohren, um das activeElement zu finden:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Eine weitere Option für den Fokus ist die delegatesFocus: true-Option, mit der das Fokusverhalten der Elemente innerhalb eines Schattenbaums erweitert wird:

  • Wenn Sie auf einen Knoten im Schatten-DOM klicken und der Knoten kein fokussierbarer Bereich ist, wird der erste fokussierbare Bereich fokussiert.
  • Wenn ein Knoten innerhalb des Schatten-DOMs stärker fokussiert wird, gilt :focus nicht nur für das fokussierte Element, sondern auch für den Host.

Beispiel: So ändert delegatesFocus: true das Fokusverhalten

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Ergebnis

delegatesFocus: Wahres Verhalten.

Oben sehen Sie das Ergebnis, wenn <x-focus> fokussiert ist (Nutzerklick, Tab, der mit focus() geöffnet wird usw.). Es wird auf „Anklickbarer Shadow-DOM-Text“ geklickt oder der interne <input> (einschließlich autofocus) fokussiert.

Wenn du delegatesFocus: false festlegen würdest, erhältst du stattdessen Folgendes:

delegatesFocus: falsch und die interne Eingabe fokussiert
delegatesFocus: false und die interne <input> liegt im Fokus.
delegatesFocus: false und x-focus erhöhen den Fokus (z.B. mit tabindex=&#39;0&#39;).
delegatesFocus: false und <x-focus> werden stärker hervorgehoben (z.B. mit tabindex="0").
delegatesFocus: false und &quot;Clickable Shadow DOM text&quot; (Klickbarer Shadow DOM-Text) wird angeklickt (oder ein anderer leerer Bereich innerhalb des Shadow DOM des Elements wird angeklickt).
delegatesFocus: false und „Klickbarer Schatten-DOM-Text“ wird angeklickt (oder auf einen anderen leeren Bereich im Schatten-DOM des Elements geklickt).

Tipps und Tricks

Im Laufe der Jahre habe ich gelernt, wie man Webkomponenten entwickelt. Diese Tipps helfen Ihnen beim Entwickeln von Komponenten und beim Debuggen von Shadow DOM nützlich.

CSS-Begrenzung verwenden

In der Regel sind Layout, Stil oder Farbe einer Webkomponente weitgehend unabhängig. Nutzen Sie die CSS-Eindämmung in :host, um den Erfolg zu optimieren:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Übernommene Stile zurücksetzen

Übernommene Stile (background, color, font, line-height usw.) werden weiterhin im Schatten-DOM übernommen. Das heißt, sie durchbrechen standardmäßig die Schatten-DOM-Grenze. Wenn Sie mit einem neuen Slate beginnen möchten, verwenden Sie all: initial;, um übernommene Stile auf ihren Anfangswert zurückzusetzen, wenn sie die Schattengrenze überschreiten.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Alle benutzerdefinierten Elemente finden, die von einer Seite verwendet werden

Manchmal ist es hilfreich, benutzerdefinierte Elemente zu finden, die auf der Seite verwendet werden. Dazu müssen Sie das Schatten-DOM aller auf der Seite verwendeten Elemente rekursiv durchlaufen.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

Elemente aus einer <template> erstellen

Anstatt eine Schattenwurzel mithilfe von .innerHTML zu füllen, können Sie einen deklarativen <template> verwenden. Vorlagen sind ein idealer Platzhalter, um die Struktur einer Webkomponente anzugeben.

Weitere Informationen finden Sie im Beispiel unter Benutzerdefinierte Elemente: Wiederverwendbare Webkomponenten erstellen.

Verlaufs- und Browserunterstützung

Wenn Sie in den letzten Jahren Webkomponenten folgen, wissen Sie, dass Chrome 35+/Opera schon seit einiger Zeit eine ältere Version des Shadow DOM veröffentlicht. Blink wird beide Versionen noch eine Zeit lang parallel unterstützen. In der V0-Spezifikation ist eine andere Methode zum Erstellen eines Schattenstamms enthalten (element.createShadowRoot anstelle von element.attachShadow von V1). Durch den Aufruf der älteren Methode wird weiterhin eine Schattenwurzel mit V0-Semantik erstellt, sodass vorhandener V0-Code nicht beeinträchtigt wird.

Wenn Sie an der alten V0-Spezifikation interessiert sind, lesen Sie die html5rocks-Artikel: 1, 2, 3. Die Unterschiede zwischen Shadow DOM v0 und v1 lassen sich gut vergleichen.

Unterstützte Browser

Shadow DOM Version 1 wird in Chrome 53 (Status), Opera 40, Safari 10 und Firefox 63 ausgeliefert. Edge hat mit der Entwicklung begonnen.

Prüfen Sie das Vorhandensein von attachShadow, um die Schatten-DOM erkennen zu können:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Bis die Browserunterstützung allgemein verfügbar ist, bieten die Polyfills shadydom und shadycss Version 1. Shady DOM ahmt den DOM-Bereich von benutzerdefinierten CSS-Eigenschaften „Shadow DOM“ und „shadycss-Polyfills“ sowie den Stilumfang der nativen API nach.

Anbringen der Polyfills:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Polyfills verwenden:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Eine Anleitung zum Shim/Scope von Stilen finden Sie unter https://github.com/webcomponents/shadycss#usage.

Fazit

Wir haben zum ersten Mal eine API-Primitive, die den korrekten CSS-Bereich sowie DOM-Bereich definiert und eine echte Zusammensetzung hat. In Kombination mit anderen Webkomponenten-APIs wie benutzerdefinierten Elementen bietet Shadow DOM die Möglichkeit, wirklich eingekapselte Komponenten ohne Hacks oder mit älterem Gepäck wie <iframe>s zu erstellen.

Verstehen Sie mich nicht falsch. Das Schatten-DOM ist mit Sicherheit ein komplexes Monster. Aber es lohnt sich, es zu lernen. Nehmen Sie sich etwas Zeit damit. Lernen Sie es und stellen Sie Fragen!

Weitere Informationen

Häufig gestellte Fragen

Kann ich Shadow DOM v1 heute verwenden?

Mit Polyfill, ja. Siehe Browserunterstützung.

Welche Sicherheitsfunktionen bietet Schatten-DOM?

Das Shadow-DOM ist keine Sicherheitsfunktion. Es ist ein schlankes Tool, mit dem Sie den CSS-Bereich festlegen und DOM-Bäume in Komponenten ausblenden können. Wenn Sie eine echte Sicherheitsgrenze festlegen möchten, verwenden Sie <iframe>.

Muss eine Webkomponente Shadow DOM verwenden?

Nein. Sie müssen keine Webkomponenten erstellen, die Shadow DOM verwenden. Wenn Sie jedoch benutzerdefinierte Elemente erstellen, die Shadow DOM verwenden, können Sie Funktionen wie CSS-Umfang, DOM-Kapselung und Komposition nutzen.

Was ist der Unterschied zwischen offenen und geschlossenen Schattenwurzeln?

Weitere Informationen finden Sie unter Geschlossene Schattenwurzeln.