← Курс/v-model в компонентах#225 из 257+30 XP

v-model в компонентах

Что такое v-model на компоненте

v-model на компоненте — это синтаксический сахар для передачи значения и подписки на его изменения. Когда вы пишете:

<MyInput v-model="username" />

Vue разворачивает это в:

<MyInput :modelValue="username" @update:modelValue="username = $event" />

defineModel() — Vue 3.4+

Начиная с Vue 3.4 рекомендуется использовать макрос defineModel():

// MyInput.vue — <script setup>
const model = defineModel()

// model.value — текущее значение
// Запись в model.value автоматически эмитирует update:modelValue
<template>
  <input :value="model" @input="model = $event.target.value" />
</template>

С типизацией и опциями:

const model = defineModel({ type: String, default: '' })
// или с TypeScript:
const model = defineModel<string>({ required: true })

Старый паттерн: emit update:modelValue

До Vue 3.4 (и для совместимости) использовался явный паттерн:

// MyInput.vue
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])

// При изменении:
emit('update:modelValue', newValue)
<input :value="props.modelValue" @input="emit('update:modelValue', $event.target.value)" />

Несколько v-model

На одном компоненте можно использовать несколько именованных v-model:

<UserForm
  v-model:firstName="user.firstName"
  v-model:lastName="user.lastName"
  v-model:email="user.email"
/>
// UserForm.vue
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
const email = defineModel('email')

Старый паттерн для именованных моделей:

const props = defineProps(['firstName', 'lastName'])
const emit = defineEmits(['update:firstName', 'update:lastName'])

Кастомные модификаторы

v-model поддерживает модификаторы — флаги, изменяющие поведение:

<MyInput v-model.trim.uppercase="text" />
// MyInput.vue
const [model, modifiers] = defineModel({
  set(value) {
    let v = value
    if (modifiers.trim) v = v.trim()
    if (modifiers.uppercase) v = v.toUpperCase()
    return v
  }
})

Или вручную через props:

const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) },
})
// props.modelModifiers.trim === true  →  если .trim передан

Когда использовать v-model в компонентах

  • Поля ввода (input, textarea, select)
  • Переключатели (checkbox, radio)
  • Сложные редакторы с двусторонней синхронизацией
  • Избегайте v-model когда нужна только передача данных сверху вниз — там достаточно простого prop
  • Примеры

    Эмуляция паттерна v-model через класс: двустороннее связывание данных между "родителем" и "дочерним" компонентом

    // Эмулируем механику v-model без Vue:
    // родитель владеет значением, дочерний компонент
    // получает его через prop и сигнализирует об изменениях через emit.
    
    class EventEmitter {
      constructor() { this._listeners = {} }
      on(event, fn) {
        if (!this._listeners[event]) this._listeners[event] = []
        this._listeners[event].push(fn)
      }
      emit(event, value) {
        (this._listeners[event] || []).forEach(fn => fn(value))
      }
    }
    
    // "Дочерний" компонент — аналог <MyInput>
    class MyInput extends EventEmitter {
      constructor(modelValue = '') {
        super()
        this.modelValue = modelValue  // prop сверху
      }
    
      // Метод, аналогичный @input="emit('update:modelValue', ...)"
      userInput(newValue) {
        // Применяем "модификатор" trim
        const processed = this.modifiers?.trim ? newValue.trim() : newValue
        this.emit('update:modelValue', processed)
      }
    
      // Настройка модификаторов (аналог v-model.trim)
      setModifiers(mods) {
        this.modifiers = mods
        return this
      }
    
      // Когда prop обновился сверху
      receiveModelValue(val) {
        this.modelValue = val
        console.log(`  [MyInput] получил новое значение: "${val}"`)
      }
    }
    
    // Именованный v-model — аналог v-model:title
    class UserForm extends EventEmitter {
      constructor({ firstName = '', lastName = '' } = {}) {
        super()
        this.firstName = firstName
        this.lastName = lastName
      }
    
      changeFirstName(val) { this.emit('update:firstName', val) }
      changeLastName(val)  { this.emit('update:lastName',  val)  }
    
      receiveProps({ firstName, lastName }) {
        if (firstName !== undefined) this.firstName = firstName
        if (lastName  !== undefined) this.lastName  = lastName
        console.log(`  [UserForm] props обновлены: ${JSON.stringify({ firstName: this.firstName, lastName: this.lastName })}`)
      }
    }
    
    // "Родительский" компонент
    class Parent {
      constructor() {
        this.state = { text: 'hello', firstName: 'Иван', lastName: 'Иванов' }
      }
    
      // v-model="state.text"  →  :modelValue + @update:modelValue
      bindVModel(component) {
        component.receiveModelValue(this.state.text)
        component.on('update:modelValue', (val) => {
          this.state.text = val
          console.log(`[Parent] state.text обновлён → "${val}"`)
          component.receiveModelValue(val)
        })
      }
    
      // v-model:firstName + v-model:lastName
      bindNamedVModel(form) {
        form.receiveProps({ firstName: this.state.firstName, lastName: this.state.lastName })
        form.on('update:firstName', val => {
          this.state.firstName = val
          console.log(`[Parent] firstName → "${val}"`)
        })
        form.on('update:lastName', val => {
          this.state.lastName = val
          console.log(`[Parent] lastName → "${val}"`)
        })
      }
    }
    
    // === Демо ===
    const parent = new Parent()
    
    console.log('--- v-model с модификатором .trim ---')
    const input = new MyInput().setModifiers({ trim: true })
    parent.bindVModel(input)
    input.userInput('  World  ')  // trim → "World"
    input.userInput('  Vue!  ')   // trim → "Vue!"
    console.log('Финальное state.text:', parent.state.text)
    
    console.log('\n--- Именованные v-model ---')
    const form = new UserForm()
    parent.bindNamedVModel(form)
    form.changeFirstName('Пётр')
    form.changeLastName('Петров')
    console.log('Финальный state:', parent.state.firstName, parent.state.lastName)