Wir haben bereits besprochen, wie die Erstellung von Web Components einfacher ist, als du denkst. Aber es gibt noch einen weiteren Aspekt der Spezifikation, den wir noch nicht besprochen haben, und zwar eine Möglichkeit, ein integriertes Element anzupassen (nein, *aufzuladen*) ein integriertes Element. Es ähnelt der Erstellung vollständig benutzerdefinierter oder „autonomer“ Elemente – wie des <zombie-profile>-Elements aus den vorherigen Artikeln –, erfordert jedoch einige Unterschiede.
Artikelreihe
- Web Components sind einfacher als man denkt
- Interaktive Web Components sind einfacher als man denkt
- Die Verwendung von Web Components in WordPress ist einfacher als man denkt
- Supercharging Built-In Elements Mit Web Components ist einfacher als du denkst (Du bist hier)
- Kontextsensitive Web Components sind einfacher als man denkt
- Web Component Pseudo-Klassen und Pseudo-Elemente sind einfacher als man denkt
Benutzerdefinierte integrierte Elemente verwenden ein is-Attribut, um dem Browser mitzuteilen, dass dieses integrierte Element kein mildtätiges, Brillen tragendes Element aus Kansas ist, sondern tatsächlich das schnellere als eine rasende Kugel, bereit, die Welt zu retten, Element vom Planeten Web Component. (Keine Beleidigung beabsichtigt, Kansaner. Ihr seid auch super.)
Das Aufladen eines mildtätigen Elements gibt uns nicht nur die Vorteile der Formatierung, Syntax und integrierten Funktionen des Elements, sondern wir erhalten auch ein Element, das Suchmaschinen und Screenreader bereits verstehen. Der Screenreader muss raten, was in einem <my-func>-Element vor sich geht, hat aber eine Vorstellung davon, was in einem <nav is="my-func">-Element passiert. (Wenn du Spaß hast, tu es bitte um Himmels willen *nicht* in ein Element. Denk an die Kinder.)
Es ist wichtig zu beachten, dass Safari (und eine Handvoll weiterer Nischenbrowser) nur autonome Elemente und keine dieser benutzerdefinierten integrierten Elemente unterstützt. Wir werden später über Polyfills dafür sprechen.
Bis wir das drauf haben, fangen wir an, das <apocalyptic-warning>-Element, das wir in unserem ersten Artikel erstellt haben, als benutzerdefiniertes integriertes Element neu zu schreiben. (Der Code ist auch im CodePen-Demo verfügbar.)
Die Änderungen sind tatsächlich ziemlich einfach. Anstatt das generische HTMLElement zu erweitern, erweitern wir ein bestimmtes Element, in diesem Fall das <div>-Element mit der Klasse HTMLDivElement. Wir fügen auch ein drittes Argument zur Funktion customElements.defines hinzu: {extends: 'div'}.
customElements.define(
"apocalyptic-warning",
class ApocalypseWarning extends HTMLDivElement {
constructor() {
super();
let warning = document.getElementById("warningtemplate");
let mywarning = warning.content;
const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
mywarning.cloneNode(true)
);
}
},
{ extends: "div" }
);
Schließlich aktualisieren wir unser HTML von <apocalyptic-warning>-Tags zu <div>-Tags, die ein is-Attribut mit dem Wert „apocalyptic-warning“ enthalten, so:
<div is="apocalyptic-warning">
<span slot="whats-coming">Undead</span>
</div>
Zur Erinnerung: Wenn du das unten in Safari siehst, wirst du keine schönen Web Component-Köstlichkeiten sehen *schüttelt die Faust in Richtung Safari*
Nur bestimmten Elementen kann ein Shadow-Root angehängt werden. Ein Teil davon liegt daran, dass das Anhängen eines Shadow-Roots an z. B. ein <a>-Element oder ein <form>-Element Sicherheitsimplikationen haben könnte. Die Liste der verfügbaren Elemente besteht hauptsächlich aus Layout-Elementen wie <article>, <section>, <aside>, <main>, <header>, <div>, <nav> und <footer>, sowie textbezogenen Elementen wie <p>, <span>, <blockquote> und <h1>–<h6>. Zu guter Letzt erhalten wir auch das Body-Element und jedes gültige autonome benutzerdefinierte Element.
Das Hinzufügen eines Shadow-Roots ist nicht das Einzige, was wir tun können, um eine Web Component zu erstellen. Im Grunde ist eine Web Component eine Möglichkeit, Funktionalität in ein Element zu backen, und wir brauchen keine zusätzliche Markierung im Schatten, um das zu tun. Erstellen wir ein Bild mit einer integrierten Lightbox-Funktion, um den Punkt zu verdeutlichen.
Wir nehmen ein normales <img>-Element und fügen zwei Attribute hinzu: erstens das is-Attribut, das angibt, dass dieses <img> ein benutzerdefiniertes integriertes Element ist; und ein Datenattribut, das den Pfad zum größeren Bild enthält, das wir in der Lightbox anzeigen werden. (Da ich ein SVG verwende, habe ich einfach dieselbe URL verwendet, aber du könntest leicht ein kleineres Rasterbild auf der Website eingebettet und eine größere Version davon in der Lightbox haben.)
<img is="light-box" src="https://assets.codepen.io/1804713/ninja2.svg" data-lbsrc="https://assets.codepen.io/1804713/ninja2.svg" alt="Silent but Undeadly Zombie Ninja" />
Da wir für dieses <img> keinen Shadow-DOM haben können, gibt es keinen Bedarf für ein <template>-Element, <slot>-Elemente oder andere Dinge. Wir werden auch keine gekapselten Stile haben.
Also, überspringen wir direkt zum JavaScript
customElements.define(
"light-box",
class LightBox extends HTMLImageElement {
constructor() {
super();
// We’re creating a div element to use as the light box. We’ll eventually insert it just before the image in question.
let lb = document.createElement("div");
// Since we can’t use a shadow DOM, we can’t encapsulate our styles there. We could add these styles to the main CSS file, but they could bleed out if we do that, so I’m setting all styles for the light box div right here
lb.style.display = "none";
lb.style.position = "absolute";
lb.style.height = "100vh";
lb.style.width = "100vw";
lb.style.top = 0;
lb.style.left = 0;
lb.style.background =
"rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";
lb.style.backgroundSize = "contain";
lb.addEventListener("click", function (evt) {
// We’ll close our light box by clicking on it
this.style.display = "none";
});
this.parentNode.insertBefore(lb, this); // This inserts the light box div right before the image
this.addEventListener("click", function (evt) {
// Opens the light box when the image is clicked.
lb.style.display = "block";
});
}
},
{ extends: "img" }
);
Jetzt, da wir wissen, wie benutzerdefinierte integrierte Elemente funktionieren, müssen wir uns darauf zubewegen, sicherzustellen, dass sie *überall* funktionieren. Ja, Safari, dieser finstere Blick ist für dich.
WebComponents.org bietet ein allgemeines Polyfill, das sowohl benutzerdefinierte integrierte Elemente als auch autonome Elemente unterstützt. Da es jedoch so viel behandeln kann, ist es möglicherweise mehr, als du benötigst, insbesondere wenn du nur benutzerdefinierte integrierte Elemente in Safari unterstützen möchtest.
Da Safari autonome benutzerdefinierte Elemente unterstützt, können wir das <img> durch ein autonomes benutzerdefiniertes Element wie <lightbox-polyfill> ersetzen. „Das wird wie zwei Codezeilen sein!“, sagte sich der Autor naiv. Siebenunddreißig Stunden lang auf einen Code-Editor gestarrt, zwei Nervenzusammenbrüche und eine ernsthafte Neubewertung seines Karrierewegs später erkannte er, dass er tippen müsste, wenn er diese zwei Codezeilen schreiben wollte. Außerdem wurden es eher sechzig Codezeilen (aber du bist wahrscheinlich gut genug, um es in etwa zehn Zeilen zu schaffen).
Der ursprüngliche Code für die Lightbox kann größtenteils unverändert bleiben (obwohl wir bald ein neues autonomes benutzerdefiniertes Element hinzufügen werden), aber er erfordert einige kleine Anpassungen. Außerhalb der Definition des benutzerdefinierten Elements müssen wir eine boolesche Variable setzen.
let customBuiltInElementsSupported = false;
Dann setzen wir innerhalb des LightBox-Konstruktors die boolesche Variable auf true. Wenn benutzerdefinierte integrierte Elemente nicht unterstützt werden, wird der Konstruktor nicht ausgeführt und die boolesche Variable wird nicht auf true gesetzt; somit haben wir einen direkten Test, ob benutzerdefinierte integrierte Elemente unterstützt werden.
Bevor wir diesen Test verwenden, um unser benutzerdefiniertes integriertes Element zu ersetzen, müssen wir ein autonomes benutzerdefiniertes Element erstellen, das als Polyfill verwendet wird, nämlich <lightbox-polyfill>.
customElements.define(
"lightbox-polyfill", // We extend the general HTMLElement instead of a specific one
class LightBoxPoly extends HTMLElement {
constructor() {
super();
// This part is the same as the customized built-in element’s constructor
let lb = document.createElement("div");
lb.style.display = "none";
lb.style.position = "absolute";
lb.style.height = "100vh";
lb.style.width = "100vw";
lb.style.top = 0;
lb.style.left = 0;
lb.style.background =
"rgba(0,0,0, 0.7) url(" + this.dataset.lbsrc + ") no-repeat center";
lb.style.backgroundSize = "contain";
// Here’s where things start to diverge. We add a `shadowRoot` to the autonomous custom element because we can’t add child nodes directly to the custom element in the constructor. We could use an HTML template and slots for this, but since we only need two elements, it's easier to just create them in JavaScript.
const shadowRoot = this.attachShadow({ mode: "open" });
// We create an image element to display the image on the page
let lbpimg = document.createElement("img");
// Grab the `src` and `alt` attributes from the autonomous custom element and set them on the image
lbpimg.setAttribute("src", this.getAttribute("src"));
lbpimg.setAttribute("alt", this.getAttribute("alt"));
// Add the div and the image to the `shadowRoot`
shadowRoot.appendChild(lb);
shadowRoot.appendChild(lbpimg);
// Set the event listeners so that you show the div when the image is clicked, and hide the div when the div is clicked.
lb.addEventListener("click", function (evt) {
this.style.display = "none";
});
lbpimg.addEventListener("click", function (evt) {
lb.style.display = "block";
});
}
}
);
Nachdem nun das autonome Element bereit ist, benötigen wir Code, um das benutzerdefinierte <img>-Element zu ersetzen, wenn es im Browser nicht unterstützt wird.
if (!customBuiltInElementsSupported) {
// Select any image with the `is` attribute set to `light-box`
let lbimgs = document.querySelectorAll('img[is="light-box"]');
for (let i = 0; i < lbimgs.length; i++) { // Go through all light-box images
let replacement = document.createElement("lightbox-polyfill"); // Create an autonomous custom element
// Grab the image and div from the `shadowRoot` of the new lighbox-polyfill element and set the attributes to those originally on the customized image, and set the background on the div.
replacement.shadowRoot.querySelector("img").setAttribute("src", lbimgs[i].getAttribute("src"));
replacement.shadowRoot.querySelector("img").setAttribute("alt", lbimgs[i].getAttribute("alt"));
replacement.shadowRoot.querySelector("div").style.background =
"rgba(0,0,0, 0.7) url(" + lbimgs[i].dataset.lbsrc + ") no-repeat center";
// Stick the new lightbox-polyfill element into the DOM just before the image we’re replacing
lbimgs[i].parentNode.insertBefore(replacement, lbimgs[i]);
// Remove the customized built-in image
lbimgs[i].remove();
}
}
Da hast du es also! Wir haben nicht nur autonome benutzerdefinierte Elemente, sondern auch *benutzerdefinierte* integrierte Elemente erstellt – einschließlich der Art und Weise, wie sie in Safari funktionieren. Und wir erhalten all die Vorteile von strukturierten, semantischen HTML-Elementen, einschließlich der Angabe von Screenreadern und Suchmaschinen, was diese benutzerdefinierten Elemente sind.
Geh hinaus und passe die integrierten Elemente nach Belieben an!
Es fühlt sich an, als hätten wir einen Kreis geschlossen…
Oder ist das eine Vereinfachung?!
Hallo, toller Artikel! Ich konnte das Safari-Polyfill nicht zum Laufen bringen… wo genau setzt du
customBuiltInElementsSupportedauftrue? ;)Hey Rasso, Entschuldigung, falls es nicht klar war. Ich setze es direkt nach dem
super();-Aufruf in der LightBox-Klasse auf true, also:class LightBox extends HTMLImageElement {constructor() {
super();
customBuiltInElementsSupported = true;
Aber es sollte fast überall im Konstruktor funktionieren. Der letzte Stift im Artikel enthält den vollständigen Kontext, falls du weitere Referenzen benötigst.
Hey, danke für den tollen Artikel!
Ich habe bisher darauf verzichtet, diesen Ansatz zu verwenden, weil das Safari-Team erklärt hat, dass sie ihn nicht mögen und nicht implementieren werden. Scheint mir dann ein totes Pferd zu sein, auch wenn andere Browserhersteller ihn unterstützen.
Ich habe gemischte Gefühle, insbesondere weil ich keine Polyfills auf Dauer hinzufügen möchte, eher eine vorübergehende Lösung. Was denkst du zu dem Thema?
Hallo Andreas, du hast einen guten Punkt. Safaris mangelnde Bereitschaft, benutzerdefinierte integrierte Elemente zu unterstützen, ist ein kniffliger Punkt für viele Entwickler und sollte es auch sein.
Ich bin ebenfalls der Meinung, dass Polyfills im Allgemeinen eine vorübergehende Lösung sein sollten.
Dennoch sehe ich jede Technologie als Werkzeug in meiner Werkzeugkiste. Für viele Anwendungen überwiegt Safaris mangelnde Bereitschaft die Vorteile für SEO und Screenreader, aber ich könnte mir Anwendungen vorstellen, bei denen diese Vorteile das Hinzufügen eines Polyfills für Safari überwiegen. Oder bei denen ein Polyfill für Safari in einem progressiven Enhancement-Kontext ignoriert werden könnte. Zum Beispiel fügt das Lightbox-Beispiel, das ich verwendet habe, keine neuen Informationen hinzu oder verändert das Verständnis der Website. Ich könnte mir vorstellen, dass es auf Safari einfach ignoriert wird, da das img-Element selbst in Safari funktioniert.
Relevanter Kommentar und Diskussion.