Олег Марков
Использование v for и slot в Vue
Введение
Если вы работаете с Vue, скорее всего, сталкивались с необходимостью рендерить списки данных и создавать переиспользуемые компоненты, содержащие произвольное содержимое. Для этих задач во Vue есть два очень мощных инструмента — директива v-for и слоты (slot). Каждый из них заслуживает отдельного внимания, но в этой статье мы не только подробно рассмотрим, как они работают сами по себе, но и разберемся, как их комбинировать для создания максимально гибких и эффективных компонентов.
v-for позволяет интенсивно использовать динамику данных, показывая повторяющиеся элементы на основе массивов или объектов. Слоты, в свою очередь, обеспечивают компонентам гибкость в принятии дополнительного контента, который внедряет родительский компонент. Совместное использование этих инструментов поможет вам делать масштабируемый, читаемый и легко поддерживаемый код.
v-for: основы работы
Синтаксис v-for
В Vue директива v-for применяется для циклического повторения элементов DOM по массиву или объекту. Смотрите, как это выглядит на практике:
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
// Мы проходимся по массиву items
, для каждого элемента создается
Давайте чуть подробнее разберем параметры v-for:
<div v-for="(value, index) in array" :key="index">
{{ index }}: {{ value }}
</div>
// Здесь index
— порядковый номер в массиве, а value
— текущее значение. Указание :key — крайне важная практика.
Работать с объектами через v-for
Vue позволяет использовать v-for и для объектов. Например:
<ul>
<li v-for="(value, key) in object" :key="key">
{{ key }}: {{ value }}
</li>
</ul>
// Мы проходим по объекту object
, получая ключ и значение каждой пары.
:key — почему это важно
Ключ (:key) помогает Vue более эффективно и корректно отслеживать изменения в списке и перестраивать DOM. Обычно в качестве ключа используют уникальный идентификатор элемента. Если уникального поля нет, используйте индекс, но делайте это только если список гарантированно не будет меняться во время жизни компонента.
slot: как устроена система слотов во Vue
Обычные (дефолтные) слоты
Слоты позволяют прокидывать содержимое внутрь компонента. Смотрите:
<!-- App.vue -->
<custom-card>
<template #default>
<p>Содержимое карточки</p>
</template>
</custom-card>
<!-- components/CustomCard.vue -->
<template>
<div class="card">
<slot>
<!-- Здесь выводится все, что размещено между тегами <custom-card> в родителе -->
</slot>
</div>
</template>
// Это обычный слот: если родитель ничего не передаст, выведется содержимое по умолчанию (если оно указано).
Именованные слоты
Если вам нужно иметь несколько областей для подстановки разного контента, используйте именованные слоты. Покажу вам пример:
<custom-dialog>
<template #header>
<h3>Заголовок</h3>
</template>
<template #body>
<p>Основная часть контента</p>
</template>
<template #footer>
<button>Закрыть</button>
</template>
</custom-dialog>
<!-- CustomDialog.vue -->
<template>
<div class="dialog">
<header>
<slot name="header" />
</header>
<main>
<slot name="body" />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
// Родитель может передать разные фрагменты в разные области компонента.
Комбинирование v-for и slot
Динамические списки с кастомным содержимым
Одна из сильнейших сторон Vue — возможность рендерить списки компонентов и предоставлять им разнообразное содержимое с помощью слотов.
Давайте посмотрим, как использовать v-for в комбинации со слотами внутри компонента.
Пример: Список карточек с шаблонным содержимым
<!-- CardList.vue (компонент-контейнер) -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<card-item>
<template #default>
{{ item.text }}
</template>
</card-item>
</li>
</ul>
</template>
<script setup>
const props = defineProps(['items'])
</script>
<!-- CardItem.vue -->
<template>
<div class="item">
<slot />
</div>
</template>
// Каждый card-item получает свой фрагмент и выводит его через слот.
v-for внутри компонента-слота
Можно использовать v-for и внутри компонента, который содержит слот — например, делать делегирование обработки списка самому компоненту. Смотрите вариант:
<!-- ListRenderer.vue -->
<template>
<ul>
<li v-for="(item, i) in items" :key="item.id">
<slot :item="item" :index="i">
<!-- Слот будет заменен предоставленным шаблоном, если он есть, иначе выводится по умолчанию -->
{{ item.name }}
</slot>
</li>
</ul>
</template>
<script setup>
const props = defineProps(['items'])
</script>
<!-- Использование ListRenderer -->
<ListRenderer :items="myArray">
<template #default="{ item, index }">
<strong>{{ index + 1 }}.</strong> {{ item.customLabel }}
</template>
</ListRenderer>
// Обратите внимание: слот принимает пропсы, переданные через :item и :index — это так называемый scoped slot (слот с доступом к данным).
Scoped slots: передача данных из дочернего компонента
Когда компоненты используют слот, иногда родительскому компоненту полезно получить данные из дочернего — например, элемент списка. Для этого используют scoped slots.
Как работают scoped slots
В дочернем компоненте объявляем слот и передаем в него данные:
// В дочернем компоненте
<slot :item="item" :index="i" />
В родителе можем получить эти данные следующим образом:
<MyListComponent :items="myItems">
<template #default="{ item, index }">
<!-- Здесь доступны item и index -->
<div>
{{ index }}: {{ item.label }}
</div>
</template>
</MyListComponent>
// Таким образом, родитель не только подменяет часть шаблона, но и получает доступ к переменным, которые предоставляет дочерний компонент.
Примеры сложных сценариев использования
Список карточек с кастомным заголовком и содержимым
Разберем ситуацию, когда карточка имеет отдельные именованные слоты для заголовка и основного содержимого, а список карточек формирует общий компонент:
<!-- CardsList.vue -->
<template>
<div>
<Card v-for="card in cards" :key="card.id">
<template #header>
<span>{{ card.title }}</span>
</template>
<template #default>
<p>{{ card.body }}</p>
</template>
</Card>
</div>
</template>
<script setup>
const props = defineProps(['cards'])
</script>
<!-- Card.vue -->
<template>
<div class="card">
<div class="header">
<slot name="header" />
</div>
<div class="body">
<slot />
</div>
</div>
</template>
// Здесь компонент CardsList циклично выводит компонент Card с разным содержимым в слотах header и default.
Генерация form-полей с помощью v-for и slot
Теперь покажу пример, как можно использовать slots и v-for для динамического создания формы:
<!-- FormFieldsRenderer.vue -->
<template>
<form>
<div v-for="field in fields" :key="field.name">
<label :for="field.name">{{ field.label }}</label>
<slot :field="field">
<!-- по умолчанию рендерим input -->
<input :id="field.name" :name="field.name" :type="field.type" />
</slot>
</div>
</form>
</template>
<script setup>
const props = defineProps(['fields'])
</script>
<!-- Использовать компонент можно так -->
<FormFieldsRenderer :fields="fieldsArray">
<template #default="{ field }">
<!-- примеры условных кастомизаций для некоторых полей -->
<input v-if="field.type === 'text'" :id="field.name" :name="field.name" />
<select v-else-if="field.type === 'select'" :id="field.name" :name="field.name">
<option v-for="opt in field.options" :value="opt.value" :key="opt.value">{{ opt.label }}</option>
</select>
<input v-else :type="field.type" :id="field.name" :name="field.name" />
</template>
</FormFieldsRenderer>
// Смотрите, как таким способом родитель не только меняет шаблон, но и динамически управляет содержимым поля.
Ограничения и особенности
Как избежать ловушек
- Не используйте индекс как key, когда список может изменяться — это приведет к багам.
- Комбинируя v-for + slot, избегайте ситуации, когда состояние компонента зависит только от внешнего индекса.
- Важно учитывать, что slot не обязательны к заполнению. Если не передать шаблон в слот — он будет пустым или выведет дефолтный контент.
- Scoped slot не поддерживают двустороннюю реактивность напрямую — данные, переданные через :item, всегда «только для чтения» на стороне родителя.
Слоты не рендерятся вне компонента
Помните, если разместить v-for вне компонента, обращаться к slot внутри этого цикла не получится.
Когда лучше применять v-for и slot вместе
- Для сложных визуальных списков, где родитель должен контролировать не только структуру, но и мелкие особенности каждого элемента.
- Если компоненты должны быть максимально переиспользуемыми и легко настраиваться за счет разнообразных шаблонов.
- Когда элементы списка должны принимать разные варианты содержимого, исходя из данных или внешних условий.
Заключение
v-for и slot — инструменты Vue, которыми вы будете пользоваться практически в каждом проекте. Используйте v-for, чтобы рендерить динамические списки и работать с коллекциями. Применяйте слоты для гибкой вставки произвольного содержимого внутрь компонента. Совмещайте эти две техники, чтобы делать мощные, масштабируемые и легко настраиваемые интерфейсы. Благодаря им код становится лаконичным, а компоненты действительно переиспользуемыми.
Частозадаваемые технические вопросы по теме статьи и ответы на них
В: Как передать обработчик событий в слот с помощью v-for?
О: Добавьте обработчик прямо в шаблон слота — он будет работать в контексте родительского компонента. Например:
vue
<MyList :items="data">
<template #default="{ item }">
<div @click="handler(item)">{{ item.name }}</div>
</template>
</MyList>
handler
должен быть объявлен у родителя.
В: Можно ли использовать v-if и v-for одновременно в одном элементе?
О: Можно, но порядок имеет значение. Vue рекомендует v-for выносить "наружу", то есть использовать v-if внутри v-for, чтобы фильтровать уже перебираемые элементы, а не наоборот.
В: Как типизировать данные, приходящие в scoped slot, если я использую TypeScript?
О: В используйте defineProps и на этапе использования слота явно указывайте типы:
vue
<MyComp>
<template #default="{ item }: { item: MyType }">
<!-- ... -->
</template>
</MyComp>
В: Почему после обновления данных список не перерисовывается?
О: Проверьте, что у каждого элемента уникальный key. Если вы используете объекты, меняйте массив реактивно (через методы массива, а не прямое присваивание).
В: Можно ли использовать slot внутри v-for, но не в компоненте, а прямо в одноуровневом списке?
О: Нет, слот работает только как часть компонента. Если требуется подобная динамика, оберните логику в отдельный компонент и используйте slot внутри него.