Реализация HTML Templates и Slots для Web Components
<template> и <slot> — два HTML-элемента, которые дополняют Custom Elements и Shadow DOM. Templates позволяют описать структуру компонента прямо в HTML без исполнения. Slots дают возможность вставлять внешний контент внутрь Shadow DOM.
HTML Template
Содержимое <template> парсится браузером, но не рендерится и не исполняется. Изображения не загружаются, скрипты не выполняются, стили не применяются — до клонирования.
<!-- В HTML документе или компонентном файле -->
<template id="card-template">
<style>
.card {
padding: 24px;
border-radius: 12px;
background: var(--card-bg, #fff);
box-shadow: 0 2px 16px rgba(0,0,0,0.08);
}
.card__header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.card__avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
}
.card__body {
line-height: 1.6;
}
</style>
<div class="card">
<div class="card__header">
<img class="card__avatar" src="" alt="">
<div class="card__meta">
<slot name="name"><strong>Имя не указано</strong></slot>
<slot name="role"><em>Роль не указана</em></slot>
</div>
</div>
<div class="card__body">
<slot>Описание не указано</slot>
</div>
</div>
</template>
class TeamCard extends HTMLElement {
private shadow: ShadowRoot
constructor() {
super()
this.shadow = this.attachShadow({ mode: 'open' })
}
connectedCallback() {
// Получаем template и клонируем
const template = document.getElementById('card-template') as HTMLTemplateElement
const clone = template.content.cloneNode(true) as DocumentFragment
// Устанавливаем данные из атрибутов
const avatar = clone.querySelector('.card__avatar') as HTMLImageElement
avatar.src = this.getAttribute('avatar') || '/placeholder.png'
avatar.alt = this.getAttribute('name') || 'Фото'
this.shadow.appendChild(clone)
}
}
customElements.define('team-card', TeamCard)
Использование:
<team-card avatar="/team/anna.jpg">
<strong slot="name">Анна Ковалёва</strong>
<span slot="role">Lead Frontend Engineer</span>
Специализируется на архитектуре React-приложений и WebGL-визуализациях.
</team-card>
Template внутри компонента (строковый подход)
Если template не нужен в HTML — используется programmatic создание:
// Создание template один раз при определении класса (не в constructor)
const template = document.createElement('template')
template.innerHTML = `
<style>
:host {
display: inline-flex;
align-items: center;
gap: 8px;
}
.badge {
padding: 4px 10px;
border-radius: 100px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.03em;
}
:host([color="green"]) .badge { background: #d4edda; color: #155724; }
:host([color="red"]) .badge { background: #f8d7da; color: #721c24; }
:host([color="blue"]) .badge { background: #d1ecf1; color: #0c5460; }
:host([color="yellow"]) .badge { background: #fff3cd; color: #856404; }
</style>
<span class="badge">
<slot></slot>
</span>
`
class StatusBadge extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({ mode: 'open' })
// Клонируем template — не пересоздаём DOM каждый раз
shadow.appendChild(template.content.cloneNode(true))
}
}
customElements.define('status-badge', StatusBadge)
Преимущество: template создаётся один раз, клонирование быстрее повторного innerHTML.
Slots: именованные и дефолтные
<template id="dialog-template">
<style>
.dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: #fff;
border-radius: 16px;
padding: 0;
max-width: 480px;
width: 90%;
overflow: hidden;
}
.dialog__header {
padding: 20px 24px;
border-bottom: 1px solid #eee;
font-weight: 700;
font-size: 18px;
}
.dialog__body {
padding: 24px;
}
.dialog__footer {
padding: 16px 24px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* Если footer slot пуст — скрываем */
.dialog__footer:not(:has(slot[name="footer"] ~ *)):empty {
display: none;
}
</style>
<div class="dialog-backdrop">
<div class="dialog" role="dialog" aria-modal="true">
<div class="dialog__header">
<!-- Именованный slot с fallback-контентом -->
<slot name="title">Диалог</slot>
</div>
<div class="dialog__body">
<!-- Дефолтный slot — весь контент без slot атрибута -->
<slot></slot>
</div>
<div class="dialog__footer">
<!-- Опциональный footer slot -->
<slot name="footer"></slot>
</div>
</div>
</div>
</template>
<modal-dialog id="confirm-modal">
<span slot="title">Подтвердить удаление</span>
<p>Это действие нельзя отменить. Вы уверены?</p>
<div slot="footer">
<button onclick="document.getElementById('confirm-modal').close()">
Отмена
</button>
<button onclick="handleDelete()">Удалить</button>
</div>
</modal-dialog>
Slotchange — реакция на изменение слотов
class DynamicList extends HTMLElement {
private shadow: ShadowRoot
private countEl!: HTMLElement
constructor() {
super()
this.shadow = this.attachShadow({ mode: 'open' })
this.shadow.innerHTML = `
<style>
.list-header { display: flex; justify-content: space-between; }
.count { opacity: 0.5; font-size: 14px; }
</style>
<div class="list-header">
<slot name="heading"></slot>
<span class="count"></span>
</div>
<slot></slot>
`
this.countEl = this.shadow.querySelector('.count')!
}
connectedCallback() {
const defaultSlot = this.shadow.querySelector('slot:not([name])')!
// Слушаем изменение контента в дефолтном слоте
defaultSlot.addEventListener('slotchange', () => {
const items = (defaultSlot as HTMLSlotElement).assignedElements()
this.countEl.textContent = `${items.length} элементов`
})
}
}
Программный доступ к слотам
// Получить все элементы в слоте
const slot = this.shadow.querySelector('slot[name="items"]') as HTMLSlotElement
const assigned = slot.assignedElements()
// или вместе с текстовыми узлами:
const nodes = slot.assignedNodes({ flatten: true })
// Проверить, заполнен ли слот
const hasContent = slot.assignedElements().length > 0
this.shadow.querySelector('.footer')?.toggleAttribute('hidden', !hasContent)
Template с id и data-атрибутами
Шаблоны для списков, где каждый элемент клонируется с данными:
// Рендер списка через template
function renderProductList(products: Product[], container: HTMLElement) {
const template = document.getElementById('product-item-tpl') as HTMLTemplateElement
// DocumentFragment для батч-вставки
const fragment = document.createDocumentFragment()
products.forEach((product) => {
const clone = template.content.cloneNode(true) as DocumentFragment
;(clone.querySelector('.product-name') as HTMLElement).textContent = product.name
;(clone.querySelector('.product-price') as HTMLElement).textContent =
new Intl.NumberFormat('ru-RU', { style: 'currency', currency: 'RUB' })
.format(product.price)
;(clone.querySelector('.product-img') as HTMLImageElement).src = product.image
const addBtn = clone.querySelector('.add-to-cart') as HTMLButtonElement
addBtn.dataset.id = String(product.id)
fragment.appendChild(clone)
})
container.innerHTML = ''
container.appendChild(fragment) // Один DOM insert
}
Сроки
Пара компонентов с templates и slots — 1 день. Полная компонентная система (5–8 компонентов) с программным доступом к слотам, slotchange-обработчиками и документацией — 1 неделя.







