Пользовательские элементы позволяют создавать собственные HTML-теги. В этом контрольном списке приведены рекомендации по созданию качественных элементов.
Пользовательские элементы позволяют расширять HTML и определять собственные теги. Это невероятно мощная функция, но она также является низкоуровневой, а это означает, что не всегда понятно, как лучше всего реализовать свой собственный элемент.
Чтобы помочь вам создавать максимально удобные приложения, мы составили этот контрольный список. В нем подробно описаны все требования, которым должен соответствовать пользовательский элемент.
Контрольный список
Shadow DOM
Создайте теневой корень, чтобы инкапсулировать стили. |
| Зачем? |
Инкапсуляция стилей в корневом элементе shadow root гарантирует, что он будет работать независимо от того, где он используется. Это особенно важно, если разработчик хочет поместить ваш элемент в корень теневого дерева другого элемента. Это относится даже к простым элементам, таким как флажок или переключатель. Возможно, единственным содержимым внутри корня теневого дерева будут сами стили.
|
| Пример |
Элемент
<howto-checkbox>.
|
Создайте теневой корень в конструкторе.
|
| Зачем? |
Конструктор используется, когда у вас есть эксклюзивные знания о вашем элементе.
Это отличное время для настройки деталей реализации, которые вы не хотите, чтобы другие элементы возились. Если вы будете выполнять эту работу в более позднем обратном вызове, например connectedCallback, вам нужно будет защититься от ситуаций, когда ваш элемент отсоединяется, а затем снова присоединяется к документу.
|
| Пример |
Элемент
<howto-checkbox>.
|
Поместите все дочерние элементы, созданные элементом, в его теневой корень.
|
| Зачем? |
Дочерние элементы, созданные вашим элементом, являются частью его реализации и должны быть закрытыми. Без защиты теневого корня внешний код JavaScript может случайно повлиять на эти дочерние элементы.
|
| Пример |
Элемент
<howto-tabs>.
|
Используйте элемент <slot>, чтобы проецировать дочерние элементы Light DOM в Shadow DOM
|
| Зачем? |
Если вы разрешите пользователям компонента указывать контент в нем в виде дочерних HTML-элементов, это сделает компонент более компонуемым. Если браузер не поддерживает пользовательские элементы, вложенный контент остается доступным, видимым и доступным.
|
| Пример |
Элемент
<howto-tabs>.
|
Задайте стиль отображения :host (например, block, inline-block, flex), если вас не устраивает вариант по умолчанию (inline).
|
| Зачем? |
По умолчанию специальные элементы имеют значение display: inline, поэтому настройка их width или height не будет иметь никакого эффекта. Это часто становится неожиданностью для разработчиков и может вызвать проблемы с макетом страницы. Если вы не хотите, чтобы по умолчанию использовалось значение inline, задайте значение display.
|
| Пример |
Элемент
<howto-checkbox>.
|
Добавьте стиль отображения :host, который учитывает скрытый атрибут.
|
| Зачем? |
Специальный элемент со стилем display по умолчанию, например :host { display: block }, переопределит встроенный
атрибут hidden с более низкой специфичностью.
Это может вас удивить, если вы ожидаете, что установка атрибута hidden для элемента сделает его display: none. Помимо стиля display по умолчанию, добавьте поддержку hidden с помощью :host([hidden]) { display: none }.
|
| Пример |
Элемент
<howto-checkbox>.
|
Атрибуты и свойства
Не переопределяйте глобальные атрибуты, заданные автором.
|
| Зачем? |
Глобальные атрибуты – это атрибуты, которые присутствуют во всех элементах HTML. Примеры: tabindex и role. Для специального элемента можно установить начальное значение tabindex, равное 0, чтобы на нем можно было фокусироваться с помощью клавиатуры. Но сначала всегда проверяйте, не установил ли разработчик, использующий ваш элемент, другое значение. Например, если для атрибута tabindex задано значение -1, это означает, что элемент не должен быть интерактивным.
|
| Пример |
Элемент
<howto-checkbox>. Подробнее об этом рассказывается в разделе Не переопределяйте автора страницы
|
Всегда принимайте примитивные данные (строки, числа, логические значения) как атрибуты или свойства.
|
| Зачем? |
Пользовательские элементы, как и их встроенные аналоги, должны быть настраиваемыми.
Конфигурацию можно передавать декларативно (с помощью атрибутов) или императивно (с помощью свойств JavaScript). В идеале каждый атрибут должен быть связан с соответствующим свойством.
|
| Пример |
Элемент
<howto-checkbox>.
|
Стремитесь синхронизировать примитивные атрибуты данных и свойства, отражая их от свойства к атрибуту и наоборот.
|
| Зачем? |
Вы никогда не знаете, как пользователь будет взаимодействовать с вашим элементом. Они могут задать свойство в JavaScript, а затем попытаться прочитать это значение с помощью API, например getAttribute(). Если у каждого атрибута есть соответствующее свойство и оба они отражаются, пользователям будет проще работать с вашим элементом. Другими словами, вызов setAttribute('foo', value) должен также задавать соответствующее свойство foo и наоборот. Конечно, из этого правила есть исключения. Не следует отражать свойства, которые часто меняются, например currentTime в видеопроигрывателе. Руководствуйтесь здравым смыслом. Если вам кажется, что пользователь будет взаимодействовать со свойством или атрибутом, и его несложно отразить, сделайте это.
|
| Пример |
Элемент
<howto-checkbox>. Подробнее об этом рассказывается в разделе Как избежать проблем с повторным входом.
|
Старайтесь принимать в качестве свойств только наглядные данные (объекты, массивы).
|
| Зачем? |
Как правило, встроенные элементы HTML не принимают наглядные данные (простые объекты и массивы JavaScript) через свои атрибуты. Наглядные данные передаются с помощью вызовов методов или свойств. У такого подхода есть два очевидных недостатка: сериализация большого объекта в строку может быть ресурсоемкой, а ссылки на объекты будут потеряны в процессе преобразования в строку. Например, если вы преобразуете в строку объект, который содержит ссылку на другой объект или узел DOM, эти ссылки будут потеряны.
|
Не сопоставляйте свойства наглядных данных с атрибутами.
|
| Зачем? |
Отражение свойств наглядных данных в атрибутах требует неоправданно больших затрат, поскольку при этом необходимо сериализовать и десериализовать одни и те же объекты JavaScript. Если у вас нет задачи, которую можно решить только с помощью этой функции, лучше ее не использовать.
|
Рекомендуем проверить свойства, которые могли быть заданы до обновления элемента.
|
| Зачем? |
Разработчик, использующий ваш элемент, может попытаться задать свойство элемента до того, как будет загружено его определение. Это особенно актуально, если разработчик использует фреймворк, который обрабатывает загрузку компонентов, штампует их на странице и привязывает их свойства к модели.
|
| Пример |
Элемент
<howto-checkbox>. Подробнее об этом рассказывается в разделе Используйте отложенную загрузку свойств.
|
Не применяйте классы самостоятельно.
|
| Зачем? |
Элементы, состояние которых должно быть известно пользователю, должны иметь соответствующие атрибуты. Атрибут class обычно считается принадлежащим разработчику, использующему ваш элемент, и запись в него может случайно затереть классы разработчика.
|
События
Отправлять события в ответ на действия внутренних компонентов.
|
| Зачем? |
У компонента могут быть свойства, которые меняются в ответ на действия, известные только ему. Например, когда завершается таймер или анимация или заканчивается загрузка ресурса. В ответ на эти изменения полезно отправлять события, чтобы уведомить хост о том, что состояние компонента изменилось.
|
Не отправляйте события в ответ на настройку свойства хостом (нисходящий поток данных).
|
| Зачем? |
Отправка события в ответ на установку свойства хостом избыточна (хост знает текущее состояние, поскольку он только что его установил). Отправка событий в ответ на настройку свойства хостом может привести к бесконечным циклам в системах привязки данных.
|
| Пример |
Элемент
<howto-checkbox>.
|
Обучающие видео
Не переопределять автора страницы
Возможно, разработчик, использующий ваш элемент, захочет переопределить некоторые из его начальных состояний. Например, изменив его ARIA-атрибут role или возможность фокусировки с помощью tabindex. Прежде чем задавать собственные значения, проверьте, заданы ли эти и другие глобальные атрибуты.
connectedCallback() {
if (!this.hasAttribute('role'))
this.setAttribute('role', 'checkbox');
if (!this.hasAttribute('tabindex'))
this.setAttribute('tabindex', 0);
Как сделать свойства ленивыми
Разработчик может попытаться задать свойство элемента до того, как будет загружено его определение. Это особенно актуально, если разработчик использует фреймворк, который обрабатывает загрузку компонентов, вставляет их на страницу и привязывает их свойства к модели.
В примере ниже Angular декларативно связывает свойство isChecked модели со свойством checked флажка. Если определение для howto-checkbox было загружено с отложенной загрузкой, возможно, Angular попытается установить свойство checked до обновления элемента.
<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>
Специальный элемент должен обрабатывать этот сценарий, проверяя, были ли уже заданы какие-либо свойства для его экземпляра. В <howto-checkbox> этот шаблон демонстрируется с помощью метода _upgradeProperty().
connectedCallback() {
...
this._upgradeProperty('checked');
}
_upgradeProperty(prop) {
if (this.hasOwnProperty(prop)) {
let value = this[prop];
delete this[prop];
this[prop] = value;
}
}
_upgradeProperty() захватывает значение из необновленного экземпляра и удаляет свойство, чтобы оно не затеняло собственный сеттер свойства пользовательского элемента.
Таким образом, когда определение элемента наконец загрузится, оно сможет немедленно отразить правильное состояние.
Как избежать проблем с повторным входом
Иногда возникает соблазн использовать attributeChangedCallback(), чтобы отразить состояние в базовом свойстве, например:
// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'checked')
this.checked = newValue;
}
Но это может привести к бесконечному циклу, если метод задания свойства также отражается на атрибуте.
set checked(value) {
const isChecked = Boolean(value);
if (isChecked)
// OOPS! This will cause an infinite loop because it triggers the
// attributeChangedCallback() which then sets this property again.
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}
В качестве альтернативы можно разрешить сеттеру свойства отражать атрибут и заставить геттер определять его значение на основе атрибута.
set checked(value) {
const isChecked = Boolean(value);
if (isChecked)
this.setAttribute('checked', '');
else
this.removeAttribute('checked');
}
get checked() {
return this.hasAttribute('checked');
}
В этом примере добавление или удаление атрибута также задает свойство.
Наконец, attributeChangedCallback() можно использовать для обработки побочных эффектов, таких как применение состояний ARIA.
attributeChangedCallback(name, oldValue, newValue) {
const hasValue = newValue !== null;
switch (name) {
case 'checked':
// Note the attributeChangedCallback is only handling the *side effects*
// of setting the attribute.
this.setAttribute('aria-checked', hasValue);
break;
...
}
}