Обычный слот позволяет родителю передать контент в компонент, но родитель не знает о внутреннем состоянии дочернего компонента. Scoped slots решают эту проблему — дочерний компонент передаёт данные обратно в слот.
Данные передаются слоту через атрибуты на <slot>:
<!-- DataList.vue — дочерний компонент -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- Передаём item и index в слот -->
<slot :item="item" :index="index" />
</li>
</ul>
</template><!-- v-slot="slotProps" — получаем все переданные данные -->
<DataList :items="products" v-slot="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }} — {{ item.price }}₽</span>
</DataList>Деструктуризация в v-slot — стандартная практика для удобства.
<!-- Table.vue -->
<template>
<table>
<thead>
<tr>
<slot name="header" :columns="columns" />
</tr>
</thead>
<tbody>
<tr v-for="row in data">
<slot name="row" :row="row" :columns="columns" />
</tr>
</tbody>
<tfoot>
<slot name="footer" :total="total" />
</tfoot>
</table>
</template><!-- Использование -->
<Table :data="data" :columns="cols">
<template #header="{ columns }">
<th v-for="col in columns">{{ col.label }}</th>
</template>
<template #row="{ row, columns }">
<td v-for="col in columns">{{ row[col.key] }}</td>
</template>
<template #footer="{ total }">
<td colspan="3">Итого: {{ total }}</td>
</template>
</Table>Самый мощный паттерн со scoped slots: компонент управляет **логикой**, а родитель полностью контролирует **внешний вид**:
<!-- MouseTracker.vue — только логика, нет своей разметки -->
<template>
<slot :x="position.x" :y="position.y" :isMoving="isMoving" />
</template>
<script setup>
const position = reactive({ x: 0, y: 0 })
const isMoving = ref(false)
onMounted(() => {
window.addEventListener('mousemove', (e) => {
position.x = e.clientX
position.y = e.clientY
isMoving.value = true
})
})
</script><!-- Использование — внешний вид на усмотрение пользователя -->
<MouseTracker v-slot="{ x, y, isMoving }">
<div :class="{ active: isMoving }">
Курсор: {{ x }}, {{ y }}
</div>
</MouseTracker>Этот паттерн аналогичен **render props** в React.
// Проверить, передан ли слот
const hasFooter = computed(() => !!slots.footer)Паттерн renderless-компонента и scoped slots на чистом JS — данные управляются внутри, отображение снаружи
// Эмулируем паттерн Renderless Component + Scoped Slots.
// Компонент предоставляет данные через "слот-функцию",
// а "родитель" решает как их отображать.
// --- Renderless DataFetcher ---
// Аналог компонента, который управляет загрузкой данных
function createDataFetcher(fetchFn) {
// Внутреннее состояние компонента
let state = {
data: null,
loading: false,
error: null,
}
let slotFn = null // "шаблон" — функция рендера из родителя
const api = {
// Аналог <slot :data="data" :loading="loading" :error="error" :refetch="refetch">
// slotFn — это то, что родитель передаёт через v-slot
setSlot(fn) {
slotFn = fn
return api
},
render() {
if (slotFn) {
// Передаём "scope" — внутренние данные компонента
return slotFn({
data: state.data,
loading: state.loading,
error: state.error,
refetch: api.fetch,
})
}
},
async fetch() {
state = { ...state, loading: true, error: null }
api.render()
try {
state.data = await fetchFn()
state.loading = false
api.render()
} catch(err) {
state.loading = false
state.error = err.message
api.render()
}
}
}
return api
}
// --- Renderless List ---
// Компонент управляет фильтрацией/сортировкой, UI — снаружи
function createFilterableList(items) {
let filter = ''
let sortBy = null
let slotFn = null
const api = {
setSlot(fn) { slotFn = fn; return api },
setFilter(text) {
filter = text
api.render()
},
setSortBy(key) {
sortBy = key
api.render()
},
render() {
if (!slotFn) return
let result = items.filter(item =>
!filter || Object.values(item).some(v =>
String(v).toLowerCase().includes(filter.toLowerCase())
)
)
if (sortBy) {
result = [...result].sort((a, b) =>
a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
)
}
// Вызываем "слот" с данными
return slotFn({
items: result,
total: items.length,
filtered: result.length,
setFilter: api.setFilter,
setSortBy: api.setSortBy,
})
}
}
return api
}
// === Демо DataFetcher ===
console.log('=== DataFetcher (Renderless) ===')
const fetcher = createDataFetcher(async () => {
await new Promise(r => setTimeout(r, 50))
return [{ id: 1, name: 'Vue' }, { id: 2, name: 'React' }]
})
// Родитель определяет UI через "слот"
fetcher.setSlot(({ data, loading, error }) => {
if (loading) console.log(' [UI] Загрузка...')
if (error) console.log(' [UI] Ошибка:', error)
if (data) console.log(' [UI] Данные:', data.map(d => d.name).join(', '))
})
fetcher.fetch().then(() => {
// === Демо FilterableList ===
console.log('\n=== FilterableList (Renderless) ===')
const products = [
{ id: 1, name: 'MacBook Pro', price: 200000, category: 'laptop' },
{ id: 2, name: 'iPhone 15', price: 90000, category: 'phone' },
{ id: 3, name: 'iPad Air', price: 80000, category: 'tablet' },
{ id: 4, name: 'AirPods', price: 20000, category: 'audio' },
{ id: 5, name: 'Apple Watch', price: 50000, category: 'watch' },
]
const list = createFilterableList(products)
// UI полностью контролируется "родителем"
list.setSlot(({ items, total, filtered }) => {
console.log(` Показано ${filtered}/${total}:`)
items.forEach(p => console.log(` - ${p.name}: ${p.price}₽`))
})
console.log('Все продукты:')
list.render()
console.log('\nФильтр "air":')
list.setFilter('air')
console.log('\nСортировка по цене:')
list.setFilter('')
list.setSortBy('price')
})
Обычный слот позволяет родителю передать контент в компонент, но родитель не знает о внутреннем состоянии дочернего компонента. Scoped slots решают эту проблему — дочерний компонент передаёт данные обратно в слот.
Данные передаются слоту через атрибуты на <slot>:
<!-- DataList.vue — дочерний компонент -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- Передаём item и index в слот -->
<slot :item="item" :index="index" />
</li>
</ul>
</template><!-- v-slot="slotProps" — получаем все переданные данные -->
<DataList :items="products" v-slot="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }} — {{ item.price }}₽</span>
</DataList>Деструктуризация в v-slot — стандартная практика для удобства.
<!-- Table.vue -->
<template>
<table>
<thead>
<tr>
<slot name="header" :columns="columns" />
</tr>
</thead>
<tbody>
<tr v-for="row in data">
<slot name="row" :row="row" :columns="columns" />
</tr>
</tbody>
<tfoot>
<slot name="footer" :total="total" />
</tfoot>
</table>
</template><!-- Использование -->
<Table :data="data" :columns="cols">
<template #header="{ columns }">
<th v-for="col in columns">{{ col.label }}</th>
</template>
<template #row="{ row, columns }">
<td v-for="col in columns">{{ row[col.key] }}</td>
</template>
<template #footer="{ total }">
<td colspan="3">Итого: {{ total }}</td>
</template>
</Table>Самый мощный паттерн со scoped slots: компонент управляет **логикой**, а родитель полностью контролирует **внешний вид**:
<!-- MouseTracker.vue — только логика, нет своей разметки -->
<template>
<slot :x="position.x" :y="position.y" :isMoving="isMoving" />
</template>
<script setup>
const position = reactive({ x: 0, y: 0 })
const isMoving = ref(false)
onMounted(() => {
window.addEventListener('mousemove', (e) => {
position.x = e.clientX
position.y = e.clientY
isMoving.value = true
})
})
</script><!-- Использование — внешний вид на усмотрение пользователя -->
<MouseTracker v-slot="{ x, y, isMoving }">
<div :class="{ active: isMoving }">
Курсор: {{ x }}, {{ y }}
</div>
</MouseTracker>Этот паттерн аналогичен **render props** в React.
// Проверить, передан ли слот
const hasFooter = computed(() => !!slots.footer)Паттерн renderless-компонента и scoped slots на чистом JS — данные управляются внутри, отображение снаружи
// Эмулируем паттерн Renderless Component + Scoped Slots.
// Компонент предоставляет данные через "слот-функцию",
// а "родитель" решает как их отображать.
// --- Renderless DataFetcher ---
// Аналог компонента, который управляет загрузкой данных
function createDataFetcher(fetchFn) {
// Внутреннее состояние компонента
let state = {
data: null,
loading: false,
error: null,
}
let slotFn = null // "шаблон" — функция рендера из родителя
const api = {
// Аналог <slot :data="data" :loading="loading" :error="error" :refetch="refetch">
// slotFn — это то, что родитель передаёт через v-slot
setSlot(fn) {
slotFn = fn
return api
},
render() {
if (slotFn) {
// Передаём "scope" — внутренние данные компонента
return slotFn({
data: state.data,
loading: state.loading,
error: state.error,
refetch: api.fetch,
})
}
},
async fetch() {
state = { ...state, loading: true, error: null }
api.render()
try {
state.data = await fetchFn()
state.loading = false
api.render()
} catch(err) {
state.loading = false
state.error = err.message
api.render()
}
}
}
return api
}
// --- Renderless List ---
// Компонент управляет фильтрацией/сортировкой, UI — снаружи
function createFilterableList(items) {
let filter = ''
let sortBy = null
let slotFn = null
const api = {
setSlot(fn) { slotFn = fn; return api },
setFilter(text) {
filter = text
api.render()
},
setSortBy(key) {
sortBy = key
api.render()
},
render() {
if (!slotFn) return
let result = items.filter(item =>
!filter || Object.values(item).some(v =>
String(v).toLowerCase().includes(filter.toLowerCase())
)
)
if (sortBy) {
result = [...result].sort((a, b) =>
a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
)
}
// Вызываем "слот" с данными
return slotFn({
items: result,
total: items.length,
filtered: result.length,
setFilter: api.setFilter,
setSortBy: api.setSortBy,
})
}
}
return api
}
// === Демо DataFetcher ===
console.log('=== DataFetcher (Renderless) ===')
const fetcher = createDataFetcher(async () => {
await new Promise(r => setTimeout(r, 50))
return [{ id: 1, name: 'Vue' }, { id: 2, name: 'React' }]
})
// Родитель определяет UI через "слот"
fetcher.setSlot(({ data, loading, error }) => {
if (loading) console.log(' [UI] Загрузка...')
if (error) console.log(' [UI] Ошибка:', error)
if (data) console.log(' [UI] Данные:', data.map(d => d.name).join(', '))
})
fetcher.fetch().then(() => {
// === Демо FilterableList ===
console.log('\n=== FilterableList (Renderless) ===')
const products = [
{ id: 1, name: 'MacBook Pro', price: 200000, category: 'laptop' },
{ id: 2, name: 'iPhone 15', price: 90000, category: 'phone' },
{ id: 3, name: 'iPad Air', price: 80000, category: 'tablet' },
{ id: 4, name: 'AirPods', price: 20000, category: 'audio' },
{ id: 5, name: 'Apple Watch', price: 50000, category: 'watch' },
]
const list = createFilterableList(products)
// UI полностью контролируется "родителем"
list.setSlot(({ items, total, filtered }) => {
console.log(` Показано ${filtered}/${total}:`)
items.forEach(p => console.log(` - ${p.name}: ${p.price}₽`))
})
console.log('Все продукты:')
list.render()
console.log('\nФильтр "air":')
list.setFilter('air')
console.log('\nСортировка по цене:')
list.setFilter('')
list.setSortBy('price')
})
Реализуй функцию `createPaginator(items, pageSize)`, которая возвращает объект-"renderless компонент" с методами: `setSlot(fn)` — устанавливает функцию рендера, `render()` — вызывает slotFn с объектом `{ items: currentPageItems, page, totalPages, hasNext, hasPrev, next(), prev(), goTo(n) }`. Переключение страниц должно автоматически вызывать render(). Страницы начинаются с 1.
getCurrentItems: return allItems.slice((currentPage - 1) * pageSize, (currentPage - 1) * pageSize + pageSize). В goTo: currentPage = Math.max(1, Math.min(n, totalPages)). Убедись что next/prev/goTo вызывают render() в конце. В render() вызывай slotFn({ ... }) только если slotFn !== null.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке