Пользовательские элементы
Как создавать пользовательские элементы в JavaScript: определение класса, регистрация через customElements.define(), колбэки жизненного цикла, отслеживание атрибутов и расширение встроенных элементов.
Пользовательские элементы — один из ключевых столпов Web Components. Они позволяют определять собственные HTML-теги, основанные на классе JavaScript, расширяя встроенный словарь браузера многократно используемыми, самодостаточными элементами с собственной структурой, стилями и поведением.
На этой странице рассматривается всё необходимое для создания пользовательского элемента: как определить и зарегистрировать его, какие колбэки жизненного цикла вызывает браузер, как реагировать на изменения атрибутов, как расширять встроенные элементы и какие практики делают компоненты надёжными и доступными.
Два вида пользовательских элементов
Спецификация определяет два варианта, и различие между ними влияет на способ их создания и использования:
- Автономные пользовательские элементы расширяют базовый
HTMLElementи используются как полностью новые теги:<my-card></my-card>. Это наиболее распространённый случай. - Настроенные встроенные элементы расширяют конкретный встроенный класс (например,
HTMLButtonElement) и используются с атрибутомis:<button is="fancy-button">. Они бесплатно наследуют доступность и поведение хост-элемента.
Одно правило применяется к обоим: имя тега должно содержать дефис (my-card, но не mycard). Дефис сообщает парсеру, что тег является пользовательским элементом, и предотвращает конфликты с будущими стандартными тегами.
Определение пользовательского элемента
Чтобы создать автономный пользовательский элемент, нужно определить class, расширяющий встроенный класс HTMLElement, а затем зарегистрировать его в браузере с помощью customElements.define(tagName, class). Класс инкапсулирует поведение элемента; регистрация связывает его с именем тега.
Распространённый подход — строить внутренний DOM элемента внутри shadow DOM, чтобы его разметка и стили были изолированы от остальной страницы.
Пример: Создание простого пользовательского элемента
<my-custom-element></my-custom-element>
<script>
class MyCustomElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<p>Hello, World!</p>`;
}
}
customElements.define('my-custom-element', MyCustomElement);
</script>В этом примере определяется пользовательский элемент с именем my-custom-element, который отображает «Hello, World!» внутри shadow DOM. Чтобы использовать его, достаточно добавить <my-custom-element></my-custom-element> в любое место HTML. Обратите внимание: у пользовательских элементов нет самозакрывающейся формы — всегда пишите парный закрывающий тег.
Custom Elements v1 поддерживается во всех современных браузерах (Chrome 54+, Firefox 52+, Safari 10.1+, Edge 79+). Всегда проверяйте совместимость с браузерами, если вам нужна поддержка устаревших окружений.
Колбэки жизненного цикла
Пользовательские элементы имеют набор колбэков жизненного цикла, позволяющих разработчикам выполнять код в определённые моменты жизни элемента:
connectedCallback(): вызывается каждый раз, когда пользовательский элемент добавляется в документ-подключённый элемент.disconnectedCallback(): вызывается каждый раз, когда пользовательский элемент отключается от DOM документа.attributeChangedCallback(name, oldValue, newValue): вызывается каждый раз, когда один из атрибутов пользовательского элемента добавляется, удаляется или изменяется.adoptedCallback(): вызывается каждый раз, когда пользовательский элемент перемещается в новый документ.
| Колбэк | Когда вызывается |
|---|---|
connectedCallback() | Элемент добавлен в DOM |
disconnectedCallback() | Элемент удалён из DOM |
attributeChangedCallback(name, oldValue, newValue) | Изменился отслеживаемый атрибут |
adoptedCallback() | Элемент перемещён в новый документ |
Полезная ментальная модель: конструктор выполняется один раз при создании экземпляра элемента (до того, как он появится в DOM, поэтому он не должен обращаться к атрибутам или дочерним элементам), тогда как connectedCallback может выполняться несколько раз, если элемент добавляется, удаляется и снова добавляется. Выполняйте инициализацию, зависящую от DOM, в connectedCallback, а в disconnectedCallback удаляйте обработчики событий и таймеры, чтобы избежать утечек памяти.
Пример: Использование колбэков жизненного цикла
<lifecycle-element></lifecycle-element>
<script>
class LifecycleElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
#status {
color: blue;
font-weight: bold;
}
</style>
<p>Lifecycle Element</p>
<p id="status">Element not connected</p>
`;
}
connectedCallback() {
this.shadowRoot.getElementById('status').textContent = 'Element connected to the page.';
}
disconnectedCallback() {
this.shadowRoot.getElementById('status').textContent = 'Element disconnected from the page.';
}
}
customElements.define('lifecycle-element', LifecycleElement);
</script>Атрибуты и свойства
Пользовательские элементы могут иметь атрибуты и свойства для управления своим состоянием и поведением. Атрибуты задаются непосредственно в HTML и всегда являются строками, тогда как свойства задаются на DOM-объекте элемента и могут быть любого типа данных.
Ключевая деталь: attributeChangedCallback срабатывает только для атрибутов, явно перечисленных в геттере static get observedAttributes() элемента. Если атрибут не включён в этот массив, его изменение не вызовет никакого колбэка. Распространённое соглашение — предоставлять пару геттер/сеттер, которая просто отражает значение в атрибуте, чтобы код JavaScript и HTML оставались синхронизированными.
Пример: Управление атрибутами и свойствами
<attribute-element id="element" data-content="Initial content"></attribute-element>
<button onclick="buttonClicked()">Click to change attribute</button>
<script>
class AttributeElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `<p>Attribute Example: <span id="content"></span></p>`;
}
static get observedAttributes() {
return ['data-content'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data-content') {
this.shadowRoot.getElementById('content').textContent = newValue;
}
}
set content(value) {
this.setAttribute('data-content', value);
}
get content() {
return this.getAttribute('data-content');
}
}
customElements.define('attribute-element', AttributeElement);
function buttonClicked() {
alert('button clicked!');
const ourCustomElement = document.getElementById('element');
ourCustomElement.content = 'New content';
}
</script>Здесь attribute-element обновляет своё содержимое на основе атрибута data-content. Свойство content предоставляет удобный способ получать и устанавливать этот атрибут программно.
Расширение встроенных элементов
Настроенные встроенные элементы расширяют конкретный встроенный класс и используются с атрибутом is. Главное преимущество — они наследуют семантику и доступность хост-элемента: <button is="fancy-button"> по-прежнему остаётся настоящей кнопкой для клавиатуры и программ чтения с экрана.
Пример: Расширение встроенного элемента
<button is="fancy-button">Click me!</button>
<script>
class FancyButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => {
alert('Fancy button clicked!');
});
}
}
customElements.define('fancy-button', FancyButton, { extends: 'button' });
</script>Здесь fancy-button расширяет стандартный элемент <button>, добавляя всплывающее сообщение при нажатии. Третий аргумент customElements.define — { extends: 'button' } — указывает браузеру, к какому тегу применяется данный настроенный элемент.
Safari не поддерживает настроенные встроенные элементы (форму is=). Для широкой совместимости предпочитайте автономные пользовательские элементы и самостоятельно реализуйте необходимую доступность, либо подключайте полифил.
Лучшие практики работы с пользовательскими элементами
- Используйте Shadow DOM: всегда инкапсулируйте внутреннюю структуру и стили пользовательского элемента с помощью Shadow DOM.
- Определяйте чёткие API: предоставляйте понятные и интуитивные API для ваших пользовательских элементов через хорошо задокументированные атрибуты и свойства.
- Управление жизненным циклом: правильно управляйте колбэками жизненного цикла элемента, чтобы обеспечить надёжное поведение и избежать утечек памяти.
- Доступность: обеспечивайте доступность пользовательских элементов, включая соответствующие роли и свойства ARIA.
- Тестирование: тщательно тестируйте пользовательские элементы в различных браузерах и окружениях, чтобы гарантировать совместимость и стабильность.
Заключение
Пользовательские элементы предлагают мощный способ расширения HTML, позволяя создавать многократно используемые, инкапсулированные компоненты с нестандартным поведением. Используя возможности пользовательских элементов — колбэки жизненного цикла, атрибуты, свойства и Shadow DOM — разработчики могут создавать сложные и поддерживаемые веб-приложения.
Начните экспериментировать с пользовательскими элементами в своих проектах уже сегодня и откройте новые возможности для веб-разработки. Приведённые здесь примеры — лишь начало. Используйте их как основу для создания собственных инновационных пользовательских элементов.
Связанные темы
- Web Components — зонтичный стандарт, частью которого являются пользовательские элементы.
- Shadow DOM — инкапсуляция внутреннего DOM и стилей элемента.
- Стилизация Shadow DOM — оформление внутренней части компонента.
- Слоты и композиция в Shadow DOM — возможность для пользователей проецировать контент в ваш элемент.
- Элемент
<template>— эффективное клонирование разметки для теневого корня элемента. - Базовый синтаксис классов — возможности классов, на которых строятся пользовательские элементы.