Vue Front-end Инженер

Vue Front-end Инженер

Роадмап навыков для прокачки

Слоты: передача контента в компоненты

VueJSComponentsSlots

Основная идея

Slots (слоты) — это механизм Vue для передачи контента из родительского компонента в дочерний. Вместо передачи данных через props, слоты позволяют передать целые фрагменты разметки, включая HTML, текст и другие компоненты.

Ключевые аспекты

  • slot в дочернем компоненте — определяет место, куда будет вставлен контент
  • Контент между тегами — родитель передаёт контент между открывающим и закрывающим тегом компонента
  • Fallback content — контент по умолчанию, если родитель ничего не передал
  • Композиция — позволяет создавать гибкие и переиспользуемые компоненты
  • Доступ к родительскому scope — переданный контент имеет доступ к данным родителя

Типичные сценарии

  • Компоненты-обёртки (Card, Modal, Panel)
  • Кнопки с кастомным содержимым
  • Layout-компоненты
  • Компоненты с настраиваемыми областями

Частые ошибки

  • Путают slots с props — slots для разметки, props для данных
  • Забывают про fallback content — слот пустой, если родитель ничего не передал
  • Пытаются получить доступ к данным дочернего компонента в слоте
  • Не используют слоты там, где они упростили бы код

Рекомендации

  • Используйте slots для передачи разметки, props — для данных
  • Добавляйте fallback content для лучшего UX
  • Для сложных случаев используйте именованные слоты
  • Слоты делают компоненты более гибкими и переиспользуемыми

Введение и проблематика

Иногда компоненту нужно получить от родителя не просто данные, а целый фрагмент разметки. Например, модальное окно должно отображать любой контент внутри себя, а кнопка — иконку и текст в любом порядке.

Слоты позволяют родителю передавать контент в дочерний компонент. Это делает компоненты гибкими и переиспользуемыми.


Базовая теория

Что такое слот

Слот — это «отверстие» в шаблоне компонента, куда родитель может вставить свой контент.

Code Example 1: Что произойдёт с содержимым, переданным между тегами <Card> и </Card>? Где оно окажется в итоговом HTML?

vue
<!-- Card.vue — дочерний компонент -->
<template>
  <div class="card">
    <slot></slot>
  </div>
</template>
vue
<!-- App.vue — родитель -->
<template>
  <Card>
    <h2>Заголовок карточки</h2>
    <p>Текст карточки</p>
  </Card>
</template>

Результат:

html
<div class="card">
  <h2>Заголовок карточки</h2>
  <p>Текст карточки</p>
</div>

Fallback content (контент по умолчанию)

Если родитель не передал контент, слот покажет fallback:

Code Example 2: Что покажет каждый из этих двух вариантов использования компонента Button? Что такое fallback content?

vue
<!-- Button.vue -->
<template>
  <button class="btn">
    <slot>Кнопка</slot>
  </button>
</template>
vue
<!-- Использование -->
<template>
  <Button />
 
  <Button>Отправить</Button>
</template>

Практические примеры

Пример 1: Карточка

Code Example 3: Какой HTML будет сгенерирован в результате? Что произойдёт, если использовать <Card /> без передачи контента?

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-body">
      <slot>Контент карточки</slot>
    </div>
  </div>
</template>
 
<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}
</style>
vue
<!-- App.vue -->
<script setup>
import Card from './Card.vue'
</script>
 
<template>
  <Card>
    <h3>Профиль пользователя</h3>
    <p>Имя: Иван</p>
    <p>Email: ivan@example.com</p>
  </Card>
</template>

Пример 2: Модальное окно

Code Example 4: Как слот помогает сделать модальное окно универсальным? Какой контент будет отображён внутри модалки?

vue
<!-- Modal.vue -->
<script setup>
defineProps(['isOpen'])
const emit = defineEmits(['close'])
</script>
 
<template>
  <div v-if="isOpen" class="modal-overlay" @click="emit('close')">
    <div class="modal-content" @click.stop>
      <slot>Содержимое модального окна</slot>
      <button @click="emit('close')">Закрыть</button>
    </div>
  </div>
</template>
vue
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
 
const showModal = ref(false)
</script>
 
<template>
  <button @click="showModal = true">Открыть</button>
 
  <Modal :is-open="showModal" @close="showModal = false">
    <h2>Подтверждение</h2>
    <p>Вы уверены, что хотите удалить?</p>
  </Modal>
</template>

Пример 3: Кнопка с иконкой

Code Example 5: Что отобразит каждый из трёх вариантов использования IconButton? Когда сработает fallback content?

vue
<!-- IconButton.vue -->
<template>
  <button class="icon-btn">
    <slot>
      Нажми меня
    </slot>
  </button>
</template>
vue
<!-- App.vue -->
<template>
  <!-- Кнопка с текстом и иконкой -->
  <IconButton>
    <span>🗑️</span>
    <span>Удалить</span>
  </IconButton>
 
  <!-- Кнопка только с иконкой -->
  <IconButton>
    <span>❤️</span>
  </IconButton>
 
  <!-- Кнопка с fallback контентом -->
  <IconButton />
</template>

Scope слота

Контент слота имеет доступ к данным родительского компонента:

Code Example 6: Почему userName доступен в контенте слота? К каким данным имеет доступ контент, переданный в слот?

vue
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Card from './Card.vue'
 
const userName = ref('Иван')
</script>
 
<template>
  <Card>
    <h2>Привет, {{ userName }}!</h2>
  </Card>
</template>
⚠️

Контент слота НЕ имеет доступа к данным дочернего компонента. Для этого нужны scoped slots (рассматриваются на следующих уровнях).


Сравнение: slots vs props

Code Example 7: Сравните два подхода к передаче контента в компонент. Когда лучше использовать props, а когда slots?

vue
<!-- Передача через props -->
<Card title="Заголовок" description="Описание" />
 
<!-- Card.vue -->
<template>
  <div class="card">
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
  </div>
</template>
Когда использоватьPropsSlots
Простые данные (строка, число)
HTML-разметка
Другие компоненты
Гибкая структура контента

Пограничные кейсы

Code Example 8: В чём разница между <Card /> и <Card></Card> с точки зрения fallback content? Что произойдёт в каждом случае?

Несколько элементов в слоте

Слот может содержать несколько элементов:

vue
<Card>
  <h2>Заголовок</h2>
  <p>Параграф 1</p>
  <p>Параграф 2</p>
  <button>Кнопка</button>
</Card>

Пустой слот vs отсутствие слота

vue
<!-- Не передано ничего — покажется fallback -->
<Card />
 
<!-- Передан пустой контент — fallback НЕ покажется -->
<Card></Card>

Динамический контент в слоте

Code Example 9: Можно ли использовать директивы Vue (например, v-for) внутри контента слота? Что будет отрисовано?

vue
<script setup>
import { ref } from 'vue'
 
const items = ref(['Яблоко', 'Банан', 'Апельсин'])
</script>
 
<template>
  <Card>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
  </Card>
</template>

Плюсы и минусы

ПлюсыМинусы
Гибкость — любой контентСложнее контролировать структуру
Переиспользуемость компонентовТребует документации для пользователей
Композиция компонентовНет доступа к данным дочернего компонента

Вопросы интервьюера

Q: Что такое слоты во Vue?

Механизм передачи контента (разметки) от родителя в дочерний компонент. Позволяет создавать гибкие компоненты-обёртки.

Q: Чем слоты отличаются от props?

Props передают данные (строки, числа, объекты), слоты — разметку (HTML, компоненты). Слоты для контента, props — для конфигурации.

Q: Что такое fallback content?

Контент по умолчанию, который показывается, если родитель не передал ничего в слот.

Q: Имеет ли слот доступ к данным дочернего компонента?

Нет, контент слота имеет доступ только к scope родителя. Для доступа к данным дочернего нужны scoped slots.


Источники

Code Example 1: Базовое использование слота

Что произойдёт с содержимым, переданным между тегами <Card> и </Card>? Где оно окажется в итоговом HTML?

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <slot></slot>
  </div>
</template>
vue
<!-- App.vue -->
<template>
  <Card>
    <h2>Заголовок карточки</h2>
    <p>Текст карточки</p>
  </Card>
</template>

Code Example 2: Fallback content

Что покажет каждый из этих двух вариантов использования компонента Button? Что такое fallback content?

vue
<!-- Button.vue -->
<template>
  <button class="btn">
    <slot>Кнопка</slot>
  </button>
</template>
vue
<!-- Использование -->
<template>
  <Button />
 
  <Button>Отправить</Button>
</template>

Code Example 3: Карточка с контентом

Какой HTML будет сгенерирован в результате? Что произойдёт, если использовать <Card /> без передачи контента?

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <div class="card-body">
      <slot>Контент карточки</slot>
    </div>
  </div>
</template>
 
<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
}
</style>
vue
<!-- App.vue -->
<script setup>
import Card from './Card.vue'
</script>
 
<template>
  <Card>
    <h3>Профиль пользователя</h3>
    <p>Имя: Иван</p>
    <p>Email: ivan@example.com</p>
  </Card>
</template>

Code Example 4: Модальное окно со слотом

Как слот помогает сделать модальное окно универсальным? Какой контент будет отображён внутри модалки?

vue
<!-- Modal.vue -->
<script setup>
defineProps(['isOpen'])
const emit = defineEmits(['close'])
</script>
 
<template>
  <div v-if="isOpen" class="modal-overlay" @click="emit('close')">
    <div class="modal-content" @click.stop>
      <slot>Содержимое модального окна</slot>
      <button @click="emit('close')">Закрыть</button>
    </div>
  </div>
</template>
vue
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
 
const showModal = ref(false)
</script>
 
<template>
  <button @click="showModal = true">Открыть</button>
 
  <Modal :is-open="showModal" @close="showModal = false">
    <h2>Подтверждение</h2>
    <p>Вы уверены, что хотите удалить?</p>
  </Modal>
</template>

Code Example 5: Кнопка с иконкой

Что отобразит каждый из трёх вариантов использования IconButton? Когда сработает fallback content?

vue
<!-- IconButton.vue -->
<template>
  <button class="icon-btn">
    <slot>
      Нажми меня
    </slot>
  </button>
</template>
vue
<!-- App.vue -->
<template>
  <IconButton>
    <span>🗑️</span>
    <span>Удалить</span>
  </IconButton>
 
  <IconButton>
    <span>❤️</span>
  </IconButton>
 
  <IconButton />
</template>

Code Example 6: Область видимости слота

Почему userName доступен в контенте слота? К каким данным имеет доступ контент, переданный в слот?

vue
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Card from './Card.vue'
 
const userName = ref('Иван')
</script>
 
<template>
  <Card>
    <h2>Привет, {{ userName }}!</h2>
  </Card>
</template>

Code Example 7: Props vs Slots

Сравните два подхода к передаче контента в компонент. Когда лучше использовать props, а когда slots?

Props:

vue
<!-- Передача через props -->
<Card title="Заголовок" description="Описание" />
 
<!-- Card.vue -->
<template>
  <div class="card">
    <h2>{{ title }}</h2>
    <p>{{ description }}</p>
  </div>
</template>

Slots:

vue
<!-- Передача через slot -->
<Card>
  <h2>Заголовок</h2>
  <p>Описание</p>
  <button>Действие</button>
</Card>
 
<!-- Card.vue -->
<template>
  <div class="card">
    <slot></slot>
  </div>
</template>

Code Example 8: Пограничные кейсы

В чём разница между <Card /> и <Card></Card> с точки зрения fallback content? Что произойдёт в каждом случае?

vue
<Card>
  <h2>Заголовок</h2>
  <p>Параграф 1</p>
  <p>Параграф 2</p>
  <button>Кнопка</button>
</Card>
vue
<!-- Не передано ничего -->
<Card />
 
<!-- Передан пустой контент -->
<Card></Card>

Code Example 9: Динамический контент в слоте

Можно ли использовать директивы Vue (например, v-for) внутри контента слота? Что будет отрисовано?

vue
<script setup>
import { ref } from 'vue'
 
const items = ref(['Яблоко', 'Банан', 'Апельсин'])
</script>
 
<template>
  <Card>
    <ul>
      <li v-for="item in items" :key="item">{{ item }}</li>
    </ul>
  </Card>
</template>