Когда я приходил на конференции и видел презентации на тему веб-компонентов, я всегда думал, что это не только изящно, но и довольно сложно. Тысяча строк JavaScript, чтобы сохранить всего 4 строки HTML. Докладчик или неизбежно скрывал за простыми вещами огромное количество JS кода, или погружался в сложные детали, тогда мои глаза начинали закрываться от скуки, и я начинал думать о том, покрывают ли мои суточные выплаты расходы на закуски.
Однако в недавнем проекте, созданном для легкого изучения HTML (Конечно, путем добавления зомби и глупых шуток), я решил, что необходимо описать каждый элемент HTML в спецификации. Не считая той конференции, я впервые начинал знакомство с <slot>
и <template>
элементами, и, когда я захотел написать что-то интересное о них в проекте, мне пришлось углубиться в тему.
И в процессе углубления я понял: веб-компоненты проще, чем я думал.
Либо веб-компоненты прошли долгий путь развития с тех пор, как я мечтал о закусках на конференции, либо я позволил моему изначальному страху помешать по-настоящему узнать их, а возможно и то, и другое.
Я здесь, чтобы сказать вам: да, вы можете создать веб-компонент. Давайте оставим страх и даже закуски за дверью, чтобы сделать все вместе.
Начнем с <template>
<template>
- это HTML элемент, позволяющий создать нам шаблон (HTML структуру для веб-компонентов).
Код
<template>
<p>The Zombies are coming!</p>
</template>
Элемент <template>
- очень важный, потому что позволяет держать все вместе. Это как база для вашего дома, база, с которой начинает строиться все, что мы называем готовым зданием. Давайте использовать этот небольшой фрагмент кода для нашего <apocalyptic-warning>
компонента, который оповещает нас о наступлении зомби-апокалипсиса.
Тогда есть компонент <slot>
<slot>
- это всего лишь HTML элемент, как и <template>
. Но в нашем случае <slot>
настраивает то, что <template>
отображает на странице.
Код
<template>
<p>The <slot>Zombies</slot> are coming!</p>
</template>
Здесь мы добавили слот со словом "Zombies" (Слово ли это?) в разметку <template>
. Если мы ничего не делаем со слотом, по умолчанию он отображает контент между тегами. В нашем случае это будет "Zombies".
Использование <slot>
похоже на placeholder
. Мы можем использовать placeholder
, тогда будет отображаться подсказка, или указать что-то другое вместоplaceholder
. В веб-компонентах это достигается с помощью атрибута name
.
Код
<template>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
name
атрибут сообщает веб-компоненту о том, какое содержание должно быть отображено в <template>
. Прямо сейчас у нас есть "whats-coming" слот. Мы предполагаем, что зомби придут первыми при наступлении зомби-апокалипсиса, но <slot>
дает нам гибкость, чтобы вставить что-то еще, если окажется, что первыми придут роботы или оборотни.
Использование веб-компонента
Технически, мы закончили писать собственный компонент и уже можем вставить в любое место, где захотим (Примечание переводчика: для полноценной работы компонента необходимо написать немного JS кода, о чем говорится далее).
Код
<apocalyptic-warning>
<span slot="whats-coming">Halitosis Laden Undead Minions</span>
</apocalyptic-warning>
<template>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
Видите, что мы делаем? Мы помещаем <apocalyptic-warning>
компонент на страницу так же, как и любой другой HTML элемент. Однако мы добавили <span>
в слот "whats-coming". <span>
и его содержимое будет отображено на месте "Zombies", когда компонент отобразится.
Стоит обратить внимание, что в названиях веб-компонентов должен быть дефис, чтобы предотвратить конфликт с HTML элементами, которые могут быть добавлены браузерами позже. Это то, о чем вы должны знать, когда дело касается веб-компонентов.
Все еще со мной? Не так уж и страшно, не правда ли? Что ж, минус зомби. Нам еще надо будет немного поработать, чтобы сделать замену <slot>
возможной, именно здесь мы начинаем знакомство с JavaScript.
Регистрация компонента
Как я уже сказал ранее, вам нужно немного JavaScript кода, чтобы все это работало, но это не очень сложный, многострочный и глубокий код, как я всегда думал. Надеюсь, я смогу убедить вас в том, что все просто.
Вам нужна функция-конструктор, которая регистрирует веб-компонент. Иначе наше компоненты будет как нежить: она есть, но не полностью живая.
Вот конструктор, который мы будем использовать.
Код
// Определяем собственный веб-компонент с подходящим именем
customElements.define("apocalyptic-warning", class extends HTMLElement {
// Наследование обеспечивает, что мы имеет свойства и методы по умолчанию встроенного HTML элемента
// Вызывается всякий раз, когда создается новый элемент
constructor() {
// Вызываем родительский конструктор, т.е конструктор для HTMLElement. Таким образом устанавливаются свойства базового HTML элемента.
super();
// Берет <template> и хранит его в переменной `warinng`
let warning = document.getElementById("warningtemplate");
// Хранит контент шаблона в переменной `mywarning`
let mywarning = warning.content;
const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
}
});
Я оставил в коде подробные построчные комментарии, но не объяснил код на последней строке.
Код
const shadowRoot = this.attachShadow({mode: "open"}).appendChild(mywarning.cloneNode(true));
Мы здесь делаем много работы. Во-первых, мы берем наш веб-компонент (this) и создаем скрытого шпиона, я имею в виду Shadow DOM.{ mode: open }
означает, что JavaScript может извне :root
обращаться к элементам Shadow DOM и управлять ими, что-то вроде настройки доступа к компоненту через черный вход. Был создан Shadow DOM и мы добавляем к ней узел (Примечание переводчика: HTML Node). Этот узел будет полной копией шаблона, включая все элементы и текст шаблона. С шаблоном, прикрепленным к Shadow DOM из пользовательского компонента, элемент <slot>
и slot атрибут берут на себя задачу сопоставления содержимого с тем, где оно должно находиться.
Проверьте это. Теперь мы можем объединить экземпляр одного и того же веб-компонента, отображая разный контент, просто изменив один элемент.
Код
JS:
customElements.define('apocalyptic-warning',
class extends HTMLElement {
constructor() {
super();
let warning = document.getElementById('warningtemplate');
let mywarning = warning.content;
const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(mywarning.cloneNode(true));
}
});
HTML:
<p>The Apocalypse will never happen!</p>
<apocalyptic-warning>
<span slot="whats-coming">Undead</span>
</apocalyptic-warning>
<apocalyptic-warning>
<span slot="whats-coming">Halitosis Laden Zombie Minions</span>
</apocalyptic-warning>
<template id="warningtemplate">
<style>
p {
background-color: pink;
padding: 0.5em;
border: 1px solid red;
}
</style>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
Пример: Codepen
Стилизация компонента
Возможно, вы заметили стилизацию в нашем примере. Как и следовало ожидать, у нас есть абсолютно все возможности для стилизации наших компонентов в CSS. На самом деле, мы можем включить <style>
элемент прямо в <template>
.
Код
<template id="warningtemplate">
<style>
p {
background-color: pink;
padding: 0.5em;
border: 1px solid red;
}
</style>
<p>The <slot name="whats-coming">Zombies</slot> are coming!</p>
</template>
Таким образом, стили применяются только для компонента, что позволяет изолировать их благодаря теневой модели Shadow DOM.
Я предположил, что пользовательский компонент берет копию шаблона, вставляет контент, который вы добавили, и внедряет его на страницу, используя Shadow DOM. Хотя может показаться, что работа Shadow DOM похожа на обычное DOM дерево, но это не так. Контент в веб-компоненте остается на месте, а теневой DOM как бы накладывается сверху.
И с этого момента контент технически находится вне шаблона, любые селекторы и классы, используемые в шаблонном <style>,
не будут действовать на контент внутри <slot>
. Это не позволяет достичь полной инкапсуляции, чего я ожидал. Но поскольку веб-компонент является элементом, мы можем использовать его как селектор элемента в любом CSS файле, включая основной файл, используемый на странице. И хотя вставленный контент технически не находится в шаблоне, он находится в веб-компоненте, где доступна стилизация через селекторы потомков CSS.
Код
apocalyptic-warning span {
color: blue;
}
Будьте осторожны, стили в основном файле CSS не могут получить доступ к внутренним элементам <template>
.
Код JavaScript точно такой же, за исключением того, что сейчас мы работаем с компонентом, у которого другое имя, <zombie-profile>.
Код
customElements.define("zombie-profile",
class extends HTMLElement {
constructor() {
super();
let profile = document.getElementById("zprofiletemplate");
let myprofile = profile.content;
const shadowRoot = this.attachShadow({mode: "open"}).appendChild(myprofile.cloneNode(true));
}
}
);
Вот шаблон HTML и инкапсулированный CSS.
Код
<template id="zprofiletemplate">
<style>
img {
width: 100%;
max-width: 300px;
height: auto;
margin: 0 1em 0 0;
}
h2 {
font-size: 3em;
margin: 0 0 0.25em 0;
line-height: 0.8;
}
h3 {
margin: 0.5em 0 0 0;
font-weight: normal;
}
.age, .infection-date {
display: block;
}
span {
line-height: 1.4;
}
.label {
color: #555;
}
li, ul {
display: inline;
padding: 0;
}
li::after {
content: ', ';
}
li:last-child::after {
content: '';
}
li:last-child::before {
content: ' and ';
}
</style>
<div class="profilepic">
<slot name="profile-image"><img src="https://assets.codepen.io/1804713/default.png" alt=""></slot>
</div>
<div class="info">
<h2><slot name="zombie-name" part="zname">Zombie Bob</slot></h2>
<span class="age"><span class="label">Age:</span> <slot name="z-age">37</slot></span>
<span class="infection-date"><span class="label">Infection Date:</span> <slot name="idate">September 12, 2025</slot></span>
<div class="interests">
<span class="label">Interests: </span>
<slot name="z-interests">
<ul>
<li>Long Walks on Beach</li>
<li>brains</li>
<li>defeating humanity</li>
</ul>
</slot>
</div>
<span class="z-statement"><span class="label">Apocalyptic Statement: </span> <slot name="statement">Moooooooan!</slot></span>
</div>
</template>
Вот CSS для нашего <zombie-profile>
элемента и его потомков из нашего основного файла CSS. Обратите внимание, что мы используем дублирование стилей, чтобы обеспечить одинаковую стилизацию и у замененных элементов, и у элементов <template>
.
Код
zombie-profile {
width: calc(50% - 1em);
border: 1px solid red;
padding: 1em;
margin-bottom: 2em;
display: grid;
grid-template-columns: 2fr 4fr;
column-gap: 20px;
}
zombie-profile img {
width: 100%;
max-width: 300px;
height: auto;
margin: 0 1em 0 0;
}
zombie-profile li, zombie-profile ul {
display: inline;
padding: 0;
}
zombie-profile li::after {
content: ', ';
}
zombie-profile li:last-child::after {
content: '';
}
zombie-profile li:last-child::before {
content: ' and ';
}
А вот и результат:
Пример: Codepen
Несмотря на то, что есть еще пару ловушек и нюансов, я надеюсь, вы получили больше возможностей для работы с веб-компонентами, чем несколько минут назад. Возможно, будет хорошо, если вы добавите в свою работу какой-то нестандартный компонент, чтобы прочувствовать их и понять, где их использование имеет смысл.
Вот и все. Чего вы сейчас боитесь больше: веб-компонентов или зомби-апокалипсиса? В недалеком прошлом я бы мог сказать, что веб-компонентов, но теперь зомби - единственное, что меня беспокоит (Ну, и покроют ли мои суточные выплаты расходы на закуски).
TheGodfather
Вставил в блокнот, сохранил как .html — не работает. Если вы пишете/переводите статью о конкретном фреймворке, то, пожалуйста, потрудитесь упомянуть во введении, о каких фреймворках речь.
sldo_ru Автор
В данном случае речь идет не о фреймворке, а о нативной реализации веб-компонентов через Shadow DOM, но, чтобы он окончательно заработал, необходимо дописать код на JavaScript. Что касается отображения в вашем случае, вот пример с рабочим кодом: codepen.io/sldo/pen/ExNMXEV. Браузер отображает текст, но необходимо дописывать JS для полноценной реализации.
hello_my_name_is_dany
Так Web Components и используется.
caniuse.com/custom-elementsv1
fedorro
В следующем разделе, «Регистрация компонента», приведен необходимый JS-код, который заставляет это работать. Сам не проверял, но должно…
justboris
Получается ситуация "каши из топора".
И показывают несколько строк html как будто все уже готово. И только потом вспоминают, что нужно еще JS и CSS...
sldo_ru Автор
Дополнил примечанием, чтобы было понятнее
justboris
спасибо, теперь перевод стал в чем-то лучше оригинала :)