Web Components + Shadow DOM
HTML bietet eine Vielzahl an fertigen Elementen wie z.B. div, span, input, table, … Eigene Elemente, die wiederverwendbar sind konnte man aber nicht direkt erstellen – zumindest nicht so, wie die vorhandenen Elemente. Frameworks wie Angular, Vue oder Aurelia bieten zwar die Möglichkeit, eigene Elemente zu erstellen, aber diese sind (waren) im Endeffekt keine richtigen Elemente – also nicht isoliert.
Erst durch Web Components ist es dem Entwickler möglich, tatsächlich Elemente zu erstellen, die sich wie die nativen Elemente verhalten. Mittlerweile unterstützen alle Browser (den IE mal ausgeschlossen) das erstellen von eigenen Komponenten. Was per heute (29.02.2020) noch nicht durchgängig möglich ist, ist das erweitern bestehende Elemente.
Ein Konzept was bei der Erstellung von Web Komponenten entscheidend ist, nennt sich „Shadow DOM“. Wie allgemein bekannt sein dürfte, erstellt der Browser aufgrund der HTML-Seite das DOM (Document Object Model). JavaScript und Styles haben Zugriff auf alle Elemente innerhalb dieses DOM. Mit Shadow DOM wird es jetzt ermöglich, einen DOM innerhalb eines anderen DOM zu haben, der wirklich in einem hohen Maße isoliert ist.
Zwei Begriffe tauchen bei der Verwendung von Shadow DOM immer wieder auf:
- Host: Element des im übergeordneten DOM, welches den Shadow DOM enthält
- Root: Root-Element im Shadow DOM
Shadow DOM erstellen
Ein Shadow DOM kann auf jedem Element erstellt werden:
const shadowRoot = myDiv.attachShadow({mode: "open"});
Hiermit hat das „myDiv“-Element einen Shadow DOM erhalten. Als Mode gibt es „open“ und „closed“. Mit „open“ kann auf den Inhalt des Shadow DOM über die Eigenschaft „shadowRoot“ zugegriffen werden, mit „closed“ nicht (dann ist shadowRoot == null).
Wichtig! Werden Funktionen wie „querySelector“ vom Parent DOM ausgeführt, dann werden NIE Elemente aus dem Shadow DOM zurückgeliefert. Sprich, eine Isolation ist immer vorhanden, egal ob der Mode „open“ oder „closed“ ist.
Shadow DOM + Styles
Grundsätzlich gilt, dass Styles, die im Parent DOM definiert sind, haben keine Auswirkung auf den Shadow DOM und Styles innerhalb des Shadow-DOM haben keine Auswirkung auf Parent DOM. Ausnahme sind vererbte Styles wie „background“, „color“, „font“, … Diese vererben sich weiter vom Parent DOM in den Shadow DOM.
Weiters gibt es noch ein paar andere Spezialitäten:
:host {
background-color: green;
}
:host(:hover) {
background-color: blue;
}
Hiermit kann von innerhalb des Shadow DOM der Style des Host-Element angepasst werden.
:host-context(.darktheme) {
background-color: black;
color: white;
}
Hiermit wird geprüft, ob ein übergeordnetes Element die Klasse „darktheme“ hat und wenn ja, dann wird der Style angepasst (also etwas, was normalerweise in CSS nicht geht :-)).
CSS Variablen sind generell gültig. Heißt, auch wenn diese im Parent DOM definiert sind, so ist ihr Gültigkeitsbereich genau so, als ob es keinen Shadow DOM gäbe.
Die meisten gängigen Events bubblen von innerhalb des Shadow DOM in den Parent DOM. Allerdings wir der Target ggf. auf den Shadow Host geändert. Beim dispatchen von eigenen Events kann mit der Option „composed“ definiert werden, ob das Event in den Parent DOM weitergeleitet werden soll (false == nein).
Let’s create a Web Component
class SuperDiv extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: "open"});
}
connectedCallback() {
this.shadowRoot.innerHTML = "<div class='inner'><slot></slot></div>";
}
}
customElements.define("super-div", SuperDiv);
Wichtig ist, dass beim Erstellen eigener Komponenten der Name immer aus min. zwei Teilen, getrennt durch ein Dash, bestehen muss. Damit ist sichergestellt, dass es zu keinem Namenskonflikt mit den nativen Elementen kommt.
Ansonsten ist die Erstellung ziemlich straight-forward. Es wird ein Klasse definiert, die von HTMLElement ableitet. Diese wird am Ende mit customElements.define registriert.
In meinem Beispiel ist die Verwendung von „slot“ noch erwähnenswert. Durch das nachfolgende Beispiel sollte aber klar sein, was dahintersteckt:
<body>
<super-div>
Ich bin der Inhalt des Slots.
</super-div>
</body>
Wichtig: der Inhalt wird zwar in den Slot im Shadow DOM projiziert, ist, aber im DOM weiterhin an der gleichen Stelle. Dies bedeutet, dass die Styles hier ganz normal greifen. Innerhalb des Shadow DOM kann das Pseudo-Element „::slotted“ verwendet werden, um Stylings zu definieren.
Das Beispiel oben arbeitet mit einem Standard-Slot. Es ist allerdings auch möglich, mehrere Slots zu definieren. Dafür müssen diese im Shadow DOM mit einem „id“-Attribut benannt werden. Im Parent DOM muss beim Element das Attribut „slot“ mit dem Wert der „id“ des Slots angegeben werden, in den der Inhalt projiziert werden soll.
Übrigens gibt es durch Shadow DOM jetzt endlich eine wirklich interessante Alternative zum IFrame :-).
Lifecycle Callbacks
Web-Components haben ein paar Lifecycle Callbacks, die erwähnenswert sind. Ein Beispiel dafür ist „connectedCallback“ im obigen Beispiel.
connectedCallback
Wird ausgelöst, wenn die Web-Component in den DOM eingefügt wird (auch beim Verschieben von Web-Components).
disconnectedCallback
Wird ausgelöst, wenn die Web-Component aus dem DOM entfernt wird (auch beim Verschieben von Web-Components).
attributeChangedCallback
Parameter:
- name
- oldValue
- newValue
Der Callback wird nur für Attribute ausgelöst, die überwacht werden. Dafür muss es in der Klasse eine statische Eigenschaft „observedAttributes“ geben, die die Namen der zu überwachenden Attribute zurückgibt.
class SuperDiv extends HTMLElement {
constructor() {
super();
...
}
static get observedAttributes() {
return ["my-observe-attribute"];
}
}