В React ты пишешь {children} — содержимое, которое пользователь передаёт в компонент. В Vue — <slot>. В нативных Web Components этот механизм называется слоты Shadow DOM. А `<template>` — это способ хранить HTML-структуру, которая не рендерится сразу, но готова к клонированию. Вместе они дают полный механизм переиспользуемых компонентов без фреймворков.
Без шаблонов и слотов каждый экземпляр компонента пересобирает HTML с нуля через innerHTML. Шаблоны парсятся один раз и клонируются — это быстрее. Слоты позволяют пользователям компонента передавать произвольный контент внутрь, как children в React.
<template><template> — инертный HTML-фрагмент. Не рендерится, не загружает ресурсы, не выполняет скрипты:
// В HTML:
// <template id="card-tmpl">
// <div class="card">
// <h2 class="card-title"></h2>
// <p class="card-body"></p>
// </div>
// </template>
const tmpl = document.getElementById('card-tmpl')
const clone = tmpl.content.cloneNode(true) // глубокое клонирование
// Заполняем клон данными
clone.querySelector('.card-title').textContent = 'Заголовок'
clone.querySelector('.card-body').textContent = 'Тело карточки'
document.body.appendChild(clone)Преимущества перед innerHTML:
<slot>)Слоты — механизм композиции: позволяют вставлять внешний контент в точки теневого DOM:
<!-- Shadow DOM компонента -->
<div class="card">
<slot name="header">Заголовок по умолчанию</slot>
<div class="body">
<slot></slot> <!-- дефолтный слот — принимает всё остальное -->
</div>
<slot name="footer"></slot>
</div>
<!-- Использование (light DOM) -->
<my-card>
<h2 slot="header">Мой заголовок</h2>
<p>Основной контент (идёт в дефолтный слот)</p>
<span slot="footer">Подвал</span>
</my-card><slot name="header"> — принимает элементы с slot="header"<slot> — принимает всё, что не назначено именованным слотам<slot>, отображается если слот не заполненclass CardComponent extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
const template = document.createElement('template')
template.innerHTML = `
<style>
:host { display: block; border: 1px solid #ddd; }
.header { background: #f5f5f5; padding: 8px; }
</style>
<div class="header"><slot name="title">Без заголовка</slot></div>
<div class="body"><slot></slot></div>
`
shadow.appendChild(template.content.cloneNode(true))
}
}Ошибка 1: Изменение template.content вместо клона
// НЕВЕРНО — изменяем оригинальный шаблон
const content = tmpl.content
content.querySelector('h2').textContent = 'Заголовок'
document.body.appendChild(content) // Шаблон теперь пустой!
// ВЕРНО — всегда клонируем
const clone = tmpl.content.cloneNode(true) // true = глубокое
clone.querySelector('h2').textContent = 'Заголовок'
document.body.appendChild(clone)Ошибка 2: Слот без Shadow DOM
// Слоты работают ТОЛЬКО в Shadow DOM
// В light DOM <slot> — просто неизвестный тег, не функционирует
class Wrong extends HTMLElement {
connectedCallback() {
this.innerHTML = '<slot name="title"></slot>' // Не работает!
}
}
// Нужен Shadow DOM:
class Right extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = '<slot name="title"></slot>' // Работает!
}
}Ошибка 3: Попытка querySelector внутри слота
// Контент в слотах остаётся в light DOM, не в shadow DOM
const shadow = element.shadowRoot
shadow.querySelector('[slot="header"]') // null! Это в light DOM
// Правильно — ищем в хосте
element.querySelector('[slot="header"]') // работает<sl-card> (Shoelace), <mwc-button> (Material Web Components)<page-layout> с слотами header/sidebar/main/footer<data-table> принимает <column> как слоты<modal-dialog> принимает заголовок, тело и футер через слотыСимуляция template + slots: генерация карточек, статей и таблиц из шаблонов
// Симуляция template + slots (без реального DOM)
// В браузере: tmpl.content.cloneNode(true) + slot-механизм Shadow DOM
// ===== Движок шаблонов =====
// Клонирует шаблон и подставляет {{переменные}}
function cloneTemplate(templateHTML, variables = {}) {
let result = templateHTML
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp('\\{\\{\\s*' + key + '\\s*\\}}', 'g'), String(value))
}
return result
}
// Заполняет именованные слоты <slot name="X">fallback</slot>
function fillSlots(templateHTML, slots = {}) {
let html = templateHTML
// Именованные слоты
for (const [name, content] of Object.entries(slots)) {
html = html.replace(
new RegExp('<slot\\s+name="' + name + '"[^>]*>([\\s\\S]*?)<\/slot>', 'g'),
String(content)
)
}
// Дефолтный слот (без имени)
if (slots.default !== undefined) {
html = html.replace(/<slot(?![\s\S]*?name=)[^>]*>([sS]*?)</slot>/g, String(slots.default))
}
// Незаполненные слоты → оставляем fallback-контент
html = html.replace(/<slot[^>]*>([sS]*?)</slot>/g, '$1')
return html
}
// ===== 1. Карточки товаров из шаблона =====
console.log('=== Карточки товаров ===')
const productTemplate = `
<article class="product">
<div class="badge-area">
<slot name="badge"></slot>
</div>
<h3>{{name}}</h3>
<p class="desc">{{description}}</p>
<div class="price">
<slot name="price">{{defaultPrice}} руб.</slot>
</div>
<slot name="actions"><button>Купить</button></slot>
</article>`
const products = [
{
vars: { name: 'Ноутбук Pro 15"', description: 'Core i7, 16GB RAM', defaultPrice: 85000 },
slots: { badge: '<span class="new">Новинка</span>', price: '<strong>85 000 руб.</strong>' }
},
{
vars: { name: 'Мышь беспроводная', description: 'Bluetooth, 3 кнопки', defaultPrice: 1200 },
slots: { badge: '<span class="sale">-20%</span>' }
// price и actions не заданы → fallback
},
{
vars: { name: 'Монитор 27"', description: '4K IPS, 144Hz', defaultPrice: 35000 },
slots: {} // все слоты из fallback
}
]
for (const { vars, slots } of products) {
// Шаг 1: клонируем шаблон с переменными
const withVars = cloneTemplate(productTemplate, vars)
// Шаг 2: заполняем слоты
const rendered = fillSlots(withVars, slots)
// Извлекаем заголовок для вывода
const titleM = rendered.match(/<h3>([sS]*?)</h3>/)
const badgeM = rendered.match(/<div class="badge-area">([sS]*?)</div>/)
const priceM = rendered.match(/<div class="price">([sS]*?)</div>/)
const title = titleM?.[1] ?? '?'
const badge = (badgeM?.[1] ?? '').replace(/<[^>]+>/g, '').trim() || 'нет бейджа'
const price = (priceM?.[1] ?? '').replace(/<[^>]+>/g, '').trim()
console.log(` ${title} | badge: ${badge} | ${price}`)
}
// ===== 2. Генерация страниц из слотов =====
console.log('\n=== Страничный шаблон со слотами ===')
const pageTemplate = `
<html>
<head>
<title><slot name="title">Без заголовка</slot></title>
</head>
<body>
<header><slot name="header"><nav>Дефолтная навигация</nav></slot></header>
<main><slot>Контент страницы</slot></main>
<footer><slot name="footer">© 2024 Company</slot></footer>
</body>
</html>`
const pages = [
{
name: 'Главная',
slots: {
title: 'Главная страница',
header: '<nav><a>Главная</a> | <a>О нас</a></nav>',
default: '<h1>Добро пожаловать!</h1><p>Лучший сайт в мире.</p>',
}
},
{
name: 'Контакты',
slots: {
title: 'Контакты',
default: '<h1>Свяжитесь с нами</h1>',
// header и footer из fallback
}
}
]
for (const page of pages) {
const html = fillSlots(pageTemplate, page.slots)
const titleM = html.match(/<title>([sS]*?)</title>/)
const hasCustomNav = html.includes('<a>Главная</a>')
const hasDefaultFooter = html.includes('© 2024')
console.log(` ${page.name}:`)
console.log(` title: "${titleM?.[1] ?? '?'}"`)
console.log(` nav кастомный: ${hasCustomNav}, footer дефолтный: ${hasDefaultFooter}`)
}
// ===== 3. Таблица из шаблона строк =====
console.log('\n=== Таблица транзакций ===')
const rowTemplate = `<tr>
<td>${'{{'}id{{'}}'}}</td>
<td>${'{{'}client{{'}}'}}</td>
<td class="${{status_class}}">${'{{'} status{{'}}'}}</td>
<td>${'{{'} amount{{'}}'}}</td>
</tr>`
// Упрощаем шаблон
const rowTmpl = '<tr><td>{{id}}</td><td>{{client}}</td><td class="{{statusClass}}">{{status}}</td><td>{{amount}}</td></tr>'
const transactions = [
{ id: 'TXN-001', client: 'Иван Петров', statusClass: 'ok', status: 'Выполнен', amount: '1 500 ₽' },
{ id: 'TXN-002', client: 'Мария Сидорова', statusClass: 'pending', status: 'В обработке', amount: '2 300 ₽' },
{ id: 'TXN-003', client: 'Алексей Козлов', statusClass: 'error', status: 'Ошибка', amount: '850 ₽' },
]
console.log('ID | Клиент | Статус | Сумма')
console.log('-'.repeat(58))
for (const tx of transactions) {
const row = cloneTemplate(rowTmpl, tx)
const cells = [...row.matchAll(/<td[^>]*>([sS]*?)</td>/g)].map(m => m[1])
console.log(cells.map((c, i) => c.padEnd(i === 1 ? 17 : 14)).join('| '))
}В React ты пишешь {children} — содержимое, которое пользователь передаёт в компонент. В Vue — <slot>. В нативных Web Components этот механизм называется слоты Shadow DOM. А `<template>` — это способ хранить HTML-структуру, которая не рендерится сразу, но готова к клонированию. Вместе они дают полный механизм переиспользуемых компонентов без фреймворков.
Без шаблонов и слотов каждый экземпляр компонента пересобирает HTML с нуля через innerHTML. Шаблоны парсятся один раз и клонируются — это быстрее. Слоты позволяют пользователям компонента передавать произвольный контент внутрь, как children в React.
<template><template> — инертный HTML-фрагмент. Не рендерится, не загружает ресурсы, не выполняет скрипты:
// В HTML:
// <template id="card-tmpl">
// <div class="card">
// <h2 class="card-title"></h2>
// <p class="card-body"></p>
// </div>
// </template>
const tmpl = document.getElementById('card-tmpl')
const clone = tmpl.content.cloneNode(true) // глубокое клонирование
// Заполняем клон данными
clone.querySelector('.card-title').textContent = 'Заголовок'
clone.querySelector('.card-body').textContent = 'Тело карточки'
document.body.appendChild(clone)Преимущества перед innerHTML:
<slot>)Слоты — механизм композиции: позволяют вставлять внешний контент в точки теневого DOM:
<!-- Shadow DOM компонента -->
<div class="card">
<slot name="header">Заголовок по умолчанию</slot>
<div class="body">
<slot></slot> <!-- дефолтный слот — принимает всё остальное -->
</div>
<slot name="footer"></slot>
</div>
<!-- Использование (light DOM) -->
<my-card>
<h2 slot="header">Мой заголовок</h2>
<p>Основной контент (идёт в дефолтный слот)</p>
<span slot="footer">Подвал</span>
</my-card><slot name="header"> — принимает элементы с slot="header"<slot> — принимает всё, что не назначено именованным слотам<slot>, отображается если слот не заполненclass CardComponent extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
const template = document.createElement('template')
template.innerHTML = `
<style>
:host { display: block; border: 1px solid #ddd; }
.header { background: #f5f5f5; padding: 8px; }
</style>
<div class="header"><slot name="title">Без заголовка</slot></div>
<div class="body"><slot></slot></div>
`
shadow.appendChild(template.content.cloneNode(true))
}
}Ошибка 1: Изменение template.content вместо клона
// НЕВЕРНО — изменяем оригинальный шаблон
const content = tmpl.content
content.querySelector('h2').textContent = 'Заголовок'
document.body.appendChild(content) // Шаблон теперь пустой!
// ВЕРНО — всегда клонируем
const clone = tmpl.content.cloneNode(true) // true = глубокое
clone.querySelector('h2').textContent = 'Заголовок'
document.body.appendChild(clone)Ошибка 2: Слот без Shadow DOM
// Слоты работают ТОЛЬКО в Shadow DOM
// В light DOM <slot> — просто неизвестный тег, не функционирует
class Wrong extends HTMLElement {
connectedCallback() {
this.innerHTML = '<slot name="title"></slot>' // Не работает!
}
}
// Нужен Shadow DOM:
class Right extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = '<slot name="title"></slot>' // Работает!
}
}Ошибка 3: Попытка querySelector внутри слота
// Контент в слотах остаётся в light DOM, не в shadow DOM
const shadow = element.shadowRoot
shadow.querySelector('[slot="header"]') // null! Это в light DOM
// Правильно — ищем в хосте
element.querySelector('[slot="header"]') // работает<sl-card> (Shoelace), <mwc-button> (Material Web Components)<page-layout> с слотами header/sidebar/main/footer<data-table> принимает <column> как слоты<modal-dialog> принимает заголовок, тело и футер через слотыСимуляция template + slots: генерация карточек, статей и таблиц из шаблонов
// Симуляция template + slots (без реального DOM)
// В браузере: tmpl.content.cloneNode(true) + slot-механизм Shadow DOM
// ===== Движок шаблонов =====
// Клонирует шаблон и подставляет {{переменные}}
function cloneTemplate(templateHTML, variables = {}) {
let result = templateHTML
for (const [key, value] of Object.entries(variables)) {
result = result.replace(new RegExp('\\{\\{\\s*' + key + '\\s*\\}}', 'g'), String(value))
}
return result
}
// Заполняет именованные слоты <slot name="X">fallback</slot>
function fillSlots(templateHTML, slots = {}) {
let html = templateHTML
// Именованные слоты
for (const [name, content] of Object.entries(slots)) {
html = html.replace(
new RegExp('<slot\\s+name="' + name + '"[^>]*>([\\s\\S]*?)<\/slot>', 'g'),
String(content)
)
}
// Дефолтный слот (без имени)
if (slots.default !== undefined) {
html = html.replace(/<slot(?![\s\S]*?name=)[^>]*>([sS]*?)</slot>/g, String(slots.default))
}
// Незаполненные слоты → оставляем fallback-контент
html = html.replace(/<slot[^>]*>([sS]*?)</slot>/g, '$1')
return html
}
// ===== 1. Карточки товаров из шаблона =====
console.log('=== Карточки товаров ===')
const productTemplate = `
<article class="product">
<div class="badge-area">
<slot name="badge"></slot>
</div>
<h3>{{name}}</h3>
<p class="desc">{{description}}</p>
<div class="price">
<slot name="price">{{defaultPrice}} руб.</slot>
</div>
<slot name="actions"><button>Купить</button></slot>
</article>`
const products = [
{
vars: { name: 'Ноутбук Pro 15"', description: 'Core i7, 16GB RAM', defaultPrice: 85000 },
slots: { badge: '<span class="new">Новинка</span>', price: '<strong>85 000 руб.</strong>' }
},
{
vars: { name: 'Мышь беспроводная', description: 'Bluetooth, 3 кнопки', defaultPrice: 1200 },
slots: { badge: '<span class="sale">-20%</span>' }
// price и actions не заданы → fallback
},
{
vars: { name: 'Монитор 27"', description: '4K IPS, 144Hz', defaultPrice: 35000 },
slots: {} // все слоты из fallback
}
]
for (const { vars, slots } of products) {
// Шаг 1: клонируем шаблон с переменными
const withVars = cloneTemplate(productTemplate, vars)
// Шаг 2: заполняем слоты
const rendered = fillSlots(withVars, slots)
// Извлекаем заголовок для вывода
const titleM = rendered.match(/<h3>([sS]*?)</h3>/)
const badgeM = rendered.match(/<div class="badge-area">([sS]*?)</div>/)
const priceM = rendered.match(/<div class="price">([sS]*?)</div>/)
const title = titleM?.[1] ?? '?'
const badge = (badgeM?.[1] ?? '').replace(/<[^>]+>/g, '').trim() || 'нет бейджа'
const price = (priceM?.[1] ?? '').replace(/<[^>]+>/g, '').trim()
console.log(` ${title} | badge: ${badge} | ${price}`)
}
// ===== 2. Генерация страниц из слотов =====
console.log('\n=== Страничный шаблон со слотами ===')
const pageTemplate = `
<html>
<head>
<title><slot name="title">Без заголовка</slot></title>
</head>
<body>
<header><slot name="header"><nav>Дефолтная навигация</nav></slot></header>
<main><slot>Контент страницы</slot></main>
<footer><slot name="footer">© 2024 Company</slot></footer>
</body>
</html>`
const pages = [
{
name: 'Главная',
slots: {
title: 'Главная страница',
header: '<nav><a>Главная</a> | <a>О нас</a></nav>',
default: '<h1>Добро пожаловать!</h1><p>Лучший сайт в мире.</p>',
}
},
{
name: 'Контакты',
slots: {
title: 'Контакты',
default: '<h1>Свяжитесь с нами</h1>',
// header и footer из fallback
}
}
]
for (const page of pages) {
const html = fillSlots(pageTemplate, page.slots)
const titleM = html.match(/<title>([sS]*?)</title>/)
const hasCustomNav = html.includes('<a>Главная</a>')
const hasDefaultFooter = html.includes('© 2024')
console.log(` ${page.name}:`)
console.log(` title: "${titleM?.[1] ?? '?'}"`)
console.log(` nav кастомный: ${hasCustomNav}, footer дефолтный: ${hasDefaultFooter}`)
}
// ===== 3. Таблица из шаблона строк =====
console.log('\n=== Таблица транзакций ===')
const rowTemplate = `<tr>
<td>${'{{'}id{{'}}'}}</td>
<td>${'{{'}client{{'}}'}}</td>
<td class="${{status_class}}">${'{{'} status{{'}}'}}</td>
<td>${'{{'} amount{{'}}'}}</td>
</tr>`
// Упрощаем шаблон
const rowTmpl = '<tr><td>{{id}}</td><td>{{client}}</td><td class="{{statusClass}}">{{status}}</td><td>{{amount}}</td></tr>'
const transactions = [
{ id: 'TXN-001', client: 'Иван Петров', statusClass: 'ok', status: 'Выполнен', amount: '1 500 ₽' },
{ id: 'TXN-002', client: 'Мария Сидорова', statusClass: 'pending', status: 'В обработке', amount: '2 300 ₽' },
{ id: 'TXN-003', client: 'Алексей Козлов', statusClass: 'error', status: 'Ошибка', amount: '850 ₽' },
]
console.log('ID | Клиент | Статус | Сумма')
console.log('-'.repeat(58))
for (const tx of transactions) {
const row = cloneTemplate(rowTmpl, tx)
const cells = [...row.matchAll(/<td[^>]*>([sS]*?)</td>/g)].map(m => m[1])
console.log(cells.map((c, i) => c.padEnd(i === 1 ? 17 : 14)).join('| '))
}Реализуй систему шаблонов для генерации HTML-контента. Реализуй: - `renderTemplate(template, slots, variables)` — сначала заменяет `{{переменные}}`, затем заполняет именованные слоты `<slot name="X">fallback</slot>`, незаполненные слоты оставляет с fallback-содержимым - `renderList(template, items)` — применяет шаблон к каждому элементу массива как набор переменных, возвращает массив HTML-строк
renderTemplate: шаг 1 — replace(regex, value), шаг 2 — replace(slotRegex, content), шаг 3 — replace(/<slot[^>]*>([\s\S]*?)<\/slot>/g, "$1"). renderList: items.map(item => renderTemplate(template, {}, item))