Custom Elements #
Custom Elements — часть стандарта веб-компонентов: вы регистрируете новый HTML-тег и связываете его с классом JavaScript. Браузер создаёт экземпляр при разборе разметки <track-card> или через document.createElement('track-card'). Так оформляют переиспользуемые блоки UI — карточку трека, счётчик корзины, бейдж статуса заказа.
Регистрация элемента #
class TrackCard extends HTMLElement {
connectedCallback() {
if (this.dataset.rendered) return
this.dataset.rendered = 'true'
const title = this.getAttribute('title') ?? 'Без названия'
const artist = this.getAttribute('artist') ?? ''
this.innerHTML = `
<article class="track-card">
<h3>${title}</h3>
<p>${artist}</p>
<button type="button" data-action="play">Слушать</button>
</article>
`
}
}
customElements.define('track-card', TrackCard)
Имя тега обязательно содержит дефис (track-card), чтобы не пересечься со встроенными элементами. Повторный define с тем же именем выбросит ошибку.
Жизненный цикл #
| Callback | Когда вызывается |
|---|---|
connectedCallback | Элемент добавлен в документ |
disconnectedCallback | Элемент удалён из документа |
adoptedCallback | Узел перенесён в другой document |
attributeChangedCallback | Изменился наблюдаемый атрибут |
class LiveCounter extends HTMLElement {
static get observedAttributes() {
return ['value']
}
connectedCallback() {
this.render()
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'value' && oldVal !== newVal) {
this.render()
}
}
render() {
const n = this.getAttribute('value') ?? '0'
this.textContent = `Слушателей: ${n}`
}
}
customElements.define('live-counter', LiveCounter)
Без observedAttributes метод attributeChangedCallback не вызывается — это частая ловушка.
Автономные и customized built-in #
Autonomous — новый тег, как выше. Customized built-in наследует встроенный элемент:
class PrimaryButton extends HTMLButtonElement {
connectedCallback() {
this.classList.add('btn-primary')
}
}
customElements.define('primary-button', PrimaryButton, { extends: 'button' })
В разметке: <button is="primary-button">Оформить</button>. Поддержка extends в Safari ограничена — для кросс-браузерности чаще выбирают автономный тег-обёртку.
Свойства vs атрибуты #
Атрибуты — строки в HTML; свойства — поля JS-объекта. Для сложных данных задайте getter/setter:
class CartBadge extends HTMLElement {
static get observedAttributes() {
return ['count']
}
get count() {
return Number(this.getAttribute('count') ?? 0)
}
set count(n) {
this.setAttribute('count', String(n))
}
attributeChangedCallback() {
this.textContent = this.count > 0 ? String(this.count) : ''
}
}
customElements.define('cart-badge', CartBadge)
document.querySelector('cart-badge').count = 3
Публичный API компонента удобнее держать в свойствах и методах класса, а атрибуты — для начальной разметки и SSR-подобных сценариев.
Upgrade и уже существующая разметка #
Если скрипт загрузился после HTML, браузер апгрейдит уже стоящие в DOM теги при define:
<track-card title="Focus" artist="JSFlow"></track-card>
<script type="module" src="/components/track-card.js"></script>
customElements.whenDefined('track-card') возвращает Promise, когда класс зарегистрирован — полезно для тестов и ленивой инициализации.
Custom Elements и Shadow DOM #
В примере выше разметка попала в light DOM через innerHTML. Для изоляции стилей и внутренней структуры подключайте Shadow DOM в connectedCallback. Слоты для внешнего контента — в статье про template и slots.
Итог #
customElements.define('tag-name', Class)регистрирует новый тег; имя содержит дефис.connectedCallback— точка инициализации после вставки в документ.observedAttributes+attributeChangedCallbackреагируют на изменения атрибутов.- Свойства класса — удобный JS API; атрибуты — declarative конфигурация в HTML.
- Для инкапсуляции разметки сочетайте Custom Elements с Shadow DOM.