Привет, Хабр!
За последнее время я заметил целый ряд статей с разного рода критикой веб-компонентов. Порой, эта критика весьма жесткая и даже попахивает хейтерством. На мой взгляд, основной проблемой тут является отсутствие наработанной практики работы с этой группой стандартов у сообщества разработчиков. Многие привычные модели не всегда органично вписываются в проекты с участием Custom Elements и Shadow DOM, на многие вещи приходится смотреть под новым углом и не всем это нравится. Я вполне успешно работаю с веб-компонентами уже несколько лет и даже разрабатываю собственную библиотеку на их основе, поэтому считаю такую ситуацию не очень справедливой. Постараюсь, хотя бы частично, это исправить, по мере моих скромных сил. Я решил сделать серию компактных публикаций, в каждой из которых я планирую затронуть один из частых аспектов критики, а так-же продемонстрировать ряд технических приемов, которые могут оказаться интересными тем, кто еще не определился с тем, на какой стороне баррикад ему быть. Сегодня я хотел бы рассказать о том, как создавать компоненты без Shadow DOM.
Основная мысль, которую я хочу донести на этот раз, это то, что веб-компоненты и Shadow DOM — это не одно и то-же. При использовании Shadow DOM, вы получаете два основных преимущества:
Однако, данная механика, также, несет в себе некоторые издержки по созданию и стилизации изолированного пространства, что вполне естественно. В некоторых случаях (большие списки, ячейки таблиц с данными и т. д.), этих издержек хочется избежать по соображениям оптимизации производительности. Сейчас мы это исправим:
Если вы уже знакомы со стандартом Custom Elements, вы сразу заметите в чем дело: вместо вызова метода
Следующий важный этап связан с жизненным циклом Custom Elements. В общий документ компоненты добавляются только после того, как полностью сработает конструктор и до этого момента, та часть DOM API, которая отвечает за работу с родителями или потомками элемента, а также, с атрибутами, будет недоступна. Поэтому, для непосредственного добавления контента в наш компонент, мы используем
При создании шаблона, мы, для простоты, использовали метод
Итого, используя в нашей разметке кастомный тег
Поскольку, избавившись от ShadowRoot, мы лишились и изоляции стилей, мы добавили стили в наш шаблон с помощью атрибута. В данном случае, они имеют приоритет, это частично решает проблему и может использоваться в важных местах. Для всех остальных случаев, доступна классическая стилизация через общую таблицу стилей, а кастомные теги выступают в роли удобных селекторов.
Веб-компоненты являются полноправными узлами вашего DOM. Это означает, помимо того, что вам доступны все стандартные методы DOM-элементов, и то, что ваш компонент — это всегда некий контейнер. То есть, если вы захотите, с помощью веб-компонента, добавить в DOM произвольную структуру элементов, все они окажутся потомками вашего компонента, что не всегда удобно. В таких случаях, можно использовать новое CSS-правило — display: contents. Поддержка браузерами: caniuse.com/#feat=css-display-contents
По умолчанию же, все компоненты имеют свойство display: inline.
А что если мы вообще не хотим никаких лишних контейнеров и кастомных тегов? Даешь чистый HTML!
Ок:
В результате получаем это:
Все события и привязки продолжают работать и контролируются нашим компонентом, который теперь существует только в памяти. В данном случае, вам придется дополнительно позаботится о отписках и прочей очистке от мусора в момент, когда вы захотите удалить компонент полностью.
Согласно стандарту, пользовательские теги должны именоваться с обязательным добавлением символа "-". Если вы используете свой тег в разметке, но, при этом, вообще не создаете никакого компонента в JS и не добавляете его конструктор в реестр компонентов — браузер считает ваш тег «неизвестным элементом» (HTMLUnknownElement). По умолчанию, такие элементы аналогичны по поведению тегу «span». Этим можно воспользоваться, если вам нужно создать простой dumb-компонент с незатейливой структурой, для которой достаточно CSS правил ::before, ::after и выражения attr(). Пример:
Использование в разметке:
За последнее время я заметил целый ряд статей с разного рода критикой веб-компонентов. Порой, эта критика весьма жесткая и даже попахивает хейтерством. На мой взгляд, основной проблемой тут является отсутствие наработанной практики работы с этой группой стандартов у сообщества разработчиков. Многие привычные модели не всегда органично вписываются в проекты с участием Custom Elements и Shadow DOM, на многие вещи приходится смотреть под новым углом и не всем это нравится. Я вполне успешно работаю с веб-компонентами уже несколько лет и даже разрабатываю собственную библиотеку на их основе, поэтому считаю такую ситуацию не очень справедливой. Постараюсь, хотя бы частично, это исправить, по мере моих скромных сил. Я решил сделать серию компактных публикаций, в каждой из которых я планирую затронуть один из частых аспектов критики, а так-же продемонстрировать ряд технических приемов, которые могут оказаться интересными тем, кто еще не определился с тем, на какой стороне баррикад ему быть. Сегодня я хотел бы рассказать о том, как создавать компоненты без Shadow DOM.
Зачем?
Основная мысль, которую я хочу донести на этот раз, это то, что веб-компоненты и Shadow DOM — это не одно и то-же. При использовании Shadow DOM, вы получаете два основных преимущества:
- Изолированный участок документа, в котором ваши стили чувствуют себя в безопасности от внешних влияний и «протечек»
- Композиционный механизм, позволяющий разделить документ на то, что является структурой самого компонента и на его содержимое (потомки DOM-элемента в дереве)
Однако, данная механика, также, несет в себе некоторые издержки по созданию и стилизации изолированного пространства, что вполне естественно. В некоторых случаях (большие списки, ячейки таблиц с данными и т. д.), этих издержек хочется избежать по соображениям оптимизации производительности. Сейчас мы это исправим:
const MY_CSS = {
title: 'color: #00f; font-size: 2em',
item: 'color: #f00; font-size: 1.2em',
};
const DATA = [
{text: 'Text 1'},
{text: 'Text 2'},
{text: 'Text 3'},
];
let template = document.createElement('template');
template.innerHTML = /*html*/ `
<div style="${MY_CSS_.title}">List items:</div>
<div class="my-list">
${DATA.map(item => /*html*/ `<div style="${MY_CSS.item}">${item.text}</div>`).join('')}
</div>
`;
class ShadowlessComponent extends HTMLElement {
constructor() {
super();
this._contents = new DocumentFragment();
this._contents.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
this.appendChild(this._contents);
}
}
window.customElements.define('shadowless-component', ShadowlessComponent);
Если вы уже знакомы со стандартом Custom Elements, вы сразу заметите в чем дело: вместо вызова метода
attachShadow
в конструкторе компонента, мы создали DocumentFragment в который клонировали заранее подготовленный шаблон. На данном этапе, компонент не рендерится браузером и его можно относительно безопасно модифицировать, например, привязать/вставить данные.Следующий важный этап связан с жизненным циклом Custom Elements. В общий документ компоненты добавляются только после того, как полностью сработает конструктор и до этого момента, та часть DOM API, которая отвечает за работу с родителями или потомками элемента, а также, с атрибутами, будет недоступна. Поэтому, для непосредственного добавления контента в наш компонент, мы используем
connectedCallback
.При создании шаблона, мы, для простоты, использовали метод
innerHTML
. Данная операция выполняется только однажды, при создании элемента «template», она не повторяется каждый раз при создании экземпляра нашего компонента. Однако, этот момент также можно дополнительно оптимизировать создавая шаблоны императивно.Итого, используя в нашей разметке кастомный тег
shadowless-component
, мы получаем следующий результат в браузере:<shadowless-component>
<div id="caption" style="color: #00f; font-size: 2em">List items:</div>
<div class="my-list">
<div style="color: #f00; font-size: 1.2em">Text 1</div>
<div style="color: #f00; font-size: 1.2em">Text 2</div>
<div style="color: #f00; font-size: 1.2em">Text 3</div>
</div>
</shadowless-component>
Поскольку, избавившись от ShadowRoot, мы лишились и изоляции стилей, мы добавили стили в наш шаблон с помощью атрибута. В данном случае, они имеют приоритет, это частично решает проблему и может использоваться в важных местах. Для всех остальных случаев, доступна классическая стилизация через общую таблицу стилей, а кастомные теги выступают в роли удобных селекторов.
display: contents
Веб-компоненты являются полноправными узлами вашего DOM. Это означает, помимо того, что вам доступны все стандартные методы DOM-элементов, и то, что ваш компонент — это всегда некий контейнер. То есть, если вы захотите, с помощью веб-компонента, добавить в DOM произвольную структуру элементов, все они окажутся потомками вашего компонента, что не всегда удобно. В таких случаях, можно использовать новое CSS-правило — display: contents. Поддержка браузерами: caniuse.com/#feat=css-display-contents
По умолчанию же, все компоненты имеют свойство display: inline.
Немного похулиганим
А что если мы вообще не хотим никаких лишних контейнеров и кастомных тегов? Даешь чистый HTML!
Ок:
constructor() {
super();
this._contents = new DocumentFragment();
this._contents.appendChild(template.content.cloneNode(true));
this._titleEl = this._contents.querySelector('#caption');
window.setInterval(() => {
this._titleEl.textContent = Date.now();
}, 1000);
}
connectedCallback() {
this.parentNode.prepend(this._contents, this);
this.remove();
}
В результате получаем это:
<div id="caption" style="color: #00f; font-size: 2em">1581075598392</div>
<div class="my-list">
<div style="color: #f00; font-size: 1.2em">Text 1</div>
<div style="color: #f00; font-size: 1.2em">Text 2</div>
<div style="color: #f00; font-size: 1.2em">Text 3</div>
</div>
Все события и привязки продолжают работать и контролируются нашим компонентом, который теперь существует только в памяти. В данном случае, вам придется дополнительно позаботится о отписках и прочей очистке от мусора в момент, когда вы захотите удалить компонент полностью.
CSS Компоненты
Согласно стандарту, пользовательские теги должны именоваться с обязательным добавлением символа "-". Если вы используете свой тег в разметке, но, при этом, вообще не создаете никакого компонента в JS и не добавляете его конструктор в реестр компонентов — браузер считает ваш тег «неизвестным элементом» (HTMLUnknownElement). По умолчанию, такие элементы аналогичны по поведению тегу «span». Этим можно воспользоваться, если вам нужно создать простой dumb-компонент с незатейливой структурой, для которой достаточно CSS правил ::before, ::after и выражения attr(). Пример:
my-container {
display: block;
padding: 10px;
border: 1px solid currentColor;
}
my-container::before {
content: attr(caption);
margin-bottom: .6em;
}
Использование в разметке:
<my-container caption="Заголовок">Содержимое</my-container>
CoolCmd
cloneNode() возвращает DocumentFragment. зачем в первой строке создавать еще один?
i360u Автор
Вы правы, можно не создавать. Я хотел показать момент создания более явным образом, т.к. вставка фрагмента из темплейта может быть опциональным действием. Но, действительно, на практике лучше сделать так: