От автора: когда я ходил на конференции и смотрел, как кто-то делает презентацию о веб-компонентах, я всегда думал, что это было довольно здорово (да, очевидно, я из 1950 года), и это всегда казалось мне очень сложным. Тысяча строк JavaScript, чтобы сэкономить четыре строки HTML. Докладчик неизбежно либо замалчивал уйму JavaScript, который заставляет это работать, либо они вдавались в мучительные детали, и мои глаза наполнялись грустью от непонимания того, о чем говорят.
Но в недавнем справочном проекте, призванном упростить изучение HTML (конечно, путем проб и ошибок), я решил, что мне нужно познакомится с каждым элементом HTML в спецификации. Помимо этих конференций, это было мое первое знакомство с элементами <slot> и <template>. Но для написания чего-нибудь простенького и увлекательного, пришлось копнуть немного глубже.
И я кое-что узнал в процессе: веб-компоненты намного проще, чем кажутся. Либо веб-компоненты прошли долгий путь с тех пор, как я в последний раз поймал себя на мечтах о работе с ними на конференции, либо я позволил своему первоначальному страху перед ними помешать по-настоящему узнать их — возможно, и то и другое.
Я здесь, чтобы сказать вам, что вы — да, вы — можете создать веб-компонент. Давайте на мгновение оставим наши отвлекающие факторы и страхи, давайте сделаем это вместе.
Начнем с <template>
A <template> — это элемент HTML, который позволяет нам создать, шаблон — структуру HTML для веб-компонента. Шаблон не обязательно должен быть огромным фрагментом кода. Он может быть очень простой:
<template> <p>The Zombies are coming!</p> </template>
Элемент <template> имеет важное значение, потому что он связывает все вместе. Это как фундамент здания; это база, на которой построено все остальное. Давайте использовать этот небольшой фрагмент HTML в качестве шаблона для нашего веб-компонента <apocalyptic-warning> — в качестве предупреждения, когда на нас надвигается зомби-апокалипсис.
Элемент <slot>
<slot> это просто еще один элемент HTML, как и <template>. Но в этом случае <slot> настраивает то, как <template> отображается на странице.
<template> <p>The <slot>Zombies</slot> are coming!</p> </template>
Здесь мы вставили слово «Zombies» в шаблонную разметку. Если мы ничего не делаем с тегом slot, по умолчанию используется содержимое между тегами.
Использование slot очень похоже на заполнитель. Мы можем использовать заполнитель как есть или вместо него указать что-нибудь другое. Мы делаем это с помощью атрибута name.
<template> <p>The <slot name="whats-coming">Zombies</slot> are coming!</p> </template>
Атрибут name сообщает о веб-компоненте, содержание которого описано в шаблоне. Прямо сейчас у нас есть слот под названием whats-coming. Мы предполагаем, что зомби придут первыми в апокалипсисе, но этот slot дает нам некоторую гибкость, чтобы вставить что-то еще, например, если это будет робот, оборотень или даже апокалипсисом веб-компонентов.
Использование компонента
Технически мы закончили «писать» компонент и можем вставить его в любое место, где захотим.
<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> на страницу точно так же, как любой другой div или что-то еще. Но мы также добавили в span ссылку на name атрибут нашего slot. И то, что между ними, в span мы можем заменить на «Zombies» при рендеринге компонента.
Вот небольшая ошибка, на которую стоит обратить внимание: в именах пользовательских элементов должен быть дефис. Это просто одна из тех вещей, которые вам нужно знать. Спецификация предписывает это для предотвращения конфликтов в случае, если в HTML появится элемент с тем же именем.
Вы все еще со мной? Не так уж и страшно, правда? Ну, если не считать зомби. Нам еще предстоит немного поработать, чтобы сделать замену slot возможной, и именно здесь мы начинаем разбираться с JavaScript.
Регистрация компонента
Как я уже сказал, вам нужен некоторый JavaScript, чтобы все это работало, но это не сверхсложный, многострочный и глубокий код, как я думал раньше. Надеюсь, я смогу убедить и вас. Вам нужна функция-конструктор, регистрирующая пользовательский элемент. Вот конструктор, который мы будем использовать:
// Определяет элемент с нашим именем, <apocalyptic-warning> customElements.define("apocalyptic-warning", // Гарантирует, что у нас есть все свойства и методы по умолчанию встроенного элемента HTML class extends HTMLElement { // Вызывается каждый раз, когда создается новый пользовательский элемент constructor() { // Вызывает родительский конструктор, то есть конструктор для элемента HTML, чтобы все было настроено точно так же, как и при создании встроенного HTML-элемента. super(); // Берёт <template> и сохраняет его в `warning` 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));
Мы здесь много делаем. Во-первых, мы берем наш custom element (this) и создаем скрытого агента — я имею в виду теневой DOM. mode: open просто означает, что JavaScript извне :root может обращаться к элементам в теневой DOM и манипулировать ими, что-то вроде настройки доступа к компоненту через черный ход.
Была создана теневая DOM, и мы добавили к ней узел. Этот узел есть полной копией шаблона, включая все элементы и текст шаблона. Из шаблона, прикрепленного к теневому DOM пользовательского элемента, подбирается содержимое атрибута slot и подставляется на свое место.
Проверь это. Теперь мы можем объединить два экземпляра одного и того же компонента, отображая разный контент, просто изменив один элемент.
Стилизация компонента
Возможно, вы заметили стиль в этой демонстрации. Как и следовало ожидать, у нас есть абсолютная возможность стилизовать наш компонент с помощью 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>
Таким образом, стили ограничиваются непосредственно компонентом, и ничего не просачивается в другие элементы на той же странице благодаря теневой модели DOM.
Я предполагал, что пользовательский элемент берет копию шаблона, вставляет добавленный нами контент, а затем внедряет его на страницу с помощью теневой модели DOM. Хотя так это выглядит во внешнем интерфейсе, на самом деле это не верно. Контент в пользовательском элементе остается прямо на месте, а теневой DOM как бы накладывается сверху, как наложение.
А поскольку содержимое технически находится за пределами шаблона, любые дочерние селекторы или классы, которые мы используем в элементе шаблона style, не будут влиять на содержимое, размещенное в slot. Это не позволяет обеспечить полную инкапсуляцию, как я ожидал. Но поскольку пользовательский элемент является элементом, мы можем использовать его в качестве селектора элемента в любом файле CSS, включая основную таблицу стилей, используемую на странице. И хотя вставленный материал технически не находится в шаблоне, он находится в пользовательском элементе, и селекторы потомков из CSS будут работать.
apocalyptic-warning span { color: blue; }
Но будьте осторожны! Стили в основном файле CSS не могут получить доступ к элементам в template теневой модели DOM.
Соберем все это вместе
Давайте посмотрим на пример, скажем, профиль для службы знакомств с зомби, вроде того, который вам может понадобиться после апокалипсиса. Чтобы стилизовать и содержимое по умолчанию и любой вставленный контент, нам нужен элемент style в template и стили в файле CSS.
Код 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. Обратите внимание на дублирование, чтобы гарантировать, что и замененные элементы, и элементы из шаблона имеют одинаковый стиль.
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 '; }
Теперь все собрано вместе!
Несмотря на то, что есть еще несколько ошибок и других нюансов, я надеюсь, что вы увидели больше возможностей работать с веб-компонентами сейчас, чем несколько минут назад. Возможно, добавьте в свою работу нестандартный компонент, чтобы почувствовать его и понять, где это имеет смысл.
Это действительно так. Чего вы боитесь больше, веб-компонентов или зомби-апокалипсиса? Я мог бы сказать, что в недалеком прошлом боялся веб-компонентов, но теперь я с гордостью могу сказать, что зомби — единственное, что меня беспокоит.
Автор: John Rhea
Источник: css-tricks.com
Редакция: Команда webformyself.
Читайте нас в Telegram, VK, Яндекс.Дзен