Различные способы инстанцирования веб-компонента
В рамках серии статей о веб-компонентах мы создали первый веб-компонент, научились добавлять параметры и настройки, а также узнали, как постепенно улучшать веб-компоненты.
«Инстанцирование» — не совсем верное слово
Я использовал слово «инстанцирование» в названии этой статьи, но технически это то, что происходит, когда выполняется метод constructor()
.
Точнее, я имею в виду настройку веб-компонента: получение дочерних элементов, настройка свойств, инъекция любого дополнительного HTML, изменение атрибутов и добавление слушателей событий.
Есть несколько разных способов и мест, где это можно сделать, с плюсами и минусами каждого из них.
Внутри constructor()
Именно так мы поступали до сих пор во всех статьях этой серии.
В constructor()
мы поместили всё, что нужно. И, в общем-то, это работает просто отлично!
Он прост, легко читается и позволяет собрать всё в одном месте.
/**
* Конструктор класса
*/
constructor () {
// Всегда вызывайте super первым в конструкторе
super();
// Свойства экземпляра
this.button = this.querySelector('button');
this.count = parseFloat(this.getAttribute('start')) || 0;
this.step = parseFloat(this.getAttribute('step')) || 1;
this.text = this.getAttribute('text') || 'Clicked {{count}} Times';
// Прослушивание события click
this.button.addEventListener('click', this);
// Объявление об обновлении UI
this.button.setAttribute('aria-live', 'polite');
}
Однако это становится вызовом, когда необходимо динамически создать и внедрить веб-компонент в существующий пользовательский интерфейс.
Представьте, что есть существующая страница, и через некоторое время после её загрузки нужно создать и внедрить на страницу новый элемент <wc-count>
.
Для начала воспользуемся методом document.createElement()
, чтобы создать элемент. Используем свойство Element.innerHTML
, чтобы задать ему контент. Затем можем использовать метод Element.append()
, чтобы внедрить его в пользовательский интерфейс.
let app = document.querySelector('#app');
let counter = document.createElement('wc-count');
// 👆 constructor() запускается здесь...
counter.innerHTML = `<button>Clicked 0 Times</button>`;
app.append(counter);
Выглядит отлично! Но… он выбрасывает ошибку Uncaught TypeError
:
Uncaught TypeError: Cannot read properties of null (reading 'addEventListener')
Метод constructor()
запускается на новом элементе <wc-count>
, как только он создаётся с помощью метода document.createElement()
.
Это запустит все действия по настройке, пока не будет использовано свойство Element.innerHTML
для добавления требуемой <button>
. Когда пытаемся добавить слушателя события click
к this.button
, получаем ошибку, потому что эта кнопка ещё не существует.
В этом примере данные выводятся в консоль. Можно включить панель Инспектор и перейти во вкладку консоль и увидеть результат выполнения скрипта там. Или перейти на сайт CodePen с этим примером.
Внутри метода connectedCallback()
Веб-компоненты имеют несколько событий жизненного цикла, автоматически запускающих методы обратного вызова, если они указаны.
Подробнее о них мы поговорим в одной из следующих статей, но одним из таких методов обратного вызова является метод connectedCallback()
, запускаемый, когда элемент действительно подключён к DOM.
Если перенести все функции настройки из constructor()
в метод connectedCallback()
, то ошибка Uncaught TyperError
больше не возникнет.
/**
* Конструктор класса
*/
constructor () {
// Всегда вызывайте super первым в конструкторе
super();
}
/**
* Настройка веб-компонента после его подключения к DOM
*/
connectedCallback () {
// Свойства экземпляра
this.button = this.querySelector('button');
this.count = parseFloat(this.getAttribute('start')) || 0;
this.step = parseFloat(this.getAttribute('step')) || 1;
this.text = this.getAttribute('text') || 'Clicked {{count}} Times';
// Прослушивание события click
this.button.addEventListener('click', this);
// Объявление об обновлении UI
this.button.setAttribute('aria-live', 'polite');
}
Значит ли это, что мы всегда должны запускать задачи настройки в методе connectedCallback()
?
К сожалению, всё не так просто.
Если по какой-то причине разработчик добавит новый элемент <wc-count>
в DOM, а затем добавит элементы, мы получим ту же самую Uncaught TypeError
.
let app = document.querySelector('#app');
let counter = document.createElement('wc-count');
app.append(counter);
// 👆 constructor() запускается здесь...
counter.innerHTML = `<button>Clicked 0 Times</button>`;
В этом примере данные выводятся в консоль. Можно включить панель Инспектор и перейти во вкладку консоль и увидеть результат выполнения скрипта там. Или перейти на сайт CodePen с этим примером.
Гибридный подход
Можно обойти эту временную проблему, перенеся все функции настройки в метод setup()
.
В нём проверяется наличие всех необходимых HTML-элементов перед завершением настройки, и если их нет, то необходимо сразу выйти. В данном случае проверяется существование this.button
.
Чтобы не запускать метод дважды, я предпочитаю устанавливать свойство ._instantiated
после настройки веб-компонента и проверять его наличие перед запуском функции setup()
.
/**
* Настройка веб-компонента после его подключения к DOM
*/
setup () {
// Не запускаем дважды
if (this._instantiated) return;
// Свойства экземпляра
this.button = this.querySelector('button');
if (!this.button) return;
this.count = parseFloat(this.getAttribute('start')) || 0;
this.step = parseFloat(this.getAttribute('step')) || 1;
this.text = this.getAttribute('text') || 'Clicked {{count}} Times';
// Прослушивание события click
this.button.addEventListener('click', this);
// Объявление об обновлении UI
this.button.setAttribute('aria-live', 'polite');
// Завершение инстанцирования
this._instantiated = true;
}
Затем можно выполнять метод внутри методов constructor()
и connectedCallback()
.
/**
* Конструктор класса
*/
constructor () {
// Всегда вызывайте super первым в конструкторе
super();
// Настройка веб-компонента
this.setup();
}
/**
* Запуск методов после подключения элемента к DOM
*/
connectedCallback () {
// Настройка веб-компонента
this.setup();
}
При необходимости можно вызвать метод непосредственно на настраиваемом элементе, если это необходимо.
let app = document.querySelector('#app');
let counter = document.createElement('wc-count');
app.append(counter);
counter.innerHTML = `<button>Clicked 0 Times</button>`;
counter.setup();
// 👆 constructor() запускается здесь...
Какой подход использовать
Как и во всём, что касается веб-разработки, всё зависит от ситуации.
- Если пишу веб-компонент для себя или клиента, который всегда использует предварительно отрендеренный или отрендеренный на сервере HTML, настраиваю веб-компонент в
constructor()
. - Если веб-компонент может загружаться асинхронно или непредсказуемым образом, использую гибридный подход.
- Если задачи по настройке становятся очень длинными, использую метод
setup()
при любом подходе.
Все статьи серии о Веб-Компонентах/Web Component
- Ваш первый веб-компонент
- Добавление опций в веб-компонент
- Улучшение веб-компонента
- Различные способы инстанцирования веб-компонента
- Методы жизненного цикла веб-компонента
- Как обнаружить изменение атрибутов веб-компонента
- Как заставить веб-компоненты общаться (часть 1)
- Как заставить веб-компоненты общаться (часть 2)