← Курс/v-model: двусторонняя привязка данных#209 из 257+20 XP

v-model: двусторонняя привязка данных

Что такое v-model

v-model создаёт **двустороннюю привязку** между элементом формы и реактивной переменной. Изменение в поле ввода сразу отражается в переменной, и наоборот — изменение переменной обновляет поле.

v-model — это синтаксический сахар над :value и @input:

<!-- Полная запись -->
<input :value="text" @input="text = $event.target.value">

<!-- Сокращение через v-model -->
<input v-model="text">

v-model с разными элементами

Input и Textarea

<input v-model="username" type="text" placeholder="Имя пользователя">
<textarea v-model="bio" placeholder="О себе"></textarea>

Checkbox

Одиночный чекбокс привязывается к булевой переменной:

<input v-model="agreed" type="checkbox">
<label>Согласен с условиями</label>
<!-- agreed: true / false -->

Группа чекбоксов привязывается к массиву:

<input v-model="selected" type="checkbox" value="vue"> Vue
<input v-model="selected" type="checkbox" value="react"> React
<input v-model="selected" type="checkbox" value="angular"> Angular
<!-- selected: [] / ['vue'] / ['vue', 'react'] и т.д. -->

Radio-кнопки

<input v-model="gender" type="radio" value="male"> Мужской
<input v-model="gender" type="radio" value="female"> Женский
<!-- gender: 'male' / 'female' -->

Select

<select v-model="city">
  <option value="">Выберите город</option>
  <option value="moscow">Москва</option>
  <option value="spb">Санкт-Петербург</option>
</select>

<!-- Multiple select -->
<select v-model="tags" multiple>
  <option v-for="tag in allTags" :key="tag" :value="tag">{{ tag }}</option>
</select>

Модификаторы v-model

.lazy

По умолчанию v-model синхронизируется на каждое событие input. Модификатор .lazy переключает на событие change (после потери фокуса или нажатия Enter):

<!-- Синхронизируется только после потери фокуса -->
<input v-model.lazy="message">

.number

Автоматически преобразует введённое значение в число через parseFloat:

<input v-model.number="age" type="number">
<!-- age всегда будет числом, не строкой -->

.trim

Автоматически обрезает пробелы в начале и конце строки:

<input v-model.trim="username">
<!-- '  Алексей  ' -> 'Алексей' -->

Модификаторы можно комбинировать:

<input v-model.lazy.trim="search">

Полный пример формы

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model.trim="form.name" placeholder="Имя">
    <input v-model.number="form.age" type="number" placeholder="Возраст">
    <input v-model.lazy="form.email" type="email" placeholder="Email">
    <button type="submit">Отправить</button>
  </form>
</template>

<script setup>
const form = reactive({ name: '', age: 0, email: '' })

function handleSubmit() {
  console.log(form)
}
</script>

Примеры

Эмуляция v-model и его модификаторов .trim, .number, .lazy через обработчики событий

// v-model — это синтаксический сахар над value + событием.
// Эмулируем поведение v-model и его модификаторов на чистом JS.

// Базовый v-model: синхронизация при каждом вводе
function createVModel(initialValue) {
  let value = initialValue
  const listeners = []

  return {
    // getter (как :value)
    get value() { return value },
    // setter (как @input обработчик)
    set value(newVal) {
      value = newVal
      listeners.forEach(fn => fn(value))
    },
    onChange(fn) { listeners.push(fn) },
    // Имитируем ввод пользователя
    simulateInput(rawInput) {
      console.log(`  Пользователь ввёл: "${rawInput}"`)
      this.value = rawInput
    }
  }
}

// --- Обычный v-model ---
console.log('=== v-model (без модификаторов) ===')
const name = createVModel('')
name.onChange(v => console.log(`  Значение обновлено: "${v}"`))
name.simulateInput('А')
name.simulateInput('Ал')
name.simulateInput('Алексей')

// --- v-model.trim ---
console.log('\n=== v-model.trim ===')
function createVModelTrim(initialValue) {
  const model = createVModel(initialValue)
  const originalInput = model.simulateInput.bind(model)
  model.simulateInput = function(rawInput) {
    console.log(`  Пользователь ввёл: "${rawInput}"`)
    model.value = rawInput.trim()  // .trim применяется перед сохранением
  }
  return model
}

const username = createVModelTrim('')
username.onChange(v => console.log(`  Значение (trimmed): "${v}"`))
username.simulateInput('  Борис  ')
username.simulateInput('  Виктор')

// --- v-model.number ---
console.log('\n=== v-model.number ===')
function createVModelNumber(initialValue) {
  const model = createVModel(initialValue)
  model.simulateInput = function(rawInput) {
    console.log(`  Пользователь ввёл: "${rawInput}" (тип: ${typeof rawInput})`)
    const parsed = parseFloat(rawInput)
    model.value = isNaN(parsed) ? rawInput : parsed
  }
  return model
}

const age = createVModelNumber(0)
age.onChange(v => console.log(`  Значение: ${v} (тип: ${typeof v})`))
age.simulateInput('25')    // строка -> число 25
age.simulateInput('abc')   // не число -> остаётся строкой

// --- v-model.lazy ---
console.log('\n=== v-model.lazy (обновление только после blur/change) ===')
function createVModelLazy(initialValue) {
  const model = createVModel(initialValue)
  let pendingValue = initialValue

  model.simulateInput = function(rawInput) {
    pendingValue = rawInput
    console.log(`  Печатает: "${rawInput}" (модель НЕ обновляется)`)
  }
  model.simulateBlur = function() {
    console.log(`  Потерял фокус — обновляем модель: "${pendingValue}"`)
    model.value = pendingValue
  }
  return model
}

const message = createVModelLazy('')
message.onChange(v => console.log(`  Модель обновлена: "${v}"`))
message.simulateInput('П')
message.simulateInput('Пр')
message.simulateInput('Привет')
message.simulateBlur()  // только здесь обновляется