Supercharging Built-In Elements Mit Web Components ist einfacher als du denkst

Avatar of John Rhea
John Rhea am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

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

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!