Custom Elements in Svelte verwenden

Avatar of Geoff Rich
Geoff Rich am

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

Svelte unterstützt Custom Elements (z. B. <my-component>) vollständig ohne benutzerdefinierte Konfiguration oder Wrapper-Komponenten und erzielt auf Custom Elements Everywhere eine perfekte Bewertung. Dennoch gibt es ein paar Eigenheiten, auf die Sie achten müssen, insbesondere im Hinblick darauf, wie Svelte Daten an Custom Elements übergibt. Bei Alaska Airlines sind wir auf viele dieser Probleme gestoßen, als wir die Custom Elements unseres Designsystems in eine Svelte-Anwendung integriert haben.

Während Svelte die Kompilierung zu Custom Elements unterstützt, liegt dies außerhalb des Rahmens dieses Beitrags. Stattdessen werde ich mich auf die Verwendung von Custom Elements konzentrieren, die mit der Lit Custom Element-Bibliothek in einer Svelte-Anwendung erstellt wurden. Diese Konzepte sollten sich auf mit oder ohne unterstützende Bibliothek erstellte Custom Elements übertragen lassen.

Eigenschaft oder Attribut?

Um Custom Elements in Svelte vollständig zu verstehen, müssen Sie verstehen, wie Svelte Daten an ein Custom Element übergibt.

Svelte verwendet eine einfache Heuristik, um zu bestimmen, ob Daten als Eigenschaft oder als Attribut an ein Custom Element übergeben werden sollen. Wenn zur Laufzeit eine entsprechende Eigenschaft auf dem Custom Element vorhanden ist, übergibt Svelte die Daten als Eigenschaft. Andernfalls übergibt es sie als Attribut. Das erscheint einfach, hat aber interessante Auswirkungen.

Nehmen wir zum Beispiel an, Sie haben ein coffee-mug Custom Element, das eine size-Eigenschaft akzeptiert. Sie können es in einer Svelte-Komponente wie folgt verwenden:

<coffee-mug class="mug" size="large"></coffee-mug>

Sie können dieses Svelte REPL öffnen, um mitzuverfolgen. Sie sollten sehen, wie das Custom Element den Text „This coffee mug’s size is: large ☕️.“ rendert.

Beim Schreiben des HTML innerhalb der Komponente sieht es so aus, als ob Sie sowohl class als auch size als Attribute setzen. Dies ist jedoch nicht der Fall. Klicken Sie mit der rechten Maustaste auf den Text „This coffee mug’s size is“ in der Ausgabe der REPL und klicken Sie auf „Inspect“. Dadurch werden die Entwicklertools geöffnet. Wenn Sie das gerenderte HTML inspizieren, werden Sie feststellen, dass nur class als Attribut gesetzt wurde – es ist, als ob size einfach verschwunden wäre! size wird jedoch irgendwie gesetzt, da „large“ immer noch im gerenderten Text des Elements erscheint.

Das liegt daran, dass size eine Eigenschaft des Elements ist, class jedoch nicht. Da Svelte eine size-Eigenschaft erkennt, wählt es stattdessen das Setzen dieser Eigenschaft aus. Es gibt keine class-Eigenschaft, also setzt Svelte sie stattdessen als Attribut. Das ist kein Problem oder etwas, das die erwartete Verhaltensweise der Komponente ändert, kann aber sehr verwirrend sein, wenn Sie sich dessen nicht bewusst sind, da es eine Diskrepanz zwischen dem HTML, das Sie zu schreiben glauben, und dem, was Svelte tatsächlich ausgibt, gibt.

Svelte ist in diesem Verhalten nicht einzigartig – Preact verwendet eine ähnliche Methode, um zu bestimmen, ob ein Attribut oder eine Eigenschaft auf Custom Elements gesetzt werden soll. Daher treten die von mir diskutierten Anwendungsfälle auch in Preact auf, obwohl die Workarounds unterschiedlich sein werden. Sie werden mit Angular oder Vue nicht auf diese Probleme stoßen, da diese über eine spezielle Syntax verfügen, mit der Sie wählen können, ob ein Attribut oder eine Eigenschaft gesetzt werden soll.

Die Heuristik von Svelte erleichtert die Übergabe komplexer Daten wie Arrays und Objekte, die als Eigenschaften gesetzt werden müssen. Konsumenten Ihrer Custom Elements sollten nicht darüber nachdenken müssen, ob sie ein Attribut oder eine Eigenschaft setzen müssen – es funktioniert einfach magisch. Wie bei jeder Magie in der Webentwicklung stoßen Sie jedoch irgendwann auf Fälle, die ein tieferes Eintauchen und Verständnis erfordern.

Lassen Sie uns einige Anwendungsfälle durchgehen, bei denen sich Custom Elements seltsam verhalten. Die endgültigen Beispiele finden Sie in diesem Svelte REPL.

Attribute als Styling-Hooks verwendet

Nehmen wir an, Sie haben ein custom-text Element, das Text anzeigt. Wenn das flag Attribut vorhanden ist, wird davor ein Flaggen-Emoji und das Wort „Flagged:“ gestellt. Das Element ist wie folgt codiert:

import { html, css, LitElement } from 'lit';
export class CustomText extends LitElement {
  static get styles() {
    return css`
      :host([flag]) p::before {
        content: '🚩';
      }
    `;
  }
  static get properties() {
    return {
      flag: {
        type: Boolean
      }
    };
  }
  constructor() {
    super();
    this.flag = false;
  }
  render() {
    return html`<p>
      ${this.flag ? html`<strong>Flagged:</strong>` : ''}
      <slot></slot>
    </p>`;
  }
}
customElements.define('custom-text', CustomText);

Sie können das Element in Aktion in diesem CodePen sehen.

Wenn Sie jedoch versuchen, das Custom Element in Svelte auf die gleiche Weise zu verwenden, funktioniert es nicht ganz. Der Text „Flagged:“ wird angezeigt, das Emoji jedoch nicht. Was ist los?

<script>
  import './custom-elements/custom-text';
</script>

<!-- This shows the "Flagged:" text, but not 🚩 -->
<custom-text flag>Just some custom text.</custom-text>

Der Schlüssel hier ist der Selektor :host([flag]). :host wählt den Shadow Root des Elements aus (d. h. das <custom-text> Element), sodass dieser Selektor nur angewendet wird, wenn das Flaggen-Attribut auf dem Element vorhanden ist. Da Svelte stattdessen wählt, die Eigenschaft zu setzen, wird dieser Selektor nicht angewendet. Der Text „Flagged:“ wird basierend auf der Eigenschaft hinzugefügt, weshalb dieser immer noch angezeigt wurde.

Was sind also unsere Optionen? Nun, das Custom Element hätte nicht davon ausgehen dürfen, dass flag immer als Attribut gesetzt wird. Es ist eine Best Practice für Custom Elements, primitive Datenattribute und -eigenschaften synchron zu halten, da Sie nicht wissen, wie der Konsument des Elements damit interagieren wird. Die ideale Lösung ist, dass der Elementautor sicherstellt, dass alle primitiven Eigenschaften zu Attributen reflektiert werden, insbesondere wenn diese Attribute zum Stylen verwendet werden. Lit erleichtert die Reflexion Ihrer Eigenschaften.

static get properties() {
  return {
    flag: {
      type: Boolean,
      reflect: true
    }
  };
}

Mit dieser Änderung wird die flag-Eigenschaft zurück zum Attribut reflektiert, und alles wird wie erwartet angezeigt.

Es kann jedoch Fälle geben, in denen Sie keine Kontrolle über die Definition des Custom Elements haben. In diesem Fall können Sie Svelte mit einer Svelte-Aktion dazu zwingen, das Attribut zu setzen.

Verwendung einer Svelte-Aktion zum Erzwingen des Setzens von Attributen

Aktionen sind ein leistungsstarkes Svelte-Feature, das eine Funktion ausführt, wenn ein bestimmter Knoten zum DOM hinzugefügt wird. Zum Beispiel können wir eine Aktion schreiben, die das flag Attribut auf unserem custom-text Element setzt.

<script>
  import './custom-elements/custom-text';
  function setAttributes(node) {
    node.setAttribute('flag', '');
  }
</script>

<custom-text use:setAttributes>
  Just some custom text.
</custom-text>

Aktionen können auch Parameter annehmen. Wir könnten diese Aktion beispielsweise generischer gestalten und ein Objekt übergeben, das die Attribute enthält, die wir auf einem Knoten setzen möchten.

<script>
  import './custom-elements/custom-text';
  function setAttributes(node, attributes) {
    Object.entries(attributes).forEach(([k, v]) => {
      if (v !== undefined) {
        node.setAttribute(k, v);
      } else {
        node.removeAttribute(k);
      }
    });
  }
</script>

<custom-text use:setAttributes={{ flag: true }}>
  Just some custom text.
</custom-text>

Schließlich können wir, wenn wir möchten, dass die Attribute auf Zustandsänderungen reagieren, ein Objekt mit einer update-Methode von der Aktion zurückgeben. Immer wenn sich die an die Aktion übergebenen Parameter ändern, wird die update-Funktion aufgerufen.

<script>
  import './custom-elements/custom-text';
  function setAttributes(node, attributes) {
    const applyAttributes = () => {
      Object.entries(attributes).forEach(([k, v]) => {
        if (v !== undefined) {
          node.setAttribute(k, v);
        } else {
          node.removeAttribute(k);
        }
      });
    };
    applyAttributes();
    return {
      update(updatedAttributes) {
        attributes = updatedAttributes;
        applyAttributes();
      }
    };
  }
  let flagged = true;
</script>
<label><input type="checkbox" bind:checked={flagged} /> Flagged</label>
<custom-text use:setAttributes={{ flag: flagged ? '' : undefined }}>
  Just some custom text.
</custom-text>

Mit diesem Ansatz müssen wir das Custom Element nicht aktualisieren, um die Eigenschaft zu reflektieren – wir können das Setzen des Attributs von unserer Svelte-App aus steuern.

Lazy Loading von Custom Elements

Custom Elements werden nicht immer definiert, wenn die Komponente zum ersten Mal gerendert wird. Sie warten möglicherweise darauf, Ihre Custom Elements zu importieren, bis die Web Component Polyfills geladen wurden. Auch in einem serverseitigen Rendering-Kontext wie Sapper oder SvelteKit erfolgt das anfängliche Server-Rendering ohne Laden der Custom Element-Definition.

In beiden Fällen, wenn das Custom Element nicht definiert ist, setzt Svelte alles als Attribute. Dies liegt daran, dass die Eigenschaft noch nicht auf dem Element vorhanden ist. Das ist verwirrend, wenn Sie sich daran gewöhnt haben, dass Svelte nur Eigenschaften auf Custom Elements setzt. Dies kann zu Problemen mit komplexen Daten wie Objekten und Arrays führen.

Betrachten wir als Beispiel das folgende Custom Element, das eine Begrüßung gefolgt von einer Liste von Namen anzeigt.

import { html, css, LitElement } from 'lit';
export class FancyGreeting extends LitElement {
  static get styles() {
    return css`
      p {
        border: 5px dashed mediumaquamarine;
        padding: 4px;
      }
    `;
  }
  static get properties() {
    return {
      names: { type: Array },
      greeting: { type: String }
    };
  }
  constructor() {
    super();
    this.names = [];
  }
  render() {
    return html`<p>
      ${this.greeting},
      ${this.names && this.names.length > 0 ? this.names.join(', ') : 'no one'}!
    </p>`;
  }
}
customElements.define('fancy-greeting', FancyGreeting);

Sie können das Element in Aktion in diesem CodePen sehen.

Wenn wir das Element statisch in einer Svelte-Anwendung importieren, funktioniert alles wie erwartet.

<script>
  import './custom-elements/fancy-greeting';
</script>
<!-- This displays "Howdy, Amy, Bill, Clara!" -->
<fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

Wenn wir jedoch die Komponente dynamisch importieren, wird das Custom Element erst definiert, nachdem die Komponente zum ersten Mal gerendert wurde. In diesem Beispiel warte ich darauf, das Element zu importieren, bis die Svelte-Komponente über die onMount Lifecycle-Funktion eingebunden wurde. Wenn wir das Laden des Custom Elements verzögern, werden die Namen nicht richtig gesetzt und der Fallback-Inhalt angezeigt.

<script>
  import { onMount } from 'svelte';
  onMount(async () => {
    await import('./custom-elements/fancy-greeting');
  });
</script>
<!-- This displays "Howdy, no one!"-->
<fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

Da die Definition des Custom Elements nicht geladen wird, wenn Svelte fancy-greeting zum DOM hinzufügt, hat fancy-greeting keine names-Eigenschaft und Svelte setzt das names-Attribut – aber als String, nicht als serialisiertes Array. Wenn Sie das Element in den Browser-Entwicklertools inspizieren, sehen Sie Folgendes:

<fancy-greeting greeting="Howdy" names="Amy,Bill,Clara"></fancy-greeting> 

Unser Custom Element versucht, das Attribut `names` mit `JSON.parse` als Array zu parsen, was eine Ausnahme auslöst. Dies wird automatisch mit Lit's Standard-Array-Konverter behandelt, gilt aber für jedes Element, das ein Attribut erwartet, das ein gültiges JSON-Array enthält.

Interessanterweise, sobald Sie die an das Custom Element übergebenen Daten aktualisieren, beginnt Svelte, die Eigenschaft wieder zu setzen. Im folgenden Beispiel habe ich das Array mit Namen in die Zustandsvariable names verschoben, damit ich es aktualisieren kann. Ich habe auch einen „Add name“-Button hinzugefügt, der den Namen „Rory“ am Ende des names-Arrays hinzufügt, wenn er geklickt wird.

Sobald der Button geklickt wird, wird das names-Array aktualisiert, was ein erneutes Rendern der Komponente auslöst. Da das Custom Element nun definiert ist, erkennt Svelte die names-Eigenschaft des Custom Elements und setzt diese anstelle des Attributs. Dies führt dazu, dass das Custom Element die Liste der Namen richtig anzeigt und nicht den Fallback-Inhalt.

<script>
  import { onMount } from 'svelte';
  onMount(async () => {
    await import('./custom-elements/fancy-greeting');
  });
  let names = ['Amy', 'Bill', 'Clara'];
  function addName() {
    names = [...names, 'Rory'];
  }
</script>

<!-- Once the button is clicked, the element displays "Howdy, Amy, Bill, Clara, Rory!" -->
<fancy-greeting greeting="Howdy" {names} />
<button on:click={addName}>Add name</button>

Wie im vorherigen Beispiel können wir Svelte mit einer Aktion zwingen, die Daten so zu setzen, wie wir es wollen. Dieses Mal möchten wir anstatt alles als Attribut zu setzen, alles als Eigenschaft setzen. Wir übergeben ein Objekt als Parameter, das die Eigenschaften enthält, die wir auf dem Knoten setzen möchten. So wird unsere Aktion auf das Custom Element angewendet:

<fancy-greeting
  greeting="Howdy"
  use:setProperties={{ names: ['Amy', 'Bill', 'Clara'] }}
/>

Unten finden Sie die Implementierung der Aktion. Wir iterieren über das Eigenschaftenobjekt und verwenden jeden Eintrag, um die Eigenschaft auf dem Custom Element-Knoten zu setzen. Wir geben auch eine Update-Funktion zurück, damit die Eigenschaften neu angewendet werden, wenn sich die an die Aktion übergebenen Parameter ändern. Sehen Sie sich den vorherigen Abschnitt an, wenn Sie eine Auffrischung wünschen, wie Sie mit einer Aktion auf Zustandsänderungen reagieren können.

function setProperties(node, properties) {
  const applyProperties = () => {
    Object.entries(properties).forEach(([k, v]) => {
      node[k] = v;
    });
  };
  applyProperties();
  return {
    update(updatedProperties) {
      properties = updatedProperties;
      applyProperties();
    }
  };
}

Durch die Verwendung der Aktion werden die Namen beim ersten Rendern korrekt angezeigt. Svelte setzt die Eigenschaft beim ersten Rendern der Komponente, und das Custom Element greift auf diese Eigenschaft zu, sobald das Element definiert wurde.

Boolesche Attribute

Das letzte Problem, auf das wir gestoßen sind, ist, wie Svelte boolesche Attribute auf einem Custom Element behandelt. Dieses Verhalten hat sich kürzlich mit Svelte 3.38.0 geändert, aber wir werden das Verhalten vor und nach 3.38 untersuchen, da nicht jeder die neueste Svelte-Version verwenden wird.

Angenommen, wir haben ein <secret-box> Custom Element mit einer booleschen Eigenschaft open, die angibt, ob die Box geöffnet ist oder nicht. Die Implementierung sieht wie folgt aus:

import { html, LitElement } from 'lit';
export class SecretBox extends LitElement {
  static get properties() {
    return {
      open: {
        type: Boolean
      }
    };
  }
  render() {
    return html`<div>The box is ${this.open ? 'open 🔓' : 'closed 🔒'}</div>`;
  }
}
customElements.define('secret-box', SecretBox);

Sie können das Element in Aktion in diesem CodePen sehen.

Wie in CodePen gezeigt, können Sie die open-Eigenschaft auf verschiedene Arten auf true setzen. Gemäß der HTML-Spezifikation repräsentiert das Vorhandensein eines booleschen Attributs den Wert true, und seine Abwesenheit repräsentiert false.

<secret-box open></secret-box>
<secret-box open=""></secret-box>
<secret-box open="open"></secret-box>

Interessanterweise zeigt nur die letzte der obigen Optionen „The box is open“ an, wenn sie in einer Svelte-Komponente verwendet wird. Die ersten beiden zeigen „The box is closed“ an, obwohl das open-Attribut gesetzt wurde. Was ist hier los?

Wie bei den anderen Beispielen geht es zurück dazu, dass Svelte Eigenschaften gegenüber Attributen bevorzugt. Wenn Sie die Elemente in den DevTools des Browsers inspizieren, werden keine Attribute gesetzt – Svelte hat alles als Eigenschaften gesetzt. Wir können die open-Eigenschaft in unserer Render-Methode `console.log`en (oder das Element in der Konsole abfragen), um herauszufinden, worauf Svelte die open-Eigenschaft gesetzt hat.

// <secret-box open> logs ''
// <secret-box open=""> logs ''
// <secret-box open="open"> logs 'open'
render() {
  console.log(this.open);
  return html`<div>The box is ${this.open ? 'open 🔓' : 'closed 🔒'}</div>`;
}

In den ersten beiden Fällen ist open gleich einem leeren String. Da ein leerer String in JavaScript falsy ist, evaluiert unsere ternäre Anweisung zum False-Fall und zeigt an, dass die Box geschlossen ist. Im letzten Fall wird die open-Eigenschaft auf den String „open“ gesetzt, der truthy ist. Die ternäre Anweisung evaluiert zum True-Fall und zeigt an, dass die Box offen ist.

Nebenbei bemerkt, stoßen Sie auf dieses Problem nicht, wenn Sie das Element per Lazy Loading laden. Da die Definition des Custom Elements beim Rendern des Elements durch Svelte nicht geladen ist, setzt Svelte stattdessen das Attribut. Siehe den obigen Abschnitt für eine Auffrischung.

Es gibt eine einfache Möglichkeit, dieses Problem zu umgehen. Wenn Sie sich daran erinnern, dass Sie die Eigenschaft und nicht das Attribut setzen, können Sie die open-Eigenschaft mit der folgenden Syntax explizit auf true setzen.

<secret-box open={true}></secret-box>

Auf diese Weise wissen Sie, dass Sie die open-Eigenschaft auf true setzen. Das Setzen auf einen nicht leeren String funktioniert auch, aber diese Methode ist am genauesten, da Sie true anstelle von etwas setzen, das zufällig truthy ist.

Bis vor kurzem war dies der einzige Weg, boolesche Eigenschaften auf Custom Elements korrekt zu setzen. Mit Svelte 3.38 wurde jedoch eine Änderung veröffentlicht, die die Heuristik von Svelte aktualisiert hat, um das Setzen von Kurzform-Booleschen Eigenschaften zu ermöglichen. Jetzt, wenn Svelte weiß, dass die zugrundeliegende Eigenschaft ein Boolean ist, behandelt es die Syntaxe `open` und `open=""` gleich wie `open={true}`.

Das ist besonders hilfreich, da dies der Weg ist, wie Beispiele in vielen Custom Element-Komponentenbibliotheken aussehen. Diese Änderung erleichtert das Kopieren und Einfügen aus der Dokumentation, ohne Probleme mit der Funktionalität eines bestimmten Attributs beheben zu müssen.

Es gibt jedoch eine Anforderung auf Seiten des Custom Element-Autors – die boolesche Eigenschaft benötigt einen Standardwert, damit Svelte weiß, dass sie vom Typ Boolean ist. Das ist ohnehin eine gute Praxis, wenn Sie möchten, dass diese Eigenschaft ein Boolean ist.

In unserem secret-box Element können wir einen Konstruktor hinzufügen und den Standardwert festlegen:

constructor() {
  super();
  this.open = true;
}

Mit dieser Änderung wird Folgendes korrekt „The box is open“ in einer Svelte-Komponente anzeigen.

<secret-box open></secret-box>
<secret-box open=""></secret-box>

Zusammenfassung

Sobald Sie verstehen, wie Svelte entscheidet, ob es ein Attribut oder eine Eigenschaft setzen soll, ergeben viele dieser scheinbar seltsamen Probleme mehr Sinn. Immer wenn Sie Probleme beim Übergeben von Daten an ein Custom Element innerhalb einer Svelte-Anwendung haben, finden Sie heraus, ob es als Attribut oder als Eigenschaft gesetzt wird, und gehen Sie von dort aus. Ich habe Ihnen in diesem Artikel einige Fluchtwege gegeben, um eines oder das andere zu erzwingen, wenn Sie es brauchen, aber sie sollten im Allgemeinen unnötig sein. Meistens funktionieren Custom Elements in Svelte einfach. Sie müssen nur wissen, wo Sie suchen müssen, wenn etwas schief geht.


Ein besonderer Dank geht an Dale Sande, Gus Naughton und Nanette Ranes für die Überprüfung einer frühen Version dieses Artikels.