Пользовательские элементы v1 – многоразовые веб-компоненты

Пользовательские элементы позволяют веб-разработчикам определять новые теги HTML, расширять существующие и создавать повторно используемые веб-компоненты.

С помощью пользовательских элементов веб-разработчики могут создавать новые теги HTML , расширять существующие теги HTML или расширять компоненты, созданные другими разработчиками. API — это основа веб-компонентов . Он предлагает основанный на веб-стандартах способ создания повторно используемых компонентов, используя не что иное, как ванильный JS/HTML/CSS. В результате получается меньше кода, модульный код и больше повторного использования в наших приложениях.

Введение

Браузер предоставляет нам отличный инструмент для структурирования веб-приложений. Это называется HTML. Возможно, вы слышали об этом! Он декларативный, портативный, хорошо поддерживается и с ним легко работать. Каким бы прекрасным ни был HTML, его словарный запас и расширяемость ограничены. В стандарте жизни HTML всегда отсутствовал способ автоматически связать поведение JS с вашей разметкой… до сих пор.

Пользовательские элементы — это ответ на модернизацию HTML, заполнение недостающих частей и объединение структуры с поведением. Если HTML не обеспечивает решение проблемы, мы можем создать специальный элемент, который это сделает. Пользовательские элементы учат браузер новым трюкам, сохраняя при этом преимущества HTML .

Определение нового элемента

Чтобы определить новый элемент HTML, нам нужны возможности JavaScript!

Глобальный customElements используется для определения пользовательского элемента и обучения браузера новому тегу. Вызовите customElements.define() указав имя тега, который вы хотите создать, и class JavaScript, расширяющий базовый HTMLElement .

Пример : определение панели мобильного ящика <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 {...});

Пример использования:

<app-drawer></app-drawer>

Важно помнить, что использование пользовательского элемента ничем не отличается от использования <div> или любого другого элемента. Экземпляры могут быть объявлены на странице, созданы динамически в JavaScript, могут быть прикреплены прослушиватели событий и т. д. Продолжайте читать, чтобы увидеть больше примеров.

Определение JavaScript API элемента

Функциональность пользовательского элемента определяется с помощью class ES2015, который расширяет HTMLElement . Расширение HTMLElement гарантирует, что пользовательский элемент наследует весь API DOM и означает, что любые свойства/методы, которые вы добавляете в класс, становятся частью интерфейса DOM элемента. По сути, используйте этот класс для создания общедоступного API JavaScript для вашего тега.

Пример — определение интерфейса DOM <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);

В этом примере мы создаем ящик, который имеет свойство open , свойство disabled и метод toggleDrawer() . Он также отражает свойства как атрибуты HTML .

Отличительной особенностью пользовательских элементов является то, что this внутри определения класса относится к самому элементу DOM, то есть к экземпляру класса. В нашем примере this относится к <app-drawer> . Вот так (😉) элемент может присоединить к себе прослушиватель click ! И вы не ограничены прослушивателями событий. Весь DOM API доступен внутри кода элемента. Используйте this для доступа к свойствам элемента, проверки его дочерних элементов ( this.children ), узлов запроса ( this.querySelectorAll('.items') ) и т. д.

Правила создания пользовательских элементов

  1. Имя пользовательского элемента должно содержать дефис (-) . Таким образом, <x-tags> , <my-element> и <my-awesome-app> — допустимые имена, а <tabs> и <foo_bar> — нет. Это требование необходимо для того, чтобы анализатор HTML мог отличать пользовательские элементы от обычных. Это также обеспечивает совместимость вперед при добавлении новых тегов в HTML.
  2. Вы не можете зарегистрировать один и тот же тег более одного раза. Попытка сделать это приведет к возникновению исключения DOMException . Как только вы сообщите браузеру о новом теге, все. Никаких возвратов.
  3. Пользовательские элементы не могут быть самозакрывающимися, поскольку HTML допускает самозакрытие только нескольких элементов . Всегда пишите закрывающий тег ( <app-drawer></app-drawer> ).

Пользовательские реакции на элементы

Пользовательский элемент может определять специальные перехватчики жизненного цикла для запуска кода в интересные моменты его существования. Это так называемые реакции на пользовательские элементы .

Имя Вызывается, когда
constructor Экземпляр элемента создается или обновляется . Полезно для инициализации состояния, настройки прослушивателей событий или создания теневого домена . См. спецификацию для ограничений на то, что вы можете делать в constructor .
connectedCallback Вызывается каждый раз, когда элемент вставляется в DOM. Полезно для запуска кода настройки, например для получения ресурсов или рендеринга. В общем, стоит постараться отложить работу до этого времени.
disconnectedCallback Вызывается каждый раз, когда элемент удаляется из DOM. Полезно для запуска кода очистки.
attributeChangedCallback(attrName, oldVal, newVal) Вызывается, когда наблюдаемый атрибут был добавлен, удален, обновлен или заменен. Также вызывается для начальных значений, когда элемент создается анализатором или обновляется . Примечание. Этот обратный вызов получат только атрибуты, перечисленные в свойстве observedAttributes .
adoptedCallback Пользовательский элемент был перемещен в новый document (например, в документ с именем document.adoptNode(el) ).

Обратные вызовы реакций являются синхронными . Если кто-то вызывает el.setAttribute() для вашего элемента, браузер немедленно вызовет attributeChangedCallback() . Аналогично, вы получите disconnectedCallback() сразу после удаления вашего элемента из DOM (например, пользователь вызывает el.remove() ).

Пример: добавление реакций пользовательского элемента в <app-drawer> :

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

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

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

Определите реакции, если/когда это имеет смысл. Если ваш элемент достаточно сложен и открывает соединение с IndexedDB в connectedCallback() , выполните необходимую работу по очистке в disconnectedCallback() . Но будьте осторожны! Вы не можете рассчитывать на удаление вашего элемента из DOM при любых обстоятельствах. Например, disconnectedCallback() никогда не будет вызван, если пользователь закроет вкладку.

Свойства и атрибуты

Отражение свойств в атрибуты

Свойства HTML обычно отражают свое значение обратно в DOM в виде атрибута HTML. Например, когда значения hidden или id изменяются в JS:

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

значения применяются к реальному DOM как атрибуты:

<div id="my-id" hidden>

Это называется « отражением свойств в атрибутах ». Почти каждое свойство HTML делает это. Почему? Атрибуты также полезны для декларативной настройки элемента, и некоторые API, такие как специальные возможности и селекторы CSS, полагаются на работу атрибутов.

Отражение свойства полезно везде, где вы хотите синхронизировать представление DOM элемента с его состоянием JavaScript . Одна из причин, по которой вам может потребоваться отразить свойство, заключается в том, что определяемый пользователем стиль применяется при изменении состояния JS.

Вспомните наш <app-drawer> . Потребитель этого компонента может захотеть затемнить его и/или запретить взаимодействие с пользователем, когда он отключен:

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

Когда disabled свойство изменяется в JS, мы хотим, чтобы этот атрибут был добавлен в DOM, чтобы селектор пользователя соответствовал. Элемент может обеспечить такое поведение, отражая значение атрибута с тем же именем:

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

Наблюдение за изменениями атрибутов

Атрибуты HTML — это удобный способ для пользователей объявить исходное состояние:

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

Элементы могут реагировать на изменения атрибутов, определяя attributeChangedCallback . Браузер будет вызывать этот метод при каждом изменении атрибутов, перечисленных в массиве 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.
  }
}

В этом примере мы устанавливаем дополнительные атрибуты в <app-drawer> при изменении disabled атрибута. Хотя здесь мы этого не делаем, вы также можете использовать attributeChangedCallback , чтобы синхронизировать свойство JS с его атрибутом .

Обновления элементов

Прогрессивно улучшенный HTML

Мы уже узнали, что пользовательские элементы определяются путем вызова customElements.define() . Но это не означает, что вам нужно определить + зарегистрировать пользовательский элемент за один раз.

Пользовательские элементы можно использовать до регистрации их определения .

Прогрессивное улучшение — это особенность пользовательских элементов. Другими словами, вы можете объявить группу элементов <app-drawer> на странице и никогда не вызывать customElements.define('app-drawer', ...) до гораздо более позднего времени. Это связано с тем, что браузер по-разному обрабатывает потенциальные пользовательские элементы из-за неизвестных тегов . Процесс вызова define() и присвоения существующему элементу определения класса называется «обновлением элемента».

Чтобы узнать, когда имя тега становится определенным, вы можете использовать window.customElements.whenDefined() . Он возвращает обещание, которое разрешается, когда элемент становится определенным.

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

Пример – отложить работу до обновления набора дочерних элементов

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

Содержимое, определяемое элементом

Пользовательские элементы могут управлять своим собственным содержимым с помощью API-интерфейсов DOM внутри кода элемента. Для этого пригодятся реакции .

Пример . Создайте элемент с HTML-кодом по умолчанию:

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

Объявление этого тега приведет к:

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

// TODO: DevSite – удален пример кода, поскольку в нем использовались встроенные обработчики событий

Создание элемента, использующего Shadow DOM

Shadow DOM предоставляет элементу возможность владеть, отображать и стилизовать часть DOM, отдельную от остальной части страницы. Черт возьми, вы даже можете спрятать все приложение в одном теге:

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

Чтобы использовать Shadow DOM в пользовательском элементе, вызовите this.attachShadow внутри своего 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));
  }
  // ...
});

Пример использования:

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

Пользовательский текст

// TODO: DevSite – удален пример кода, поскольку в нем использовались встроенные обработчики событий

Создание элементов из <template>

Для тех, кто не знаком, элемент <template> позволяет объявлять фрагменты DOM, которые анализируются, инертны при загрузке страницы и могут быть активированы позже во время выполнения. Это еще один примитив API в семействе веб-компонентов. Шаблоны являются идеальным заполнителем для объявления структуры пользовательского элемента .

Пример: регистрация элемента с содержимым Shadow DOM, созданным из <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>

Эти несколько строк кода впечатляют. Давайте разберемся в ключевых моментах:

  1. Мы определяем новый элемент в HTML: <x-foo-from-template>
  2. Shadow DOM элемента создается на основе <template>
  3. DOM элемента является локальным для элемента благодаря Shadow DOM.
  4. Внутренний CSS элемента привязан к элементу благодаря Shadow DOM.

// TODO: DevSite – удален пример кода, поскольку в нем использовались встроенные обработчики событий

Стилизация пользовательского элемента

Даже если ваш элемент определяет свой собственный стиль с помощью Shadow DOM, пользователи могут стилизовать ваш пользовательский элемент со своей страницы. Они называются «пользовательскими стилями».

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

Вы можете спросить себя, как работает специфика CSS, если у элемента есть стили, определенные в Shadow DOM. С точки зрения специфичности выигрывают пользовательские стили. Они всегда будут переопределять стиль, определенный элементом. См. раздел Создание элемента, использующего Shadow DOM .

Предварительное оформление незарегистрированных элементов

Прежде чем элемент будет обновлен, вы можете указать его в CSS, используя псевдокласс :defined . Это полезно для предварительного оформления компонента. Например, вы можете захотеть предотвратить макет или другие визуальные FOUC, скрыв неопределенные компоненты и затенив их, когда они станут определенными.

Пример — скрыть <app-drawer> до его определения:

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

После того, как <app-drawer> становится определенным, селектор ( app-drawer:not(:defined) ) больше не соответствует.

Расширение элементов

API пользовательских элементов полезен для создания новых элементов HTML, а также для расширения других пользовательских элементов или даже встроенного HTML-кода браузера.

Расширение пользовательского элемента

Расширение другого пользовательского элемента осуществляется путем расширения определения его класса.

Пример . Создайте <fancy-app-drawer> , который расширяет <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);

Расширение собственных HTML-элементов

Допустим, вы хотите создать более красивую кнопку <button> . Вместо копирования поведения и функциональности <button> лучшим вариантом будет постепенное улучшение существующего элемента с использованием пользовательских элементов.

Настраиваемый встроенный элемент — это настраиваемый элемент, который расширяет один из встроенных HTML-тегов браузера. Основное преимущество расширения существующего элемента — получение всех его функций (свойств DOM, методов, доступности). Нет лучшего способа написать прогрессивное веб-приложение , чем постепенно улучшать существующие элементы HTML .

Чтобы расширить элемент, вам необходимо создать определение класса, наследуемое от правильного интерфейса DOM. Например, пользовательский элемент, расширяющий <button> должен наследовать от HTMLButtonElement а не от HTMLElement . Аналогично, элемент, расширяющий <img> должен расширять HTMLImageElement .

Пример — расширение <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'});

Обратите внимание, что вызов метода define() немного меняется при расширении собственного элемента. Обязательный третий параметр сообщает браузеру, какой тег вы расширяете. Это необходимо, поскольку многие теги HTML используют один и тот же интерфейс DOM. <section> , <address> и <em> (среди прочих) используют общий HTMLElement ; и <q> , и <blockquote> используют общий HTMLQuoteElement ; и т. д. Указав {extends: 'blockquote'} браузер узнает, что вы создаете усовершенствованный <blockquote> вместо <q> . Полный список интерфейсов HTML DOM см . в спецификации HTML .

Потребители настраиваемого встроенного элемента могут использовать его несколькими способами. Они могут объявить это, добавив атрибут is="" в собственный тег:

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

создайте экземпляр в 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);

или используйте new оператор:

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

Вот еще один пример расширения <img> .

Пример — расширение <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'});

Пользователи объявляют этот компонент как:

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

или создайте экземпляр в 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);

Разные детали

Неизвестные элементы и неопределенные пользовательские элементы

HTML прост и гибок в работе. Например, объявите <randomtagthatdoesntexist> на странице, и браузер с радостью его примет. Почему нестандартные теги работают? Ответ в том, что спецификация HTML позволяет это. Элементы, не определенные спецификацией, анализируются как HTMLUnknownElement .

Этого нельзя сказать о пользовательских элементах. Потенциальные пользовательские элементы анализируются как HTMLElement , если они созданы с допустимым именем (включая «-»). Вы можете проверить это в браузере, который поддерживает пользовательские элементы. Запустите консоль: Ctrl + Shift + J (или Cmd + Opt + J на ​​Mac) и вставьте следующие строки кода:

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

Справочник по API

Глобальный объект customElements определяет полезные методы для работы с пользовательскими элементами.

define(tagName, constructor, options)

Определяет новый пользовательский элемент в браузере.

Пример

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

get(tagName)

Учитывая допустимое имя тега пользовательского элемента, возвращает конструктор элемента. Возвращает undefined , если определение элемента не было зарегистрировано.

Пример

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

whenDefined(tagName)

Возвращает обещание, которое разрешается при определении пользовательского элемента. Если элемент уже определен, разрешите немедленно. Отклоняется, если имя тега не является допустимым именем пользовательского элемента.

Пример

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

Поддержка истории и браузера

Если вы следили за веб-компонентами последние пару лет, вы знаете, что в Chrome 36+ реализована версия API пользовательских элементов, которая использует document.registerElement() вместо customElements.define() . Сейчас это считается устаревшей версией стандарта, называемой v0. customElements.define() — это новая мода, которую производители браузеров начинают внедрять. Он называется Custom Elements v1.

Если вас заинтересовала старая спецификация v0, прочтите статью о html5rocks .

Поддержка браузера

Chrome 54 ( статус ), Safari 10.1 ( статус ) и Firefox 63 ( статус ) имеют пользовательские элементы v1. Edge начал разработку .

Чтобы функция обнаруживала пользовательские элементы, проверьте наличие window.customElements :

const supportsCustomElementsV1 = 'customElements' in window;

Полифилл

Пока поддержка браузеров не станет широко доступной, для Custom Elements v1 доступен отдельный полифил . Однако мы рекомендуем использовать загрузчик webcomComponents.js для оптимальной загрузки полифилов веб-компонентов. Загрузчик использует обнаружение функций для асинхронной загрузки только необходимых заполнений, требуемых браузером.

Установите его:

npm install --save @webcomponents/webcomponentsjs

Использование:

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

Заключение

Пользовательские элементы дают нам новый инструмент для определения новых HTML-тегов в браузере и создания повторно используемых компонентов. Объедините их с другими примитивами новой платформы, такими как Shadow DOM и <template> , и мы начнем понимать грандиозную картину веб-компонентов:

  • Кроссбраузерность (веб-стандарт) для создания и расширения повторно используемых компонентов.
  • Для начала не требуется никакой библиотеки или фреймворка. Ванильный JS/HTML FTW!
  • Предоставляет знакомую модель программирования. Это просто DOM/CSS/HTML.
  • Хорошо работает с другими новыми функциями веб-платформы (Shadow DOM, <template> , пользовательские свойства CSS и т. д.).
  • Тесно интегрирован с DevTools браузера.
  • Используйте существующие функции доступности.