Расскажу Вам о том, как использовать чудо-юдо под названием "Кастомные 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-элементы, это легко, весело, полезно и поддержка у них хорошая.