← Vue 3/v-model в компонентах#331 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: React или VueПрактика: Vue setТермин: Composition APIТема: Vue 3 и Composition API
← НазадДалее →

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)
    

    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)
    

    Задание

    Создай компонент `CustomInput` — кастомное поле ввода с v-model. Компонент принимает `modelValue` и эмитирует `update:modelValue`. Родительское приложение использует v-model для связи с компонентом и отображает введённое значение в реальном времени.

    Подсказка

    Компонент принимает `props: ["modelValue"]` и эмитирует `emits: ["update:modelValue"]`. В шаблоне компонента: `:value="modelValue"` и `@input="$emit("update:modelValue", $event.target.value)"`. В родителе: `v-model="name"` — Vue автоматически разворачивает это в `:modelValue="name"` и `@update:modelValue="name = $event"`.

    Загружаем среду выполнения...
    Загружаем AI-помощника...