Benutzerdefinierte Elemente v1 – Wiederverwendbare Webkomponenten

Mit benutzerdefinierten Elementen können Webentwickler neue HTML-Tags definieren, vorhandene Tags erweitern und wiederverwendbare Webkomponenten erstellen.

Mit Custom Elements können Webentwickler neue HTML-Tags erstellen, vorhandene HTML-Tags optimieren oder die von anderen Entwicklern erstellten Komponenten erweitern. Die API ist die Grundlage von Webkomponenten. Sie bietet eine webstandardsbasierte Möglichkeit zur Erstellung wiederverwendbarer Komponenten mit nur einfachem JS/HTML/CSS. Das Ergebnis: weniger Code, modularer Code und eine größere Wiederverwendung in unseren Anwendungen.

Einleitung

Der Browser ist ein hervorragendes Tool zur Strukturierung von Webanwendungen. Es heißt HTML. Vielleicht haben Sie schon davon gehört! Sie ist deklarativ, handlich, gut unterstützt und leicht zu handhaben. Auch wenn es HTML mag, ist das Vokabular und die Erweiterbarkeit begrenzt. Beim lebenden HTML-Standard gab es bisher keine Möglichkeit, das JS-Verhalten automatisch Ihrem Markup zuzuordnen.

Benutzerdefinierte Elemente sind die Antwort auf die Modernisierung von HTML, das Auffüllen fehlender Teile und die Bündelung der Struktur mit Verhalten. Falls HTML keine Lösung für ein Problem bietet, können Sie ein entsprechendes benutzerdefiniertes Element erstellen. Benutzerdefinierte Elemente bringen dem Browser neue Tricks bei und behalten gleichzeitig die Vorteile von HTML bei.

Neues Element definieren

Um ein neues HTML-Element zu definieren, benötigen wir die Leistungsfähigkeit von JavaScript.

Das globale customElements wird verwendet, um ein benutzerdefiniertes Element zu definieren und dem Browser ein neues Tag mitzuteilen. Rufen Sie customElements.define() mit dem Tag-Namen auf, den Sie erstellen möchten, und einem JavaScript-class, der die Basis-HTMLElement erweitert.

Beispiel – Definition eines Bereichs für die mobile Leiste (<app-drawer>):

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

Beispiel:

<app-drawer></app-drawer>

Ein benutzerdefiniertes Element unterscheidet sich nicht von der Verwendung eines <div>-Elements oder eines anderen Elements. Instanzen können auf der Seite deklariert, dynamisch in JavaScript erstellt oder Ereignis-Listener angehängt werden. Hier finden Sie weitere Beispiele.

JavaScript API eines Elements definieren

Die Funktionalität eines benutzerdefinierten Elements wird mit einem ES2015-class definiert, der HTMLElement erweitert. Durch die Erweiterung von HTMLElement wird dafür gesorgt, dass das benutzerdefinierte Element die gesamte DOM API übernimmt. Dadurch werden alle Attribute/Methoden, die Sie der Klasse hinzufügen, Teil der DOM-Schnittstelle des Elements. Verwenden Sie die Klasse im Wesentlichen, um eine öffentliche JavaScript API für Ihr Tag zu erstellen.

Beispiel – Definition der DOM-Schnittstelle von <app-drawer>:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

In diesem Beispiel erstellen wir eine Leiste mit den Eigenschaften open, disabled und toggleDrawer(). Außerdem werden Attribute als HTML-Attribute angegeben.

Ein besonderes Merkmal von benutzerdefinierten Elementen ist, dass sich this innerhalb einer Klassendefinition auf das DOM-Element selbst bezieht, also auf die Instanz der Klasse. In unserem Beispiel bezieht sich this auf <app-drawer>. So kann das Element einen click-Listener mit sich selbst verknüpfen. Das ist nicht auf Event-Listener beschränkt. Die gesamte DOM API ist im Elementcode verfügbar. Verwenden Sie this, um auf die Eigenschaften des Elements zuzugreifen, seine untergeordneten Elemente (this.children), Abfrageknoten (this.querySelectorAll('.items')) usw. zu überprüfen.

Regeln zum Erstellen benutzerdefinierter Elemente

  1. Der Name eines benutzerdefinierten Elements muss einen Bindestrich (-) enthalten. Daher sind <x-tags>, <my-element> und <my-awesome-app> gültige Namen, <tabs> und <foo_bar> nicht. Durch diese Anforderung kann der HTML-Parser benutzerdefinierte Elemente von regulären Elementen unterscheiden. Außerdem wird die Aufwärtskompatibilität sichergestellt, wenn neue Tags zu HTML hinzugefügt werden.
  2. Sie können ein Tag nur einmal registrieren. Bei einem entsprechenden Versuch wird DOMException ausgelöst. Sobald Sie dem Browser ein neues Tag mitgeteilt haben, brauchen Sie nichts weiter zu tun. Keine Rücknahme.
  3. Benutzerdefinierte Elemente dürfen nicht selbstschließend sein, da in HTML nur wenige Elemente selbstschließend sein können. Schreiben Sie immer ein schließendes Tag (<app-drawer></app-drawer>).

Reaktionen auf benutzerdefiniertes Element

Ein benutzerdefiniertes Element kann spezielle Lebenszyklus-Hooks zum Ausführen von Code zu interessanten Zeiten definieren, in denen es vorhanden ist. Diese werden als Reaktionen benutzerdefinierter Elemente bezeichnet.

Name Angerufen, wenn
constructor Eine Instanz des Elements wird erstellt oder aktualisiert. Nützlich zum Initialisieren des Status, zum Einrichten von Event-Listenern oder zum Erstellen eines Schatten-Dom. In den Spezifikationen finden Sie Einschränkungen für die Aktionen, die Sie in der constructor ausführen können.
connectedCallback Wird jedes Mal aufgerufen, wenn das Element in das DOM eingefügt wird. Nützlich zum Ausführen von Einrichtungscode, z. B. zum Abrufen von Ressourcen oder Rendern. Im Allgemeinen sollten Sie versuchen, die Arbeit bis zu diesem Zeitpunkt zu verzögern.
disconnectedCallback Wird jedes Mal aufgerufen, wenn das Element aus dem DOM entfernt wird. Nützlich zum Ausführen von Bereinigungscode.
attributeChangedCallback(attrName, oldVal, newVal) Wird aufgerufen, wenn ein beobachtetes Attribut hinzugefügt, entfernt, aktualisiert oder ersetzt wurde. Wird auch für Anfangswerte aufgerufen, wenn ein Element vom Parser erstellt wird. Es wird als Upgrade bezeichnet. Hinweis:Nur Attribute, die im Attribut observedAttributes aufgeführt sind, erhalten diesen Callback.
adoptedCallback Das benutzerdefinierte Element wurde in ein neues document verschoben (z.B. document.adoptNode(el)).

Reaktions-Callbacks sind synchron. Wenn ein Nutzer el.setAttribute() für Ihr Element aufruft, ruft der Browser sofort attributeChangedCallback() auf. Ebenso erhalten Sie ein disconnectedCallback(), sobald das Element aus dem DOM entfernt wurde (z.B. wenn der Nutzer el.remove() aufruft).

Beispiel:Reaktionen mit benutzerdefinierten Elementen zu <app-drawer> hinzufügen:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

Definieren Sie Reaktionen, wenn sie Sinn ergeben. Wenn das Element ausreichend komplex ist und in connectedCallback() eine Verbindung zu IndexedDB hergestellt wird, führen Sie die erforderlichen Bereinigungsschritte in disconnectedCallback() aus. Aber Vorsicht! Sie können sich nicht immer darauf verlassen, dass Ihr Element aus dem DOM entfernt wird. Beispielsweise wird disconnectedCallback() nie aufgerufen, wenn der Nutzer den Tab schließt.

Eigenschaften und Attribute

Eigenschaften von Attributen wiedergeben

Häufig geben HTML-Eigenschaften ihren Wert als HTML-Attribut im DOM zurück. Wenn beispielsweise die Werte von hidden oder id in JS geändert werden:

div.id = 'my-id';
div.hidden = true;

werden die Werte als Attribute auf das Live-DOM angewendet:

<div id="my-id" hidden>

Dies wird als „Attribute für Attribute wiedergeben“ bezeichnet. Dies ist bei fast jeder Eigenschaft in HTML möglich. Warum? Attribute sind auch nützlich, um ein Element deklarativ zu konfigurieren. Bestimmte APIs wie Barrierefreiheit und CSS-Selektoren benötigen Attribute, um zu funktionieren.

Die Darstellung einer Eigenschaft ist überall dort nützlich, wo Sie die DOM-Darstellung des Elements mit dem JavaScript-Status synchron halten möchten. Eine Eigenschaft kann unter anderem dadurch wiedergegeben werden, dass benutzerdefinierte Stile angewendet werden, wenn sich der JS-Status ändert.

Dann sehen Sie sich unsere <app-drawer> an. Ein Nutzer dieser Komponente möchte sie möglicherweise ausblenden und/oder Nutzerinteraktionen verhindern, wenn sie deaktiviert ist:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

Wenn die Eigenschaft disabled in JS geändert wird, soll das Attribut in das DOM eingefügt werden, damit die Auswahl des Nutzers übereinstimmt. Das Element kann dieses Verhalten erreichen, indem es den Wert für ein Attribut mit dem gleichen Namen widerspiegelt:

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

Änderungen an Attributen beobachten

HTML-Attribute bieten Nutzern eine bequeme Möglichkeit, den Anfangszustand anzugeben:

<app-drawer open disabled></app-drawer>

Elemente können auf Attributänderungen reagieren, indem sie ein attributeChangedCallback definieren. Der Browser ruft diese Methode bei jeder Änderung an Attributen auf, die im Array observedAttributes aufgeführt sind.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

Im Beispiel legen wir zusätzliche Attribute für <app-drawer> fest, wenn ein disabled-Attribut geändert wird. Obwohl wir dies hier nicht tun, können Sie auch attributeChangedCallback verwenden, um eine JS-Eigenschaft mit ihrem Attribut synchron zu halten.

Upgrades von Elementen

Schrittweises erweitertes HTML

Wie Sie wissen, werden benutzerdefinierte Elemente durch Aufrufen von customElements.define() definiert. Das bedeutet jedoch nicht, dass Sie ein benutzerdefiniertes Element auf einmal definieren und registrieren müssen.

Benutzerdefinierte Elemente können verwendet werden, bevor ihre Definition registriert wird.

Progressive Enhancement ist eine Funktion benutzerdefinierter Elemente. Sie können also eine Reihe von <app-drawer>-Elementen auf der Seite deklarieren und customElements.define('app-drawer', ...) erst viel später aufrufen. Das liegt daran, dass der Browser potenzielle benutzerdefinierte Elemente aufgrund von unbekannten Tags unterschiedlich behandelt. Das Aufrufen von define() und das Zuweisen einer Klassendefinition zu einem vorhandenen Element wird als „Elementupgrades“ bezeichnet.

Mit window.customElements.whenDefined() können Sie feststellen, wann ein Tag-Name definiert wird. Sie gibt ein Versprechen zurück, das aufgelöst wird, wenn das Element definiert wird.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

Beispiel: Die Arbeit wird verzögert, bis eine Reihe von untergeordneten Elementen aktualisiert wurde.

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

Elementdefinierter Inhalt

Benutzerdefinierte Elemente können ihre eigenen Inhalte mithilfe der DOM APIs innerhalb des Elementcodes verwalten. Reaktionen sind dabei sehr praktisch.

Beispiel – Erstellen Sie ein Element mit Standard-HTML-Code:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

Die Deklaration dieses Tags hat folgende Konsequenzen:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// AUFGABE: DevSite - Codebeispiel entfernt, da Inline-Event-Handler verwendet wurden

Element erstellen, das Shadow-DOM verwendet

Mit Shadow DOM kann ein Element einen vom Rest der Seite getrennten DOM-Chunk besitzen, rendern und gestalten. Sie können sogar eine ganze App mit einem einzigen Tag verbergen:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

Wenn Sie Shadow DOM in einem benutzerdefinierten Element verwenden möchten, rufen Sie this.attachShadow in Ihrem constructor auf:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

Beispiel:

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

Benutzerdefinierter Text des Nutzers

// AUFGABE: DevSite - Codebeispiel entfernt, da Inline-Event-Handler verwendet wurden

Elemente aus einem <template> erstellen

Für unbekannte Nutzer können Sie mit dem <template>-Element Fragmente des DOM deklarieren, die geparst, beim Seitenaufbau inaktiv werden und später zur Laufzeit aktiviert werden können. Es ist eine weitere API-Primitive in der Webkomponentenfamilie. Vorlagen sind ein idealer Platzhalter für die Deklaration der Struktur eines benutzerdefinierten Elements.

Beispiel:Registrieren eines Elements mit Shadow DOM-Inhalten, die aus einem <template> erstellt wurden:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

Diese wenigen Codezeilen sind eine echte Bereicherung. Lassen Sie uns die wichtigsten Punkte verstehen:

  1. Wir definieren ein neues Element in HTML: <x-foo-from-template>
  2. Das Shadow DOM des Elements wird aus einem <template> erstellt
  3. Das DOM des Elements ist dank des Shadow-DOM lokal für das Element sichtbar
  4. Der interne CSS-Code des Elements ist dank des Shadow DOM auf das Element beschränkt.

Ich bin im Shadow DOM. Mein Markup wurde aus einer <template> gebildet.

// AUFGABE: DevSite - Codebeispiel entfernt, da Inline-Event-Handler verwendet wurden

Benutzerdefiniertes Element gestalten

Selbst wenn Ihr Element mithilfe von Shadow DOM einen eigenen Stil definiert, können Nutzer Ihr benutzerdefiniertes Element auf ihrer Seite gestalten. Diese werden als „benutzerdefinierte Stile“ bezeichnet.

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

Sie fragen sich vielleicht, wie die CSS-Spezifität funktioniert, wenn für das Element Stile innerhalb von Shadow-DOM definiert sind. Was die Spezifität angeht, hat der Stil der Nutzer Vorrang. Sie überschreiben immer die elementdefinierten Stile. Weitere Informationen finden Sie im Abschnitt Element erstellen, das Shadow-DOM verwendet.

Nicht registrierte Elemente vorab gestalten

Bevor ein Element aktualisiert wird, können Sie es in CSS mithilfe der Pseudoklasse :defined ausrichten. Dies ist nützlich, um eine Komponente vorab zu gestalten. Sie können beispielsweise Layout- oder andere visuelle Fehler verhindern, indem Sie nicht definierte Komponenten ausblenden und einblenden, wenn sie definiert werden.

Beispiel – Blenden Sie <app-drawer> aus, bevor es definiert wird:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

Nachdem <app-drawer> definiert wurde, stimmt der Selektor (app-drawer:not(:defined)) nicht mehr überein.

Elemente erweitern

Die Custom Elements API ist nützlich, um neue HTML-Elemente zu erstellen, aber sie ist auch nützlich, um andere benutzerdefinierte Elemente oder sogar den integrierten HTML-Code des Browsers zu erweitern.

Benutzerdefiniertes Element erweitern

Das Erweitern eines anderen benutzerdefinierten Elements erfolgt durch Erweitern der zugehörigen Klassendefinition.

Beispiel: Erstellen Sie <fancy-app-drawer>, das <app-drawer> erweitert:

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

Native HTML-Elemente erweitern

Angenommen, Sie möchten ein noch ansprechenderes <button>-Objekt erstellen. Statt Verhalten und Funktionalität von <button> zu replizieren, ist es besser, das vorhandene Element mithilfe von benutzerdefinierten Elementen nach und nach zu verbessern.

Ein benutzerdefiniertes integriertes Element ist ein benutzerdefiniertes Element, das eines der integrierten HTML-Tags des Browsers erweitert. Der Hauptvorteil der Erweiterung eines vorhandenen Elements besteht darin, alle zugehörigen Funktionen (DOM-Eigenschaften, Methoden, Zugänglichkeit) zu erhalten. Es gibt keine bessere Möglichkeit, eine Progressive Web-App zu schreiben, als vorhandene HTML-Elemente schrittweise zu verbessern.

Wenn Sie ein Element erweitern möchten, müssen Sie eine Klassendefinition erstellen, die von der korrekten DOM-Schnittstelle übernommen wird. Beispielsweise muss ein benutzerdefiniertes Element, das <button> erweitert, von HTMLButtonElement anstelle von HTMLElement übernommen werden. Ebenso muss ein Element, das <img> erweitert, HTMLImageElement erweitern.

Beispiel<button> wird erweitert:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

Der Aufruf von define() ändert sich geringfügig, wenn ein natives Element erweitert wird. Über den erforderlichen dritten Parameter wird dem Browser mitgeteilt, welches Tag Sie erweitern. Das ist notwendig, da viele HTML-Tags dieselbe DOM-Oberfläche verwenden. <section>, <address> und <em> (unter anderem) teilen HTMLElement. Sowohl <q> als auch <blockquote> teilen HTMLQuoteElement usw. Wenn Sie {extends: 'blockquote'} angeben, wird der Browser darüber informiert, dass Sie einen hochgestuften <blockquote> anstelle eines <q> erstellen. Die vollständige Liste der DOM-Schnittstellen von HTML finden Sie in der HTML-Spezifikation.

Nutzer, die ein benutzerdefiniertes integriertes Element verwenden, können es auf verschiedene Arten verwenden. Er kann sie deklarieren, indem er dem nativen Tag das Attribut is="" hinzufügt:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

eine Instanz in JavaScript erstellen:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

oder verwenden Sie den Operator new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

Hier ist ein weiteres Beispiel zur Erweiterung von <img>.

Beispiel<img> wird erweitert:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

Die Nutzer deklarieren diese Komponente so:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

oder erstellen Sie eine Instanz in JavaScript:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

Sonstige Details

Unbekannte Elemente im Vergleich zu nicht definierten benutzerdefinierten Elementen

HTML ist nachsichtig und flexibel. Wenn du beispielsweise <randomtagthatdoesntexist> auf einer Seite deklarierst, akzeptiert der Browser die Anfrage problemlos. Warum funktionieren Nicht-Standard-Tags? Die Antwort darauf ist die HTML-Spezifikation. Elemente, die nicht durch die Spezifikation definiert sind, werden als HTMLUnknownElement geparst.

Dasselbe gilt nicht für benutzerdefinierte Elemente. Potenzielle benutzerdefinierte Elemente werden als HTMLElement geparst, wenn sie mit einem gültigen Namen erstellt wurden (einschließlich „-“). Sie können dies in einem Browser prüfen, der benutzerdefinierte Elemente unterstützt. Starten Sie die Console: Strg + Umschalttaste + J (oder Cmd + Opt + J auf einem Mac) und fügen Sie die folgenden Codezeilen ein:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

API-Referenz

Der globale customElements definiert nützliche Methoden für die Arbeit mit benutzerdefinierten Elementen.

define(tagName, constructor, options)

Definiert ein neues benutzerdefiniertes Element im Browser.

Beispiel

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

Bei Angabe eines gültigen benutzerdefinierten Tags wird der Konstruktor des Elements zurückgegeben. Gibt undefined zurück, wenn keine Elementdefinition registriert wurde.

Beispiel

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

Gibt ein Versprechen zurück, das aufgelöst wird, wenn das benutzerdefinierte Element definiert ist. Wenn das Element bereits definiert ist, wird es sofort aufgelöst. Wird abgelehnt, wenn der Tag-Name kein gültiger Name für ein benutzerdefiniertes Element ist.

Beispiel

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

Verlaufs- und Browserunterstützung

Wenn du in den letzten Jahren Webkomponenten folgst, weißt du, dass in Chrome 36 und höher eine Version der Custom Elements API implementiert ist, die document.registerElement() statt customElements.define() verwendet. Dies gilt inzwischen als eingestellte Version des Standards mit dem Namen v0. customElements.define() ist der neue Trend und welche Browseranbieter derzeit zu implementieren sind. Es heißt „Custom Elements V1“.

Wenn Sie an der alten V0-Spezifikation interessiert sind, lesen Sie den HTML5rocks-Artikel.

Unterstützte Browser

Chrome 54 (Status), Safari 10.1 (Status) und Firefox 63 (Status) verfügen über Custom Elements (Version 1). Edge hat mit der Entwicklung begonnen.

Prüfen Sie, ob window.customElements vorhanden ist, damit benutzerdefinierte Elemente erkannt werden:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

Bis die Browserunterstützung allgemein verfügbar ist, ist für Custom Elements v1 ein eigenständiges Polyfill verfügbar. Wir empfehlen jedoch, das Ladeprogramm „webcomponents.js“ zu verwenden, damit die Polyfills für die Webkomponenten optimal geladen werden. Das Ladeprogramm nutzt die Funktionserkennung, um asynchron nur die erforderlichen Pollyfills zu laden, die vom Browser benötigt werden.

Installieren:

npm install --save @webcomponents/webcomponentsjs

Verwendung:

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

Fazit

Mit benutzerdefinierten Elementen können wir neue HTML-Tags im Browser definieren und wiederverwendbare Komponenten erstellen. Wenn Sie sie mit den anderen neuen Plattform-Primitiven wie Shadow DOM und <template> kombinieren, werden wir langsam das große Ganze von Webkomponenten erkennen:

  • Browserübergreifend (Webstandard) zum Erstellen und Erweitern wiederverwendbarer Komponenten
  • Für den Einstieg ist weder eine Bibliothek noch ein Framework erforderlich. Vanilla JS/HTML FTW!
  • Bietet ein vertrautes Programmiermodell. Es ist nur DOM/CSS/HTML.
  • Funktioniert gut mit anderen neuen Webplattformfunktionen (Shadow DOM, <template>, benutzerdefinierte CSS-Eigenschaften usw.)
  • Eng in die Entwicklertools des Browsers integriert
  • Vorhandene Bedienungshilfen nutzen