9. Props и Emits
Props и Emits — общение компонентов 📡
Заголовок раздела «Props и Emits — общение компонентов 📡»Компоненты — это изолированные острова. Как острова общаются между собой? Через мосты! В Vue: props — данные текут СВЕРХУ ВНИЗ от родителя к ребёнку, emits — события летят СНИЗУ ВВЕРХ от ребёнка к родителю. Этот однонаправленный поток делает приложение предсказуемым и лёгким для дебага! 🌊
Однонаправленный поток данных 🌊
Заголовок раздела «Однонаправленный поток данных 🌊»Родитель │ props (данные вниз ⬇️) ▼Ребёнок │ emit (события вверх ⬆️) ▼Родитель
Никогда напрямую НЕ меняй props!props.value = 'новое' — ❌ нарушение однонаправленного потока!defineProps — объявление props 📥
Заголовок раздела «defineProps — объявление props 📥»Generic синтаксис (рекомендуется в Vue 3)
Заголовок раздела «Generic синтаксис (рекомендуется в Vue 3)»<script setup lang="ts">// Способ 1: Простой интерфейсconst props = defineProps<{ title: string count: number isVisible?: boolean // ? = опциональный items: string[] user: { id: number name: string email: string }}>()
// Способ 2: Импортированный интерфейс (Vue 3.3+)import type { User, Product } from '@/types'
const props = defineProps<{ user: User products: Product[] selectedId?: number}>()</script>withDefaults — значения по умолчанию
Заголовок раздела «withDefaults — значения по умолчанию»<script setup lang="ts">import { withDefaults } from 'vue'
interface ButtonProps { label: string variant?: 'primary' | 'secondary' | 'danger' | 'ghost' size?: 'sm' | 'md' | 'lg' disabled?: boolean loading?: boolean icon?: string}
const props = withDefaults(defineProps<ButtonProps>(), { variant: 'primary', // строка size: 'md', // строка disabled: false, // boolean loading: false, // boolean icon: undefined // явно undefined
// ⚠️ Объекты и массивы — ВСЕГДА фабрика! // items: () => [], // config: () => ({ timeout: 3000 })})
// TypeScript знает точные типы (без undefined):// props.variant — 'primary' | 'secondary' | 'danger' | 'ghost'// props.size — 'sm' | 'md' | 'lg'</script>Runtime синтаксис (без TypeScript)
Заголовок раздела «Runtime синтаксис (без TypeScript)»// Для проектов без TypeScript или с runtime валидациейconst props = defineProps({ // Простой тип title: String,
// Несколько типов id: [String, Number],
// С опциями user: { type: Object, required: true },
count: { type: Number, default: 0, validator(value) { return value >= 0 // только неотрицательные числа } },
status: { type: String, default: 'active', validator(value) { return ['active', 'inactive', 'pending'].includes(value) } }})Работа с props в шаблоне и скрипте
Заголовок раздела «Работа с props в шаблоне и скрипте»<script setup lang="ts">import { computed, toRefs } from 'vue'
const props = defineProps<{ firstName: string lastName: string age: number}>()
// ✅ Читаем через props.xxxconsole.log(props.firstName)
// ✅ toRefs для деструктуризации с реактивностьюconst { firstName, lastName } = toRefs(props)
// ✅ Вычисляем на основе propsconst fullName = computed(() => `${props.firstName} ${props.lastName}`)const isAdult = computed(() => props.age >= 18)
// ❌ НИКОГДА не изменяй props!// props.firstName = 'Иван' // Warning + игнорируется Vue
// ✅ Если нужно изменяемое локальное состояние — копируй!import { ref } from 'vue'const localCount = ref(props.age) // локальная копия</script>
<template> <!-- Props доступны в шаблоне напрямую --> <h2>{{ firstName }} {{ lastName }}</h2> <p>Возраст: {{ age }}</p> <p>{{ fullName }} — {{ isAdult ? 'совершеннолетний' : 'несовершеннолетний' }}</p></template>Prop drilling и проброс атрибутов 🎁
Заголовок раздела «Prop drilling и проброс атрибутов 🎁»<!-- Родитель --><template> <!-- class, id, data-* автоматически передаются в корневой элемент дочернего --> <UserCard :user="user" class="featured" data-testid="user-card-main" /></template>
<!-- UserCard.vue — класс 'featured' прилетит в корневой div --><template> <div class="user-card"> <!-- ← получит class="user-card featured" --> <p>{{ user.name }}</p> </div></template>
<!-- Отключить наследование + ручное управление --><script setup lang="ts">import { useAttrs } from 'vue'defineOptions({ inheritAttrs: false })const attrs = useAttrs()</script>
<template> <div class="wrapper"> <!-- Передаём attrs только на нужный элемент --> <input v-bind="attrs" /> </div></template>defineEmits — объявление событий 📤
Заголовок раздела «defineEmits — объявление событий 📤»<script setup lang="ts">// Современный callable синтаксис (Vue 3.3+) — рекомендуетсяconst emit = defineEmits<{ // событие без данных click: []
// событие с данными (именованные параметры) change: [value: string] update: [id: number, data: Partial<User>] delete: [userId: number]
// kebab-case событие 'item-selected': [item: MenuItem] 'update:modelValue': [value: string] // для v-model!}>()
// Использованиеconst handleSave = () => { emit('click') emit('change', newValue) emit('update', user.id, { name: 'Новое имя' }) emit('update:modelValue', inputValue.value)}</script>v-model с кастомными компонентами 🔄
Заголовок раздела «v-model с кастомными компонентами 🔄»v-model — это синтаксический сахар над prop + emit:
<!-- Что пишем --><MyInput v-model="searchText" />
<!-- Что Vue разворачивает в --><MyInput :modelValue="searchText" @update:modelValue="searchText = $event"/>Реализация компонента с v-model
Заголовок раздела «Реализация компонента с v-model»<template> <div class="input-wrapper"> <label v-if="label">{{ label }}</label> <input :value="modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" v-bind="$attrs" /> </div></template>
<script setup lang="ts">defineOptions({ inheritAttrs: false })
defineProps<{ modelValue: string label?: string}>()
const emit = defineEmits<{ 'update:modelValue': [value: string]}>()</script>Множественные v-model (Vue 3!) 🎯
Заголовок раздела «Множественные v-model (Vue 3!) 🎯»<!-- Родитель --><template> <UserForm v-model:firstName="user.firstName" v-model:lastName="user.lastName" v-model:email="user.email" /></template>
<!-- UserForm.vue — несколько v-model --><template> <div> <input :value="firstName" @input="emit('update:firstName', $event.target.value)" /> <input :value="lastName" @input="emit('update:lastName', $event.target.value)" /> <input :value="email" @input="emit('update:email', $event.target.value)" /> </div></template>
<script setup lang="ts">defineProps<{ firstName: string lastName: string email: string}>()
const emit = defineEmits<{ 'update:firstName': [value: string] 'update:lastName': [value: string] 'update:email': [value: string]}>()</script>v-model модификаторы 🔧
Заголовок раздела «v-model модификаторы 🔧»<!-- Встроенные модификаторы --><input v-model.trim="text" /> <!-- обрезать пробелы --><input v-model.number="count" /> <!-- преобразовать в number --><input v-model.lazy="search" /> <!-- обновлять по blur -->
<!-- Кастомный модификатор --><MyInput v-model.capitalize="text" /><!-- MyInput.vue — поддержка кастомного модификатора --><script setup lang="ts">const props = defineProps<{ modelValue: string modelModifiers?: { capitalize?: boolean; trim?: boolean }}>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const handleInput = (event: Event) => { let value = (event.target as HTMLInputElement).value
// Применяем модификаторы if (props.modelModifiers?.capitalize) { value = value.charAt(0).toUpperCase() + value.slice(1) } if (props.modelModifiers?.trim) { value = value.trim() }
emit('update:modelValue', value)}</script>
<template> <input :value="modelValue" @input="handleInput" /></template>Prop валидация — паттерны 🛡️
Заголовок раздела «Prop валидация — паттерны 🛡️»// Полная валидация props (runtime синтаксис)const props = defineProps({ // Валидатор с сообщением age: { type: Number, required: true, validator: (value: number) => { if (value < 0) { console.warn(`age не может быть отрицательным: ${value}`) return false } if (value > 150) { console.warn(`age кажется нереальным: ${value}`) return false } return true } },
// Enum validator status: { type: String, default: 'active', validator: (value: string) => ['active', 'inactive', 'pending', 'banned'].includes(value) },
// Объект с обязательными полями config: { type: Object, default: () => ({ timeout: 3000, retries: 3 }), validator: (value: object) => 'timeout' in value }})Реальный пример: Форма регистрации 📝
Заголовок раздела «Реальный пример: Форма регистрации 📝»<template> <form @submit.prevent="handleSubmit" class="form"> <BaseInput v-model="form.email" label="Email" type="email" :error="errors.email" required />
<BaseInput v-model="form.password" label="Пароль" type="password" :error="errors.password" required />
<BaseCheckbox v-model="form.acceptTerms" label="Принимаю условия использования" :error="errors.acceptTerms" />
<BaseButton type="submit" :loading="isSubmitting" :disabled="!isValid" > Зарегистрироваться </BaseButton> </form></template>
<script setup lang="ts">import { reactive, computed } from 'vue'
const emit = defineEmits<{ success: [userId: number] error: [message: string]}>()
const form = reactive({ email: '', password: '', acceptTerms: false})
const isSubmitting = ref(false)
const errors = computed(() => { const e: Record<string, string> = {} if (!form.email.includes('@')) e.email = 'Некорректный email' if (form.password.length < 8) e.password = 'Минимум 8 символов' if (!form.acceptTerms) e.acceptTerms = 'Необходимо принять условия' return e})
const isValid = computed(() => Object.keys(errors.value).length === 0)
const handleSubmit = async () => { if (!isValid.value) return
isSubmitting.value = true try { const { userId } = await register(form) emit('success', userId) } catch (e) { emit('error', e.message) } finally { isSubmitting.value = false }}</script>Резюме 📡
Заголовок раздела «Резюме 📡»Props и Emits:
defineProps<T>()— типизированные входные данныеwithDefaults()— значения по умолчанию для generic props- Props readonly! — никогда не изменяй props напрямую
defineEmits<T>()— типизированные исходящие событияv-model—modelValueprop +update:modelValueemit- Несколько v-model —
v-model:firstName,v-model:lastName - Кастомные модификаторы —
modelModifiersprop - Однонаправленный поток — props вниз ⬇️, events вверх ⬆️
Следующий урок — Слоты! 🎰