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 sieciowe wielokrotnego użytku.

Dzięki elementom niestandardowym deweloperzy stron internetowych mogą tworzyć nowe tagi HTML, dodawać istniejące tagi HTML i rozszerzać komponenty opracowane przez innych deweloperów. Interfejs API jest podstawą komponentów internetowych. Umożliwia ona tworzenie komponentów wielokrotnego użytku na podstawie standardów internetowych, korzystając wyłącznie z podstawowych wersji języków JS/HTML/CSS. W efekcie mamy mniej kodu, kod modułowy i częstsze ponowne używanie kodu w naszych aplikacjach.

Wprowadzenie

Przeglądarka jest doskonałym narzędziem do strukturyzowania aplikacji internetowych. Nazywa się on HTML. Być może już o tym słyszeliście. Jest to usługa deklaratywna, przenośna, dobrze obsługiwana i łatwa w obsłudze. Mimo że HTML jest świetnym językiem, jego słownictwo i rozszerzalność są ograniczone. Standard HTML zawsze nie miał sposobu na automatyczne powiązanie zachowania JavaScriptu z oznacznikiem. Do tej pory.

Elementy niestandardowe to odpowiedź na potrzebę modernizacji HTML, uzupełnienie brakujących elementów i połączenie struktury z zachowaniem. Jeśli HTML nie rozwiązuje problemu, możemy utworzyć element niestandardowy, który to zrobi. Elementy niestandardowe uczą przeglądarkę nowych sztuczek, zachowując przy tym zalety HTML.

Definiowanie nowego elementu

Aby zdefiniować nowy element HTML, potrzebujemy mocy JavaScriptu.

Tag globalny customElements służy do definiowania elementu niestandardowego i informowania przeglądarki o nowym tagu. Wywołaj funkcję customElements.define() z nazwą tagu, który chcesz utworzyć, oraz kod JavaScript class, który rozszerza podstawowy kod HTMLElement.

Przykład – definiowanie mobilnego panelu szuflady, <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 użycie elementu niestandardowego nie różni się od użycia elementu <div> ani żadnego innego. Identyfikatory mogą być deklarowane na stronie, tworzone dynamicznie w JavaScript, dołączane do zdarzeń itp. Poniżej znajdziesz więcej przykładów.

Definiowanie interfejsu JavaScript API elementu

Funkcjonalność elementu niestandardowego jest definiowana za pomocą elementu ES2015 class, który rozszerza element HTMLElement. Rozszerzenie klasy HTMLElement zapewnia, że element niestandardowy dziedziczy cały interfejs DOM API, a właściwości i metody dodane do klasy stają się częścią interfejsu DOM elementu. Musisz użyć klasy, by utworzyć publiczny interfejs API JavaScript dla swojego tagu.

Przykład: definiowanie interfejsu DOM elementu <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ę, która ma właściwość open, właściwość disabled i metodę toggleDrawer(). Odzwierciedla ona właściwości jako atrybuty HTML.

Ciekawa funkcja elementów niestandardowych polega na tym, że this w definicji klasy odnosi się do samego elementu DOM, czyli instancji klasy. W naszym przykładzie this odnosi się do <app-drawer>. W ten sposób (😉) element może dołączyć do siebie samego obiekt click listener. Nie musisz się ograniczać do detektorów zdarzeń. Cały interfejs DOM API jest dostępny w kodzie elementu. Aby uzyskać dostęp do właściwości elementu, sprawdzić jego elementy podrzędne (this.children), zapytać węzły (this.querySelectorAll('.items')) itp., użyj funkcji this.

Zasady tworzenia elementów niestandardowych

  1. Nazwa elementu niestandardowego musi zawierać łącznik (-). Dlatego <x-tags>, <my-element><my-awesome-app> to prawidłowe nazwy, a <tabs><foo_bar> – nie. To wymaganie pozwala parsownikowi HTML odróżnić elementy niestandardowe od zwykłych. Zapewnia to też zgodność do przodu po dodaniu do HTML nowych tagów.
  2. Nie możesz zarejestrować tego samego tagu więcej niż raz. Spróbuj to zrobić, a uzyskasz błąd DOMException. Powiadomienie przeglądarki o nowym tagu wystarczy. Nie przyjmujemy zwrotów.
  3. Elementy niestandardowe nie mogą się samozamykać, ponieważ kod HTML zezwala na samo zamykanie kilku elementów. Zawsze zapisuj zamykający tag (<app-drawer></app-drawer>).

Reakcje na elementy niestandardowe

Element niestandardowy może definiować specjalne uchwyty cyklu życia do uruchamiania kodu w określonych momentach jego istnienia. Są to tzw. reakcje elementu niestandardowego.

Nazwa Wywoływany, gdy
constructor instancja elementu została utworzona lub ulepszona; Jest to przydatne do inicjowania stanu, konfigurowania odbiorników zdarzeń lub tworzenia kopii zapasowej domowego sterownika urządzeń. Zapoznaj się ze specyfikacją , aby poznać ograniczenia dotyczące czynności, które wykonuje constructor.
connectedCallback Jest wywoływany za każdym razem, gdy element jest wstawiany do DOM. Przydaje się do uruchamiania kodu konfiguracji, np. pobierania zasobów lub renderowania. Ogólnie rzecz biorąc, warto odłożyć pracę na ten czas.
disconnectedCallback Wywoływana za każdym razem, gdy element jest usuwany z DOM. przydatne do uruchamiania kodu porządkującego.
attributeChangedCallback(attrName, oldVal, newVal) Wywoływany po dodaniu, usunięciu, zaktualizowaniu lub zastąpieniu obserwowanego atrybutu. Jest też wywoływany w przypadku wartości początkowych, gdy element jest tworzony przez parsowanie lub uaktualniany. Uwaga: ta funkcja jest wywoływana tylko w przypadku atrybutów wymienionych w właściwości observedAttributes.
adoptedCallback Element niestandardowy został przeniesiony do nowego elementu document (np. osoby o nazwie document.adoptNode(el)).

Zwroty wywołania reakcji są synchroniczne. Jeśli ktoś wywoła funkcję el.setAttribute() w Twoim elemencie, przeglądarka natychmiast wywoła funkcję attributeChangedCallback(). Podobnie, gdy element zostanie usunięty z DOM (np. użytkownik wywoła funkcję el.remove()), otrzymasz zdarzenie disconnectedCallback().

Przykład: dodawanie reakcji na element niestandardowy do <app-drawer>:

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

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

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

Określ reakcje, jeśli to ma sens. Jeśli element jest wystarczająco złożony i otwiera połączenie z IndexedDB w connectedCallback(), wykonaj niezbędne działania w narzędziu disconnectedCallback(). Zachowaj jednak ostrożność. W każdej sytuacji nie można polegać na usunięciu elementu z DOM. Na przykład funkcja disconnectedCallback() nigdy nie zostanie wywołana, jeśli użytkownik zamknie kartę.

Właściwości i atrybuty

Odzwierciedlanie właściwości w atrybutach

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

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

wartości są stosowane w aktywności DOM jako atrybuty:

<div id="my-id" hidden>

Nazywa się to „odzwierciedlaniem właściwości w atrybutach”. Właściwości te ma prawie każda usługa w HTML. Dlaczego? Atrybuty są też przydatne do deklaratywnego konfigurowania elementu, a niektóre interfejsy API, takie jak ułatwienia dostępu i selektory arkusza CSS, działają na podstawie atrybutów.

Odzwierciedlanie właściwości jest przydatne wszędzie tam, gdzie chcesz utrzymać zgodność reprezentacji DOM elementu z jego stanem w JavaScriptzie. Jednym z powodów, dla których warto odzwierciedlić właściwość, jest zastosowanie stylizacji zdefiniowanej przez użytkownika po zmianie stanu JS.

Przypomnij sobie: <app-drawer>. Konsumenci tego komponentu mogą chcieć go wyciszyć lub uniemożliwić interakcję użytkownika, gdy jest wyłączony:

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

Gdy właściwość disabled zostanie zmieniona w JS, chcemy, aby ten atrybut został dodany do DOM, aby selektor użytkownika pasował. 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 określenie stanu początkowego:

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

Elementy mogą reagować na zmiany atrybutów przez zdefiniowanie wartości attributeChangedCallback. Przeglądarka wywoła 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> po zmianie atrybutu disabled. Chociaż tutaj tego nie robimy, możesz też użyć funkcji attributeChangedCallback, aby zachować zgodność właściwości JS z jej atrybutem.

Uaktualnienia elementów

HTML z progresywnym ulepszaniem

Wiemy już, że elementy niestandardowe są definiowane przez wywołanie funkcji customElements.define(). Nie oznacza to jednak, że musisz zdefiniować i zarejestrować element niestandardowy za jednym razem.

Elementy niestandardowe można używać przed zarejestrowaniem ich definicji.

Progresywne ulepszanie to funkcja elementów niestandardowych. Innymi słowy, możesz zadeklarować na stronie wiele elementów <app-drawer> i nigdy nie wywoływać customElements.define('app-drawer', ...) aż do późniejszego momentu. Dzieje się tak, ponieważ przeglądarka traktuje potencjalne elementy niestandardowe inaczej dzięki nieznanym elementom. Proces wywoływania funkcji define() i przypisywania istniejącego elementu do definicji klasy nazywa się „uaktualnieniem elementu”.

Aby dowiedzieć się, kiedy nazwa tagu zostanie zdefiniowana, możesz użyć window.customElements.whenDefined(). Zwraca obietnicę, która zostanie spełniona, gdy element zostanie zdefiniowany.

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

Przykład: opóźnienie pracy 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 element

Elementy niestandardowe mogą zarządzać swoimi treściami za pomocą interfejsów DOM API w kodzie elementu. W tym przypadku przydatne są reakcje.

Przykład – utwórz element za pomocą domyślnego kodu 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 – przykładowy kod usunięty, ponieważ użyto wbudowanych modułów obsługi zdarzeń

Tworzenie elementu, który korzysta z shadow DOM

Model Shadow DOM umożliwia elementowi posiadanie, renderowanie i stylizowanie fragmentu modelu DOM, który jest oddzielony od reszty strony. Możesz nawet ukryć całą aplikację w jednym tagu:

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

Aby użyć Shadow DOM w komponencie niestandardowym, wywołaj funkcję this.attachShadow w komponencie 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>

Użytkownik: tekst niestandardowy

// TODO: DevSite - Code sample removed as it used inline event handlers

Tworzenie elementów z <template>

Jeśli nie wiesz, jak to zrobić, element <template> umożliwia zadeklarowanie fragmentów DOM, które są analizowane, obojętne podczas wczytywania strony i można je aktywować później w czasie działania. Jest to kolejny element API w rodzaju komponentów internetowych. Szablony to idealne miejsce na zadeklarowanie 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>

Te kilka linijek kodu to nie lada wyzwanie. Poznaj najważniejsze informacje:

  1. Definiujemy nowy element w kodzie HTML: <x-foo-from-template>
  2. Model Shadow DOM elementu jest tworzony z poziomu <template>
  3. DOM elementu jest lokalny dzięki interfejsowi Shadow DOM.
  4. Wewnętrzny kod CSS elementu jest ograniczony do niego dzięki interfejsowi Shadow DOM.

Korzystam z Shadow DOM. Moja sygnatura została dodana za pomocą tagu <template>.

// TODO: DevSite - Code sample removed as it used inline event handlers

Określanie stylu elementu niestandardowego

Nawet jeśli Twój element definiuje własny styl za pomocą DOM cieni, użytkownicy mogą stylizować Twój element niestandardowy na swojej stronie. Są to tak zwane „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 się zastanawiać, jak działa specyficzność CSS, jeśli element ma style zdefiniowane w Shadow DOM. Jeśli chodzi o konkretność, lepsze są style użytkownika. Zawsze będą zastępować styl definiowany przez element. Zapoznaj się z sekcją Tworzenie elementu, który korzysta z shadow DOM.

Wstępne określanie stylu niezarejestrowanych elementów

Zanim element zostanie uaktualniony, możesz go uwzględnić w CSS za pomocą pseudoklasy :defined. Jest to przydatne, gdy chcesz wstępnie nadać styl komponentowi. Możesz na przykład zapobiec wyświetlaniu informacji o zbieraniu danych w ramach elementów wizualnych, ukrywając niezdefiniowane komponenty i pokazując je, gdy zostaną zdefiniowane.

Przykład: ukryj element <app-drawer>, zanim zostanie zdefiniowany:

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

Gdy <app-drawer> zostanie zdefiniowany, selektor (app-drawer:not(:defined)) nie będzie już pasować.

Rozwijanie elementów

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

Rozszerzanie elementu niestandardowego

Kolejny element niestandardowy można rozszerzyć przez rozszerzenie jego definicji klasy.

Przykład: utwórz klasę <fancy-app-drawer> dziedziczącą z klasy <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);

Rozciąganie natywnych elementów HTML

Załóżmy, że chcesz utworzyć bardziej rozbudowany <button>. Zamiast powielać zachowanie i funkcje komponentu <button>, lepiej stopniowo ulepszać istniejący element za pomocą elementów niestandardowych.

Niestandardowy wbudowany element to element niestandardowy, który rozszerza jeden z wbudowanych tagów HTML przeglądarki. Główną zaletą rozszerzania istniejącego elementu jest możliwość korzystania ze 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, musisz utworzyć definicję klasy, która dziedziczy dane z prawidłowego interfejsu DOM. Na przykład element niestandardowy z rozszerzeniem <button> musi dziedziczyć element z elementu HTMLButtonElement, a nie HTMLElement. Podobnie element, który rozszerza element <img>, musi rozszerzać element HTMLImageElement.

Przykład: rozszerzanie <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'});

Pamiętaj, że wywołanie funkcji define() zmienia się nieznacznie, gdy rozszerzasz element natywny. Wymagany trzeci parametr informuje przeglądarkę, który tag rozszerzasz. Jest to konieczne, ponieważ wiele tagów HTML korzysta z tego samego interfejsu DOM. <section>, <address><em> (między innymi) mają wspólny adres HTMLElement; <q><blockquote> mają wspólny adres HTMLQuoteElement; itd. Określenie {extends: 'blockquote'} informuje przeglądarkę, że tworzysz rozszerzoną wersję <blockquote> zamiast <q>. Pełna lista interfejsów DOM HTML znajduje się w specyfikacji HTML.

Użytkownicy niestandardowego elementu wbudowanego mogą go używać na kilka sposobów. Mogą to zrobić, dodając atrybut is="" do tagu natywnego:

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

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

lub użyj operatora new:

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

Oto kolejny przykład rozszerzenia <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 instancję w 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);

Szczegóły

Elementy nieznane a niezdefiniowane elementy niestandardowe

HTML jest elastyczny i łatwy w użyciu. Jeśli na przykład zadeklarujesz <randomtagthatdoesntexist> na stronie, przeglądarka będzie mogła go zaakceptować. Dlaczego tagi niestandardowe działają? Odpowiedź brzmi: dozwolone są specyfikacje HTML. Elementy, które nie są zdefiniowane w specyfikacji, są parsowane jako HTMLUnknownElement.

Nie dotyczy to 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 obsługującej 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

Element globalny customElements definiuje przydatne metody do 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)

Zwraca konstruktor elementu na podstawie prawidłowej nazwy tagu elementu niestandardowego. Zwraca undefined, jeśli nie została zarejestrowana żadna definicja elementu.

Przykład

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

whenDefined(tagName)

Zwraca obietnicę, która zostanie spełniona po zdefiniowaniu elementu niestandardowego. Jeśli element jest już zdefiniowany, rozwiąż problem natychmiast. Odrzucenie, 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 od kilku lat obserwujesz komponenty internetowe, wiesz, że w Chrome w wersji 36 i nowszych zaimplementowano interfejs API Custom Elements API w wersji, która używa interfejsu document.registerElement() zamiast customElements.define(). Jest ona teraz uważana za przestarzałą wersję standardu, zwaną wersją 0. customElements.define() to nowy trend, który zaczynają wdrażać dostawcy przeglądarek. Nazywamy ją „Elementy niestandardowe w wersji 1”.

Jeśli interesuje Cię specyfikacja w wersji 0, przeczytaj artykuł na temat HTML5.

Obsługa przeglądarek

Chrome 54 (stan), Safari 10.1 (stan) i Firefox 63 (stan) mają elementy niestandardowe w wersji 1. Edge rozpoczął się rozwijać.

Aby wykrywać elementy niestandardowe, sprawdź, czy występują: window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Watolina

Dopóki obsługa w przeglądarkach nie będzie powszechna, w przypadku elementów niestandardowych w wersji 1 dostępna jest samodzielna implementacja zastępcza. Zalecamy jednak korzystanie z ładowarki webcomponents.js, aby optymalnie wczytywać polyfille komponentów internetowych. Ładowarka używa wykrywania funkcji, aby asynchronicznie wczytywać tylko te komponenty pollyfill, których potrzebuje przeglądarka.

Zainstaluj:

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 to nowe narzędzie do definiowania nowych tagów HTML w przeglądarce i tworzenia komponentów wielokrotnego użytku. W połączeniu z innymi podstawowymi elementami platformy, takimi jak Shadow DOM czy <template>, możemy uzyskać bardzo efektowny obraz komponentów sieciowych:

  • Wspieranie tworzenia i rozszerzania komponentów wielokrotnego użytku w różnych przeglądarkach (standard internetowy).
  • Nie wymaga żadnej biblioteki ani platformy. Vanilla JS/HTML FTW!
  • Udostępnia znajomy model programowania. To tylko DOM/CSS/HTML.
  • Dobrze współpracuje z innymi nowymi funkcjami platform internetowych (Shadow DOM, <template>, właściwości niestandardowe w CSS itp.).
  • Ścisła integracja z Narzędziami deweloperskimi w przeglądarce.
  • Wykorzystaj istniejące ułatwienia dostępu.