Использование пользовательских элементов
Одним из ключевых свойств веб-компонентов является возможность создавать пользовательские элементы: то есть HTML-элементы, поведение которых определяется веб-разработчиком и которые расширяют набор элементов, доступных в браузере.
В этой статье описываются пользовательские элементы и приводятся некоторые примеры.
Типы пользовательских элементов
Существует два типа пользовательских элементов:
-
Настраиваемые встроенные элементы наследуют от стандартных HTML-элементов, таких как
HTMLImageElementилиHTMLParagraphElement. Их реализация расширяет поведение отдельных экземпляров стандартного элемента.Примечание: Safari не планирует поддерживать настраиваемые встроенные элементы. Для получения дополнительной информации см.
isатрибут. -
Автономные пользовательские элементы наследуют от базового класса HTML-элемента
HTMLElement. Их поведение нужно реализовывать с нуля.
Реализация пользовательского элемента
Пользовательский элемент реализуется как класс, который расширяет HTMLElement (в случае автономных элементов) или интерфейс, который вы хотите настроить (в случае настраиваемых встроенных элементов).
Вот реализация минимального пользовательского элемента, который настраивает элемент <p>:
class WordCount extends HTMLParagraphElement {
constructor() {
super();
}
// Element functionality written in here
}
Вот реализация минимального автономного пользовательского элемента:
class PopupInfo extends HTMLElement {
constructor() {
super();
}
// Element functionality written in here
}
В классе конструктор вы можете настроить начальное состояние и значения по умолчанию, зарегистрировать обработчики событий и, возможно, создать теневой корень. На этом этапе вы не должны проверять атрибуты или дочерние элементы элемента, или добавлять новые атрибуты или дочерние элементы. См. Требования к конструкторам пользовательских элементов и реакциям для полного набора требований.
Обработчики жизненного цикла пользовательского элемента
После регистрации вашего пользовательского элемента браузер будет вызывать определенные методы вашего класса, когда код на странице взаимодействует с вашим пользовательским элементом определенным образом. Предоставив реализацию этих методов, которые спецификация называет обработчиками жизненного цикла, вы можете выполнить код в ответ на эти события.
Обработчики жизненного цикла пользовательских элементов включают:
-
connectedCallback(): вызывается каждый раз, когда элемент добавляется в документ. Спецификация рекомендует, насколько это возможно, реализовывать настройку пользовательского элемента в этом обработчике, а не в конструкторе. -
disconnectedCallback(): вызывается каждый раз, когда элемент удаляется из документа. -
adoptedCallback(): вызывается каждый раз, когда элемент перемещается в новый документ. -
attributeChangedCallback(): вызывается при изменении, добавлении, удалении или замене атрибутов. Дополнительные сведения об этом обработчике см. в разделе Обработка изменений атрибутов.
Вот минимальный пользовательский элемент, который регистрирует эти события жизненного цикла:
// Create a class for the element
class MyCustomElement extends HTMLElement {
static observedAttributes = ["color", "size"];
constructor() {
// Always call super first in constructor
super();
}
connectedCallback() {
console.log("Custom element added to page.");
}
disconnectedCallback() {
console.log("Custom element removed from page.");
}
adoptedCallback() {
console.log("Custom element moved to new page.");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} has changed.`);
}
}
customElements.define("my-custom-element", MyCustomElement);
Регистрация пользовательского элемента
Чтобы сделать пользовательский элемент доступным на странице, вызовите метод define() объекта Window.customElements.
Метод define() принимает следующие аргументы:
name-
Имя элемента. Оно должно начинаться с маленькой буквы, содержать дефис и удовлетворять определенным другим правилам, указанным в определении спецификации допустимого имени в определении допустимого имени.
constructor-
Функция-конструктор пользовательского элемента.
options-
Включается только для настраиваемых встроенных элементов, это объект, содержащий единственное свойство
extends, которое представляет собой строку с именем встроенного элемента для расширения.
Например, этот код регистрирует настраиваемый встроенный элемент WordCount:
customElements.define("word-count", WordCount, { extends: "p" });
Этот код регистрирует автономный пользовательский элемент PopupInfo:
customElements.define("popup-info", PopupInfo);
Использование пользовательского элемента
После определения и регистрации пользовательского элемента вы можете использовать его в своём коде.
Чтобы использовать настраиваемый встроенный элемент, используйте встроенный элемент, но с пользовательским именем в качестве значения атрибута is:
<p is="word-count"></p>
Чтобы использовать автономный пользовательский элемент, используйте пользовательское имя так же, как встроенный HTML-элемент:
<popup-info> <!-- content of the element --> </popup-info>
Обработка изменений атрибутов
Как и встроенные элементы, пользовательские элементы могут использовать HTML-атрибуты для настройки поведения элемента. Для эффективного использования атрибутов элемент должен уметь реагировать на изменения значения атрибута. Для этого пользовательский элемент должен добавить следующие члены в класс, реализующий пользовательский элемент:
- Статическое свойство с именем
observedAttributes. Оно должно быть массивом, содержащим имена всех атрибутов, для которых элемент нуждается в уведомлениях об изменениях. - Реализация обработчика жизненного цикла
attributeChangedCallback().
Обработчик attributeChangedCallback() вызывается каждый раз, когда добавляется, изменяется, удаляется или заменяется атрибут, имя которого указано в свойстве observedAttributes элемента.
Обработчик получает три аргумента:
- Имя изменённого атрибута.
- Старое значение атрибута.
- Новое значение атрибута.
Например, этот автономный элемент будет наблюдать за атрибутом size и регистрировать старые и новые значения при их изменении:
// Create a class for the element
class MyCustomElement extends HTMLElement {
static observedAttributes = ["size"];
constructor() {
super();
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(
`Attribute ${name} has changed from ${oldValue} to ${newValue}.`,
);
}
}
customElements.define("my-custom-element", MyCustomElement);
Обратите внимание, что если в HTML-объявлении элемента указан наблюдаемый атрибут, то attributeChangedCallback() будет вызван после инициализации атрибута, когда элемент впервые анализируется. Таким образом, в следующем примере attributeChangedCallback() будет вызван при разборе DOM, даже если атрибут больше никогда не будет изменён:
<my-custom-element size="100"></my-custom-element>
Полный пример использования attributeChangedCallback(), см. в разделе Обработчики жизненного цикла на этой странице.
Пользовательские состояния и псевдоклассы CSS селекторов пользовательских состояний
Встроенные HTML-элементы могут иметь различные состояния, такие как "наведение курсора", "отключено" и "только чтение". Некоторые из этих состояний могут устанавливаться в качестве атрибутов с помощью HTML или JavaScript, в то время как другие являются внутренними и не могут. Независимо от того, внешние они или внутренние, часто эти состояния имеют соответствующие CSS псевдоклассы, которые можно использовать для выбора и стилизации элемента, когда он находится в определённом состоянии.
Автономные пользовательские элементы (но не элементы, основанные на встроенных элементах) также позволяют определять состояния и выбирать их с помощью функции псевдокласса :state(). Приведённый ниже код демонстрирует, как это работает на примере автономного пользовательского элемента, который имеет внутреннее состояние "collapsed".
Состояние collapsed представлено как булево свойство (с методами установщика и получателя), которое невидимо снаружи элемента. Чтобы сделать это состояние доступным для выбора в CSS, пользовательский элемент сначала вызывает HTMLElement.attachInternals() в своём конструкторе для прикрепления объекта ElementInternals, который, в свою очередь, предоставляет доступ к CustomStateSet через свойство ElementInternals.states. Метод-установщик для (внутреннего) состояния сжатия добавляет идентификатор hidden к CustomStateSet при состоянии true, и удаляет его при состоянии false. Идентификатор — это просто строка: в данном случае мы назвали его hidden, но мы могли бы так же легко назвать его collapsed.
class MyCustomElement extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
get collapsed() {
return this._internals.states.has("hidden");
}
set collapsed(flag) {
if (flag) {
// Existence of identifier corresponds to "true"
this._internals.states.add("hidden");
} else {
// Absence of identifier corresponds to "false"
this._internals.states.delete("hidden");
}
}
}
// Register the custom element
customElements.define("my-custom-element", MyCustomElement);
Мы можем использовать идентификатор, добавленный к CustomStateSet пользовательского элемента (this._internals.states) для сопоставления пользовательского состояния элемента. Это достигается путём передачи идентификатора в псевдокласс :state(). Например, ниже мы выбираем состояние hidden равное true (и, следовательно, состояние элемента collapsed) с помощью селектора :hidden, и удаляем границу.
my-custom-element {
border: dashed red;
}
my-custom-element:state(hidden) {
border: none;
}
Псевдокласс :state() также может использоваться внутри функции псевдокласса :host() для сопоставления пользовательского состояния в теневом DOM пользовательского элемента. Кроме того, псевдокласс :state() может использоваться после псевдоэлемента ::part() для сопоставления теневых частей пользовательского элемента, находящегося в определённом состоянии.
Несколько примеров в CustomStateSet демонстрируют работу с этим.
Примеры
В остальной части этого руководства мы рассмотрим несколько примеров пользовательских элементов. Исходный код всех этих примеров, и многих других, вы можете найти в репозитории web-components-examples, а увидеть их в действии можно по адресу https://mdn.github.io/web-components-examples/.
Автономный пользовательский элемент
Сначала мы рассмотрим автономный пользовательский элемент. Пользовательский элемент <popup-info> принимает значок изображения и строку текста в качестве атрибутов и встраивает значок на страницу. Когда на значок наводится курсор, он отображает текст в всплывающем информационном окне для предоставления дополнительной контекстной информации.
Для начала, файл JavaScript определяет класс под названием PopupInfo, который расширяет класс HTMLElement.
// Create a class for the element
class PopupInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
}
connectedCallback() {
// Create a shadow root
const shadow = this.attachShadow({ mode: "open" });
// Create spans
const wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
const icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
const info = document.createElement("span");
info.setAttribute("class", "info");
// Take attribute content and put it inside the info span
const text = this.getAttribute("data-text");
info.textContent = text;
// Insert icon
let imgUrl;
if (this.hasAttribute("img")) {
imgUrl = this.getAttribute("img");
} else {
imgUrl = "img/default.png";
}
const img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);
// Create some CSS to apply to the shadow dom
const style = document.createElement("style");
console.log(style.isConnected);
style.textContent = `
.wrapper {
position: relative;
}
.info {
font-size: 0.8rem;
width: 200px;
display: inline-block;
border: 1px solid black;
padding: 10px;
background: white;
border-radius: 10px;
opacity: 0;
transition: 0.6s all;
position: absolute;
bottom: 20px;
left: 10px;
z-index: 3;
}
img {
width: 1.2rem;
}
.icon:hover + .info, .icon:focus + .info {
opacity: 1;
}
`;
// Attach the created elements to the shadow dom
shadow.appendChild(style);
console.log(style.isConnected);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
Определение класса содержит конструктор constructor() для класса, который всегда начинается с вызова super(), чтобы установить правильную цепочку прототипов.
Внутри метода connectedCallback(), мы определяем всю функциональность элемента, когда он подключен к DOM. В данном случае мы подключаем теневой корень к пользовательскому элементу, используем манипуляции с DOM для создания внутренней структуры теневого DOM элемента — которая затем прикрепляется к теневому корню — и, наконец, добавляем CSS в теневой корень для его стилизации. Мы не выполняем эту работу в конструкторе, так как атрибуты элемента недоступны до его подключения к DOM.
Наконец, мы регистрируем наш пользовательский элемент в CustomElementRegistry с помощью метода define() , о котором мы упоминали ранее — в параметрах мы указываем имя элемента, а затем имя класса, который определяет его функциональность:
customElements.define("popup-info", PopupInfo);
Теперь он доступен для использования на нашей странице. В нашем HTML мы используем его так:
<popup-info img="img/alt.png" data-text="Your card validation code (CVC) is an extra security feature — it is the last 3 or 4 numbers on the back of your card."></popup-info>
Ссылка на внешние стили
В приведенном выше примере мы применяем стили к теневому DOM с помощью элемента <style>, но вы можете сослаться на внешний документ стилей из элемента <link> вместо этого. В этом примере мы изменим пользовательский элемент <popup-info> на использование внешнего файла стилей.
Вот определение класса:
// Create a class for the element
class PopupInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
}
connectedCallback() {
// Create a shadow root
const shadow = this.attachShadow({ mode: "open" });
// Create spans
const wrapper = document.createElement("span");
wrapper.setAttribute("class", "wrapper");
const icon = document.createElement("span");
icon.setAttribute("class", "icon");
icon.setAttribute("tabindex", 0);
const info = document.createElement("span");
info.setAttribute("class", "info");
// Take attribute content and put it inside the info span
const text = this.getAttribute("data-text");
info.textContent = text;
// Insert icon
let imgUrl;
if (this.hasAttribute("img")) {
imgUrl = this.getAttribute("img");
} else {
imgUrl = "img/default.png";
}
const img = document.createElement("img");
img.src = imgUrl;
icon.appendChild(img);
// Apply external styles to the shadow dom
const linkElem = document.createElement("link");
linkElem.setAttribute("rel", "stylesheet");
linkElem.setAttribute("href", "style.css");
// Attach the created elements to the shadow dom
shadow.appendChild(linkElem);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);
}
}
Это похоже на исходный пример <popup-info>, за исключением того, что мы ссылаемся на внешний файл стилей с помощью элемента <link>, который мы добавляем в теневой DOM.
Обратите внимание, что элементы <link> не блокируют отрисовку теневого корня, поэтому может произойти миг нестилизованного содержимого (FOUC) во время загрузки файла стилей.
Многие современные браузеры реализуют оптимизацию для тегов <style>, которые либо скопированы из общего узла, либо имеют идентичный текст, чтобы они могли использовать один и тот же файл стилей. Благодаря этой оптимизации производительность внешних и внутренних стилей должна быть похожей.
Настраиваемые встроенные элементы
Теперь давайте рассмотрим пример настраиваемого встроенного элемента. Этот пример расширяет встроенный элемент <ul> для поддержки раскрытия и сворачивания элементов списка.
Примечание: Пожалуйста, обратитесь к справке по атрибуту is для замечаний по реалистичности реализации пользовательских встроенных элементов.
Прежде всего, мы определяем класс нашего элемента:
// Create a class for the element
class ExpandingList extends HTMLUListElement {
constructor() {
// Always call super first in constructor
// Return value from super() is a reference to this element
self = super();
}
connectedCallback() {
// Get ul and li elements that are a child of this custom ul element
// li elements can be containers if they have uls within them
const uls = Array.from(self.querySelectorAll("ul"));
const lis = Array.from(self.querySelectorAll("li"));
// Hide all child uls
// These lists will be shown when the user clicks a higher level container
uls.forEach((ul) => {
ul.style.display = "none";
});
// Look through each li element in the ul
lis.forEach((li) => {
// If this li has a ul as a child, decorate it and add a click handler
if (li.querySelectorAll("ul").length > 0) {
// Add an attribute which can be used by the style
// to show an open or closed icon
li.setAttribute("class", "closed");
// Wrap the li element's text in a new span element
// so we can assign style and event handlers to the span
const childText = li.childNodes[0];
const newSpan = document.createElement("span");
// Copy text from li to span, set cursor style
newSpan.textContent = childText.textContent;
newSpan.style.cursor = "pointer";
// Add click handler to this span
newSpan.addEventListener("click", (e) => {
// next sibling to the span should be the ul
const nextUl = e.target.nextElementSibling;
// Toggle visible state and update class attribute on ul
if (nextUl.style.display == "block") {
nextUl.style.display = "none";
nextUl.parentNode.setAttribute("class", "closed");
} else {
nextUl.style.display = "block";
nextUl.parentNode.setAttribute("class", "open");
}
});
// Add the span and remove the bare text node from the li
childText.parentNode.insertBefore(newSpan, childText);
childText.parentNode.removeChild(childText);
}
});
}
}
Обратите внимание, что на этот раз мы расширяем HTMLUListElement, а не HTMLElement. Это означает, что мы получаем стандартное поведение списка и нам нужно только реализовать собственные настройки.
Как и прежде, большая часть кода находится в обратном вызове жизненного цикла connectedCallback().
Далее мы регистрируем элемент с помощью метода define() , как и раньше, за исключением того, что на этот раз он также включает объект опций, который описывает, от какого элемента наследуется наш пользовательский элемент:
customElements.define("expanding-list", ExpandingList, { extends: "ul" });
Использование встроенного элемента в веб-документе также выглядит несколько иначе:
<ul is="expanding-list"> … </ul>
Вы используете элемент <ul> как обычно, но указываете имя пользовательского элемента внутри атрибута is.
Обратите внимание, что в этом случае мы должны убедиться, что скрипт, определяющий наш пользовательский элемент, выполняется после полной обработки DOM, потому что connectedCallback() вызывается сразу после добавления раскрывающегося списка в DOM, а на тот момент его дочерние элементы еще не добавлены, поэтому вызовы querySelectorAll() не найдут никаких элементов. Один из способов гарантировать это — добавить атрибут defer к строке, включающей скрипт:
<script src="main.js" defer></script>
Обратные вызовы жизненного цикла
До сих пор мы видели только один обратный вызов жизненного цикла в действии: connectedCallback(). В последнем примере, <custom-square>, мы увидим некоторые другие. Пользовательский элемент <custom-square> рисует квадрат, размер и цвет которого определяются двумя атрибутами, названными "size" и "color".
В конструкторе класса мы подключаем теневой DOM к элементу, а затем подключаем пустые элементы <div> и <style> к теневому корню:
constructor() {
// Always call super first in constructor
super();
const shadow = this.attachShadow({ mode: "open" });
const div = document.createElement("div");
const style = document.createElement("style");
shadow.appendChild(style);
shadow.appendChild(div);
}
Ключевой функцией в этом примере является updateStyle() — она принимает элемент, получает его теневой корень, находит элемент <style> и добавляет width, height и background-color к стилю.
function updateStyle(elem) {
const shadow = elem.shadowRoot;
shadow.querySelector("style").textContent = `
div {
width: ${elem.getAttribute("size")}px;
height: ${elem.getAttribute("size")}px;
background-color: ${elem.getAttribute("color")};
}
`;
}
Фактические обновления обрабатываются обратными вызовами жизненного цикла. Обратный вызов connectedCallback() выполняется каждый раз, когда элемент добавляется в DOM — здесь мы выполняем функцию updateStyle() , чтобы убедиться, что квадрат стилизован так, как определено в его атрибутах:
connectedCallback() {
console.log("Custom square element added to page.");
updateStyle(this);
}
Обратные вызовы disconnectedCallback() и adoptedCallback() записывают сообщения в консоль, чтобы сообщить нам, когда элемент удаляется из DOM или перемещается на другую страницу:
disconnectedCallback() {
console.log("Custom square element removed from page.");
}
adoptedCallback() {
console.log("Custom square element moved to new page.");
}
Обратный вызов attributeChangedCallback() выполняется всякий раз, когда какой-либо из атрибутов элемента изменяется. Как видно из его параметров, можно действовать на атрибуты индивидуально, анализируя их имя и старые и новые значения атрибутов. Однако в данном случае мы просто снова выполняем функцию updateStyle() , чтобы убедиться, что стиль квадрата обновляется в соответствии с новыми значениями:
attributeChangedCallback(name, oldValue, newValue) {
console.log("Custom square element attributes changed.");
updateStyle(this);
}
Обратите внимание, что для запуска обратного вызова attributeChangedCallback() при изменении атрибута необходимо отслеживать атрибуты. Это делается путем указания метода static get observedAttributes() внутри класса пользовательского элемента — он должен возвращать массив, содержащий имена атрибутов, которые необходимо отслеживать:
static get observedAttributes() {
return ["color", "size"];
}
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements