Перейти к содержанию

Директива v-model

Пример использования

v-model можно использовать в компоненте для реализации двусторонней привязки.

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

vue
<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>v-model, связанная с родительским компонентом: {{ model }}</div>
  <button @click="update">Увеличить</button>
</template>

Родитель может связать значение с v-model:

template
<!-- Parent.vue -->
<Child v-model="countModel" />

Значение, возвращаемое функцией defineModel(), является ссылкой. К нему можно обращаться и изменять, как к любому другому ref, за исключением того, что он действует как двусторонняя привязка между родительским значением и локальным:

  • Его .value синхронизируется со значением, связанным с родительской v-model;
  • Когда оно изменяется дочерним элементом, это приводит к обновлению родительского связанного значения.

Это означает, что вы также можете привязать эту ссылку к нативному элементу ввода с помощью v-model, что позволяет легко обернуть нативные элементы ввода, обеспечивая при этом использование v-model:

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

<template>
  <input v-model="model" />
</template>

Попробовать в Песочнице

Под капотом

defineModel — это удобный макрос. Компилятор расширит его до следующего вида:

  • Свойство с именем modelValue, с которым синхронизируется значение локальной ссылки;
  • Событие с именем update:modelValue, которое испускается при изменении значения локальной ссылки.

Вот как можно реализовать тот же дочерний компонент, показанный выше, до версии 3.4:

vue
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

Тогда v-model="foo" в родительском компоненте будет скомпилировано в:

template
<!-- Parent.vue -->
<Child :modelValue="foo" @update:modelValue="($event) => (foo = $event)" />

Как видите, он более многословен. Однако полезно понимать, что происходит под капотом.

Поскольку defineModel объявляет prop, вы можете объявить опции базового prop, передав его в defineModel:

js
// делаем v-model обязательным
const model = defineModel({ required: true })

// предоставляем значение по умолчанию
const model = defineModel({ default: 0 })

Предупреждение

Если у вас есть значение default для свойства defineModel и вы не предоставляете никакого значения для этого свойства из родительского компонента, это может привести к рассинхронизации между родительским и дочерним компонентами. В примере ниже родительский myRef не определен, а дочерний model равен 1:

Дочерний компонент:

js
const model = defineModel({ default: 1 })

Родительский компонент:

js
const myRef = ref()
html
<Child v-model="myRef"></Child>

Сначала давайте рассмотрим, как v-model используется в нативном элементе:

template
<input v-model="searchText" />

Под капотом компилятор шаблонов расширяет v-model до более подробного эквивалента для нас. Таким образом, приведённый выше код делает то же самое, что и следующий:

template
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

При использовании в компоненте v-model вместо этого расширяется до этого:

template
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

Для того чтобы это действительно работало, компонент <CustomInput> должен выполнять две вещи:

  1. Привяжите атрибут value собственного элемента <input> к свойству modelValue
  2. Когда срабатывает собственное событие input, создайте пользовательское событие update:modelValue с новым значением

Вот это в действии:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

Теперь v-model должен отлично работать с этим компонентом:

template
<CustomInput v-model="searchText" />

Попробовать в Песочнице

Другой способ реализации v-model в этом компоненте — использовать записываемое свойство computed с геттером и сеттером. Метод get должен возвращать свойство modelValue, а метод set должен вызывать соответствующее событие:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>

Аргументы v-model

v-model на компоненте также может принимать аргумент:

template
<MyComponent v-model:title="bookTitle" />

В дочернем компоненте мы можем поддержать соответствующий аргумент, передав строку в defineModel() в качестве его первого аргумента:

vue
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>

<template>
  <input type="text" v-model="title" />
</template>

Попробовать в Песочнице

Если также необходимы параметры пропсов, их следует передать после имени модели:

js
const title = defineModel('title', { required: true })
Использование до 3.4
vue
<!-- MyComponent.vue -->
<script setup>
defineProps({
  title: {
    required: true
  }
})
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

Попробовать в Песочнице

В этом случае вместо стандартного свойства modelValue и события update:modelValue дочерний компонент должен ожидать свойства title и вызывать событие update:title для обновления родительского значения:

vue
<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['update:title']
}
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

Попробовать в Песочнице

Несколько привязок v-model

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

Каждая v-model будет синхронизироваться с отдельным параметром, без необходимости использования дополнительных опций в компоненте:

template
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
vue
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

Попробовать в Песочнице

Использование до 3.4
vue
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

Попробовать в Песочнице

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  />
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  />
</template>

Попробовать в Песочнице

Обработка модификаторов v-model

Когда мы изучали привязки ввода формы, мы увидели, что у v-model есть встроенные модификаторы.trim, .number и .lazy. В некоторых случаях вы также можете захотеть, чтобы v-model вашего компонента ввода поддерживала пользовательские модификаторы.

Давайте создадим пример пользовательского модификатора capitalize, который выделит первую букву строки, предоставленной привязкой v-model:

template
<MyComponent v-model.capitalize="myText" />

Модификаторы, добавленные в компонент v-model, могут быть доступны в дочернем компоненте путём деструктуризации возвращаемого значения defineModel() следующим образом:

vue
<script setup>
const [model, modifiers] = defineModel()

console.log(modifiers) // { capitalize: true }
</script>

<template>
  <input type="text" v-model="model" />
</template>

Чтобы условно настроить чтение/запись значения на основе модификаторов, мы можем передать get и set опции в defineModel(). Эти две опции получают значение при get/set модели ref и должны возвращать преобразованное значение. Вот как мы можем использовать опцию set для реализации модификатора capitalize:

vue
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

Попробовать в Песочнице

Использование до 3.4
vue
<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="props.modelValue" @input="emitValue" />
</template>

Попробовать в Песочнице

Модификаторы, добавленные к компоненту v-model, будут предоставлены компоненту через свойство modelModifiers. В приведённом ниже примере мы создали компонент, содержащий параметр modelModifiers, который по умолчанию содержит пустой объект:

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
}
</script>

<template>
  <input
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

Заметьте, что свойство компонента modelModifiers содержит capitalize и его значение true — благодаря тому, что оно установлено на привязке v-model v-model.capitalize="myText".

Теперь, когда у нас настроен параметр, мы можем проверить ключи объекта modelModifiers и написать обработчик для изменения выдаваемого значения. В приведённом ниже коде мы будем выделять строку заглавными буквами всякий раз, когда элемент <input /> будет вызывать событие input.

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  }
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

Попробовать в Песочнице

Модификаторы для v-model с аргументами

Для привязок v-model, содержащих аргументы и модификаторы, сгенерированное имя параметра будет arg + "Modifiers". Например:

template
<MyComponent v-model:title.capitalize="myText">

Соответствующие декларации должны быть:

js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}

Вот ещё один пример использования модификаторов с несколькими v-model с разными аргументами:

template
<UserName
  v-model:first-name.capitalize="first"
  v-model:last-name.uppercase="last"
/>
vue
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')

console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
Использование до 3.4
vue
<script setup>
const props = defineProps({
firstName: String,
lastName: String,
firstNameModifiers: { default: () => ({}) },
lastNameModifiers: { default: () => ({}) }
})
defineEmits(['update:firstName', 'update:lastName'])

console.log(props.firstNameModifiers) // { capitalize: true }
console.log(props.lastNameModifiers) // { uppercase: true }
</script>
vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String,
    firstNameModifiers: {
      default: () => ({})
    },
    lastNameModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:firstName', 'update:lastName'],
  created() {
    console.log(this.firstNameModifiers) // { capitalize: true }
    console.log(this.lastNameModifiers) // { uppercase: true }
  }
}
</script>
Директива v-model