Props позволяют передавать данные. Но что если нужно передать **разметку или компоненты**? Например, у тебя есть компонент Card — и ты хочешь помещать в него разный контент без изменения самого Card.
Слоты решают эту задачу: они позволяют родителю «вставить» произвольный контент в определённые места дочернего компонента.
Самый простой вид — один слот по умолчанию:
<!-- Card.vue -->
<template>
<div class="card">
<slot /> <!-- сюда попадёт контент от родителя -->
</div>
</template>
<!-- Родитель -->
<Card>
<p>Это контент внутри карточки</p>
<button>Кнопка</button>
</Card>Если слот не заполнен — отображается содержимое внутри <slot>:
<slot>
<p>Нет контента — показываем заглушку</p>
</slot>Для нескольких зон вставки используют именованные слоты:
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- default slot -->
</main>
<footer>
<slot name="footer">
© 2024 Default Footer <!-- fallback -->
</slot>
</footer>
</div>
</template>
<!-- Родитель -->
<Layout>
<template #header>
<h1>Заголовок страницы</h1>
</template>
<p>Основной контент</p> <!-- попадёт в default slot -->
<!-- footer не передан — используется fallback -->
</Layout>Scoped slots позволяют дочернему компоненту **передавать данные обратно** в слот родителя — это мощный паттерн инверсии контроля:
<!-- DataList.vue — знает данные, но не знает как рендерить -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index" />
</li>
</ul>
</template>
<!-- Родитель — знает как рендерить, получает данные из слота -->
<DataList :items="users">
<template #default="{ item, index }">
<strong>{{ index + 1 }}. {{ item.name }}</strong>
<span>{{ item.email }}</span>
</template>
</DataList><!-- Headless компонент — только логика, рендеринг через scoped slot -->
<MouseTracker>
<template #default="{ x, y }">
Мышь: {{ x }}, {{ y }}
</template>
</MouseTracker>
<!-- Renderless компонент (аналог) -->
<DataFetcher url="/api/users">
<template #default="{ data, loading, error }">
<Spinner v-if="loading" />
<ErrorMessage v-else-if="error" :message="error" />
<UserList v-else :users="data" />
</template>
</DataFetcher>Паттерн render-функций и children как функции — аналог scoped slots в чистом JS
// Аналог именованных слотов через объект с функциями-рендерерами
function createLayout({ header, default: defaultSlot, footer } = {}) {
const FALLBACK_FOOTER = '<footer>© 2024 Default Footer</footer>'
const FALLBACK_CONTENT = '<p>Нет контента</p>'
const headerHTML = header ? `<header>${header}</header>` : ''
const contentHTML = `<main>${defaultSlot || FALLBACK_CONTENT}</main>`
const footerHTML = `<footer>${footer || FALLBACK_FOOTER}</footer>`
return `<div class="layout">${headerHTML}${contentHTML}${footerHTML}</div>`
}
console.log(createLayout({
header: '<h1>Мой сайт</h1>',
default: '<p>Добро пожаловать!</p>',
footer: '<p>Контакты: hello@site.ru</p>',
}))
console.log(createLayout({
header: '<h1>Страница</h1>',
// footer не передан — будет fallback
}))
// Аналог scoped slots — children как функция
// Компонент получает данные, но ДЕЛЕГИРУЕТ рендеринг наружу
function createDataList(items, renderItem) {
const itemsHTML = items
.map((item, index) => `<li>${renderItem(item, index)}</li>`)
.join('')
return `<ul>${itemsHTML}</ul>`
}
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Carol', role: 'moderator' },
]
// renderItem — это аналог scoped slot
const html = createDataList(users, (user, index) =>
`<strong>${index + 1}. ${user.name}</strong> [${user.role}]`
)
console.log(html)
// Headless компонент — только логика, рендеринг снаружи
function createCounter(initialValue, render) {
let count = initialValue
function update() {
// "Перерендер" — вызываем render с текущими данными и методами
return render({
count,
increment() { count++; return update() },
decrement() { count--; return update() },
reset() { count = initialValue; return update() },
})
}
return update()
}
// Используем headless counter — сами решаем как рендерить
const result = createCounter(0, ({ count, increment, decrement }) => {
console.log(`Счётчик: ${count}`)
return { count, increment, decrement }
})
result.increment() // Счётчик: 1
result.increment() // Счётчик: 2
result.decrement() // Счётчик: 1Props позволяют передавать данные. Но что если нужно передать **разметку или компоненты**? Например, у тебя есть компонент Card — и ты хочешь помещать в него разный контент без изменения самого Card.
Слоты решают эту задачу: они позволяют родителю «вставить» произвольный контент в определённые места дочернего компонента.
Самый простой вид — один слот по умолчанию:
<!-- Card.vue -->
<template>
<div class="card">
<slot /> <!-- сюда попадёт контент от родителя -->
</div>
</template>
<!-- Родитель -->
<Card>
<p>Это контент внутри карточки</p>
<button>Кнопка</button>
</Card>Если слот не заполнен — отображается содержимое внутри <slot>:
<slot>
<p>Нет контента — показываем заглушку</p>
</slot>Для нескольких зон вставки используют именованные слоты:
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- default slot -->
</main>
<footer>
<slot name="footer">
© 2024 Default Footer <!-- fallback -->
</slot>
</footer>
</div>
</template>
<!-- Родитель -->
<Layout>
<template #header>
<h1>Заголовок страницы</h1>
</template>
<p>Основной контент</p> <!-- попадёт в default slot -->
<!-- footer не передан — используется fallback -->
</Layout>Scoped slots позволяют дочернему компоненту **передавать данные обратно** в слот родителя — это мощный паттерн инверсии контроля:
<!-- DataList.vue — знает данные, но не знает как рендерить -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index" />
</li>
</ul>
</template>
<!-- Родитель — знает как рендерить, получает данные из слота -->
<DataList :items="users">
<template #default="{ item, index }">
<strong>{{ index + 1 }}. {{ item.name }}</strong>
<span>{{ item.email }}</span>
</template>
</DataList><!-- Headless компонент — только логика, рендеринг через scoped slot -->
<MouseTracker>
<template #default="{ x, y }">
Мышь: {{ x }}, {{ y }}
</template>
</MouseTracker>
<!-- Renderless компонент (аналог) -->
<DataFetcher url="/api/users">
<template #default="{ data, loading, error }">
<Spinner v-if="loading" />
<ErrorMessage v-else-if="error" :message="error" />
<UserList v-else :users="data" />
</template>
</DataFetcher>Паттерн render-функций и children как функции — аналог scoped slots в чистом JS
// Аналог именованных слотов через объект с функциями-рендерерами
function createLayout({ header, default: defaultSlot, footer } = {}) {
const FALLBACK_FOOTER = '<footer>© 2024 Default Footer</footer>'
const FALLBACK_CONTENT = '<p>Нет контента</p>'
const headerHTML = header ? `<header>${header}</header>` : ''
const contentHTML = `<main>${defaultSlot || FALLBACK_CONTENT}</main>`
const footerHTML = `<footer>${footer || FALLBACK_FOOTER}</footer>`
return `<div class="layout">${headerHTML}${contentHTML}${footerHTML}</div>`
}
console.log(createLayout({
header: '<h1>Мой сайт</h1>',
default: '<p>Добро пожаловать!</p>',
footer: '<p>Контакты: hello@site.ru</p>',
}))
console.log(createLayout({
header: '<h1>Страница</h1>',
// footer не передан — будет fallback
}))
// Аналог scoped slots — children как функция
// Компонент получает данные, но ДЕЛЕГИРУЕТ рендеринг наружу
function createDataList(items, renderItem) {
const itemsHTML = items
.map((item, index) => `<li>${renderItem(item, index)}</li>`)
.join('')
return `<ul>${itemsHTML}</ul>`
}
const users = [
{ id: 1, name: 'Alice', role: 'admin' },
{ id: 2, name: 'Bob', role: 'user' },
{ id: 3, name: 'Carol', role: 'moderator' },
]
// renderItem — это аналог scoped slot
const html = createDataList(users, (user, index) =>
`<strong>${index + 1}. ${user.name}</strong> [${user.role}]`
)
console.log(html)
// Headless компонент — только логика, рендеринг снаружи
function createCounter(initialValue, render) {
let count = initialValue
function update() {
// "Перерендер" — вызываем render с текущими данными и методами
return render({
count,
increment() { count++; return update() },
decrement() { count--; return update() },
reset() { count = initialValue; return update() },
})
}
return update()
}
// Используем headless counter — сами решаем как рендерить
const result = createCounter(0, ({ count, increment, decrement }) => {
console.log(`Счётчик: ${count}`)
return { count, increment, decrement }
})
result.increment() // Счётчик: 1
result.increment() // Счётчик: 2
result.decrement() // Счётчик: 1Реализуй функцию `createCard({ header, default: defaultSlot, footer })`, которая собирает HTML-строку карточки из трёх зон. Требования: - Принимает объект с тремя необязательными слотами: `header`, `default`, `footer` - Если слот передан — использовать его содержимое - Если слот не передан — использовать fallback: - header fallback: `'<span>Без заголовка</span>'` - default fallback: `'<p>Нет содержимого</p>'` - footer fallback: `'<span>Без футера</span>'` - Итоговая строка должна иметь структуру: `<div class="card"><div class="card-header">...</div><div class="card-body">...</div><div class="card-footer">...</div></div>` ``` createCard({ header: 'Заголовок', default: 'Контент' }) // <div class="card"> // <div class="card-header">Заголовок</div> // <div class="card-body">Контент</div> // <div class="card-footer"><span>Без футера</span></div> // </div> ```
Используй тернарный оператор: const headerContent = header !== undefined ? header : HEADER_FALLBACK. Затем собери строку через шаблонные литералы: return '<div class="card">...'
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке