W3docs

Пользовательские элементы

Как создавать пользовательские элементы в 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=). Для широкой совместимости предпочитайте автономные пользовательские элементы и самостоятельно реализуйте необходимую доступность, либо подключайте полифил.

Лучшие практики работы с пользовательскими элементами

  1. Используйте Shadow DOM: всегда инкапсулируйте внутреннюю структуру и стили пользовательского элемента с помощью Shadow DOM.
  2. Определяйте чёткие API: предоставляйте понятные и интуитивные API для ваших пользовательских элементов через хорошо задокументированные атрибуты и свойства.
  3. Управление жизненным циклом: правильно управляйте колбэками жизненного цикла элемента, чтобы обеспечить надёжное поведение и избежать утечек памяти.
  4. Доступность: обеспечивайте доступность пользовательских элементов, включая соответствующие роли и свойства ARIA.
  5. Тестирование: тщательно тестируйте пользовательские элементы в различных браузерах и окружениях, чтобы гарантировать совместимость и стабильность.

Заключение

Пользовательские элементы предлагают мощный способ расширения HTML, позволяя создавать многократно используемые, инкапсулированные компоненты с нестандартным поведением. Используя возможности пользовательских элементов — колбэки жизненного цикла, атрибуты, свойства и Shadow DOM — разработчики могут создавать сложные и поддерживаемые веб-приложения.

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

Связанные темы

Практика

Практика
Какие из следующих утверждений о пользовательских элементах в JavaScript верны?
Какие из следующих утверждений о пользовательских элементах в JavaScript верны?
Was this page helpful?