Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Scoped Slots во Vue: мощный паттерн для гибких компонентов
Scoped Slots (названные слоты с доступом к данным компонента) - это один из самых мощных паттернов во Vue для создания переиспользуемых и гибких компонентов. Они позволяют родительскому компоненту получить доступ к данным дочернего компонента и контролировать как этих данные отображаются.
Основная концепция
Обычные slots позволяют только вставить содержимое. Scoped slots позволяют передать данные из дочернего компонента в родительский:
<!-- Дочерний компонент (ChildComponent.vue) -->
<template>
<ul>
<!-- Передаём data в slot через v-bind -->
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index">
<!-- Default содержимое если parent не определил slot -->
{{ item.name }}
</slot>
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
};
}
};
</script>
<!-- Родительский компонент -->
<template>
<ChildComponent>
<!-- Получаем доступ к item и index через v-slot -->
<template v-slot="{ item, index }">
<strong>{{ index + 1 }}. {{ item.name }}</strong>
</template>
</ChildComponent>
</template>
Зачем нужны Scoped Slots
1. Максимальная гибкость компонента
Позволяет parent контролировать представление данных:
<!-- DataTable.vue - умный список -->
<template>
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.key">
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="column in columns" :key="column.key">
<!-- Передаём row и column в slot -->
<slot :name="column.key" :row="row" :column="column">
{{ row[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script>
export default {
props: ['data', 'columns']
};
</script>
<!-- Использование в родителе -->
<template>
<DataTable :data="users" :columns="userColumns">
<!-- Кастомное отображение email с ссылкой -->
<template #email="{ row }">
<a :href="`mailto:${row.email}`">{{ row.email }}</a>
</template>
<!-- Кастомное отображение статуса с цветом -->
<template #status="{ row }">
<span :class="{ 'text-green-600': row.status === 'active', 'text-red-600': row.status === 'inactive' }">
{{ row.status }}
</span>
</template>
</DataTable>
</template>
2. Разделение логики и представления
Дочерний компонент отвечает за логику, parent за представление:
<!-- ProductList.vue - умный список товаров -->
<template>
<div>
<div v-if="loading" class="spinner">Loading...</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="products.length > 0">
<!-- Передаём product с полными данными -->
<slot
v-for="product in products"
:key="product.id"
:product="product"
:addToCart="addToCart"
/>
</div>
<div v-else>No products</div>
</div>
</template>
<script>
export default {
data() {
return {
products: [],
loading: true,
error: null
};
},
methods: {
addToCart(product) {
// Логика добавления в корзину
console.log('Added to cart:', product);
}
},
async mounted() {
try {
const response = await fetch('/api/products');
this.products = await response.json();
} catch (e) {
this.error = e.message;
} finally {
this.loading = false;
}
}
};
</script>
<!-- Использование - parent решает как отображать -->
<template>
<ProductList v-slot="{ product, addToCart }">
<div class="product-card">
<img :src="product.image" />
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<span class="price">${{ product.price }}</span>
<button @click="addToCart(product)">Add to Cart</button>
</div>
</ProductList>
</template>
3. Создание headless компонентов
Компонент без UI - parent полностью отвечает за внешний вид:
<!-- Dropdown.vue - headless dropdown -->
<template>
<div class="dropdown">
<button @click="isOpen = !isOpen">
<slot name="trigger" :isOpen="isOpen">
Menu
</slot>
</button>
<ul v-if="isOpen" class="dropdown-menu">
<li v-for="item in items" :key="item.id">
<slot :item="item" :select="selectItem">
<button @click="selectItem(item)">
{{ item.label }}
</button>
</slot>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['items'],
data() {
return { isOpen: false };
},
methods: {
selectItem(item) {
this.$emit('select', item);
this.isOpen = false;
}
}
};
</script>
<!-- Использование - полный контроль над представлением -->
<template>
<Dropdown :items="menuItems" @select="onSelect">
<template #trigger="{ isOpen }">
<button :class="{ 'open': isOpen }">
More Options
</button>
</template>
<template #default="{ item, select }">
<button
@click="select(item)"
class="custom-menu-item"
>
<icon :name="item.icon" /> {{ item.label }}
</button>
</template>
</Dropdown>
</template>
4. Фильтры и форматирование данных
Передача фильтр-функций в slot:
<!-- UserList.vue -->
<template>
<div>
<div v-for="user in users" :key="user.id">
<slot
:user="user"
:formatDate="formatDate"
:formatCurrency="formatCurrency"
/>
</div>
</div>
</template>
<script>
export default {
props: ['users'],
methods: {
formatDate(date) {
return new Date(date).toLocaleDateString();
},
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
}
};
</script>
<!-- Использование -->
<template>
<UserList :users="users" v-slot="{ user, formatDate, formatCurrency }">
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>Member since: {{ formatDate(user.joinDate) }}</p>
<p>Balance: {{ formatCurrency(user.balance) }}</p>
</div>
</UserList>
</template>
5. Именованные scoped slots
Множество слотов с разными данными:
<!-- Form.vue -->
<template>
<form @submit.prevent="submit">
<slot name="header"></slot>
<div class="form-fields">
<div v-for="field in fields" :key="field.name" class="form-group">
<!-- Каждое поле может иметь кастомное отображение -->
<slot
:name="field.name"
:field="field"
:value="formData[field.name]"
:error="errors[field.name]"
:updateValue="(val) => formData[field.name] = val"
>
<!-- Default input -->
<label>{{ field.label }}</label>
<input
:value="formData[field.name]"
@input="e => formData[field.name] = e.target.value"
:type="field.type || 'text'"
/>
<p v-if="errors[field.name]" class="error">
{{ errors[field.name] }}
</p>
</slot>
</div>
</div>
<slot name="footer" :submit="submit" :isValid="isValid">
<button type="submit" :disabled="!isValid">Submit</button>
</slot>
</form>
</template>
<script>
export default {
props: ['fields'],
data() {
return {
formData: {},
errors: {}
};
},
computed: {
isValid() {
return Object.keys(this.errors).length === 0;
}
},
methods: {
submit() {
this.$emit('submit', this.formData);
}
}
};
</script>
<!-- Использование с кастомными полями -->
<template>
<Form :fields="fields" @submit="onSubmit">
<template #email="{ field, value, updateValue, error }">
<label>{{ field.label }}</label>
<input
:value="value"
@input="e => updateValue(e.target.value)"
type="email"
class="custom-input"
/>
<small v-if="error" class="error">{{ error }}</small>
</template>
<template #password="{ field, value, updateValue }">
<label>{{ field.label }}</label>
<input
:value="value"
@input="e => updateValue(e.target.value)"
type="password"
class="custom-input"
/>
</template>
<template #footer="{ submit, isValid }">
<div class="button-group">
<button @click="submit" :disabled="!isValid" class="btn-primary">
Create Account
</button>
<button type="button" @click="reset" class="btn-secondary">
Reset
</button>
</div>
</template>
</Form>
</template>
Сравнение: Scoped Slot vs Props
<!-- БЕЗ Scoped Slots - много props -->
<!-- BadList.vue -->
<template>
<div>
<ItemComponent
v-for="item in items"
:key="item.id"
:item="item"
:showDescription="showDescription"
:showPrice="showPrice"
:priceFormatter="priceFormatter"
:descriptionTruncate="descriptionTruncate"
@click="onItemClick"
/>
</div>
</template>
<!-- С Scoped Slots - очень гибко -->
<!-- GoodList.vue -->
<template>
<div>
<div v-for="item in items" :key="item.id">
<slot
:item="item"
:formatPrice="formatPrice"
/>
</div>
</div>
</template>
Вложенные scoped slots
<!-- Grid.vue -->
<template>
<div class="grid">
<div v-for="row in rows" :key="row.id" class="grid-row">
<div v-for="cell in row.cells" :key="cell.id" class="grid-cell">
<!-- Вложенный slot с данными из разных уровней -->
<slot
:row="row"
:cell="cell"
:rowIndex="rows.indexOf(row)"
:cellIndex="row.cells.indexOf(cell)"
/>
</div>
</div>
</div>
</template>
<!-- Использование -->
<template>
<Grid :rows="gridData" v-slot="{ row, cell, rowIndex, cellIndex }">
<div class="custom-cell">
Cell [{{ rowIndex }},{{ cellIndex }}]: {{ cell.value }}
</div>
</Grid>
</template>
Вывод
Scoped Slots нужны потому что они:
- Максимизируют переиспользование - один компонент может выглядеть по-разному
- Разделяют логику и представление - компонент отвечает за логику, parent за UI
- Создают headless компоненты - полный контроль у parent
- Избегают prop drilling - данные передаются точно туда где нужны
- Обеспечивают гибкость - parent решает как отображать данные
Это один из ключевых паттернов для создания высокочеловечного и переиспользуемого кода во Vue.