Elementy niestandardowe, wersja 1 – komponenty internetowe wielokrotnego użytku

Elementy niestandardowe pozwalają twórcom stron internetowych definiować nowe tagi HTML, rozszerzać istniejące i tworzyć komponenty internetowe wielokrotnego użytku.

Dzięki elementom niestandardowym programiści stron internetowych mogą tworzyć nowe tagi HTML, ulepszać istniejące tagi HTML i rozszerzać komponenty utworzone przez innych programistów. Interfejs API jest podstawą komponentów sieciowych. To oparty na standardach internetowych sposób tworzenia komponentów wielokrotnego użytku nie tylko w waniliowym JS/HTML/CSS. Efektem jest mniejsza ilość kodu i kod modułowy, a także więcej możliwości ponownego wykorzystywania w naszych aplikacjach.

Wstęp

Przeglądarka jest doskonałym narzędziem do tworzenia struktury aplikacji internetowych. Jest to HTML. Być może już o niej słyszeliście. Jest deklaracyjny, przenośny, dobrze obsługiwany i łatwy w obsłudze. Mimo że HTML może być dobry, jego słownictwo i elastyczność są ograniczone. Dotychczasowemu standardowi HTML zawsze brakowało sposobu na automatyczne powiązanie działania JavaScriptu z Twoimi znacznikami (do tej pory).

Elementy niestandardowe są odpowiedzią na modernizację kodu HTML, uzupełnianie brakujących elementów i łączenie struktury w funkcje. Jeśli HTML nie stanowi rozwiązania problemu, możemy utworzyć element niestandardowy, który to umożliwia. Elementy niestandardowe uczą przeglądarki nowych sztuczek, zachowując jednocześnie zalety języka HTML.

Definiowanie nowego elementu

Do definiowania nowego elementu HTML potrzebna jest siła JavaScriptu.

Global customElements służy do definiowania elementu niestandardowego i uczenia przeglądarki o nowym tagu. Wywołaj customElements.define() z nazwą tagu, który chcesz utworzyć, i JavaScriptem class, który rozszerza podstawową wartość HTMLElement.

Przykład – definiowanie panelu panelu na urządzeniu mobilnym, <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 {...});

Przykład użycia:

<app-drawer></app-drawer>

Pamiętaj, że z elementami niestandardowymi możesz korzystać tak samo jak z elementu <div> czy dowolnego innego elementu. Instancje można zadeklarować na stronie, tworzyć dynamicznie w kodzie JavaScript, dołączać detektory zdarzeń itp. Czytaj dalej, aby poznać więcej przykładów.

Definiowanie interfejsu JavaScript API elementu

Funkcjonalność elementu niestandardowego jest definiowana za pomocą tagu class ES2015, który stanowi rozszerzenie HTMLElement. Rozszerzenie HTMLElement sprawia, że element niestandardowy dziedziczy cały interfejs DOM API, a wszelkie dodane do klasy właściwości i metody stają się częścią interfejsu DOM elementu. Użyj klasy do utworzenia publicznego interfejsu API JavaScript dla swojego tagu.

Przykład – definiowanie interfejsu DOM dla <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);

W tym przykładzie tworzymy szufladę z właściwością open, disabled i metodą toggleDrawer(). Odzwierciedla też właściwości jako atrybuty HTML.

Zaletą elementów niestandardowych jest to, że this w definicji klasy odwołuje się do samego elementu DOM, tj. do wystąpienia klasy. W naszym przykładzie this odnosi się do <app-drawer>. W ten sposób (😉) element może dołączyć do siebie detektor click. Nie musisz ograniczać się do detektorów zdarzeń. Cały interfejs DOM API jest dostępny w kodzie elementu. this umożliwia dostęp do właściwości elementu, sprawdzania jego elementów podrzędnych (this.children), węzłów zapytań (this.querySelectorAll('.items')) itp.

Reguły tworzenia elementów niestandardowych

  1. Nazwa elementu niestandardowego musi zawierać łącznik (-). Dlatego nazwy <x-tags>, <my-element> i <my-awesome-app> są prawidłowymi nazwami, w przeciwieństwie do nazw <tabs> i <foo_bar>. Jest to wymagane, aby parser HTML mógł odróżnić elementy niestandardowe od zwykłych. Zapewnia też zgodność z przekazywaniem zmian w przypadku dodawania do kodu HTML nowych tagów.
  2. Nie możesz zarejestrować tego samego tagu więcej niż raz. Próba wykonania tej czynności spowoduje wywołanie funkcji DOMException. Po poinformowaniu przeglądarki o nowym tagu, to wszystko. Bez możliwości cofnięcia.
  3. Elementy niestandardowe nie mogą się automatycznie zamykać, ponieważ kod HTML umożliwia samozamykanie tylko kilku elementów. Zawsze zapisuj tag zamykający (<app-drawer></app-drawer>).

Reakcje elementów niestandardowych

Element niestandardowy może definiować specjalne punkty zaczepienia cyklu życia umożliwiające uruchamianie kodu w interesujących momentach jego istnienia. Są to tak zwane reakcje elementów niestandardowych.

Nazwa Wywołane, gdy
constructor Zostanie utworzona lub uaktualniona instancja elementu. Ta opcja przydaje się do inicjowania stanu, konfigurowania detektorów zdarzeń i tworzenia obiektu shadow DOM. Informacje o ograniczeniach, które możesz wykonywać w constructor, znajdziesz w specyfikacji .
connectedCallback Wywoływane za każdym razem, gdy element jest wstawiony do modelu DOM. Ta opcja jest przydatna przy uruchamianiu kodu konfiguracji, na przykład do pobierania zasobów lub renderowania. Warto spróbować opóźnić prace do tego czasu.
disconnectedCallback Wywoływane za każdym razem, gdy element jest usuwany z DOM. Ta opcja jest przydatna przy uruchamianiu kodu czyszczenia.
attributeChangedCallback(attrName, oldVal, newVal) Wywoływane, gdy obserwowany atrybut został dodany, usunięty, zaktualizowany lub zastąpiony. Wywoływana również w przypadku wartości początkowych, gdy element został utworzony przez parser lub uaktualniony. Uwaga: wywołanie zwrotne będzie otrzymywać tylko atrybuty wymienione we właściwości observedAttributes.
adoptedCallback Element niestandardowy został przeniesiony do nowego elementu document (np. kogoś o nazwie document.adoptNode(el)).

Wywołania zwrotne reakcji są synchroniczne. Jeśli ktoś wywoła w Twoim elemencie funkcję el.setAttribute(), przeglądarka natychmiast wywoła metodę attributeChangedCallback(). Podobnie po usunięciu elementu z modelu DOM otrzymasz też kod disconnectedCallback() (np. użytkownik wywoła metodę el.remove()).

Przykład: dodawanie niestandardowych reakcji do elementu <app-drawer>:

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

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

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

Zdefiniuj reakcje, jeśli mają sens. Jeśli element jest wystarczająco złożony i otwiera połączenie z IndexedDB w connectedCallback(), wykonaj niezbędne czynności czyszczenia w disconnectedCallback(). Zachowaj jednak ostrożność. Nie możesz polegać na usunięciu elementu z DOM w każdej sytuacji. Na przykład interfejs disconnectedCallback() nie zostanie nigdy wywołany, jeśli użytkownik zamknie kartę.

Właściwości i atrybuty

Odbicie właściwości do atrybutów

Właściwości HTML często odzwierciedlają ich wartość w modelu DOM jako atrybut HTML. Jeśli na przykład wartości hidden lub id zmienią się w JS:

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

wartości są stosowane do aktywnego DOM jako atrybuty:

<div id="my-id" hidden>

Jest to tzw. „odzwierciedlenie właściwości”. Tak wygląda prawie każda właściwość w kodzie HTML. Dlaczego? Atrybuty są też przydatne przy deklaratywnej konfiguracji elementu, a niektóre interfejsy API, takie jak ułatwienia dostępu i selektory arkusza CSS, wymagają jego działania.

Odzwierciedlenie właściwości jest przydatne wszędzie tam, gdzie chcesz utrzymywać synchronizację reprezentacji DOM elementu ze stanem JavaScriptu. Jednym z powodów, dla których warto odzwierciedlić właściwości, jest stosowanie w przypadku zmiany stanu kodu JavaScript stylu zdefiniowanego przez użytkownika.

Wycofaj <app-drawer>. Konsument tego komponentu może zechcieć go wyciszyć lub uniemożliwić interakcję użytkownika, gdy jest wyłączony:

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

Po zmianie właściwości disabled w języku JS chcesz, aby ten atrybut został dodany do DOM, tak aby selektor użytkownika był zgodny. Element może zapewniać takie działanie, odzwierciedlając wartość atrybutu o tej samej nazwie:

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();
}

Obserwowanie zmian atrybutów

Atrybuty HTML to wygodny sposób na deklarowanie stanu początkowego:

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

Elementy mogą reagować na zmiany atrybutów, definiując attributeChangedCallback. Przeglądarka będzie wywoływać tę metodę po każdej zmianie atrybutów wymienionych w tablicy observedAttributes.

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.
  }
}

W tym przykładzie ustawiamy dodatkowe atrybuty w <app-drawer>, gdy zmieni się atrybut disabled. Chociaż tutaj tego nie robimy, możesz też użyć attributeChangedCallback, aby zsynchronizować właściwość JS z jej atrybutem.

Uaktualnienia elementów

Stopniowo ulepszany kod HTML

Nauczyliśmy się już, że elementy niestandardowe definiuje się, wywołując metodę customElements.define(). Nie oznacza to jednak, że musisz od razu definiować i rejestrować element niestandardowy.

Elementy niestandardowe można stosować przed zarejestrowaniem ich definicji.

Stopniowe ulepszanie to funkcja elementów niestandardowych. Oznacza to, że możesz zadeklarować na stronie kilka elementów <app-drawer> i nie wywoływać metody customElements.define('app-drawer', ...) dużo później. Dzieje się tak, ponieważ przeglądarka traktuje potencjalne elementy niestandardowe inaczej z powodu nieznanych tagów. Proces wywoływania metody define() i nadawania istniejącemu elementowi definicji klasy nazywa się „uaktualnienia elementów”.

Aby dowiedzieć się, kiedy nazwa tagu zostanie zdefiniowana, możesz użyć parametru window.customElements.whenDefined(). Zwraca obietnicę, która przestaje obowiązywać po zdefiniowaniu elementu.

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

Przykład – opóźnienie działania do czasu uaktualnienia zestawu elementów podrzędnych

<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.
});

Treści zdefiniowane przez elementy

Elementy niestandardowe mogą zarządzać własną treścią za pomocą interfejsów DOM API w kodzie elementu. Przydatne w tym celu są reakcje.

Przykład – tworzenie elementu z domyślnym kodem HTML:

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

Zadeklarowanie tego tagu spowoduje:

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

// DO ZROBIENIA: DevSite – usunięto przykładowy kod, ponieważ używał on wbudowanych modułów obsługi zdarzeń

Tworzenie elementu korzystającego z modelu Shadow DOM

Shadow DOM umożliwia elementowi posiadanie, renderowanie i określanie stylu fragmentu DOM, który jest oddzielony od reszty strony. Możesz nawet ukryć całą aplikację za pomocą jednego tagu:

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

Aby użyć modelu Shadow DOM w elemencie niestandardowym, wywołaj this.attachShadow w constructor:

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));
  }
  // ...
});

Przykład użycia:

<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>

Tekst niestandardowy użytkownika

// DO ZROBIENIA: DevSite – usunięto przykładowy kod, ponieważ używał on wbudowanych modułów obsługi zdarzeń

Tworzenie elementów na podstawie: <template>

Dla osób, których nie znasz, element <template> umożliwia zadeklarowanie fragmentów DOM, które są analizowane, obojętne podczas wczytywania strony i mogą zostać aktywowane później w czasie działania. To kolejny element podstawowy interfejsu API w rodzinie komponentów sieciowych. Szablony są idealnym obiektem zastępczym do deklarowania struktury elementu niestandardowego.

Przykład: rejestrowanie elementu z zawartością Shadow DOM utworzoną na podstawie <template>:

<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>

Wystarczy kilka linijek kodu. Omówmy teraz najważniejsze kwestie:

  1. Definiujemy nowy element w kodzie HTML: <x-foo-from-template>
  2. Model Shadow DOM elementu jest tworzony na podstawie <template>
  3. DOM elementu jest lokalnym elementem dzięki modelowi Shadow DOM.
  4. Dzięki interfejsowi Shadow DOM wewnętrzny CSS elementu jest ograniczony do tego elementu.

Jestem w Shadow DOM. Moje znaczniki zostały oznaczone tagiem <template>.

// DO ZROBIENIA: DevSite – usunięto przykładowy kod, ponieważ używał on wbudowanych modułów obsługi zdarzeń

Wybieranie stylu elementu niestandardowego

Nawet jeśli Twój element ma własny styl za pomocą modelu Shadow DOM, użytkownicy mogą na swojej stronie dobrać styl do tego elementu. Są to tzw. „style zdefiniowane przez użytkownika”.

<!-- 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>

Możesz zadawać sobie pytanie, jak działa specyfika CSS, jeśli element ma style zdefiniowane w modelu Shadow DOM. Jeśli chodzi o szczegółowość, style użytkownika są najlepsze. Zawsze zastępują one styl zdefiniowany przez element. Zapoznaj się z sekcją poświęconą tworzeniu elementu korzystającego z modelu Shadow DOM.

Wstępne stylizowanie niezarejestrowanych elementów

Zanim element zostanie uaktualniony, możesz kierować na niego reklamy w CSS za pomocą pseudoklasy :defined. Jest to przydatne podczas wstępnego określania stylu komponentu. Możesz na przykład zapobiec układowi lub innym wizualnym elementom FOUC, ukrywając niezdefiniowane komponenty i zanikając, gdy zostaną zdefiniowane.

Przykład – ukryj pole <app-drawer>, zanim zostanie zdefiniowane:

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;
}

Po zdefiniowaniu atrybutu <app-drawer> selektor (app-drawer:not(:defined)) przestanie być zgodny.

Elementy rozszerzające

Interfejs Custom Elements API jest przydatny do tworzenia nowych elementów HTML, ale jest też przydatny do rozszerzania innych elementów niestandardowych, a nawet do wbudowanego kodu HTML przeglądarki.

Rozszerzanie elementu niestandardowego

Rozszerzanie innego elementu niestandardowego odbywa się przez rozszerzenie definicji jego klasy.

Przykład – utwórz <fancy-app-drawer> z rozszerzeniem <app-drawer>:

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);

Rozszerzanie natywnych elementów HTML

Załóżmy, że chcesz utworzyć bardziej elegancki <button>. Zamiast replikować działanie i funkcjonalność <button>, lepszą opcją jest stopniowe ulepszanie istniejącego elementu za pomocą elementów niestandardowych.

Niestandardowy element wbudowany to element niestandardowy, który stanowi rozszerzenie jednego z wbudowanych tagów HTML przeglądarki. Główną korzyścią wynikającą z rozszerzania istniejącego elementu jest dostęp do wszystkich jego funkcji (właściwości DOM, metody, ułatwienia dostępu). Nie ma lepszego sposobu na pisanie progresywnej aplikacji internetowej niż stopniowe ulepszanie istniejących elementów HTML.

Aby rozszerzyć element, należy utworzyć definicję klasy dziedziczoną z właściwego interfejsu DOM. Na przykład element niestandardowy, który rozszerza zakres <button>, musi dziedziczyć z elementu HTMLButtonElement, a nie HTMLElement. Podobnie element rozszerzający <img> musi mieć rozszerzenie HTMLImageElement.

Przykład – rozszerzenie <button>:

// 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'});

Zwróć uwagę, że wywołanie define() nieco się zmienia podczas rozszerzania elementu natywnego. Wymagany trzeci parametr informuje przeglądarkę, który tag rozszerzasz. Jest to konieczne, ponieważ wiele tagów HTML ma ten sam interfejs DOM. <section>, <address> i <em> (między innymi) udostępniają te dane: HTMLElement, <q> i <blockquote> udostępniają HTMLQuoteElement itd. Gdy określisz wartość {extends: 'blockquote'}, przeglądarka będzie wiedzieć, że zamiast <q> tworzysz podsumowanie <blockquote>. Pełną listę interfejsów DOM znajdziesz w specyfikacji HTML.

Konsumenci niestandardowego elementu wbudowanego mogą go używać na kilka sposobów. Mogą ją zadeklarować, dodając atrybut is="" w tagu natywnym:

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

utwórz instancję w JavaScript:

// 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);

albo użyj operatora new:

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

Oto kolejny przykład z rozszerzeniem <img>.

Przykład – rozszerzenie <img>:

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'});

Użytkownicy deklarują ten komponent jako:

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

lub utwórz wystąpienie w skrypcie 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);

Różne szczegóły

Nieznane elementy a niezdefiniowane elementy niestandardowe

Format HTML jest łatwy i elastyczny. Na przykład zadeklaruj na stronie właściwość <randomtagthatdoesntexist>, a przeglądarka z przyjemnością ją zaakceptuje. Dlaczego działają niestandardowe tagi? Odpowiedź brzmi: specyfikacja HTML na to pozwala. Elementy, które nie są zdefiniowane w specyfikacji, są analizowane jako HTMLUnknownElement.

To samo dotyczy elementów niestandardowych. Potencjalne elementy niestandardowe są analizowane jako HTMLElement, jeśli zostały utworzone z prawidłową nazwą (zawierającą znak „-”). Możesz to sprawdzić w przeglądarce, która obsługuje elementy niestandardowe. Uruchom konsolę: Ctrl+Shift+J (lub Cmd+Opt+J na Macu) i wklej te wiersze kodu:

// "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

Dokumentacja API

Global customElements definiuje przydatne metody pracy z elementami niestandardowymi.

define(tagName, constructor, options)

Definiuje nowy element niestandardowy w przeglądarce.

Przykład

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

get(tagName)

W przypadku prawidłowej nazwy tagu elementu niestandardowego zwraca konstruktor elementu. Zwraca wartość undefined, jeśli nie zarejestrowano żadnej definicji elementu.

Przykład

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

whenDefined(tagName)

Zwraca obietnicę, która przestaje obowiązywać po zdefiniowaniu elementu niestandardowego. Jeśli element jest już zdefiniowany, rozwiąż problem od razu. Odrzuca, jeśli nazwa tagu nie jest prawidłową nazwą elementu niestandardowego.

Przykład

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

Obsługa historii i przeglądarki

Jeśli korzystasz z komponentów internetowych od kilku lat, wiesz, że w Chrome 36 i nowszych wersjach zaimplementowany został interfejs Custom Elements API w wersji, która używa interfejsu document.registerElement() zamiast customElements.define(). Obecnie uważamy to za wycofaną wersję standardu o nazwie v0. customElements.define() to nowa popularna funkcja i informacje o tym, jakie przeglądarki zaczynają wdrażać. Nosi on nazwę Elementy niestandardowe w wersji 1.

Jeśli interesuje Cię stara specyfikacja w wersji 0, przeczytaj artykuł o HTML5rocks.

Obsługiwane przeglądarki

Chrome 54 (stan), Safari 10.1 (stan) i Firefox 63 (stan) mają elementy niestandardowe w wersji 1. Rozpoczęliśmy pracę nad Edge.

Aby wykrywać elementy niestandardowe, sprawdź, czy istnieje element window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Włókno poliestrowe

Dopóki obsługa przeglądarek nie stanie się powszechnie dostępna, dla elementów niestandardowych w wersji 1 będzie dostępny samodzielny kod polyfill. Zalecamy jednak korzystanie z modułu ładowania komponentu webcomponents.js, który pozwala optymalnie wczytywać komponenty polyfill z komponentami internetowymi. Moduł ładowania korzysta z wykrywania funkcji, aby asynchronicznie wczytywać tylko te pytania, które są niezbędne w przeglądarce.

Zainstaluj ją:

npm install --save @webcomponents/webcomponentsjs

Sposób użycia:

<!-- 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>

Podsumowanie

Elementy niestandardowe dają nam nowe narzędzie do definiowania nowych tagów HTML w przeglądarce i tworzenia komponentów wielokrotnego użytku. Po połączeniu ich z innymi nowymi platformami podstawowymi, takimi jak Shadow DOM i <template>, zaczynamy zauważać wspaniały obraz komponentów sieciowych:

  • Działanie w różnych przeglądarkach (standard internetowy) do tworzenia i rozszerzania komponentów wielokrotnego użytku.
  • Aby rozpocząć, nie musisz mieć żadnej biblioteki ani platformy. Wanilla JS/HTML!
  • Zawiera znajomy model programowania. To tylko DOM/CSS/HTML.
  • Sprawdza się w przypadku innych nowych funkcji platformy internetowej (Shadow DOM, <template>, niestandardowych właściwości CSS itp.).
  • Ścisła integracja z Narzędziami deweloperskimi w przeglądarce.
  • Korzystanie z istniejących ułatwień dostępu.