Метка: Frontend Architecture

  • Vue 3 Slots передача параметров

    Slots (слоты) в Vue 3 предоставляют мощный механизм для создания гибких и переиспользуемых компонентов. В этой статье мы разберём все способы передачи параметров в слоты с примерами из реальной практики.

    Основные типы слотов в Vue 3

    • Слоты по умолчанию — базовый способ передачи контента
    • Именованные слоты — для точного позиционирования контента
    • Scoped slots — с передачей параметров из дочернего компонента

    1. Scoped Slots (Основной способ передачи параметров)

    <!-- Дочерний компонент -->
    <template>
      <div>
        <slot :item="item" :index="index"></slot>
      </div>
    </template>
    
    <!-- Родительский компонент -->
    <ChildComponent>
      <template v-slot:default="slotProps">
        {{ slotProps.item }} - {{ slotProps.index }}
      </template>
    </ChildComponent>

    2. Именованные scoped slots

    <!-- Дочерний компонент -->
    <template>
      <div>
        <slot name="header" :title="title"></slot>
        <slot name="content" :data="contentData"></slot>
      </div>
    </template>
    
    <!-- Родительский компонент -->
    <DataContainer>
      <template #header="{ title }">
        <h2>{{ title }}</h2>
      </template>
      
      <template #content="{ data }">
        <p>{{ data.description }}</p>
      </template>
    </DataContainer>

    3. Динамические параметры слотов

    <!-- Дочерний компонент -->
    <script setup>
    const slotProps = computed(() => ({
      user: currentUser.value,
      timestamp: new Date()
    }))
    </script>
    
    <template>
      <slot v-bind="slotProps"></slot>
    </template>

    4. Деструктуризация параметров

    <template #item="{ id, name }">
      <div>{{ id }}: {{ name }}</div>
    </template>

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

    Пример 1: Гибкий список

    <SmartList :items="users">
      <template #item="{ user }">
        <UserCard :user="user" />
      </template>
    </SmartList>

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

    <ModalDialog>
      <template #header="{ close }">
        <button @click="close">×</button>
      </template>
      
      <template #default="{ data }">
        {{ data.message }}
      </template>
    </ModalDialog>

    Лучшие практики

    • Используйте осмысленные имена для параметров слотов
    • Для сложных компонентов документируйте структуру слотов
    • Избегайте глубокой вложенности scoped slots
    • Используйте TypeScript для типизации параметров

    Заключение

    Scoped slots в Vue 3 предоставляют мощный инструмент для создания гибких компонентов. Освоив передачу параметров в слоты, вы сможете создавать по-настоящему переиспользуемые UI-компоненты.

    Для более сложных сценариев рассмотрите использование Composition API вместе со слотами.

    Vue 3 Composition API + Slots: Сложные сценарии использования

    В сочетании с Composition API слоты Vue 3 раскрывают свою настоящую мощь. Рассмотрим продвинутые паттерны для сложных UI-компонентов.

    1. Динамические слоты с реактивными параметрами

    <!-- DynamicTable.vue -->
    <script setup>
    import { ref, computed } from 'vue'
    
    const props = defineProps(['data'])
    const sortDirection = ref('asc')
    
    const sortedData = computed(() => {
      return [...props.data].sort((a, b) => {
        return sortDirection.value === 'asc' 
          ? a.value - b.value 
          : b.value - a.value
      })
    })
    </script>
    
    <template>
      <table>
        <slot name="header" :toggleSort="() => sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'"></slot>
        <tr v-for="(item, index) in sortedData" :key="index">
          <slot :item="item" :index="index"/>
        </tr>
      </table>
    </template>

    Применение: Таблицы с сортировкой, где логика инкапсулирована в компоненте, а рендеринг контролируется через слоты.

    2. Состояние модальных окон

    <!-- useModal.js -->
    import { ref } from 'vue'
    
    export function useModal() {
      const isOpen = ref(false)
      
      const open = () => isOpen.value = true
      const close = () => isOpen.value = false
      
      return { isOpen, open, close }
    }
    
    <!-- ModalComponent.vue -->
    <script setup>
    import { useModal } from './useModal'
    const modal = useModal()
    </script>
    
    <template>
      <slot :open="modal.open" :close="modal.close" :isOpen="modal.isOpen"/>
    </template>

    Использование:

    <ModalComponent v-slot="{ open, close, isOpen }">
      <button @click="open">Открыть</button>
      
      <div v-if="isOpen" class="modal">
        <slot name="content"/>
        <button @click="close">Закрыть</button>
      </div>
    </ModalComponent>

    3. Композиционные слоты для форм

    <!-- useFormField.js -->
    import { ref, computed } from 'vue'
    
    export function useFormField(initialValue) {
      const value = ref(initialValue)
      const isValid = computed(() => value.value.length > 0)
      
      return { value, isValid }
    }
    
    <!-- FormField.vue -->
    <script setup>
    import { useFormField } from './useFormField'
    
    const props = defineProps(['initialValue'])
    const field = useFormField(props.initialValue)
    </script>
    
    <template>
      <div class="form-field" :class="{ invalid: !field.isValid }">
        <slot :value="field.value" :isValid="field.isValid"/>
      </div>
    </template>

    Использование с кастомным input:

    <FormField initialValue="" v-slot="{ value, isValid }">
      <input 
        v-model="value"
        :class="{ error: !isValid }"
        placeholder="Введите текст"
      >
      <span v-if="!isValid">Поле обязательно</span>
    </FormField>

    4. Сложные компоненты данных

    <!-- DataFetcher.vue -->
    <script setup>
    import { ref, onMounted } from 'vue'
    
    const props = defineProps(['url'])
    const data = ref(null)
    const error = ref(null)
    const isLoading = ref(false)
    
    onMounted(async () => {
      isLoading.value = true
      try {
        const response = await fetch(props.url)
        data.value = await response.json()
      } catch (err) {
        error.value = err
      } finally {
        isLoading.value = false
      }
    })
    </script>
    
    <template>
      <slot 
        :data="data" 
        :error="error" 
        :isLoading="isLoading"
        :reload="onMounted"
      />
    </template>

    Использование:

    <DataFetcher url="/api/users" v-slot="{ data, isLoading }">
      <div v-if="isLoading">Загрузка...</div>
      <UserList v-else :users="data" />
    </DataFetcher>

    5. Продвинутый пример: Компонент вкладок

    <!-- TabsContainer.vue -->
    <script setup>
    import { ref } from 'vue'
    
    const activeTab = ref(0)
    const tabs = ref([])
    
    const registerTab = (title) => {
      const id = tabs.value.length
      tabs.value.push({ id, title })
      return id
    }
    
    const setActiveTab = (id) => {
      activeTab.value = id
    }
    </script>
    
    <template>
      <div class="tabs-container">
        <div class="tabs-header">
          <button 
            v-for="tab in tabs" 
            @click="setActiveTab(tab.id)"
            :class="{ active: activeTab === tab.id }"
          >
            {{ tab.title }}
          </button>
        </div>
        
        <div class="tabs-content">
          <slot :activeTab="activeTab"/>
        </div>
      </div>
    </template>

    Использование:

    <TabsContainer v-slot="{ activeTab }">
      <template #default>
        <Tab :register="registerTab" title="Профиль">
          <div v-if="activeTab === 0">...</div>
        </Tab>
        
        <Tab :register="registerTab" title="Настройки">
          <div v-if="activeTab === 1">...</div>
        </Tab>
      </template>
    </TabsContainer>

    Заключение

    Сочетание Composition API и слотов позволяет создавать:

    • Полностью инкапсулированную логику компонентов
    • Максимально гибкие API для переиспользуемых компонентов
    • Сложные stateful-компоненты с простым интерфейсом
    • Легко тестируемые решения (бизнес-логика отделена от рендеринга)