mrssea

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

Статья вышла в 2017 году, но и сейчас не теряет актуальности. т.к. я не нашла ее в переводе на русский язык, решила перевести сама.

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

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

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

Чеклист

Теневой DOM(Shadow DOM)

Подробнее про сам Shadow DOM 
Подробнее про декларативный Shadow DOM

Создавайте Shadow root для инкапсуляции стилей

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

Пример: Элемент <howto-checkbox>

Создавайте свой shadow root в constructor

Использовать метод конструктор для эксклюзивных знаний о вашем элементе. Это хорошее место для настройки деталей реализации, с которыми вы не хотите возиться в дальнейшем. Если же сделать это, например, в вызове connectCallback (метод для пользовательских элементов, браузер вызывает этот метод при добавлении элемента в документ, метод может вызываться много раз, если элемент многократно добавляется/удаляется из DOM), вам нужно будет учесть ситуации, когда происходит удаление элемента из DOM, detach, а затем снова добавляется в документ.

Пример

constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

 Элемент <howto-checkbox> 

Поместите все дочерние элементы, созданные элементом, в его shadow root

Дочерние элементы, созданные вашим элементом, являются частью его реализации и должны быть закрытыми. Без защиты shadow root внешний JavaScript может непреднамеренно навредить или изменить поведение этим дочерним элементам.

Пример: элемент <howto-tabs> 

Используйте <slot> для проецирования дочерних элементов light DOM в ваш shadow DOM

HTML-код внутри пользовательского элемента — это его Light DOM — то, что добавил в него пользователь этого компонента.

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

Пример

<howto-tabs>
  <howto-tab role="heading" slot="tab">Tab 1</howto-tab>
  <howto-panel role="region" slot="panel">Content 1</howto-panel>
  <howto-tab role="heading" slot="tab">Tab 2</howto-tab>
  <howto-panel role="region" slot="panel">Content 2</howto-panel>
  <howto-tab role="heading" slot="tab">Tab 3</howto-tab>
  <howto-panel role="region" slot="panel">Content 3</howto-panel>
</howto-tabs>

Элемент <howto-tabs>

Установите стиль отображения :host (например, block, inline-block, flex), если вам не подходит inline (по умолчанию)

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

Пример

const template = document.createElement('template');

  template.innerHTML = `
    <style>
      :host {
        display: inline-block;
        width: 24px;
        height: 24px;
      }
      :host([hidden]) {
        display: none;
      }
    </style>
  `;

 Элемент <howto-tabs>

Добавьте стиль отображения :host, учитывающий hidden атрибут

Пользовательский элемент со стилем отображения по умолчанию, например. :host { display: block } переопределяет встроенный атрибут hidden с более низкой специфичностью. Это может вас удивить, если вы ожидаете, что атрибут hidden вашего элемента отобразит его display: none. В дополнение к стилю отображения по умолчанию добавьте поддержку hidden с помощью :host([hidden]) { display: none }.

Пример: элемент <howto-checkbox>

Атрибуты и свойства

Не переопределяйте установленные автором глобальные атрибуты

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

Элемент <howto-checkbox>. Подробнее это объясняется в разделе «Не переопределяйте автора страницы».

Всегда принимайте примитивные типы данных (строки, числа, булевы значения) как атрибуты или свойства

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

Пример: элемент <howto-checkbox>

Старайтесь принимать только расширенные данные (объекты, массивы) в качестве свойств

Вообще говоря, нет примеров встроенных элементов HTML, которые принимают расширенные данные (простые объекты и массивы JavaScript) через свои атрибуты. Вместо этого расширенные данные принимаются либо через вызовы методов, либо через свойства. У принятия расширенных данных в качестве атрибутов есть несколько очевидных недостатков: сериализация большого объекта в строку может быть дорогостоящей, и любые ссылки на объекты будут потеряны в этом процессе преобразования строк. Например, если вы преобразуете в строку объект, который имеет ссылку на другой объект или, возможно, узел DOM, эти ссылки будут потеряны.

Не отслеживайте свойства расширенных данных в атрибутах

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

Рассмотрите возможность проверки свойств, которые могли быть установлены до обновления элемента

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

Пример: Элемент <howto-checkbox>. Подробнее в разделе Make properties lazy.

Не применяйте self-apply классы

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

События

Отправлять события в ответ на действия внутреннего компонента

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

Не отправлять события в ответ на настройку хостом свойства (нисходящий поток данных)

Отправка события в ответ на установку хостом свойства является излишней (хост знает текущее состояние, потому что он только что установил его), т.к. может привести к бесконечным циклам в системах привязки данных.

Пример: Элемент <howto-checkbox>

Пояснения

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

Возможно, разработчик, использующий ваш элемент, захочет переопределить некоторые из его начальных состояний. Например, изменение его роли 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 может попытаться установить свойство 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() для отражения состояния базового свойства, например:

//При изменении атрибута [checked] установите свойство checked, чтобы оно совпадало.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

Но это может создать бесконечный цикл, если установщик свойств также отражает атрибут.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! Это вызовет бесконечный цикл, потому что он запускает
    // attributeChangedCallback(), который затем снова устанавливает это свойство.
    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;
...
}
}

Оригинал статьи на английском https://web.dev/custom-elements-best-practices/