This page was translated from English by the community. Learn more and join the MDN Web Docs community.

View in English Always switch to English

Использование пользовательских элементов

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

Примечание: Пользовательские элементы поддерживаются по умолчанию в Firefox, Chrome и Opera. Safari пока поддерживает только автономные пользовательские компоненты, Edge также работает над реализацией.

Высокоуровневый обзор

Контроллером пользовательских элементов веб-документа является объект CustomElementRegistry — этот элемент позволяет регистрировать пользовательские элементы на веб-странице, возвращает информацию о зарегистрированных элементах и т.п.

Чтобы зарегистрировать пользовательский элемент на странице, используйте метод CustomElementRegistry.define() method. Он принимает аргументы:

  • DOMString - имя элемента. Обратите внимание, что в именах пользовательских элементах должен содержаться дефис; они не могут состоять только из одного слова.
  • Объект типа class, определяющий поведение элемента.
  • Опционально объект options, имеющий свойство extends, соответствующее встроенному элементу, от которого наследует объект.

К примеру, мы можем определить пользовательский элемент word-count element:

js
customElements.define("word-count", WordCount, { extends: "p" });

Этот элемент называется word-count, объект соответствующего класса называется WordCount, и он наследует элементу <p>.

Объект класса пользовательского элемента определяется с помощью синтаксиса классов ES 2015. Например, WordCount имеют следующую структуру:

js
class WordCount extends HTMLParagraphElement {
  constructor() {
    // Всегда первым делом вызывайте super() в конструкторе
    super();

    // Далее пишется функциональность элемента

    ...
  }
}

Это простой пример, но его можно дополнить. Можно определить специальные lifecycle callbacks, которые будут вызваны в определённые моменты жизненного цикла элемента. Например, connectedCallback будет вызван, когда пользовательский элемент оказывается впервые встроен в DOM, а attributeChangedCallback вызывается, когда пользовательскому элементу добавляют, удаляют или изменяют какой-то атрибут.

Подробнее об этом в секции Using the lifecycle callbacks ниже.

Есть два типа пользовательских элементов:

  • Автономные пользовательские элементы независимы — они не наследуют встроенным HTML-элементам. Их используют на странице просто как обычный HTML-элемент. Например, <popup-info> или document.createElement("popup-info").
  • Модифицированные встроенные элементы наследуют базовым HTML-элементам. Чтобы создать один из них, вы определяете элемент, от которого они унаследованы (как в примерах выше), и используете их как обычный базовый элемент, но с добавлением атрибута/свойства с именем пользовательского элемента is. Например <p is="word-count">, или document.createElement("p", { is: "word-count" }).

Разбор простых примеров

А сейчас давайте разберём ещё несколько простых примеров, иллюстрирующих подробности создания пользовательских элементов.

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

Рассмотрим пример автономного пользовательского элемента — <popup-info-box> (см. работающий пример). Он содержит изображение и строку, и встраивает изображение в страницу. Когда на изображение наводят фокус, компонент показывает всплывающую подсказку с текстом.

Прежде всего файл JavaScript определяет класс PopUpInfo, наследующий от HTMLElement. Автономные пользовательские элементы почти всегда наследуют HTMLElement.

js
class PopUpInfo extends HTMLElement {
  constructor() {
    // Всегда первым делом вызывайте super() в конструкторе
    super();

    // далее следует функциональность элемента

    ...
  }
}

В этом фрагменте кода содержится определение конструктора constructor класса, которое всегда начинается с вызова super() чтобы отработала цепочка прототипного наследования.

Внутри конструктора мы определяем всю функциональность, которую получит элемент при создании его объекта. В данном случае мы добавляем shadow root к пользовательскому элементу, производим манипуляции с DOM, чтобы создать определённую структуру shadow DOM внутри элемента — которая затем присоединяется к shadow root — и наконец добавляем CSS к shadow root, чтобы задать его стиль.

js
// Создание shadow root
var shadow = this.attachShadow({ mode: "open" });

// Создание spans
var wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
var icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
var info = document.createElement("span");
info.setAttribute("class", "info");

// Берём содержимое атрибута и добавляем его в span
var text = this.getAttribute("text");
info.textContent = text;

// Вставляем иконку
var imgUrl;
if (this.hasAttribute("img")) {
  imgUrl = this.getAttribute("img");
} else {
  imgUrl = "img/default.png";
}
var img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);

// Создаём CSS для shadow dom
var style = document.createElement("style");

style.textContent =
  ".wrapper {" +
  // CSS truncated for brevity

  // добавляем созданные элементы к shadow dom

  shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);

Наконец, регистрируем пользовательский элемент в CustomElementRegistry с помощью метода define(), который упоминался ранее — в качестве параметров мы передаём ему имя элемента и имя класса, который содержит его функциональность:

js
customElements.define("popup-info", PopUpInfo);

Теперь он доступен для использования на нашей странице. В HTML мы используем его так:

html
<popup-info
  img="img/alt.png"
  text="Код валидации вашей карты (CVC)
  это дополнительная мера безопасности — это последние 3 или 4 цифры
  на обороте вашей карты."></popup-info>

Примечание: Вы можете прочитать полный исходный код на JavaScript здесь.

Модифицированные встроенные элементы

Теперь давайте взглянем на другой пример модифицированного пользовательского элемента — раскрывающийся список (см. действующий пример). Он превращает любой ненумерованный список в раскрывающееся/складывающееся меню.

Первым делом определим класс элемента наподобие того, как это делалось выше:

js
class ExpandingList extends HTMLUListElement {
  constructor() {
    // Всегда первым делом вызываем super() в конструкторе
    super();

    // ниже следует функциональность элемента

    ...
  }
}

Здесь мы не будем во всех подробностях описывать функциональность элемента, вы можете понять как он работает, посмотрев исходный код. Единственное принципиальное различие с предыдущим примером состоит в том, что мы используем интерфейс HTMLUListElement, а не HTMLElement. Так что у него есть все характеристики элемента <ul>, плюс дополнительная функциональность, которую определили мы. Это и отличает модифицированный встроенный элемент от автономного пользовательского элемента.

Далее мы регистрируем этот элемент с помощью метода define() как в прошлом примере, только на сей раз мы добавляем объект options, который определяет, какому встроенному элементу наследует данный:

js
customElements.define("expanding-list", ExpandingList, { extends: "ul" });

Встроенный элемент используется на веб-странице немного по-другому:

html
<ul is="expanding-list">
  ...
</ul>

Вы задаёте элемент <ul> как обычно, но указываете имя модифицированного элемента в атрибуте is.

Примечание: Полный исходный код на JavaScript доступен здесь.

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

Вы можете определить несколько разных колбэков в конструкторе пользовательских элементов, которые сработают на разных этапах жизненного цикла элемента:

  • connectedCallback: Срабатывает, когда пользовательский элемент впервые добавляется в DOM.
  • disconnectedCallback: Срабатывает, когда пользовательский элемент удаляется из DOM.
  • adoptedCallback: Срабатывает, когда пользовательский элемент перемещён в новый документ.
  • attributeChangedCallback: Срабатывает, когда пользовательскому элементу добавляют, удаляют или изменяют атрибут.

Посмотрим на них в действии. Код ниже взят из примера life-cycle-callbacks (см. его в действии). Это тривиальный пример, создающий на странице цветной квадрат. Вот как выглядит код пользовательского элемента:

html
<custom-square l="100" c="red"></custom-square>

Конструктор класса очень простой — мы просто добавляем shadow DOM к элементу, а затем добавляем пустые элементы <div> и <style> к shadow root:

js
var shadow = this.attachShadow({ mode: "open" });

var div = document.createElement("div");
var style = document.createElement("style");
shadow.appendChild(style);
shadow.appendChild(div);

Наиболее важная функция в этом примере updateStyle() — она принимает элемент, находит его shadow root, находит его элемент <style>, и добавляет width, height, и background-color к стилям.

js
function updateStyle(elem) {
  var shadow = elem.shadowRoot;
  var childNodes = shadow.childNodes;
  for (var i = 0; i < childNodes.length; i++) {
    if (childNodes[i].nodeName === "STYLE") {
      childNodes[i].textContent =
        "div {" +
        " width: " +
        elem.getAttribute("l") +
        "px;" +
        " height: " +
        elem.getAttribute("l") +
        "px;" +
        " background-color: " +
        elem.getAttribute("c");
    }
  }
}

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

js
connectedCallback() {
  console.log('Пользовательский элемент квадрат добавлен на страницу.');
  updateStyle(this);
}

колбэки disconnectedCallback() и adoptedCallback() логируют простые сообщения на консоль, которые уведомляют нас, что элемент удалён из DOM или перемещён на другую страницу:

js
disconnectedCallback() {
  console.log('Пользовательский элемент квадрат удален.');
}

adoptedCallback() {
  console.log('Пользовательский элемент квадрат перемещён на другую страницу.');
}

Колбэк attributeChangedCallback() запускается когда один из атрибутов элемента меняется. Как видно из его свойств, можно воздействовать на индивидуальные атрибуты, глядя на их имена, и новые и старые значения атрибутов. В данном случае, однако, мы просто снова запускаем функцию updateStyle() чтобы убедиться, что атрибуты квадрата получили новые значения:

js
attributeChangedCallback(name, oldValue, newValue) {
  console.log('Атрибуты пользовательского элемента квадрат изменились.');
  updateStyle(this);
}

Обратите внимание, что нужно наблюдать за атрибутами, чтобы запустить колбэк attributeChangedCallback() когда они изменятся. Это делается через вызов геттера observedAttributes() в конструкторе, который содержит оператор return возвращающий массив с именами атрибутов, которые вы хотите наблюдать:

js
static get observedAttributes() {return ['w', 'l']; }

В нашем случае он расположен в начале конструктора.

Примечание: Смотрите полный исходный код на JavaScript здесь.