Fortgeschrittene Werkzeuge für Web Components

Avatar of Caleb Williams
Caleb Williams am

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

In den letzten vier Artikeln dieser fünfteiligen Serie haben wir die Technologien, aus denen die Web Components-Standards bestehen, eingehend betrachtet. Zuerst haben wir uns angesehen, wie wiederverwendbare HTML-Vorlagen erstellt werden können, die später verwendet werden können. Zweitens haben wir uns mit der Erstellung unseres eigenen benutzerdefinierten Elements befasst. Danach haben wir die Stile und Selektoren unseres Elements in das Shadow DOM gekapselt, damit unser Element vollständig eigenständig ist.

Wir haben die Leistungsfähigkeit dieser Werkzeuge durch die Erstellung eines eigenen benutzerdefinierten Modal-Dialogs untersucht, eines Elements, das in den meisten modernen Anwendungskontexten unabhängig vom zugrunde liegenden Framework oder der Bibliothek verwendet werden kann. In diesem Artikel werden wir uns ansehen, wie unser Element in den verschiedenen Frameworks verwendet werden kann, und einige fortgeschrittene Werkzeuge untersuchen, um Ihre Fähigkeiten im Bereich Web Components wirklich auszubauen.

Artikelserie

  1. Eine Einführung in Web Components
  2. Erstellung wiederverwendbarer HTML-Vorlagen
  3. Creating a Custom Element from Scratch
  4. Kapselung von Stil und Struktur mit Shadow DOM
  5. Fortgeschrittene Werkzeuge für Web Components (Dieser Beitrag)

Framework-agnostisch

Unsere Dialogkomponente funktioniert in fast jedem Framework oder sogar ohne eines hervorragend. (Vorausgesetzt, wenn JavaScript deaktiviert ist, ist alles umsonst.) Angular und Vue behandeln Web Components als vollwertige Bürger: Die Frameworks wurden im Hinblick auf Webstandards entwickelt. React ist etwas eigenwilliger, aber nicht unmöglich zu integrieren.

Angular

Zuerst werfen wir einen Blick darauf, wie Angular benutzerdefinierte Elemente behandelt. Standardmäßig gibt Angular einen Template-Fehler aus, wenn es auf ein Element trifft, das es nicht erkennt (d. h. die Standard-Browserelemente oder eine der von Angular definierten Komponenten). Dieses Verhalten kann durch die Einbeziehung von CUSTOM_ELEMENTS_SCHEMA geändert werden.

…ermöglicht es einem NgModule, Folgendes zu enthalten

  • Nicht-Angular-Elemente mit Dashcase-Benennung (-).
  • Elementeigenschaften mit Dashcase-Benennung (-). Dashcase ist die Namenskonvention für benutzerdefinierte Elemente.

Angular Documentation

Die Verwendung dieses Schemas ist so einfach wie seine Hinzufügung zu einem Modul

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@NgModule({
  /** Omitted */
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]
})
export class MyModuleAllowsCustomElements {}

Das ist alles. Danach können wir unser benutzerdefiniertes Element überall verwenden, wo wir wollen, mit den standardmäßigen Property- und Event-Bindings.

<one-dialog [open]="isDialogOpen" (dialog-closed)="dialogClosed($event)">
  <span slot="heading">Heading text</span>
  <div>
    <p>Body copy</p>
  </div>
</one-dialog>

Vue

Die Kompatibilität von Vue mit Web Components ist noch besser als die von Angular, da keine spezielle Konfiguration erforderlich ist. Sobald ein Element registriert ist, kann es mit der Standard-Templating-Syntax von Vue verwendet werden.

<one-dialog v-bind:open="isDialogOpen" v-on:dialog-closed="dialogClosed">
  <span slot="heading">Heading text</span>
  <div>
    <p>Body copy</p>
  </div>
</one-dialog>

Eine Einschränkung bei Angular und Vue sind jedoch ihre Standard-Formularsteuerelemente. Wenn wir etwas wie reaktive Formulare oder [(ng-model)] in Angular oder v-model in Vue auf einem benutzerdefinierten Element mit einem Formularsteuerelement verwenden möchten, müssen wir die entsprechende Logik einrichten, was jedoch den Rahmen dieses Artikels sprengt.

React

React ist etwas komplizierter als Angular. Reacts virtuelles DOM wandelt effektiv einen JSX-Baum in ein großes Objekt um. Anstatt also HTML-Elemente direkt zu modifizieren, wie es Angular oder Vue tun, verwendet React eine Objekt-Syntax, um Änderungen zu verfolgen, die am DOM vorgenommen werden müssen, und aktualisiert sie in großen Mengen. Dies funktioniert in den meisten Fällen einwandfrei. Das `open`-Attribut unseres Dialogs ist an seine Eigenschaft gebunden und reagiert perfekt auf sich ändernde Props.

Das Problem tritt auf, wenn wir beginnen, uns die CustomEvent anzusehen, die ausgelöst wird, wenn sich unser Dialog schließt. React implementiert eine Reihe von nativen Event-Listenern für uns mit seinem synthetischen Event-System. Leider bedeutet das, dass Steuerelemente wie onDialogClosed keine tatsächlichen Event-Listener an unsere Komponente anhängen, sodass wir einen anderen Weg finden müssen.

Die offensichtlichste Methode zum Hinzufügen benutzerdefinierter Event-Listener in React ist die Verwendung von DOM-Refs. In diesem Modell können wir unseren HTML-Knoten direkt referenzieren. Die Syntax ist etwas umständlich, funktioniert aber hervorragend.

import React, { Component, createRef } from 'react';

export default class MyComponent extends Component {
  constructor(props) {
    super(props);
    // Create the ref
    this.dialog = createRef();
    // Bind our method to the instance
    this.onDialogClosed = this.onDialogClosed.bind(this);

    this.state = {
      open: false
    };
  }

  componentDidMount() {
    // Once the component mounds, add the event listener
    this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
  }

  componentWillUnmount() {
    // When the component unmounts, remove the listener
    this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
  }

  onDialogClosed(event) { /** Omitted **/ }

  render() {
    return <div>
      <one-dialog open={this.state.open} ref={this.dialog}>
        <span slot="heading">Heading text</span>
        <div>
          <p>Body copy</p>
        </div>
      </one-dialog>
    </div>
  }
}

Oder wir können zustandslose funktionale Komponenten und Hooks verwenden.

import React, { useState, useEffect, useRef } from 'react';

export default function MyComponent(props) {
  const [ dialogOpen, setDialogOpen ] = useState(false);
  const oneDialog = useRef(null);
  const onDialogClosed = event => console.log(event);

  useEffect(() => {
    oneDialog.current.addEventListener('dialog-closed', onDialogClosed);
    return () => oneDialog.current.removeEventListener('dialog-closed', onDialogClosed)
  });

  return <div>
      <button onClick={() => setDialogOpen(true)}>Open dialog</button>
      <one-dialog ref={oneDialog} open={dialogOpen}>
        <span slot="heading">Heading text</span>
        <div>
          <p>Body copy</p>
        </div>
      </one-dialog>
    </div>
}

Das ist nicht schlecht, aber Sie sehen, wie wiederverwendbar diese Komponente schnell umständlich werden kann. Glücklicherweise können wir eine Standard-React-Komponente exportieren, die unser benutzerdefiniertes Element mit denselben Werkzeugen umschließt.

import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';

export default class OneDialog extends Component {
  constructor(props) {
    super(props);
    // Create the ref
    this.dialog = createRef();
    // Bind our method to the instance
    this.onDialogClosed = this.onDialogClosed.bind(this);
  }

  componentDidMount() {
    // Once the component mounds, add the event listener
    this.dialog.current.addEventListener('dialog-closed', this.onDialogClosed);
  }

  componentWillUnmount() {
    // When the component unmounts, remove the listener
    this.dialog.current.removeEventListener('dialog-closed', this.onDialogClosed);
  }

  onDialogClosed(event) {
    // Check to make sure the prop is present before calling it
    if (this.props.onDialogClosed) {
      this.props.onDialogClosed(event);
    }
  }

  render() {
    const { children, onDialogClosed, ...props } = this.props;
    return <one-dialog {...props} ref={this.dialog}>
      {children}
    </one-dialog>
  }
}

OneDialog.propTypes = {
  children: children: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.node),
      PropTypes.node
  ]).isRequired,
  onDialogClosed: PropTypes.func
};

…oder wieder als zustandslose, funktionale Komponente.

import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';

export default function OneDialog(props) {
  const { children, onDialogClosed, ...restProps } = props;
  const oneDialog = useRef(null);
  
  useEffect(() => {
    onDialogClosed ? oneDialog.current.addEventListener('dialog-closed', onDialogClosed) : null;
    return () => {
      onDialogClosed ? oneDialog.current.removeEventListener('dialog-closed', onDialogClosed) : null;  
    };
  });

  return <one-dialog ref={oneDialog} {...restProps}>{children}</one-dialog>
}

Jetzt können wir unseren Dialog nativ in React verwenden, aber immer noch dieselbe API über alle unsere Anwendungen hinweg beibehalten (und Klassen fallen lassen, wenn das Ihr Ding ist).

import React, { useState } from 'react';
import OneDialog from './OneDialog';

export default function MyComponent(props) {
  const [open, setOpen] = useState(false);
  return <div>
    <button onClick={() => setOpen(true)}>Open dialog</button>
    <OneDialog open={open} onDialogClosed={() => setOpen(false)}>
      <span slot="heading">Heading text</span>
      <div>
        <p>Body copy</p>
      </div>
    </OneDialog>
  </div>
}

Fortgeschrittene Werkzeuge

Es gibt eine Reihe von großartigen Werkzeugen zur Erstellung eigener benutzerdefinierter Elemente. Eine Suche auf npm zeigt eine Vielzahl von Werkzeugen zur Erstellung hochgradig reaktiver benutzerdefinierter Elemente (einschließlich meines eigenen Pet-Projekts), aber das mit Abstand beliebteste ist derzeit lit-html vom Polymer-Team und speziell für Web Components LitElement.

LitElement ist eine Basisklasse für benutzerdefinierte Elemente, die eine Reihe von APIs für alle bisher behandelten Dinge bereitstellt. Es kann in einem Browser ohne Build-Schritt ausgeführt werden, aber wenn Sie Tools wie Decorators verwenden, die zukunftsgerichtet sind, gibt es dafür auch Dienstprogramme.

Bevor wir uns damit befassen, wie lit oder LitElement verwendet werden, nehmen Sie sich eine Minute Zeit, um sich mit tagged template literals vertraut zu machen, die eine spezielle Art von Funktion sind, die für Template-Literal-Strings in JavaScript aufgerufen wird. Diese Funktionen nehmen ein Array von Strings und eine Sammlung von interpolierten Werten entgegen und können alles zurückgeben, was Sie möchten.

function tag(strings, ...values) {
  console.log({ strings, values });
  return true;
}
const who = 'world';

tag`hello ${who}`; 
/** would log out { strings: ['hello ', ''], values: ['world'] } and return true **/

LitElement bietet uns Live-Aktualisierungen von allem, was an dieses Werte-Array übergeben wird. Wenn sich also eine Eigenschaft ändert, wird die Render-Funktion des Elements aufgerufen und der resultierende DOM neu gerendert.

import { LitElement, html } from 'lit-element';

class SomeComponent {
  static get properties() {
    return { 
      now: { type: String }
    };
  }

  connectedCallback() {
    // Be sure to call the super
    super.connectedCallback();
    this.interval = window.setInterval(() => {
      this.now = Date.now();
    });
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    window.clearInterval(this.interval);
  }

  render() {
    return html`<h1>It is ${this.now}</h1>`;
  }
}

customElements.define('some-component', SomeComponent);

Siehe den Pen
LitElement jetzt Beispiel
von Caleb Williams (@calebdwilliams)
auf CodePen.

Sie werden feststellen, dass wir jede Eigenschaft, die LitElement beobachten soll, mit dem Getter static properties definieren müssen. Die Verwendung dieser API teilt der Basisklasse mit, render aufzurufen, wenn eine Änderung an den Eigenschaften der Komponente vorgenommen wird. render aktualisiert im Gegenzug nur die Knoten, die geändert werden müssen.

Für unser Dialogbeispiel würde dies mit LitElement wie folgt aussehen:

Siehe den Pen
Dialog-Beispiel mit LitElement
von Caleb Williams (@calebdwilliams)
auf CodePen.

Es gibt mehrere Varianten von lit-html, darunter Haunted, eine Bibliothek im React-Hooks-Stil für Web Components, die auch virtuelle Komponenten unter Verwendung von lit-html als Basis nutzen kann.

Letztendlich sind die meisten modernen Werkzeuge für Web Components verschiedene Varianten dessen, was LitElement ist: eine Basisklasse, die gängige Logik von unseren Komponenten abstrahiert. Zu den weiteren Varianten gehören Stencil, SkateJS, Angular Elements und Polymer.

Was kommt als Nächstes

Web Components-Standards entwickeln sich ständig weiter, und neue Funktionen werden kontinuierlich diskutiert und in Browser integriert. Bald werden Web Component-Autoren APIs für die Interaktion mit Webformularen auf hoher Ebene haben (einschließlich anderer Element-Internals, die über den Rahmen dieser einführenden Artikel hinausgehen), wie native HTML- und CSS-Modulimporte, native Template-Instanziierung und Aktualisierung von Steuerelementen und vieles mehr, was auf dem W3C/web components issues board auf GitHub verfolgt werden kann.

Diese Standards sind heute mit den entsprechenden Polyfills für ältere Browser und Edge einsatzbereit. Und obwohl sie Ihr bevorzugtes Framework möglicherweise nicht ersetzen, können sie neben diesen verwendet werden, um Ihre und die Arbeitsabläufe Ihrer Organisation zu erweitern.