Web Components Sind 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!

Als ich auf einer Konferenz war (als wir so etwas noch tun konnten) und jemand eine Präsentation über Web Components hielt, fand ich es immer ziemlich raffiniert (ja, anscheinend komme ich aus den 1950ern), aber es schien immer kompliziert und übertrieben. Tausend Zeilen JavaScript, um vier Zeilen HTML zu sparen. Der Sprecher überging entweder unweigerlich die Unmengen an JavaScript, um es zum Laufen zu bringen, oder er ging ins quälende Detail und meine Augen wurden glasig, als ich darüber nachdachte, ob mein Taggeld Snacks abdeckte.

Aber in einem kürzlichen Referenzprojekt zur Erleichterung des HTML-Lernens (durch Hinzufügen von Zombies und albernen Witzen, natürlich) beschloss ich als Vervollständiger, dass ich jedes HTML-Element in der Spezifikation behandeln musste. Über diese Konferenzpräsentationen hinaus war dies meine erste Einführung in die Elemente <slot> und <template>. Aber als ich versuchte, etwas Genaues und Ansprechendes zu schreiben, war ich gezwungen, etwas tiefer einzutauchen.

Und ich habe dabei etwas gelernt: Web Components sind viel einfacher, als ich mich erinnere.

Artikelreihe

Entweder haben sich Web Components seit dem letzten Mal, als ich mir bei einer Konferenz Snacks erträumte, stark weiterentwickelt, oder ich habe meine anfängliche Angst vor ihnen davon abhalten lassen, sie wirklich zu verstehen – wahrscheinlich beides.

Ich bin hier, um dir zu sagen, dass du – ja, *du* – eine Web Component erstellen kannst. Lass uns für einen Moment unsere Ablenkungen, Ängste und sogar unsere Snacks vor der Tür lassen und es gemeinsam tun.

Beginnen wir mit dem <template>

Ein <template> ist ein HTML-Element, das es uns erlaubt, eine Vorlage zu erstellen – die HTML-Struktur für die Web Component. Eine Vorlage muss kein riesiger Codebrocken sein. Sie kann so einfach sein wie

<template>
  <p>The Zombies are coming!</p>
</template>

Das <template>-Element ist wichtig, weil es Dinge zusammenhält. Es ist wie das Fundament eines Gebäudes; es ist die Basis, auf der alles andere aufgebaut wird. Nehmen wir diesen kleinen HTML-Schnipsel als Vorlage für eine <apokalyptische-warnung> Web Component – wissen Sie, als Warnung, wenn die Zombie-Apokalypse vor der Tür steht.

Dann gibt es noch den <slot>

<slot> ist lediglich ein weiteres HTML-Element, genau wie <template>. Aber in diesem Fall passt <slot> an, was die <template> auf der Seite rendert.

<template>
  <p>The <slot>Zombies</slot> are coming!</p>
</template>

Hier haben wir das Wort „Zombies“ in das geteilte Markup eingefügt. Wenn wir nichts mit dem Slot machen, wird standardmäßig der Inhalt zwischen den Tags verwendet. Das wäre in diesem Beispiel „Zombies“.

Die Verwendung von <slot> ist ähnlich wie ein Platzhalter. Wir können den Platzhalter wie er ist verwenden oder etwas anderes definieren, das stattdessen dort platziert wird. Das machen wir mit dem Attribut name.

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

Das Attribut name sagt der Web Component, welcher Inhalt wohin in der Vorlage gehört. Momentan haben wir einen Slot namens whats-coming. Wir gehen davon aus, dass Zombies zuerst in der Apokalypse kommen, aber der <slot> gibt uns die Flexibilität, etwas anderes einzufügen, zum Beispiel wenn es sich um einen Roboter, einen Werwolf oder sogar um eine Web Component-Apokalypse handelt.

Die Komponente verwenden

Technisch gesehen sind wir mit dem „Schreiben“ der Komponente fertig und können sie überall einfügen, wo wir sie verwenden möchten.

<apocalyptic-warning>
  <span slot="whats-coming">Halitosis Laden Undead Minions</span>
</apocalyptic-warning>

<template>
  <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

Siehst du, was wir da gemacht haben? Wir haben die Komponente <apokalyptische-warnung> auf der Seite platziert, genau wie jedes andere <div> oder so. Aber wir haben auch ein <span> hineingeworfen, das auf das Attribut name unseres <slot> verweist. Und was sich zwischen diesem <span> befindet, ist das, was wir anstelle von „Zombies“ einsetzen wollen, wenn die Komponente gerendert wird.

Hier ist ein kleiner Hinweis, der es wert ist, hervorgehoben zu werden: Namen für benutzerdefinierte Elemente müssen einen Bindestrich enthalten. Das ist einfach eines dieser Dinge, die man wissen muss, bevor man anfängt. Die Spezifikation schreibt dies vor, um Konflikte zu vermeiden, falls HTML ein neues Element mit demselben Namen veröffentlicht.

Immer noch dabei? Nicht zu gruselig, oder? Nun, abgesehen von den Zombies. Wir müssen noch ein wenig arbeiten, um den Tausch des <slot> zu ermöglichen, und dort kommen wir zu JavaScript.

Registrierung der Komponente

Wie ich bereits sagte, benötigt man etwas JavaScript, um das alles zum Laufen zu bringen, aber es ist nicht der superkomplexe, tausendzeilige, tiefgehende Code, den ich immer dachte. Hoffentlich kann ich dich auch überzeugen.

Man benötigt eine Konstruktorfunktion, die das benutzerdefinierte Element registriert. Andernfalls ist unsere Komponente wie die Untoten: Sie ist da, aber nicht ganz lebendig.

Hier ist der Konstruktor, den wir verwenden werden

// Defines the custom element with our appropriate name, <apocalyptic-warning>
customElements.define("apocalyptic-warning",

  // Ensures that we have all the default properties and methods of a built in HTML element
  class extends HTMLElement {

    // Called anytime a new custom element is created
    constructor() {

      // Calls the parent constructor, i.e. the constructor for `HTMLElement`, so that everything is set up exactly as we would for creating a built in HTML element
      super();

      // Grabs the <template> and stores it in `warning`
      let warning = document.getElementById("warningtemplate");

      // Stores the contents of the template in `mywarning`
      let mywarning = warning.content;

      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
    }
  });

Ich habe detaillierte Kommentare eingefügt, die die Dinge Zeile für Zeile erklären. Außer der letzten Zeile

const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));

Wir machen hier viel. Zuerst nehmen wir unser benutzerdefiniertes Element (this) und erstellen einen heimlichen Agenten – ich meine, einen Shadow DOM. mode: open bedeutet einfach, dass JavaScript von außerhalb des :root auf die Elemente innerhalb des Shadow DOM zugreifen und diese manipulieren kann, sozusagen wie ein Hintertüreneingang zur Komponente.

Von dort aus wurde der Shadow DOM erstellt und wir fügen einen Knoten hinzu. Dieser Knoten wird eine tiefe Kopie der Vorlage sein, einschließlich aller Elemente und Texte der Vorlage. Mit der an den Shadow DOM des benutzerdefinierten Elements angehängten Vorlage übernehmen der <slot> und das Attribut slot die Aufgabe, Inhalte zuzuordnen, wo sie hingehören.

Schau dir das an. Jetzt können wir zwei Instanzen derselben Komponente einfügen, die unterschiedliche Inhalte rendern, indem wir einfach ein Element ändern.

Styling der Komponente

Du hast vielleicht Styling in dieser Demo bemerkt. Wie du vielleicht erwartet hast, haben wir absolut die Möglichkeit, unsere Komponente mit CSS zu stylen. Tatsächlich können wir ein <style>-Element direkt in der <template> einschließen.

<template id="warningtemplate">
  <style>
    p {
      background-color: pink;
      padding: 0.5em;
      border: 1px solid red;
    }
  </style>

    <p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>

Auf diese Weise sind die Stile direkt auf die Komponente beschränkt und dank des Shadow DOM dringt nichts nach außen zu anderen Elementen auf derselben Seite.

In meinem Kopf habe ich angenommen, dass ein benutzerdefiniertes Element eine Kopie der Vorlage nimmt, den von dir hinzugefügten Inhalt einfügt und ihn dann mithilfe des Shadow DOM auf die Seite injiziert. Während es auf dem Frontend so aussieht, ist das nicht, wie es im DOM tatsächlich funktioniert. Der Inhalt einer benutzerdefinierten Komponente bleibt, wo er ist, und der Shadow DOM wird sozusagen wie eine Überlagerung darüber gelegt.

Screenshot of the HTML source of the zombie-warning component. The custom element is expanded in the shadow dam, including the style block, the custom element, and the template.

Und da der Inhalt technisch gesehen *außerhalb* der Vorlage liegt, haben alle nachfolgenden Selektoren oder Klassen, die wir im <style>-Element der Vorlage verwenden, keine Auswirkungen auf den eingefügten Inhalt. Dies ermöglicht keine vollständige Kapselung, wie ich es gehofft oder erwartet hatte. Aber da eine benutzerdefinierte Komponente *ein* Element ist, können wir sie als Elementselektor in jeder beliebigen CSS-Datei verwenden, einschließlich des Haupt-Stylesheets, das auf einer Seite verwendet wird. Und obwohl das eingefügte Material technisch gesehen nicht in der Vorlage ist, ist es *in* der benutzerdefinierten Komponente, und nachfolgende Selektoren aus dem CSS funktionieren.

apocalyptic-warning span {
  color: blue;
}

Aber Vorsicht! Stile in der Haupt-CSS-Datei können nicht auf Elemente in der <template> oder im Shadow DOM zugreifen.

Fassen wir das alles zusammen

Betrachten wir ein Beispiel, sagen wir ein Profil für einen Zombie-Dating-Service, wie man ihn nach der Apokalypse vielleicht brauchen könnte. Um sowohl den Standardinhalt als auch jeden eingefügten Inhalt zu stylen, benötigen wir *sowohl* ein <style>-Element in der <template> *als auch* Styling in einer CSS-Datei.

Der JavaScript-Code ist exakt derselbe, außer dass wir jetzt mit einem anderen Komponentennamen arbeiten, <zombie-profile>.

customElements.define("zombie-profile",
  class extends HTMLElement {
    constructor() {
      super();
      let profile = document.getElementById("zprofiletemplate");
      let myprofile = profile.content;
      const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));
    }
  }
);

Hier ist die HTML-Vorlage, einschließlich des gekapselten CSS

<template id="zprofiletemplate">
  <style>
    img {
      width: 100%;
      max-width: 300px;
      height: auto;
      margin: 0 1em 0 0;
    }
    h2 {
      font-size: 3em;
      margin: 0 0 0.25em 0;
      line-height: 0.8;
    }
    h3 {
      margin: 0.5em 0 0 0;
      font-weight: normal;
    }
    .age, .infection-date {
      display: block;
    }
    span {
      line-height: 1.4;
    }
    .label {
      color: #555;
    }
    li, ul {
      display: inline;
      padding: 0;
    }
    li::after {
      content: ', ';
    }
    li:last-child::after {
      content: '';
    }
    li:last-child::before {
      content: ' and ';
    }
  </style>

  <div class="profilepic">
    <slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>
  </div>

  <div class="info">
    <h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>

    <span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>
    <span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>

    <div class="interests">
      <span class="label">Interests: </span>
      <slot name="z-interests">
        <ul>
          <li>Long Walks on Beach</li>
          <li>brains</li>
          <li>defeating humanity</li>
        </ul>
      </slot>
    </div>

    <span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>

  </div>
</template>

Hier ist das CSS für unser <zombie-profile>-Element und seine Nachkommen aus unserer Haupt-CSS-Datei. Beachten Sie die Duplizierung, um sicherzustellen, dass sowohl die ersetzten Elemente als auch die Elemente aus der Vorlage gleich gestylt werden.

zombie-profile {
  width: calc(50% - 1em);
  border: 1px solid red;
  padding: 1em;
  margin-bottom: 2em;
  display: grid;
  grid-template-columns: 2fr 4fr;
  column-gap: 20px;
}
zombie-profile img {
  width: 100%;
  max-width: 300px;
  height: auto;
  margin: 0 1em 0 0;
}
zombie-profile li, zombie-profile ul {
  display: inline;
  padding: 0;
}
zombie-profile li::after {
  content: ', ';
}
zombie-profile li:last-child::after {
  content: '';
}
zombie-profile li:last-child::before {
  content: ' and ';
}

Alles zusammen jetzt!

Obwohl es immer noch ein paar Haken und andere Nuancen gibt, hoffe ich, dass du dich jetzt ermächtiger fühlst, mit Web Components zu arbeiten, als du es vor ein paar Minuten warst. Tauche deine Zehen ein, wie wir es hier getan haben. Streue vielleicht eine benutzerdefinierte Komponente in deine Arbeit ein und her, um ein Gefühl dafür zu bekommen und wo sie Sinn macht.

Das ist wirklich alles. Nun, wovor hast du mehr Angst, vor Web Components oder vor der Zombie-Apokalypse? Ich hätte das vielleicht in der nicht allzu fernen Vergangenheit über Web Components gesagt, aber jetzt bin ich stolz zu sagen, dass Zombies das Einzige sind, was mich beunruhigt (nun, das und ob mein Taggeld Snacks abdeckt…)