Слоты и композиция Shadow DOM
Слоты и композиция в JavaScript Shadow DOM: дефолтные и именованные слоты, light DOM и shadow DOM, выровненное дерево, событие slotchange и методы assignedNodes/assignedElements с примерами пользовательских элементов.
Слоты и композиция делают Shadow DOM по-настоящему переиспользуемым. Автор компонента один раз описывает фиксированную внутреннюю структуру, а потребители заполняют её своей разметкой — при этом оба уровня никогда не пересекаются. На этой странице рассматривается элемент <slot> (дефолтные и именованные слоты), то, как light DOM и shadow DOM объединяются в выровненное дерево (flattened tree), событие slotchange, а также методы assignedNodes() / assignedElements(), с помощью которых можно узнать, что попало в слот.
Если вы новичок в Shadow DOM, сначала прочитайте Shadow DOM для ознакомления с основами attachShadow() и теневыми корнями, а затем Web Components, чтобы понять, как слоты вписываются в концепцию рядом с пользовательскими элементами и шаблонами.
Light DOM и shadow DOM
Композиция включает два дерева:
- Light DOM — разметка, которую пользователь пишет между тегами вашего элемента:
<my-card>...эта часть...</my-card>. Она располагается в обычном документе и остаётся там. - Shadow DOM — разметка, которую вы подключаете с помощью
attachShadow(). Она инкапсулирована и не доступна напрямую из внешнего документа.
<slot> — это окно: он находится в shadow DOM и проецирует дочерние элементы light DOM в нужное место. Узлы light DOM не перемещаются — они лишь отображаются в позиции слота. Это объединённое представление называется выровненным деревом (flattened tree) — именно его браузер отрисовывает и стилизует.
Понимание слотов в Shadow DOM
Слот — это заполнитель в вашем shadow DOM, куда браузер подставляет контент, переданный из light DOM. Слоты позволяют обобщённому компоненту выглядеть по-разному в каждом экземпляре, используя при этом единый внутренний шаблон.
Определение дефолтного слота
Используйте элемент <slot>. Слот без атрибута name является дефолтным слотом: он принимает любого потомка light DOM, не имеющего атрибута slot. Текст внутри <slot> — это резервный контент, отображаемый только тогда, когда ничего не назначено.
<body>
<script>
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<div class="container">
<slot>Default content</slot>
</div>
`;
}
}
customElements.define('custom-element', CustomElement);
</script>
<!-- No children: the slot shows its fallback, "Default content" -->
<custom-element></custom-element>
<!-- Children with no slot attribute go into the default slot -->
<custom-element><strong>Hello from the light DOM!</strong></custom-element>
</body>Первый <custom-element> отображает «Default content», потому что ничего не было назначено. Второй отображает жирный текст — его потомок из light DOM заменяет резервный контент. Обратите внимание: разметка по-прежнему находится в документе; слот лишь отображает её.
Именованные слоты
Если компонент имеет более одной точки вставки, присвойте каждому <slot> атрибут name и укажите соответствие из light DOM с помощью атрибута slot="...". Именно так нужный контент попадает в нужное место.
<body>
<!-- Define Custom Element -->
<script>
// Define Custom Element Class
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Define styles for the component */
.container {
border: 1px solid #ccc;
padding: 20px;
}
</style>
<div class="container">
<slot name="content">Default content</slot>
</div>
`;
}
}
// Define Custom Element
customElements.define('custom-element', CustomElement);
</script>
<!-- Displaying the custom element -->
<custom-element>
<div slot="content">Content from parent</div>
</custom-element>
</body><div slot="content"> сопоставляется с <slot name="content">, поэтому «Content from parent» заменяет резервный контент. Всё, что не совпадает с именованным слотом, попадёт в дефолтный слот, если он существует, или просто не будет отрисовано.
Улучшение композиции в Shadow DOM
Композиция в контексте Shadow DOM означает сборку компонентов пользовательского интерфейса и контента путём объединения слотов и распределяемого содержимого для создания более сложных, переиспользуемых структур. Применяясь в контексте Shadow DOM, композиция позволяет создавать высококастомизируемые и переиспользуемые веб-компоненты.
Чтобы стилизовать контент, распределяемый в слоты из родительского элемента, используйте CSS-псевдоэлемент ::slotted() — например, ::slotted(div) { color: blue; }. Полная картина использования ::slotted(), :host и пользовательских CSS-свойств описана в разделе Shadow DOM Styling.
Компоновка компонентов со слотами
Один из мощных способов использования композиции — объединить несколько слотов в структурированный макет. Здесь составной компонент определяет области заголовка, содержимого и подвала:
<body>
<script>
// Define Composite Element Class
class CompositeElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Define styles for the composite component */
.container {
border: 1px solid #ccc;
padding: 20px;
}
</style>
<div class="container">
<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>
</div>
`;
}
}
// Define Composite Element
customElements.define('composite-element', CompositeElement);
</script>
<composite-element>
<div slot="header">Header</div>
<div slot="content">Content</div>
<div slot="footer">Footer</div>
</composite-element>
</body>Каждый потомок с slot="..." направляется в соответствующий именованный слот, формируя аккуратный макет «заголовок/содержимое/подвал», который каждый экземпляр может заполнять по-своему.
Реакция на изменения слота с помощью slotchange
Содержимое слотов динамично: потребитель может добавлять, удалять или заменять потомков light DOM в любое время. Событие slotchange срабатывает на элементе <slot> всякий раз, когда изменяются назначенные ему узлы, — это позволяет компоненту реагировать: перерисовывать сводку, выполнять валидацию, загружать данные по запросу и т.д. Прослушивайте его изнутри теневого корня:
<body>
<script>
class TabList extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `<p id="count"></p><slot></slot>`;
this._slot = root.querySelector('slot');
this._count = root.querySelector('#count');
}
connectedCallback() {
this._slot.addEventListener('slotchange', () => this.update());
this.update();
}
update() {
// assignedElements() returns only element nodes in the slot
const items = this._slot.assignedElements();
this._count.textContent = `Tabs: ${items.length}`;
}
}
customElements.define('tab-list', TabList);
</script>
<tab-list>
<button>One</button>
<button>Two</button>
</tab-list>
<script>
// Adding a child later fires slotchange → count updates to 3
const list = document.querySelector('tab-list');
const extra = document.createElement('button');
extra.textContent = 'Three';
list.appendChild(extra);
</script>
</body>Изначально компонент отображает «Tabs: 2». Когда добавляется третья кнопка <button>, срабатывает slotchange и счётчик обновляется до «Tabs: 3».
Чтение содержимого слота: assignedNodes() и assignedElements()
Оба метода вызываются на элементе <slot> и возвращают то, что браузер назначил ему из light DOM:
slot.assignedNodes()возвращает все узлы — как элементы, так и текстовые узлы (включая пробельные символы между тегами).slot.assignedElements()возвращает только элементные узлы. Как правило, именно это вам и нужно.
Передайте { flatten: true }, чтобы обойти вложенные слоты, когда слоты выстроены в цепочку между компонентами:
// All nodes, including stray text/whitespace nodes
slot.assignedNodes(); // e.g. [text, <button>, text, <button>, text]
// Elements only — cleaner for iteration
slot.assignedElements(); // e.g. [<button>, <button>]
// Flatten through nested <slot> assignments
slot.assignedElements({ flatten: true });Предпочитайте assignedElements(), если только вам специально не нужны текстовые узлы — это избавляет от необходимости фильтровать пробельные символы.
Выровненное дерево: резюме
Браузер буквально не перемещает узлы light DOM в shadow DOM. Вместо этого он строит выровненное дерево (flattened tree), подставляя в каждый слот назначенные ему узлы для отрисовки и стилизации. Практические следствия:
- Элементы слота остаются в документе, поэтому
document.querySelector()по-прежнему их находит, а их исходныеclassиidпродолжают действовать. - Они стилизуются CSS страницы, тогда как компонент обращается к ним только через
::slotted(). - Обработчики событий, прикреплённые в light DOM, продолжают работать — события всплывают через выровненное дерево.
Заключение
Слоты и композиция превращают инкапсулированный shadow DOM в гибкий, переиспользуемый компонент: вы определяете структуру, а потребители наполняют её контентом через дефолтные и именованные слоты. Запомните ключевые концепции: light DOM и shadow DOM, выровненное дерево, которое отрисовывает браузер, событие slotchange для реакции на изменения и assignedElements() для чтения содержимого слота.
Для дальнейшего изучения смотрите Web Components — общая картина, Custom Elements — жизненный цикл элементов, Shadow DOM Styling — ::slotted() и :host, а также Shadow DOM — основы.