template и slots #
<template> хранит фрагмент HTML без отображения на странице; slot — место в Shadow DOM, куда проецируется контент из light DOM родителя. Вместе они дают декларативный каркас веб-компонента: один шаблон — много экземпляров с разным наполнением.
Элемент template #
Содержимое <template> лежит в DocumentFragment и не рендерится, пока вы его не клонируете:
<template id="order-row-template">
<tr>
<td class="id"></td>
<td class="status"></td>
<td class="total"></td>
</tr>
</template>
const tpl = document.getElementById('order-row-template')
function appendOrderRow(tableBody, order) {
const row = tpl.content.cloneNode(true)
row.querySelector('.id').textContent = order.id
row.querySelector('.status').textContent = order.status
row.querySelector('.total').textContent = `${order.total} ₽`
tableBody.append(row)
}
cloneNode(true) копирует всё дерево; исходный template остаётся нетронутым — удобно для списков заказов и строк плейлиста.
Default slot #
Слот без имени принимает любой дочерний текст и элементы, не назначенные именованному слоту:
class NoticeBanner extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.banner { padding: 12px 16px; border-radius: 6px; }
</style>
<div class="banner" part="surface">
<slot></slot>
</div>
`
}
}
customElements.define('notice-banner', NoticeBanner)
<notice-banner>Черновик сохранён локально</notice-banner>
Текст «Черновик…» окажется внутри .banner на месте <slot>.
Именованные слоты #
Атрибут slot на дочернем элементе light DOM сопоставляется с name у <slot>:
shadow.innerHTML = `
<article class="track">
<header><slot name="cover"></slot></header>
<div class="body">
<slot name="title"></slot>
<slot name="meta"></slot>
</div>
<footer><slot name="actions"></slot></footer>
</article>
`
<track-card>
<img slot="cover" src="/art/trk-12.jpg" alt="" />
<h3 slot="title">Morning Loop</h3>
<p slot="meta">4:12 · electronic</p>
<button slot="actions" type="button">В очередь</button>
</track-card>
Порядок узлов в HTML родителя не обязан совпадать с порядком <slot> в shadow — сопоставление идёт по имени.
Fallback-контент #
Между открывающим и закрывающим <slot> можно задать запасной контент, если слот пуст:
<slot name="actions">
<button type="button" disabled>Недоступно</button>
</slot>
Если slot="actions" не передали, пользователь увидит disabled-кнопку.
slotchange #
Когда состав projected-узлов меняется, слот генерирует slotchange:
shadow.querySelector('slot[name="title"]').addEventListener('slotchange', (e) => {
const nodes = e.target.assignedNodes({ flatten: true })
const text = nodes.map((n) => n.textContent?.trim()).filter(Boolean).join(' ')
this.dataset.headline = text
})
assignedNodes() возвращает только узлы, назначенные этому слоту; { flatten: true } включает вложенные слоты.
template внутри компонента #
Частый паттерн — <template> в light DOM компонента или статический template в модуле:
const cardTemplate = document.createElement('template')
cardTemplate.innerHTML = `
<style>...</style>
<div class="card"><slot></slot></div>
`
class PromoCard extends HTMLElement {
connectedCallback() {
if (this.shadowRoot) return
this.attachShadow({ mode: 'open' }).append(cardTemplate.content.cloneNode(true))
}
}
Шаблон парсится один раз; каждый экземпляр получает клон — меньше работы парсера при множестве карточек на витрине.
template/slots и обычный DOM #
<template> полезен и вне веб-компонентов: таблицы, <select> options, фрагменты для DocumentFragment. Слоты же работают только внутри shadow tree. Не путайте <slot> с фреймворковыми «слотами» — здесь механизм нативный и привязан к shadow projection.
Итог #
<template>хранит неактивную разметку; рендер — черезcontent.cloneNode(true).<slot>в shadow принимает детей хоста;slot="name"— именованное сопоставление.- Fallback внутри
<slot>показывается, когда контент не передан. - Событие
slotchangeиassignedNodes()отслеживают изменения проекции. - Комбинируйте template (каркас) + slots (контент) + Custom Elements (поведение).