Спецификация Custom Elements

Lit построен поверх веб-стандарта Custom Elements. Это означает, что любой компонент Lit — это прежде всего пользовательский HTML-элемент, определённый через спецификацию Web Components. Понимание Custom Elements критично для правильного проектирования компонентов, их жизненного цикла, взаимодействия с DOM и интеграции с другими фреймворками.

Custom Elements позволяют создавать собственные HTML-теги с инкапсулированной логикой, состоянием и поведением, которые работают нативно в браузере без транспиляции или сторонних рантаймов.


Регистрация пользовательского элемента

Регистрация происходит через глобальный реестр customElements. Каждый элемент должен иметь уникальное имя, содержащее дефис.

customElements.define('my-element', MyElement);

Класс элемента обязан наследоваться от HTMLElement или одного из его потомков. В Lit используется LitElement, который сам расширяет HTMLElement и добавляет реактивность, шаблоны и управление обновлениями.

import { LitElement, html } from 'lit';

class MyElement extends LitElement {
  render() {
    return html`<p>Пример</p>`;
  }
}

customElements.define('my-element', MyElement);

Повторная регистрация с тем же именем приводит к исключению. Поэтому в библиотеках часто применяется проверка:

if (!customElements.get('my-element')) {
  customElements.define('my-element', MyElement);
}

Жизненный цикл Custom Elements

Спецификация определяет строгий жизненный цикл, который Lit использует и расширяет.

constructor

Вызывается при создании экземпляра элемента. В этот момент:

  • элемент ещё не подключён к DOM
  • атрибуты уже доступны
  • нельзя читать размеры или обращаться к DOM-дереву
constructor() {
  super();
  this.value = 0;
}

В Lit в constructor инициализируется внутреннее состояние, но не выполняется рендеринг.


connectedCallback

Вызывается каждый раз, когда элемент добавляется в DOM.

  • безопасно работать с DOM
  • можно подписываться на события
  • возможны повторные вызовы
connectedCallback() {
  super.connectedCallback();
  window.addEventListener('resize', this.onResize);
}

Lit использует этот хук для запуска реактивного обновления и первого рендера.


disconnectedCallback

Вызывается при удалении элемента из DOM.

  • используется для очистки ресурсов
  • отмены таймеров и подписок
disconnectedCallback() {
  window.removeEventListener('resize', this.onResize);
  super.disconnectedCallback();
}

attributeChangedCallback

Вызывается при изменении наблюдаемых атрибутов.

static get observedAttributes() {
  return ['title'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue !== newValue) {
    this[name] = newValue;
  }
}

Lit автоматически синхронизирует атрибуты и свойства через систему @property, поэтому ручная реализация почти никогда не требуется.


Свойства и атрибуты

Custom Elements различают атрибуты (строки в HTML) и свойства (значения JavaScript). Lit предоставляет декларативный механизм связывания.

static properties = {
  count: { type: Number, reflect: true }
};
  • type управляет преобразованием
  • reflect: true отражает значение свойства обратно в HTML-атрибут
  • изменение свойства инициирует перерисовку

Пример использования:

<my-counter count="5"></my-counter>
this.count += 1;

Lit автоматически выполнит:

  • парсинг атрибута в число
  • обновление DOM
  • синхронизацию при необходимости

Обновления и реактивность

Custom Elements по спецификации не имеют встроенного механизма обновлений. Lit добавляет собственный реактивный слой поверх жизненного цикла.

Каждое изменение реактивного свойства помечает компонент как «грязный» и планирует обновление микротаской.

Ключевые методы:

  • requestUpdate()
  • shouldUpdate(changedProperties)
  • update(changedProperties)
  • updated(changedProperties)
  • firstUpdated(changedProperties)
shouldUpdate(changed) {
  return changed.has('count');
}

Эта логика не является частью Custom Elements, но полностью зависит от корректной работы их жизненного цикла.


Расширение встроенных элементов

Спецификация поддерживает наследование от стандартных тегов (HTMLButtonElement, HTMLInputElement), но браузерная поддержка и синтаксис сложны.

class FancyButton extends HTMLButtonElement {}

customElements.define('fancy-button', FancyButton, {
  extends: 'button'
});

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

<button is="fancy-button"></button>

Lit не рекомендует этот подход из-за ограниченной поддержки и сложности стилизации. Предпочтение отдаётся автономным элементам.


Shadow DOM и Custom Elements

Хотя Shadow DOM формально является отдельной спецификацией, Custom Elements практически всегда используются вместе с ним.

Lit по умолчанию создаёт shadow root:

createRenderRoot() {
  return this.attachShadow({ mode: 'open' });
}

Преимущества:

  • инкапсуляция стилей
  • изоляция разметки
  • предсказуемое поведение

Shadow DOM не обязателен. Его можно отключить:

createRenderRoot() {
  return this;
}

Это превращает компонент в обычный DOM-узел без изоляции.


События и взаимодействие

Custom Elements используют стандартную систему событий DOM. Lit не вводит собственный механизм, а лишь упрощает работу.

Создание события:

this.dispatchEvent(new CustomEvent('change', {
  detail: { value: this.value },
  bubbles: true,
  composed: true
}));
  • bubbles — прохождение вверх по DOM
  • composed — выход за пределы Shadow DOM

Подписка:

<my-element @change=${this.onChange}></my-element>

Это полностью соответствует стандартам Custom Elements и не требует адаптеров.


Асинхронное определение элементов

Custom Elements могут использоваться в DOM до регистрации. Браузер автоматически «апгрейдит» элементы после define.

<my-element></my-element>
<script type="module">
  import './my-element.js';
</script>

Lit корректно обрабатывает такой сценарий:

  • конструктор вызывается после регистрации
  • атрибуты уже доступны
  • состояние сохраняется

Для ожидания регистрации используется:

customElements.whenDefined('my-element').then(() => {
  // элемент гарантированно доступен
});

Ограничения спецификации

Custom Elements накладывают ряд строгих ограничений:

  • имя обязательно содержит дефис
  • нельзя переопределять существующие HTML-теги
  • нельзя менять имя после регистрации
  • нельзя наследоваться от другого пользовательского элемента

Lit учитывает эти ограничения и строит архитектуру без нарушения спецификации.


Custom Elements и экосистема

Компоненты Lit, как Custom Elements:

  • работают в любом фреймворке
  • совместимы с React, Vue, Angular
  • не требуют адаптеров
  • могут использоваться напрямую в HTML

Это достигается именно за счёт строгого следования спецификации Custom Elements без абстракций поверх неё.


Итоговая роль Custom Elements в Lit

Lit не заменяет Custom Elements, а реализует поверх них декларативный, типобезопасный и производительный слой. Все ключевые механизмы Lit — свойства, обновления, шаблоны, стили — существуют только благодаря возможностям, предоставляемым спецификацией Custom Elements.