template и slots

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 (поведение).

См. также #