Различные способы инстанцирования веб-компонента

Источник: «The different ways to instantiate a Web Component»
Сегодня мы познакомимся с различными способами инстанцирования веб-компонентов (и проблемами, связанными с каждым из этих способов).

В рамках серии статей о веб-компонентах мы создали первый веб-компонент, научились добавлять параметры и настройки, а также узнали, как постепенно улучшать веб-компоненты.

«Инстанцирование» — не совсем верное слово

Я использовал слово «инстанцирование» в названии этой статьи, но технически это то, что происходит, когда выполняется метод 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 с этим примером.

See the Pen

Внутри метода 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');

}

See the Pen

Значит ли это, что мы всегда должны запускать задачи настройки в методе 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 с этим примером.

See the Pen

Гибридный подход

Можно обойти эту временную проблему, перенеся все функции настройки в метод 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() запускается здесь...

See the Pen

Какой подход использовать

Как и во всём, что касается веб-разработки, всё зависит от ситуации.

Все статьи серии о Веб-Компонентах/Web Component

Дополнительные материалы

Предыдущая Статья

Что может сломать aspect-ratio в CSS

Следующая Статья

Методы жизненного цикла веб-компонента