О кастомных HTML-тегах по-человечески и как их использовать

Kate

Administrator
Команда форума
Расскажу Вам о том, как использовать чудо-юдо под названием "Кастомные HTML-теги" понятным языком.

Предисловие​

Причины создания данной статьи таковы:

  • Никто, за редчайшими исключениями, не использует кастомные теги, не говоря уже про их API. А очень зря.
  • Почти весь материал по ним либо на английском языке, либо написан так, что лучше бы не писали. А иногда и то и другое.
Я попробую изложить суть кастомных html-элементов наиболее доступно.

Просто используйте их!​

<div class='my-block' data-block-number="15">
<div class='my-block__header'>
<span class='my-block__title'>Заголовок</span>
</div>

<div class='my-block__main my-block__main--smaller'>
<div class='centring'>
<p class='my-block__content'>
...
</p>
<div class='my-block__decor'>ΫΫΫ</div>
</div>
</div>

<div class='my-block__footer'>
<span>@ Права не защищены.</span>
</div>
</div>
Красивая портянка, не правда ли? Предлагаю сделать её проще и понятней:

<my-block block-number="15">
<my-block-header>
<my-block-title>Заголовок</my-block-title>
</my-block-header>


<my-block-main class='smaller'>
<centered-content>
<p class='content'>
...
</p>
<my-block-decor>ΫΫΫ</my-block-decor>
</centered-content>
</my-block-main>

<my-block-footer>
<span>@ Права не защищены.</span>
</my-block-footer>
</my-block>
Красиво, не правда ли? А ещё, для того чтобы этот код работал, вам не нужно делать ничего. Он работает «из коробки». Никаких React-ов и уличной магии.
Вы можете поиграть с кастомными html‑элементами в этом codepen.

Подробнее​

Почти сразу оговорюсь об их поддержке. На момент написания статьи: полная поддержка — 79%, частичная — 97.3%. Вы можете смело использовать кастомные html‑элементы, просто игнорируя функции, что описаны в разделе «О модифицированных встроенных элементах», пользуясь поддержкой в 97.3%. И не волнуйтесь, до того раздела я использовать плохо поддерживаемые функции не буду.

Уверен, что вы периодически используете теги div и span когда создаёте разметку сайта. Эти теги наиболее ходовые, потому как арсенал зарезервированных html-тегов не велик (о большинстве из них и вовсе не догадываются), а делать структуру из чего-то надо. Сами по себе теги div и span не представляют из себя ничто, ни семантики, ни функционала. Отличие лишь в том, что div - блочный, а span - строчный.

Чтобы не утонуть в коллизиях, например, в процессе стилизации, к ним применяют классы и уже через них стилизуют:

<div class='my-block'></div>
В 80% случаев (цифры взяты навскидку) кастомные html-элементы позволяют заменить использование класса и даже атрибута, защищая уникальным именем от коллизий:

<my-block></my-block>
А ещё, это банально читабельней и короче.

Полезно знать​

  • Для работы кастомных html-тегов вам не требуется ничего, кроме браузера.
  • Стили применяются к ним, как к обычным тегам с соответствующей специфичностью.
  • JavaScript работает с ними, как и с другими элементами - “из коробки”.
  • Имя кастомного тега должно иметь дефис где-то внутри названия (ни в начале, ни в конце).
  • Кастомные теги не могут быть самозакрывающимися, как например img.
  • Задавать атрибуты для кастомных тегов можно без data- префикса.

Семантика и стили​

С точки зрения семантики, стилей и функционала кастомные html-элементы равноценны тегу span целиком и полностью. То бишь не имеют семантики и стилей. Если вместо них использовать нагромождение span-ов, с этой точки зрения, ничего не не изменится.
Да да, все ваши кастомные элементы изначально строчные. Учитывайте это при стилизации.

Расширенное использование или JS API​

Впрочем, это не всё. Кастомные html-элементы могут больше, чем просто теги.
К слову, ошибка почти всех материалов по этой теме в том, что начинают они рассказ по теме именно с этой части, а не той, что выше.

Вы когда нибудь задумывались о том, как работает элемент dialog или details? Самое время задуматься, ибо вы можете сделать из кастомных html-элементов нечто схожее, а то и большее по масштабу и сложности. Для этого сделали API, который сейчас рассмотрим.
Использование API не обязательно, они и без него работают, с.м примеры выше.

Чтобы работать с API, необходимо немного потанцевать с бубном написать следующий код, где расписано всё, с чем предстоит работать:
(все методы - не обязательны)

/** Имя Вашего класса может быть произвольным. */
class HTMLMyElement extends HTMLElement {
/** Заменяет функцию observedAttributes(), с.м ниже. */
static observedAttributes = [/* имена атрибутов */]

constructor() {
super()
}

/**
* Срабатывает всякий раз, когда элемент попадает в документ (включая инициализацию).
*/
connectedCallback() { }

/**
* Срабатывает всякий раз, когда элемент удаляется из документа.
*/
disconnectedCallback() { }

/**
* С его помощью можно задать группу атрибутов, изменение которых вызовет метод ниже.
*/
static get observedAttributes() {
return [/* массив имён атрибутов */]
}

/**
* Срабатывает всякий раз при изменении одного из атрибутов перечисленных в observedAttributes.
*/
attributeChangedCallback(name, oldValue, newValue) { }

/**
* Срабатывает всякий раз при перемещении элемента в иной документ (почти никогда не нужен).
*/
adoptedCallback() {
}

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

/** Регистрация элемента. Без этого ничего толком работать не будет. */
customElements.define('my-element', HTMLMyElement)
После того, как вы напишете вышеуказанный код, экземпляр класса будет создан для каждого my-element в вашем документе и для них будут работать методы.

Разберу каждый метод по отдельности:

  • constructor()
    Вызывается первым из всей братии. Крайне желательно вызвать внутри функцию super() и лишь потом писать оставшийся внутренний код.
    В нём можно задать значения по умолчанию (например, задать стили), зарегистрировать прослушиватели событий и даже построить коммунизм создать теневую разметку.
    В нём не следует проверять атрибуты элемента или дочерние элементы (их на момент срабатывания ещё может не быть), а также добавлять новые атрибуты или дочерние элементы.
  • connectedCallback()
    Срабатывает всякий раз, когда элемент попадает в документ (включая инициализацию/чтение документа).
    Согласно спецификации, всю настройку/инициализацию элемента (атрибуты, работу с детишками и иже с ними) нужно проводить именно в этом методе, а не в конструкторе.
  • disconnectedCallback()
    Срабатывает всякий раз, когда элемент удаляется из документа. Здесь можно взять и указать, что произойдёт в этих случаях.
  • adoptedCallback()
    Срабатывает всякий раз при перемещении элемента в иной документ (почти никогда не нужен).
  • observedAttributes
    Это может быть функция:
    static get observedAttributes() {
    return [/* массив имён атрибутов */]
    }
    А может быть свойство:
    static observedAttributes = [/* массив имён атрибутов */]

    Через него можно задать атрибуты, изменение которых вызывает attributeChangedCallback().
  • attributeChangedCallback(name, oldValue, newValue)
    Срабатывает всякий раз при изменении одного из атрибутов элемента, перечисленных в observedAttributes.
    Получает имя изменяемого атрибута, старое и новое значения.

Полезно знать о JS API​

Во первых: можно ссылаться на текущий кастомный элемент, используя this:

connectedCallback() {
this.hasAttribute('some-attr')
}
Во вторых: рендеринг элементов происходит от родителей к потомкам сверху вниз. Попытки обратиться к потомкам через конструктор или connectedCallback() чреваты ошибками. Чтобы избежать этого, рекомендую использовать вспомогательные методы и событие DOMContentLoaded:

connectedCallback() {
// некий код...

document.addEventListener('DOMContentLoaded', this.init.bind(this))
}
init() {
// Запуститься только тогда, когда будет загружен и проанализирован весь DOM.
// Манипуляции с потомками и так далее.
}

Пример использования JS API​

class MyCustomElement extends HTMLElement {
static observedAttributes = ['attr-1', 'attr-2'];

constructor() {
super();
}

connectedCallback() {
console.log('Кастомный элемент добавлен на страницу!');
this.role = 'tabpanel'

document.addEventListener('DOMContentLoaded', this.init.bind(this))
}
init() {
// Тут можно спокойно обращаться к потомкам.
for (let child of this.children) {
child.setAttribute('data-tab-item', '')
}
}

disconnectedCallback() {
console.log('Кастомный элемент был бесчеловечно... удалён.');
}

adoptedCallback() {
console.log('Кастомный элемент был перемещён в глубинные глубины иного документа.');
}

attributeChangedCallback(name, oldValue, newValue) {
console.log(`Атрибут ${name} взял и изменился.`);

switch (name) {
case 'attr-1':
if (newValue == 'true') {
this.setAttribute('attr-2', 'false')
}
break;
}
}
}

customElements.define('my-custom-element', MyCustomElement);

О модифицированных встроенных элементах​

Спецификация подразумевает два типа кастомных html-элементов:

  • Автономные элементы.
  • Кастомизированные встроенные элементы.
Всё, что описывалось выше не учитывало кастомизированные встроенные элементы, автор их игнорировал до сего момента.
Вкратце: второй вариант использования позволяет модифицировать встроенные по умолчанию элементы, такие как button и section , расширяя их функционал.

Для этого предполагается использование атрибута is:

<button is='hello-button'>...</button>
Инициализация класса видоизменяется и выглядит так:

class HelloButton extends HTMLButtonElement
А регистрация элемента:

customElements.define('hello-button', HelloButton, {extends: 'button'})
О кастомизации встроенных элементов можно было бы порассуждать, но есть нюанс: их поддержка оставляет желать лучшего. Нет, ну что там, 79% на момент написания статьи - казалось бы, ещё чучуть и можно использовать! Но у разработчиков Safari есть своё, и ещё раз своё мнение о том, что реализовать нужно, а что - нет.
Рассчитывать, что на них снизойдёт благодать и они изменят своё решение не стоит, ибо кастомные html-элементы реализованы уже давно (года с 2016 в отдельных браузерах), а их решение по прежнему неизменно.
Безусловно, вы можете воспользоваться полифилом, но это уже не уровень “из коробки”.

Итого​

Арсенал html‑тегов неплох, но его следует расширить. Используя кастомные теги вы можете избавится от львиной доли классов в вашей разметке, заменив их на уникальные имена тегов (особенно хорошо это будет смотреться в рамках Systematic CSS, где элементы блока обозначаются без классов).
Полезны будут и атрибуты, что можно задать без data- префикса, может заменить модификаторы в части случаев.

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

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

Не стесняйтесь использовать кастомные html-элементы, это легко, весело, полезно и поддержка у них хорошая.

Источники​


 
Сверху