Реалізація 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 — використовується програмне створення:
// Створення 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('uk-UA', { style: 'currency', currency: 'UAH' })
.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 тиждень.







