Custom Elements

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.

См. также #