Best Practices für benutzerdefinierte Elemente

Mit benutzerdefinierten Elementen können Sie Ihre eigenen HTML-Tags erstellen. Diese Checkliste enthält Best Practices zum Erstellen hochwertiger Elemente.

Mit benutzerdefinierten Elementen können Sie HTML-Code erweitern und eigene Tags definieren. Sie sind eine unglaublich leistungsstarke Funktion, aber sie befinden sich auch auf einer niedrigen Ebene, was bedeutet, dass es nicht immer klar ist, wie Sie Ihr eigenes Element am besten implementieren.

Wir haben diese Checkliste zusammengestellt, um Ihnen dabei zu helfen, die bestmögliche Nutzererfahrung zu bieten. Es schlüsselt alle Aspekte auf, die unserer Meinung nach ein gut funktionierendes benutzerdefiniertes Element sein müssen.

Checkliste

Schatten-DOM

Erstellen Sie einen Schattenstamm, um Stile zu kapseln.

Why? Wenn Sie Stile im Schattenstamm des Elements einschließen, funktioniert es unabhängig davon, wo es verwendet wird. Das ist besonders wichtig, wenn ein Entwickler Ihr Element innerhalb des Schattenstamms eines anderen Elements platzieren möchte. Das gilt auch für einfache Elemente wie Kästchen oder Optionsfelder. Es kann sein, dass der Schattenstamm nur die Stile selbst enthält.
Beispiel Das Element <howto-checkbox>.

Erstellen Sie den Schattenstamm im Konstruktor.

Why? Der Konstruktor erfolgt, wenn Sie das Element exklusiv kennen. Dies ist ein guter Zeitpunkt, um Implementierungsdetails einzurichten, die keine anderen Elemente verursachen sollen. Wenn Sie dies in einem späteren Callback wie connectedCallback ausführen, müssen Sie auf Situationen achten, in denen das Element getrennt und dann wieder an das Dokument angehängt wird.
Beispiel Das Element <howto-checkbox>.

Platzieren Sie alle untergeordneten Elemente, die das Element erstellt, in seinen Schattenstamm.

Why? Von Ihrem Element erstellte untergeordnete Elemente sind Teil der Implementierung und sollten privat sein. Ohne den Schutz einer Schattenwurzel kann externes JavaScript diese untergeordneten Elemente ungewollt stören.
Beispiel Das Element <howto-tabs>.

Verwenden Sie <slot>, um untergeordnete Light-DOM-Elemente in Ihr Shadow DOM zu projizieren.

Why? Ermöglicht es Nutzern Ihrer Komponente, Inhalte in Ihrer Komponente anzugeben, da untergeordnete HTML-Elemente Ihre Komponente besser zusammensetzbar machen. Wenn ein Browser keine benutzerdefinierten Elemente unterstützt, bleiben verschachtelte Inhalte verfügbar, sichtbar und zugänglich.
Beispiel Das Element <howto-tabs>.

Lege einen :host-Anzeigestil fest (z.B. block, inline-block, flex), es sei denn, du bevorzugst die Standardeinstellung inline.

Why? Benutzerdefinierte Elemente sind standardmäßig auf display: inline festgelegt. Daher hat das Festlegen von width oder height keine Auswirkungen. Dies überrascht oft von den Entwicklern und kann zu Problemen beim Layout der Seite führen. Wenn du keine inline-Anzeige bevorzugst, solltest du immer einen Standardwert für display festlegen.
Beispiel Das Element <howto-checkbox>.

Fügen Sie einen :host-Anzeigestil hinzu, der das ausgeblendete Attribut berücksichtigt.

Why? Ein benutzerdefiniertes Element mit einem Standardstil für display, z.B. :host { display: block }, überschreibt das integrierte Attribut hidden mit niedrigerer Spezifität. Dies wird Sie möglicherweise überraschen, wenn Sie erwarten, dass das Attribut hidden für Ihr Element festgelegt wird, um es display: none zu rendern. Zusätzlich zum Standardstil display können Sie mit :host([hidden]) { display: none } Unterstützung für hidden hinzufügen.
Beispiel Das Element <howto-checkbox>.

Attribute und Eigenschaften

Vom Autor festgelegte, globale Attribute dürfen nicht überschrieben werden.

Why? Globale Attribute sind Attribute, die in allen HTML-Elementen vorhanden sind. Dazu gehören beispielsweise tabindex und role. Ein benutzerdefiniertes Element kann die anfängliche tabindex auf 0 setzen, damit es über die Tastatur fokussiert werden kann. Sie sollten jedoch immer zuerst prüfen, ob der Entwickler, der Ihr Element verwendet, diesen Wert auf einen anderen Wert gesetzt hat. Wird beispielsweise tabindex auf -1 gesetzt, ist das ein Signal dafür, dass das Element nicht interaktiv sein soll.
Beispiel Das Element <howto-checkbox>. Weitere Informationen dazu finden Sie unter Seitenautor nicht überschreiben.

Akzeptieren Sie immer primitive Daten (Strings, Zahlen, boolesche Werte) als Attribute oder Attribute.

Why? Benutzerdefinierte Elemente sollten wie ihre integrierten Elemente konfigurierbar sein. Die Konfiguration kann deklarativ, über Attribute oder zwingend über JavaScript-Properties übergeben werden. Idealerweise sollte jedes Attribut auch mit einer entsprechenden Property verknüpft sein.
Beispiel Das Element <howto-checkbox>.

Versuchen Sie, primitive Datenattribute und -eigenschaften synchron zu halten, indem Sie sie von Property zu Attribut widerspiegeln und umgekehrt.

Why? Sie wissen nie, wie Nutzende mit Ihrem Element interagieren werden. Er kann eine Property in JavaScript festlegen und erwartet dann, diesen Wert mit einer API wie getAttribute() zu lesen. Wenn jedes Attribut eine entsprechende Eigenschaft hat und beide Attribute widerspiegeln, können Nutzer einfacher mit Ihrem Element arbeiten. Mit anderen Worten: Wenn Sie setAttribute('foo', value) aufrufen, sollte auch ein entsprechendes foo-Attribut festgelegt werden und umgekehrt. Es gibt natürlich auch Ausnahmen von dieser Regel. Sie sollten keine Eigenschaften mit hoher Häufigkeit wie currentTime in einem Videoplayer wiedergeben. Nutze dein eigenes Urteilsvermögen. Wenn es den Anschein hat, dass ein Nutzer mit einer Property oder einem Attribut interagiert, und es nicht umständlich ist, dies widerzuspiegeln, sollten Sie so vorgehen.
Beispiel Das Element <howto-checkbox>. Weitere Informationen dazu finden Sie unter Probleme mit dem erneuten Anmelden vermeiden.

Versuchen Sie, nur Rich-Daten (Objekte, Arrays) als Properties zu akzeptieren.

Why? Im Allgemeinen gibt es keine Beispiele für integrierte HTML-Elemente, die Rich-Daten (einfache JavaScript-Objekte und -Arrays) über ihre Attribute akzeptieren. Rich-Daten werden stattdessen entweder über Methodenaufrufe oder Properties akzeptiert. Die Annahme von Rich Data als Attribute hat einige offensichtliche Nachteile: Die Serialisierung eines großen Objekts zu einem String kann teuer sein und alle Objektverweise gehen bei diesem Stringifizierungsprozess verloren. Wenn Sie beispielsweise ein Objekt aneinanderreihen, das einen Verweis auf ein anderes Objekt oder möglicherweise einen DOM-Knoten hat, gehen diese Verweise verloren.

Gib Rich-Daten-Eigenschaften nicht für Attribute an.

Why? Die Darstellung komplexer Dateneigenschaften in Attribute ist unnötig teuer, da dieselben JavaScript-Objekte Serialisiert und deserialisiert werden müssen. Sofern Sie keinen Anwendungsfall haben, der nur mit diesem Feature gelöst werden kann, sollte er vermieden werden.

Prüfen Sie nach Attributen, die möglicherweise vor dem Upgrade des Elements festgelegt wurden.

Why? Ein Entwickler, der Ihr Element verwendet, versucht möglicherweise, eine Eigenschaft für das Element festzulegen, bevor seine Definition geladen wurde. Dies gilt insbesondere, wenn der Entwickler ein Framework verwendet, das das Laden von Komponenten übernimmt, sie auf die Seite stempelt und ihre Attribute an ein Modell bindet.
Beispiel Das Element <howto-checkbox>. Eine ausführlichere Beschreibung finden Sie unter Attribute verlangsamen.

Wenden Sie sich nicht selbst an Kursen an.

Why? Für Elemente, die ihren Zustand ausdrücken müssen, sollte dies mithilfe von Attributen geschehen. Es wird im Allgemeinen davon ausgegangen, dass das Attribut class dem Entwickler gehört, der Ihr Element verwendet. Wenn Sie selbst in dieses Attribut schreiben, kann dies unbeabsichtigt dazu führen, dass sich der Entwickler in Entwicklerklassen verirrt.

Veranstaltungen

Lösen Sie Ereignisse als Reaktion auf Aktivitäten interner Komponenten aus.

Why? Ihre Komponente kann Eigenschaften haben, die sich als Reaktion auf Aktivitäten ändern, die nur Ihrer Komponente bekannt sind, z. B. wenn ein Timer oder eine Animation abgeschlossen ist oder das Laden einer Ressource abgeschlossen ist. Es ist hilfreich, als Reaktion auf diese Änderungen Ereignisse auszulösen, um den Host darüber zu informieren, dass der Status der Komponente anders ist.

Senden Sie keine Ereignisse als Reaktion darauf, dass der Host ein Attribut festlegt (Datenfluss nach unten).

Why? Das Auslösen eines Ereignisses als Reaktion darauf, dass ein Organisator eine Eigenschaft festlegt, ist überflüssig (der Host kennt den aktuellen Status, weil er ihn gerade festgelegt hat). Das Auslösen von Ereignissen als Reaktion darauf, dass ein Host ein Attribut festlegt, kann zu Endlosschleifen mit Datenbindungssystemen führen.
Beispiel Das Element <howto-checkbox>.

Erklärvideos

Seitenautor nicht überschreiben

Es ist möglich, dass ein Entwickler, der Ihr Element verwendet, einen Teil seines Anfangszustands überschreiben möchte. Beispiel: Ändern der ARIA-role oder der Fokussierbarkeit mit tabindex. Prüfen Sie, ob diese und andere globale Attribute festgelegt wurden, bevor Sie Ihre eigenen Werte anwenden.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Verzögern Sie die Eigenschaften

Ein Entwickler versucht möglicherweise, eine Eigenschaft für Ihr Element festzulegen, bevor die Definition geladen wurde. Dies gilt insbesondere, wenn der Entwickler ein Framework verwendet, das Komponenten lädt, sie in die Seite einfügt und ihre Attribute an ein Modell bindet.

Im folgenden Beispiel bindet Angular das Attribut isChecked seines Modells deklarativ an das Attribut checked des Kästchens. Wenn die Definition für das Kästchen für das Kästchen Lazy Loading verwendet wurde, versucht Angular möglicherweise, die aktivierte Eigenschaft festzulegen, bevor das Element aktualisiert wurde.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

Ein benutzerdefiniertes Element sollte dieses Szenario verarbeiten, indem geprüft wird, ob bereits Attribute für seine Instanz festgelegt wurden. <howto-checkbox> veranschaulicht dieses Muster mithilfe einer Methode namens _upgradeProperty().

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty() erfasst den Wert aus der nicht aktualisierten Instanz und löscht die Eigenschaft, damit sie nicht den eigenen Property-Setter des benutzerdefinierten Elements überdeckt. Wenn die Definition des Elements schließlich geladen wird, kann auf diese Weise sofort der richtige Zustand wiedergegeben werden.

Probleme mit dem Wiedereintritt vermeiden

Es ist verlockend, den attributeChangedCallback() zu verwenden, um den Status einem zugrunde liegenden Attribut widerzuspiegeln. Beispiel:

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

Dies kann jedoch zu einer Endlosschleife führen, wenn der Property-Setter auch das Attribut widerspiegelt.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

Eine Alternative besteht darin, dem Attribut-Setter die Spiegelung des Attributs zu ermöglichen und den Getter seinen Wert anhand des Attributs bestimmen zu lassen.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

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

In diesem Beispiel wird durch Hinzufügen oder Entfernen des Attributs auch die Property festgelegt.

Schließlich kann attributeChangedCallback() verwendet werden, um Nebenwirkungen wie das Anwenden von ARIA-Zuständen zu verarbeiten.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}