Shadow DOM: <slot>
Glossary overview

Shadow DOM: <slot>

Source: https://learn.javascript.ru/web-components

Пользовательские элементы (Custom Elements)

Мы можем создавать пользовательские HTML-элементы, описываемые нашим классом, со своими методами и свойствами, событиями и так далее.

Как только пользовательский элемент определён, мы можем использовать его наравне со встроенными HTML-элементами.

Это замечательно, ведь словарь HTML-тегов богат, но не бесконечен. Не существует <easy-tabs><sliding-carousel><beautiful-upload>… Просто подумайте о любом другом теге, который мог бы нам понадобиться.

Мы можем определить их с помощью специального класса, а затем использовать, как если бы они всегда были частью HTML.

Существует два вида пользовательских элементов:

  1. Автономные пользовательские элементы – «полностью новые» элементы, расширяющие абстрактный класс HTMLElement.
  2. Пользовательские встроенные элементы – элементы, расширяющие встроенные, например кнопку HTMLButtonElement и т.п.

Сначала мы разберёмся с автономными элементами, а затем перейдём к пользовательским встроенным.

Чтобы создать пользовательский элемент, нам нужно сообщить браузеру ряд деталей о нём: как его показать, что делать, когда элемент добавляется или удаляется со страницы и т.д.

Это делается путём создания класса со специальными методами. Это просто, так как существует всего несколько методов, и все они являются необязательными.

Вот набросок с полным списком:

class MyElement extends HTMLElement {
  constructor() {
    super();
    // элемент создан
  }

  connectedCallback() {
    // браузер вызывает этот метод при добавлении элемента в документ
    // (может вызываться много раз, если элемент многократно добавляется/удаляется)
  }

  disconnectedCallback() {
    // браузер вызывает этот метод при удалении элемента из документа
    // (может вызываться много раз, если элемент многократно добавляется/удаляется)
  }

  static get observedAttributes() {
    return [/* массив имён атрибутов для отслеживания их изменений */];
  }

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

  adoptedCallback() {
    // вызывается, когда элемент перемещается в новый документ
    // (происходит в document.adoptNode, используется очень редко)
  }

  // у элемента могут быть ещё другие методы и свойства
}

После этого нам нужно зарегистрировать элемент:

// сообщим браузеру, что <my-element> обслуживается нашим новым классом
customElements.define("my-element", MyElement

Теперь для любых HTML-элементов с тегом <my-element> создаётся экземпляр MyElement и вызываются вышеупомянутые методы. Также мы можем использовать document.createElement('my-element') в JavaScript.

Имя пользовательского элемента должно содержать дефис -

Имя пользовательского элемента должно содержать дефис -, например, my-element и super-button – валидные имена, а myelement – нет.

Это чтобы гарантировать отсутствие конфликтов имён между встроенными и пользовательскими элементами HTML.

Пример: «time-formatted»

Например, элемент <time> уже существует в HTML для даты/времени. Но сам по себе он не выполняет никакого форматирования. Давайте создадим элемент <time-formatted>, который отображает время в удобном формате с учётом языка:

<script>
class TimeFormatted extends HTMLElement { // (1)

  connectedCallback() {
    let date = new Date(this.getAttribute('datetime') || Date.now());

    this.innerHTML = new Intl.DateTimeFormat("default", {
      year: this.getAttribute('year') || undefined,
      month: this.getAttribute('month') || undefined,
      day: this.getAttribute('day') || undefined,
      hour: this.getAttribute('hour') || undefined,
      minute: this.getAttribute('minute') || undefined,
      second: this.getAttribute('second') || undefined,
      timeZoneName: this.getAttribute('time-zone-name') || undefined,
    }).format(date);
  }

}

customElements.define("time-formatted", TimeFormatted); // (2)
</script>

<!-- (3) -->
<time-formatted datetime="2019-12-01"
  year="numeric" month="long" day="numeric"
  hour="numeric" minute="numeric" second="numeric"
  time-zone-name="short"
></time-formatted>

// Output
1 декабря 2019 г. в 2:00:00 GMT+2
  1. Класс имеет только один метод connectedCallback() – браузер вызывает его, когда элемент <time-formatted> добавляется на страницу (или когда HTML-парсер обнаруживает его), и он использует встроенный форматировщик данных Intl.DateTimeFormat, хорошо поддерживаемый в браузерах, чтобы показать красиво отформатированное время.
  2. Нам нужно зарегистрировать наш новый элемент, используя customElements.define(tag, class).
  3. И тогда мы сможем использовать его везде.

** Я не все записал в контекст **

Порядок рендеринга

Когда HTML-парсер строит DOM, элементы обрабатываются друг за другом, родители до детей. Например, если у нас есть <outer><inner></inner></outer>, то элемент <outer> создаётся и включается в DOM первым, а затем <inner>.

Это приводит к важным последствиям для пользовательских элементов.

Например, если пользовательский элемент пытается получить доступ к innerHTML в connectedCallback, он ничего не получает:

<script>
customElements.define('user-info', class extends HTMLElement {

  connectedCallback() {
    alert(this.innerHTML); // пусто (*)
  }

});
</script>

<user-info>Джон</user-info>

Если вы запустите это, alert будет пуст.

Это происходит именно потому, что на этой стадии ещё не существуют дочерние элементы, DOM не завершён. HTML-парсер подключил пользовательский элемент <user-info> и теперь собирается перейти к его дочерним элементам, но пока не сделал этого.

Если мы хотим передать информацию в пользовательский элемент, мы можем использовать атрибуты. Они доступны сразу.

Или, если нам действительно нужны дочерние элементы, мы можем отложить доступ к ним, используя setTimeout с нулевой задержкой.

Это работает:

<script>
customElements.define('user-info', class extends HTMLElement {

  connectedCallback() {
    setTimeout(() => alert(this.innerHTML)); // Джон (*)
  }

});
</script>

<user-info>Джон</user-info>

Теперь alert в строке (*) показывает «Джон», поскольку мы запускаем его асинхронно, после завершения парсинга HTML. Мы можем обработать дочерние элементы при необходимости и завершить инициализацию.

С другой стороны, это решение также не идеально. Если вложенные пользовательские элементы тоже используют setTimeout для инициализации, то они встают в очередь: первым запускается внешний setTimeout, а затем внутренний.

Так что внешний элемент завершает инициализацию раньше внутреннего.

Продемонстрируем это на примере:

<script>
customElements.define('user-info', class extends HTMLElement {
  connectedCallback() {
    alert(`${this.id} connected.`);
    setTimeout(() => alert(`${this.id} initialized.`));
  }
});
</script>

<user-info id="outer">
  <user-info id="inner"></user-info>
</user-info>

Порядок вывода:

  1. outer connected.
  2. inner connected.
  3. outer initialized.
  4. inner initialized.

Мы ясно видим, что внешний элемент outer завершает инициализацию (3) до внутреннего inner (4).

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

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

Новые элементы, которые мы создаём, такие как <time-formatted>, не имеют связанной с ними семантики. Они не известны поисковым системам, а устройства для людей с ограниченными возможностями не могут справиться с ними.

Но такие вещи могут быть важны. Например, поисковой системе было бы интересно узнать, что мы показываем именно время. А если мы делаем специальный вид кнопки, почему не использовать существующую функциональность <button>?

Мы можем расширять и модифицировать встроенные HTML-элементы, наследуя их классы.

Например, кнопки <button> являются экземплярами класса HTMLButtonElement, давайте построим элемент на его основе.

  1. Унаследуем HTMLButtonElement нашим классом:
class HelloButton extends HTMLButtonElement { /* методы пользовательского элемента */ }

2. Предоставим третий аргумент в customElements.define, указывающий тег:

customElements.define('hello-button', HelloButton, {extends: 'button'});

Бывает, что разные теги имеют одинаковый DOM-класс, поэтому указание тега необходимо.

3. В конце, чтобы использовать наш пользовательский элемент, вставим обычный тег <button>, но добавим к нему is="hello-button":

    <button is="hello-button">...</button>

    Вот полный пример:

    <script>
    // Кнопка, говорящая "привет" по клику
    class HelloButton extends HTMLButtonElement {
      constructor() {
        super();
        this.addEventListener('click', () => alert("Привет!"));
      }
    }
    
    customElements.define('hello-button', HelloButton, {extends: 'button'});
    </script>
    
    <button is="hello-button">Нажми на меня</button>
    
    <button is="hello-button" disabled>Отключена</button>

    Output:

    Наша новая кнопка расширяет встроенную. Так что она сохраняет те же стили и стандартные возможности, наподобие атрибута disabled.

    Shadow DOM

    Теневой DOM («Shadow DOM») используется для инкапсуляции. Благодаря ему в компоненте есть собственное «теневое» DOM-дерево, к которому нельзя просто так обратиться из главного документа, у него могут быть изолированные CSS-правила и т.д.

    Встроенный теневой DOM

    Задумывались ли вы о том, как устроены и стилизованы сложные браузерные элементы управления?

    Например, <input type="range">:

    Браузер рисует их своими силами и по своему усмотрению. Их DOM-структура обычно нам не видна, но в инструментах разработчика можно её посмотреть. К примеру, в Chrome для этого нужно активировать пункт «Show user agent shadow DOM».

    После этого <input type="range"> выглядит так:

    То, что находится под #shadow-root – и называется «shadow DOM» (теневой DOM).

    Мы не можем получить доступ к теневому DOM встроенных элементов с помощью обычных JavaScript-вызовов или с помощью селекторов. Это не просто обычные потомки, это мощное средство инкапсуляции.

    В примере выше можно увидеть полезный атрибут pseudo. Он нестандартный и существует по историческим причинам. С его помощью можно стилизовать подэлементы через CSS, например, так:

    <style>
    /* делаем цвет шкалы ползунка красным */
    input::-webkit-slider-runnable-track {
      background: red;
    }
    </style>
    
    <input type="range">

    Ещё раз заметим, что pseudo – нестандартный атрибут. Если говорить хронологически, то сначала браузеры начали экспериментировать с инкапсуляцией внутренних DOM-структур для элементов, а уже потом, через некоторое время, появился стандарт Shadow DOM, который позволяет делать то же самое нам, разработчикам.

    Далее мы воспользуемся современным стандартом Shadow DOM, описанным в спецификации DOM spec и других спецификациях.

    Теневое дерево

    Каждый DOM-элемент может иметь 2 типа поддеревьев DOM:

    1. Light tree – обычное, «светлое», DOM-поддерево, состоящее из HTML-потомков. Все поддеревья, о которых мы говорили в предыдущих главах, были «light».
    2. Shadow tree – скрытое, «теневое», DOM-поддерево, не отражённое в HTML, скрытое от посторонних глаз.

    Если у элемента имеются оба поддерева, браузер отрисовывает только теневое дерево. Также мы всё же можем задать «композицию» теневого и обычного деревьев. Позже в главе Слоты теневого DOM, композиция мы рассмотрим детали.

    Теневое дерево можно использовать в пользовательских элементах (Custom Elements), чтобы спрятать внутренности компонента и применить к ним локальные стили.

    Например, этот <show-hello> элемент прячет свой внутренний DOM в теневом дереве:

    <script>
    customElements.define('show-hello', class extends HTMLElement {
      connectedCallback() {
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<p>
          Hello, ${this.getAttribute('name')}
        </p>`;
      }
    });
    </script>
    
    <show-hello name="John"></show-hello>
    
    // Output
    Hello, John

    А вот как получившийся DOM выглядит в инструментах разработчика в Chrome, весь контент внутри «#shadow-root»:

    Итак, вызов elem.attachShadow({mode: …}) создаёт теневое дерево.

    Есть два ограничения:

    1. Для каждого элемента мы можем создать только один shadow root.
    2. В качестве elem может быть использован пользовательский элемент (Custom Element), либо один из следующих элементов: «article», «aside», «blockquote», «body», «div», «footer», «h1…h6», «header», «main», «nav», «p», «section» или «span». Остальные, например, <img>, не могут содержать теневое дерево.

    Свойство mode задаёт уровень инкапсуляции. У него может быть только два значения:

    • "open" – корень теневого дерева («shadow root») доступен как elem.shadowRoot.Любой код может получить теневое дерево elem.
    • "closed" – elem.shadowRoot всегда возвращает null.До теневого DOM в таком случае мы сможем добраться только по ссылке, которую возвращает attachShadow (и, скорее всего, она будет спрятана внутри класса). Встроенные браузерные теневые деревья, такие как у <input type="range">, закрыты. До них не добраться.

    С возвращаемым методом attachShadow объектом корнем теневого дерева, можно работать как с обычным DOM-элементом: менять его innerHTML или использовать методы DOM, такие как append, чтобы заполнить его.

    Элемент с корнем теневого дерева называется – «хозяин» (host) теневого дерева, и он доступен в качестве свойства host у shadow root:

    // при условии, что {mode: "open"}, иначе elem.shadowRoot равен null
    alert(elem.shadowRoot.host === elem); // true

    Инкапсуляция

    Теневой DOM отделён от главного документа:

    1. Элементы теневого DOM не видны из обычного DOM через querySelector. В частности, элементы теневого DOM могут иметь такие же идентификаторы, как у элементов в обычном DOM (light DOM). Они должны быть уникальными только внутри теневого дерева.
    2. У теневого DOM свои стили. Стили из внешнего DOM не применятся.

    Например:

    <style>
      /* стили документа не применятся в теневом дереве внутри #elem (1) */
      p { color: red; }
    </style>
    
    <div id="elem"></div>
    
    <script>
      elem.attachShadow({mode: 'open'});
        // у теневого дерева свои стили (2)
      elem.shadowRoot.innerHTML = `
        <style> p { font-weight: bold; } </style>
        <p>Hello, John!</p>
      `;
    
      // <p> виден только запросам внутри теневого дерева (3)
      alert(document.querySelectorAll('p').length); // 0
      alert(elem.shadowRoot.querySelectorAll('p').length); // 1
    </script>
    1. Стили главного документа не влияют на теневое дерево.
    2. …Но свои внутренние стили работают.
    3. Чтобы добраться до элементов в теневом дереве, нам нужно искать их изнутри самого дерева.

    Итого

    Теневой DOM – это способ создать свой, изолированный, DOM для компонента.

    1. shadowRoot = elem.attachShadow({mode: open|closed}) – создаёт теневой DOM для elem. Если mode="open", он доступен через свойство elem.shadowRoot.
    2. Мы можем создать подэлементы внутри shadowRoot с помощью innerHTML или других методов DOM.

    Элементы теневого DOM:

    • Обладают собственной областью видимости идентификаторов
    • Невидимы JavaScript селекторам из главного документа, таким как querySelector,
    • Стилизуются своими стилями из теневого дерева, не из главного документа.

    Теневой DOM, если имеется, отрисовывается браузером вместо обычных потомков (light DOM). В главе Слоты теневого DOM, композиция мы разберём, как делать их композицию.

    Интересные комментарии:

    1. Для этого существуют CSS модули или StyledComponents (одинаковый механизм). Шадоу дом это кошмар и мрак, не нужно его использовать, у него нет преимуществ в современной разработке, банально он нарушает принцип KISS.
    2. Очень хорошо прячут в него рекламные баннеры. Уже используют для вывода рекламы, смотрю. Расширениями типа Stylish такое не удалить. Adblock тоже не особо помогает. Видимо, только user скриптами как-то можно.
    3. Таки не понял для чего ее нужно применять? Практическое применение, таксать. Если я правильно понял, то для различных виджетов, чтобы на них не имели влияние стили применяемые к странице.
    4. Вот как раз из-за невозможности контроля инкапсуляции shadowdom мы от них и отказались.
      Никому не нужны виджеты, которые нельзя стилями родительского приложения стилизовать под стиль родительского приложения.
      А костыли с css переменными не катят, ибо часто надо переписать полностью стили даже box-mode, да и могут появиться новые атрибуты, для которых и переменных нет. Ну или вам придется описывать все возможные переменные на всех детях своего дома css переменными. В итоге lightdom лучше оказался для жизни + scoped css костыль, если надо.
      Так что увы не взлетели веб компоненты.
    5. так вот вопрос, если shadow dom используется для инкапсуляции, то если мы хотим работать с элементом, далее по жизненному циклу интерфейса, я так пониманию mode должен быть open. но тогда этот элемент всеравно можно ИЗ JS получить. или элемент создается. изменяет и потом запечатывается, но внутри скриптом связанным с этим элементом что-то можно менять. это пока так и не понятно. Ответ: mode играет роль лишь для внешнего доступа к содержимому shadow-root, если open то мы можем у элемента хоста запросить его shadowRoot, если closed тогда вы получите отказ со значением null, словно shadow-root и не было. В целом смысл прост, хост элемент при closed просто не раскрывает вам о том имеет ли он shadow-root, а поэтому ни один скрипт внешний не сможет достучаться в этот shadow-root и напакостить. Но что важно, это то что логика находящаяся внутри shadow-root может взаимодействовать с внешней (получить доступ к каким-либо элементам)

    Элемент “template”

    Встроенный элемент <template> предназначен для хранения шаблона HTML. Браузер полностью игнорирует его содержимое, проверяя лишь синтаксис, но мы можем использовать этот элемент в JavaScript, чтобы создать другие элементы.

    В теории, для хранения разметки мы могли бы создать невидимый элемент в любом месте HTML. Что такого особенного в <template>?

    Во-первых, его содержимым может быть любой корректный HTML-код, даже такой, который обычно нуждается в специальном родителе.

    К примеру, мы можем поместить сюда строку таблицы <tr>:

    <template>
      <tr>
        <td>Содержимое</td>
      </tr>
    </template>

    Обычно, если элемент <tr> мы поместим, скажем, в <div>, браузер обнаружит неправильную структуру DOM и «исправит» её, добавив снаружи <table>. Это может оказаться не тем, что мы хотели. <template> же оставит разметку ровно такой, какой мы её туда поместили.

    Также внутри <template> можно поместить стили и скрипты:

    <template>
      <style>
        p { font-weight: bold; }
      </style>
      <script>
        alert("Привет");
      </script>
    </template>

    Браузер рассматривает содержимое <template> как находящееся «вне документа»: стили, определённые в нём, не применяются, скрипты не выполнятся<video autoplay> не запустится и т.д.

    Содержимое оживёт (скрипт выполнится), когда мы поместим его в нужное нам место.

    Использование template

    Содержимое шаблона доступно по его свойству content в качестве DocumentFragment – особый тип DOM-узла.

    Можно обращаться с ним так же, как и с любыми другими DOM-узлами, за исключением одной особенности: когда мы его куда-то вставляем, то в это место вставляется не он сам, а его дети.

    Пример:

    <template id="tmpl">
      <script>
        alert("Привет");
      </script>
      <div class="message">Привет, Мир!</div>
    </template>
    
    <script>
      let elem = document.createElement('div');
    
      // Клонируем содержимое шаблона для того, чтобы переиспользовать его несколько раз
      elem.append(tmpl.content.cloneNode(true));
    
      document.body.append(elem);
      // Сейчас скрипт из <template> выполнится
    </script>

    Давайте перепишем пример Shadow DOM из прошлой главы учебника с помощью <template>:

    <template id="tmpl">
      <style> p { font-weight: bold; } </style>
      <p id="message"></p>
    </template>
    
    <div id="elem">Нажми на меня</div>
    
    <script>
      elem.onclick = function() {
        elem.attachShadow({mode: 'open'});
    
        elem.shadowRoot.append(tmpl.content.cloneNode(true)); // (*)
    
        elem.shadowRoot.getElementById('message').innerHTML = "Привет из теней!";
      };
    </script>

    Когда мы клонируем и вставляем tmpl.content в строке (*), то, так как это DocumentFragment, вместо него вставляются его потомки (<style><p>).

    Именно они и формируют теневой DOM:

    <div id="elem">
      #shadow-root
        <style> p { font-weight: bold; } </style>
        <p id="message"></p>
    </div>

    Итого

    Подводим итоги:

    • Содержимым <template> может быть любой синтаксически корректный HTML.
    • Содержимое <template> считается находящимся «вне документа», поэтому оно ни на что не влияет.
    • Мы можем получить доступ к template.content из JavaScript, клонировать его и переиспользовать в новом компоненте.

    Элемент <template> уникальный по следующим причинам:

    • Браузер проверяет правильность HTML-синтаксиса в нём (в отличие от строк в скриптах).
    • …При этом позволяет использовать любые HTML-теги, даже те, которые без соответствующей обёртки не используются (например <tr>).
    • Его содержимое оживает (скрипты выполняются, <video autoplay> проигрывается и т. д.), когда помещается в документ.

    Элемент <template> не поддерживает итерацию, связывания данных или подстановки переменных. Однако эти возможности можно реализовать поверх него.

    Слоты теневого DOM, композиция

    Многим типам компонентов, таким как вкладки, меню, галереи изображений и другие, нужно какое-то содержимое для отображения.

    Так же, как встроенный в браузер <select> ожидает получить контент пунктов <option>, компонент <custom-tabs> может ожидать, что будет передано фактическое содержимое вкладок, а <custom-menu> – пунктов меню.

    Код, использующий меню <custom-menu>, может выглядеть так:

    <custom-menu>
      <title>Сладости</title>
      <item>Леденцы</item>
      <item>Фруктовые тосты</item>
      <item>Кексы</item>
    </custom-menu>

    …Затем компонент должен правильно его отобразить – как обычное меню с заданным названием и пунктами, обрабатывать события меню и т.д.

    Как это реализовать?

    Можно попробовать проанализировать содержимое элемента и динамически скопировать и переставить DOM-узлы. Это возможно, но если мы будем перемещать элементы в теневой DOM, CSS-стили документа не будут применяться, и мы потеряем визуальное оформление. Кроме того, нужно будет писать дополнительный код.

    К счастью, нам этого делать не нужно. Теневой DOM поддерживает элементы <slot>, которые автоматически наполняются контентом из обычного, «светлого» DOM-дерева.

    Именованные слоты

    Давайте рассмотрим работу слотов на простом примере.

    Теневой DOM <user-card> имеет два слота, заполняемых из обычного DOM:

    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `
          <div>Имя:
            <slot name="username"></slot>
          </div>
          <div>Дата рождения:
            <slot name="birthday"></slot>
          </div>
        `;
      }
    });
    </script>
    
    <user-card>
      <span slot="username">Иван Иванов</span>
      <span slot="birthday">01.01.2001</span>
    </user-card>
    
    // Output
    Имя: Иван Иванов
    Дата рождения: 01.01.2001

    В теневом DOM <slot name="X"> определяет «точку вставки» – место, где отображаются элементы с slot="X".

    Затем браузер выполняет «композицию»: берёт элементы из обычного DOM-дерева и отображает их в соответствующих слотах теневого DOM-дерева. В результате мы получаем именно то, что хотели – компонент, который можно наполнить данными.

    После выполнения скрипта структура DOM выглядит следующим образом (без учёта композиции):

    <user-card>
      #shadow-root
        <div>Имя:
          <slot name="username"></slot>
        </div>
        <div>Дата рождения:
          <slot name="birthday"></slot>
        </div>
      <span slot="username">Иван Иванов</span>
      <span slot="birthday">01.01.2001</span>
    </user-card>

    Мы создали теневой DOM, он изображён под #shadow-root. Теперь у элемента есть два DOM-дерева: обычное («светлое») и теневое.

    Чтобы отобразить содержимое, для каждого <slot name="..."> в теневом DOM браузер ищет slot="..." с таким же именем в обычном DOM. Эти элементы отображаются внутри слотов:

    В результате выстраивается так называемое «развёрнутое» (flattened) DOM-дерево:

    <user-card>
      #shadow-root
        <div>Имя:
          <slot name="username">
            <!-- элемент слота вставляется в слот -->
            <span slot="username">Иван Иванов</span>
          </slot>
        </div>
        <div>Дата рождения:
          <slot name="birthday">
            <span slot="birthday">01.01.2001</span>
          </slot>
        </div>
    </user-card>

    …Но развёрнутое DOM-дерево существует только для целей отображения и обработки событий. Это то, что мы видим на экране. Оно, в некотором плане, «виртуальное». Фактически в документе расположение узлов не меняется.

    Это можно легко проверить, запустив querySelectorAll: все узлы находятся на своих местах.

    // узлы светлого DOM находятся в том же месте, в `<user-card>`
    alert( document.querySelectorAll('user-card span').length ); // 2

    Так что развёрнутый DOM составляется из теневого вставкой в слоты. Браузер использует его для рендеринга и при всплытии событий (об этом позже). Но JavaScript видит документ «как есть» – до построения развёрнутого DOM-дерева.

    Пример меню

    Давайте вернёмся к меню <custom-menu>, упомянутому в начале главы.

    Мы можем использовать слоты для распределения элементов.

    Вот разметка для меню <custom-menu>:

    <custom-menu>
      <span slot="title">Сладости</span>
      <li slot="item">Леденцы</li>
      <li slot="item">Фруктовые тосты</li>
      <li slot="item">Кексы</li>
    </custom-menu>

    Шаблон теневого DOM-дерева с правильными слотами:

    <template id="tmpl">
      <style> /* стили меню */ </style>
      <div class="menu">
        <slot name="title"></slot>
        <ul><slot name="item"></slot></ul>
      </div>
    </template>
    1. <span slot="title"> попадает в <slot name="title">.
    2. В шаблоне много элементов <li slot="item">, но только один слот <slot name="item">. Поэтому все такие <li slot="item"> добавляются в <slot name="item"> один за другим, формируя список.

    Развёрнутое DOM-дерево становится таким:

    <custom-menu>
      #shadow-root
        <style> /* стили меню */ </style>
        <div class="menu">
          <slot name="title">
            <span slot="title">Сладости</span>
          </slot>
          <ul>
            <slot name="item">
              <li slot="item">Леденцы</li>
              <li slot="item">Фруктовые тосты</li>
              <li slot="item">Кексы</li>
            </slot>
          </ul>
        </div>
    </custom-menu>

    Можно заметить, что в валидном DOM-дереве тег <li> должен быть прямым потомком тега <ul>. Но это развёрнутый DOM, который описывает то, как компонент отображается, в нём такая ситуация нормальна.

    Осталось только добавить обработчик click для открытия и закрытия списка, и меню <custom-menu> готово:

    customElements.define('custom-menu', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
    
        // tmpl -- шаблон для теневого DOM-дерева (выше)
        this.shadowRoot.append( tmpl.content.cloneNode(true) );
    
        // мы не можем выбирать узлы светлого DOM, поэтому обработаем клики на слоте
        this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
          // открыть/закрыть меню
          this.shadowRoot.querySelector('.menu').classList.toggle('closed');
        };
      }
    });

    Вот полное демо:

    Конечно, мы можем расширить функциональность меню, добавив события, методы и т.д.

    Обновление слотов

    Что если внешний код хочет динамически добавить или удалить пункты меню?

    Браузер наблюдает за слотами и обновляет отображение при добавлении и удалении элементов в слотах.

    Также, поскольку узлы светлого DOM-дерева не копируются, а только отображаются в слотах, изменения внутри них сразу же становятся видны.

    Таким образом, нам ничего не нужно делать для обновления отображения. Но если код компонента хочет узнать об изменениях в слотах, можно использовать событие slotchange.

    Например, здесь пункт меню вставляется динамически через 1 секунду, и заголовок меняется через 2 секунды:

    <custom-menu id="menu">
      <span slot="title">Сладости</span>
    </custom-menu>
    
    <script>
    customElements.define('custom-menu', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `<div class="menu">
          <slot name="title"></slot>
          <ul><slot name="item"></slot></ul>
        </div>`;
    
        // shadowRoot не может иметь обработчиков событий, поэтому используется первый потомок
        this.shadowRoot.firstElementChild.addEventListener('slotchange',
          e => alert("slotchange: " + e.target.name)
        );
      }
    });
    
    setTimeout(() => {
      menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Леденцы</li>')
    }, 1000);
    
    setTimeout(() => {
      menu.querySelector('[slot="title"]').innerHTML = "Новое меню";
    }, 2000);
    </script>

    Отображение меню обновляется каждый раз без нашего вмешательства.

    Здесь есть два события slotchange:

    1. При инициализации:slotchange: title запускается сразу же, как только slot="title" из обычного дерева попадает в соответствующий слот.
    2. Через 1 секунду:slotchange: item запускается, когда добавляется новый элемент <li slot="item">.

    Обратите внимание, что событие slotchange не запускается через 2 секунды, когда меняется контент slot="title". Это происходит потому, что сам слот не меняется. Мы изменяем содержимое элемента, который находится в слоте, а это совсем другое.

    Если мы хотим отслеживать внутренние изменения обычного DOM-дерева из JavaScript, можно также использовать более обобщённый механизм: MutationObserver.

    API слотов

    И, наконец, давайте поговорим о методах JavaScript, связанных со слотами.

    Как мы видели раньше, JavaScript смотрит на «реальный», а не на развёрнутый DOM. Но если у теневого дерева стоит {mode: 'open'}, то мы можем выяснить, какие элементы находятся в слоте, и, наоборот, определить слот по элементу, который в нём находится:

    • node.assignedSlot – возвращает элемент <slot>, в котором находится node.
    • slot.assignedNodes({flatten: true/false}) – DOM-узлы, которые находятся в слоте. Опция flatten имеет значение по умолчанию false. Если явно изменить значение на true, она просматривает развёрнутый DOM глубже и возвращает вложенные слоты, если есть вложенные компоненты, и резервный контент, если в слоте нет узлов.
    • slot.assignedElements({flatten: true/false}) – DOM-элементы, которые находятся в слоте (то же самое, что выше, но только узлы-элементы).

    Эти методы можно использовать не только для отображения содержимого, которое находится в слотах, но и для его отслеживания в JavaScript.

    Например, если компонент <custom-menu> хочет знать, что он показывает, он может отследить событие slotchange и получить пункты меню из slot.assignedElements:

    <custom-menu id="menu">
      <span slot="title">Сладости</span>
      <li slot="item">Леденцы</li>
      <li slot="item">Фруктовые тосты</li>
    </custom-menu>
    
    <script>
    customElements.define('custom-menu', class extends HTMLElement {
      items = []
    
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `<div class="menu">
          <slot name="title"></slot>
          <ul><slot name="item"></slot></ul>
        </div>`;
    
        // слотовый элемент добавляется/удаляется/заменяется
        this.shadowRoot.firstElementChild.addEventListener('slotchange', e => {
          let slot = e.target;
          if (slot.name == 'item') {
            this.items = slot.assignedElements().map(elem => elem.textContent);
            alert("Items: " + this.items);
          }
        });
      }
    });
    
    // пункты меню обновятся через 1 секунду
    setTimeout(() => {
      menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Кексы</li>')
    }, 1000);
    </script>

    Итого

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

    Существует два вида слотов:

    • Именованные слоты: <slot name="X">...</slot> – получают элементы светлого DOM с slot="X".
    • Слот по умолчанию: первый <slot> без имени (последующие неименованные слоты игнорируются) – показывает элементы элементов светлого дерева, которые не находятся в других слотах.
    • Если одному слоту назначено несколько элементов, они добавляются один за другим.
    • Содержимое элемента <slot> используется как резервное. Оно отображается, если в слоте нет элементов из светлого дерева.

    Процесс отображения элементов внутри слота называется «композицией». В результате композиции строится «развёрнутый DOM».

    При композиции не происходит перемещения узлов – с точки зрения JavaScript, DOM остаётся прежним.

    JavaScript может получить доступ к слотам с помощью следующих методов:

    • slot.assignedNodes/Elements() – возвращает узлы/элементы, которые находятся внутри slot.
    • node.assignedSlot – обратный метод, возвращает слот по узлу.

    Если мы хотим знать, что показываем, мы можем отследить контент слота следующими способами:

    • событие slotchange – запускается, когда слот наполняется контентом в первый раз, и при каждой операции добавления/удаления/замещения элемента в слоте, за исключением его потомков. Сам слот будет event.target.
    • MutationObserver для более глубокого просмотра содержимого элемента в слоте и отслеживания изменений в нём.

    Теперь, когда мы научились показывать элементы светлого DOM в теневом DOM, давайте посмотрим, как их правильно стилизовать. Основное правило звучит так: теневые элементы стилизуются внутри, а обычные элементы – снаружи; однако есть заметные исключения.

    Настройка стилей теневого DOM

    Теневой DOM может содержать теги <style> и <link rel="stylesheet" href="…">. В последнем случае таблицы стилей кешируются по протоколу HTTP, так что они не будут загружаться повторно при использовании одного шаблона для многих компонентов.

    Как правило, локальные стили работают только внутри теневого DOM, а стили документа – вне его. Но есть несколько исключений.

    :host

    Селектор :host позволяет выбрать элемент-хозяин (элемент, содержащий теневое дерево).

    Например, мы создаём элемент <custom-dialog> который нужно расположить по-центру. Для этого нам необходимо стилизовать сам элемент <custom-dialog>.

    Это именно то, что делает :host:

    <template id="tmpl">
      <style>
        /* стиль будет применён изнутри к элементу <custom-dialog> */
        :host {
          position: fixed;
          left: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          display: inline-block;
          border: 1px solid red;
          padding: 10px;
        }
      </style>
      <slot></slot>
    </template>
    
    <script>
    customElements.define('custom-dialog', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
      }
    });
    </script>
    
    <custom-dialog>
      Hello!
    </custom-dialog>

    Каскадирование

    Элемент-хозяин (элемент <custom-dialog>) находится в светлом DOM, поэтому к нему применяются CSS-стили документа.

    Если есть некоторое свойство, стилизованное как в :host локально, так и в документе, то стиль документа будет приоритетным.

    Например, если в документе из примера поставить:

    <style>
    custom-dialog {
      padding: 0;
    }
    </style>

    …то <custom-dialog> будет без padding.

    Это очень удобно, поскольку мы можем задать стили «по умолчанию» в компоненте в его правиле :host, а затем, при желании, легко переопределить их в документе.

    Исключение составляет тот случай, когда локальное свойство помечено как !important, для таких свойств приоритет имеют локальные стили.

    :host(selector)

    То же, что и :host, но применяется только в случае, если элемент-хозяин подходит под селектор selector.

    Например, мы бы хотели выровнять по центру <custom-dialog>, только если он содержит атрибут centered:

    <template id="tmpl">
      <style>
        :host([centered]) {
          position: fixed;
          left: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          border-color: blue;
        }
    
        :host {
          display: inline-block;
          border: 1px solid red;
          padding: 10px;
        }
      </style>
      <slot></slot>
    </template>
    
    <script>
    customElements.define('custom-dialog', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
      }
    });
    </script>
    
    
    <custom-dialog centered>
      Centered!
    </custom-dialog>
    
    <custom-dialog>
      Not centered.
    </custom-dialog>

    Теперь дополнительные стили для выравнивания по центру применяются только к первому элементу: <custom-dialog centered>.

    Подводя итог, мы можем использовать семейство селекторов :host для стилизации основного элемента компонента. Эти стили (если только не стоит !important) могут быть переопределены документом.

    Применение стилей к содержимому слотов

    Теперь давайте рассмотрим ситуацию со слотами.

    Элементы слотов происходят из светлого DOM, поэтому они используют стили документа. Локальные стили не влияют на содержимое слотов.

    В примере ниже текст в <span> жирный в соответствии со стилями документа, но не берёт background из локальных стилей:

    <style>
      span { font-weight: bold }
    </style>
    
    <user-card>
      <div slot="username"><span>John Smith</span></div>
    </user-card>
    
    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `
          <style>
          span { background: red; }
          </style>
          Имя: <slot name="username"></slot>
        `;
      }
    });
    </script>

    В результате текст жирный, но не красный.

    Если мы хотим стилизовать слотовые элементы в нашем компоненте, то есть два варианта.

    Первое – можно стилизовать сам <slot> и полагаться на наследование CSS:

    <user-card>
      <div slot="username"><span>John Smith</span></div>
    </user-card>
    
    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `
          <style>
          slot[name="username"] { font-weight: bold; }
          </style>
          Имя: <slot name="username"></slot>
        `;
      }
    });
    </script>

    Здесь <span>John Smith</span> выделяется жирным шрифтом, потому что наследование CSS действует между <slot> и его содержимым. Но в CSS как таковом не все свойства наследуются.

    Другой вариант – использовать псевдокласс ::slotted(селектор). Соответствует элементам, если выполняются два условия:

    1. Это слотовый элемент, пришедший из светлого DOM. Имя слота не имеет значения. Просто любой элемент, вставленный в <slot>, но только сам элемент, а не его потомки.
    2. Элемент соответствует селектору.

    В нашем примере ::slotted(div) выбирает в точности <div slot="username">, но не его дочерние элементы:

    <user-card>
      <div slot="username">
        <div>John Smith</div>
      </div>
    </user-card>
    
    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `
          <style>
          ::slotted(div) { border: 1px solid red; }
          </style>
          Name: <slot name="username"></slot>
        `;
      }
    });
    </script>
    

    Обратите внимание, что селектор ::slotted не может спускаться дальше в слот. Эти селекторы недействительны:

    ::slotted(div span) {
      /* наш слот <div> не соответствует этому */
    }
    
    ::slotted(div) p {
      /* не может войти в светлый DOM */
    }

    Кроме того, ::slotted можно использовать только в CSS. Мы не можем использовать его в querySelector.

    CSS-хуки с пользовательскими свойствами

    Как можно стилизовать внутренние элементы компонента из основного документа?

    Селекторы типа :host применяют правила к элементу <custom-dialog> или <user-card>, но как стилизовать элементы теневого DOM внутри них? Например, в <user-card> мы хотели бы разрешить внешнему документу изменять внешний вид пользовательских полей.

    Аналогично тому, как мы предусматриваем у компонента методы, чтобы взаимодействовать с ним, мы можем использовать переменные CSS (пользовательские свойства CSS) для его стилизации.

    Пользовательские свойства CSS существуют одновременно на всех уровнях, как светлом, так и в тёмном DOM.

    Например, в теневом DOM мы можем использовать CSS-переменную --user-card-field-color для стилизации полей, а документ будет её устанавливать:

    <style>
      .field {
        color: var(--user-card-field-color, black);
        /* если переменная --user-card-field-color не определена, будет использован цвет black */
      }
    </style>
    <div class="field">Имя: <slot name="username"></slot></div>
    <div class="field">Дата рождения: <slot name="birthday"></slot></div>

    Затем мы можем объявить это свойство во внешнем документе для <user-card>:

    user-card {
      --user-card-field-color: green;
    }

    Пользовательские CSS свойства проникают через теневой DOM, они видны повсюду, поэтому внутреннее правило .field будет использовать его.

    Вот полный пример:

    <style>
      user-card {
        --user-card-field-color: green;
      }
    </style>
    
    <template id="tmpl">
      <style>
        .field {
          color: var(--user-card-field-color, black);
        }
      </style>
      <div class="field">Имя: <slot name="username"></slot></div>
      <div class="field">Дата рождения: <slot name="birthday"></slot></div>
    </template>
    
    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true));
      }
    });
    </script>
    
    <user-card>
      <span slot="username">John Smith</span>
      <span slot="birthday">01.01.2001</span>
    </user-card>

    Итого

    Теневой DOM может включать в себя стили, такие как <style> или <link rel="stylesheet">.

    Локальные стили могут влиять на:

    • теневое дерево,
    • элемент-хозяин, при помощи псевдоклассов :host и :host(),
    • слотовые элементы (из светлого DOM), ::slotted(селектор) позволяет стилизовать сами слотовые элементы, но не их дочерние элементы.

    Стили документов могут влиять на:

    • элемент-хозяин (так как он находится во внешнем документе)
    • слотовые элементы и их содержимое (так как они также физически присутствуют во внешнем документе)

    Когда свойства CSS конфликтуют, обычно стили документа имеют приоритет, если только свойство не помечено как !important. Тогда предпочтение отдаётся локальным стилям.

    Пользовательские свойства CSS проникают через теневой DOM. Они используются как «хуки» для придания элементам стиля:

    1. Компонент использует пользовательское CSS-свойство для стилизации ключевых элементов, например var(--component-name-title, <значение по умолчанию>).
    2. Автор компонента публикует эти свойства для разработчиков, они так же важны, как и другие общедоступные методы компонента.
    3. Когда разработчик хочет стилизовать заголовок, он назначает CSS-свойство --component-name-title для элемента-хозяина или выше.
    4. Profit!

    Теневой DOM и события

    Смысл создания теневого DOM-дерева – это инкапсуляция внутренних деталей компонента.

    Допустим, клик произошёл внутри теневого DOM на компоненте <user-card>. Но скрипты основного документа ничего не знают о внутреннем устройстве теневой DOM-структуры, в особенности, если компонент создан сторонней библиотекой.

    Поэтому, чтобы не нарушать инкапсуляцию, браузер меняет у этого события целевой элемент.

    События, которые произошли в теневом DOM, но пойманы снаружи этого DOM, имеют элемент-хозяин в качестве целевого элемента event.target.

    Рассмотрим простой пример:

    <user-card></user-card>
    
    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `<p>
          <button>Нажми меня</button>
        </p>`;
        this.shadowRoot.firstElementChild.onclick =
          e => alert("Внутренний целевой элемент: " + e.target.tagName);
      }
    });
    
    document.onclick =
      e => alert("Внешний целевой элемент: " + e.target.tagName);
    </script>

    Если нажать на кнопку, то выведется следующее:

    1. Внутренний целевой элемент: BUTTON – внутренний обработчик событий получает правильный целевой элемент – элемент, находящийся внутри теневого DOM.
    2. Внешний целевой элемент: USER-CARD – обработчик событий на уровне документа получает элемент-хозяин в качестве целевого.

    Хорошо, что браузер подменяет целевые элементы событий. Потому что внешний документ ничего не знает о внутреннем устройстве компонента. С его (внешнего документа) точки зрения, событие происходит на <user-card>.

    Подмена целевого элемента не происходит, если событие берёт начало на элементе из слота, который фактически находится в обычном, светлом DOM.

    Например, если пользователь кликнет на <span slot="username"> в примере ниже – целевой элемент события будет именно этот span для обоих обработчиков – теневого и обычного (светлого):

    <user-card id="userCard">
      <span slot="username">John Smith</span>
    </user-card>
    
    <script>
    customElements.define('user-card', class extends HTMLElement {
      connectedCallback() {
        this.attachShadow({mode: 'open'});
        this.shadowRoot.innerHTML = `<div>
          <b>Имя:</b> <slot name="username"></slot>
        </div>`;
    
        this.shadowRoot.firstElementChild.onclick =
          e => alert("Внутренний целевой элемент: " + e.target.tagName);
      }
    });
    
    userCard.onclick = e => alert(`Внешний целевой элемент: ${e.target.tagName}`);
    </script>

    Если клик произойдёт на "John Smith", то для обоих обработчиков – внутреннего и внешнего – целевым элементом будет <span slot="username">. Это элемент обычного (светлого) DOM, так что подмены не происходит.

    С другой стороны, если клик произойдёт на элементе, который находится в теневом DOM, например, на <b>Имя</b>, то как только всплытие выйдет за пределы теневой DOM-структуры, его event.target станет <user-card>.

    Всплытие и метод event.composedPath()

    Для обеспечения всплытия событий используется развёрнутый DOM.

    Таким образом, если у нас есть элемент в слоте, и событие происходит где-то внутри него, то оно всплывает до <slot> и выше.

    Полный путь к изначальному целевому элементу, со всеми теневыми элементами, можно получить, воспользовавшись методом event.composedPath(). Как видно из названия, этот метод возвращает путь после композиции.

    В примере выше развёрнутое DOM-дерево будет таким:

    <user-card id="userCard">
      #shadow-root
        <div>
          <b>Имя:</b>
          <slot name="username">
            <span slot="username">John Smith</span>
          </slot>
        </div>
    </user-card>

    Так что, при клике по <span slot="username"> вызов метода event.composedPath() вернёт массив: [spanslotdivshadow-rootuser-cardbodyhtmldocumentwindow]. Что в точности отражает цепочку родителей от целевого элемента в развёрнутой DOM-структуре после композиции.

    Свойство: event.composed

    Большинство событий успешно всплывают сквозь границу теневого DOM. Но не все.

    Это поведение регулируется с помощью свойства composed объекта события. Если оно true, то событие пересекает границу. Иначе, оно может быть поймано лишь внутри теневого DOM.

    Если посмотреть в спецификацию UI Events, то большинство событий имеют composed: true:

    • blurfocusfocusinfocusout,
    • clickdblclick,
    • mousedownmouseup mousemovemouseoutmouseover,
    • wheel,
    • beforeinputinputkeydownkeyup.

    Все события курсора и сенсорные события также имеют composed: true.

    Хотя есть и события, имеющие composed: false:

    • mouseentermouseleave (они вообще не всплывают),
    • loadunloadaborterror,
    • select,
    • slotchange.

    Эти события могут быть пойманы только на элементах того же DOM, в котором находится целевой элемент события.

    Генерация событий

    Когда мы генерируем своё событие, то, чтобы оно всплывало за пределы компонента, нужно установить оба свойства: bubbles и composed – в значение true.

    Например, здесь мы создаём элемент div#inner в теневом DOM-дереве элемента div#outer и генерируем на нём два события. Только одно с флагом composed: true выйдет наружу, в документ:

    <div id="outer"></div>
    
    <script>
    outer.attachShadow({mode: 'open'});
    
    let inner = document.createElement('div');
    outer.shadowRoot.append(inner);
    
    /*
    div(id=outer)
      #shadow-dom
        div(id=inner)
    */
    
    document.addEventListener('test', event => alert(event.detail));
    
    inner.dispatchEvent(new CustomEvent('test', {
      bubbles: true,
      composed: true,
      detail: "composed"
    }));
    
    inner.dispatchEvent(new CustomEvent('test', {
      bubbles: true,
      composed: false,
      detail: "not composed"
    }));
    </script>

    Итого

    Только те события пересекают границы теневого DOM, у которых флаг composed установлен в значение true.

    У большинства встроенных событий стоит composed: true, это описано в соответствующих спецификациях:

    У некоторых встроенных событий всё же стоит composed: false:

    • mouseentermouseleave (вообще не всплывают),
    • loadunloadaborterror,
    • select,
    • slotchange.

    Эти события могут быть пойманы только на элементах, принадлежащих тому же DOM-дереву.

    Если мы генерируем своё событие CustomEvent, то должны явно поставить флаг composed: true.

    Обратите внимание, что в случае вложенных компонентов теневые DOM могут быть вложены друг в друга. События с флагом composed всплывают через границы всех теневых DOM. Поэтому, если событие предназначено только для ближайшего внешнего компонента-родителя, мы можем инициировать его на элементе-хозяине и установить флаг composed: false. Тогда оно будет уже вне теневого DOM компонента, но не выплывает наружу в «ещё более внешний» DOM.

    Моя практика.

    Использовал Slot на проекте Gstaadguy для стилизации маркеров на карте.

    У нас на месте маркеров есть slot.

    В них мы можем вставить уже свою разметку.

    Нажимаем на reveal и нас редиректит.

    Если нажмем на slot, нас обратно перекинет на slot.

    Это популярный инстурмент?

    В HTML и веб-разработке <slot> — это специальный элемент, используемый внутри Web Components (особенно в Shadow DOM), который обозначает место для вставки контента извне.

    <slot> — это часть технологии Shadow DOM, которая позволяет инкапсулировать разметку и стили компонента, при этом давая возможность вставить пользовательский контент внутрь компонента.

    Хороший вопрос. <slot> сам по себе не очень популярен среди обычных HTML-разработчиков, потому что он используется в контексте Web Components, а эта технология пока не так широко распространена, как, например, React, Vue или Angular. Но есть нюансы.

    📈 Насколько популярен <slot> и Web Components?

    ✅ Где <slot> и Web Components действительно используются:

    • Дизайн-системы и UI-библиотеки (например, Shoelace, FAST, Lit)
    • Фреймворки без зависимости от React/Vue, где важна легковесность и нативность
    • Кросс-фреймворк компоненты, которые можно использовать и в Angular, и в Vue, и даже в обычном HTML
    • Корпоративные решения, где требуется стабильность и независимость от сторонних библиотек

    🚫 Где Web Components и <slot> используются редко:

    • В небольших проектах
    • В React (у него свой способ управления компонентами)
    • В большинстве CMS, таких как WordPress

    Почему <slot> пока не массовый инструмент?

    • Порог входа: Shadow DOM и кастомные элементы требуют понимания новых концепций
    • Сложность стилей: не все CSS-функции работают одинаково внутри Shadow DOM
    • Фреймворки “захватывают рынок”: такие как React, Vue, которые используют свои подходы к шаблонам и вставке контента

    Вывод:

    • <slot>мощный инструмент, но нишевый
    • Подходит для кастомных UI-компонентов, дизайн-систем и кросс-фреймворк решений
    • Не так популярен, как JSX в React или шаблоны Vue