Рекомендации по использованию пользовательских элементов

Пользовательские элементы позволяют создавать собственные HTML-теги. В этом контрольном списке представлены лучшие практики, которые помогут вам создавать высококачественные элементы.

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

Чтобы помочь вам получить наилучшие впечатления, мы составили этот контрольный список. Он разрушает все, что, по нашему мнению, необходимо для создания хорошо управляемого пользовательского элемента.

Контрольный список

Тень ДОМ

Создайте теневой корень для инкапсуляции стилей.

Почему? Инкапсуляция стилей в теневом корне вашего элемента гарантирует, что он будет работать независимо от того, где он используется. Это особенно важно, если разработчик желает разместить ваш элемент внутри теневого корня другого элемента. Это относится даже к простым элементам, таким как флажок или переключатель. Возможно, единственным содержимым внутри вашего теневого корня будут сами стили.
Пример Элемент <howto-checkbox> .

Создайте свой теневой корень в конструкторе.

Почему? Конструктор — это когда у тебя есть эксклюзивные знания о своем элементе. Это прекрасное время для настройки деталей реализации, с которыми вы не хотите, чтобы другие элементы возились. Выполнение этой работы в более позднем обратном вызове, таком как connectedCallback , означает, что вам нужно будет принять меры против ситуаций, когда ваш элемент отсоединяется, а затем снова присоединяется к документу.
Пример Элемент <howto-checkbox> .

Поместите всех дочерних элементов, создаваемых элементом, в его теневой корень.

Почему? Дочерние элементы, созданные вашим элементом, являются частью его реализации и должны быть закрытыми. Без защиты теневого корня внешний JavaScript может случайно помешать этим дочерним элементам.
Пример Элемент <howto-tabs> .

Используйте <slot> для проецирования светлых дочерних элементов DOM в теневой 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> .

Объяснители

Не переопределяйте автора страницы

Вполне возможно, что разработчик, использующий ваш элемент, захочет переопределить некоторые его исходные состояния. Например, изменение role ARIA или возможности фокусировки с помощью tabindex . Прежде чем применять собственные значения, проверьте, установлены ли эти и другие глобальные атрибуты.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Сделать свойства ленивыми

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

В следующем примере Angular декларативно привязывает свойство isChecked своей модели к свойству checked флажка. Если определение Howto-Checkbox было загружено отложенно, возможно, Angular попытается установить проверенное свойство до обновления элемента.

<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;
    ...
  }
}