Vue: двустороннее связывание Печать
Добавил(а) microsin   

Разберем двустороннее связывание (two-way binding) на примере синхронизации хранимого в переменной значения и его визуального представления на метке и поле ввода.


Предположим, что у нас есть метка, на которой отображается имя города и соответствующая переменная city. Также есть поле ввода, где можно менять значение этой переменной. Это называется двусторонним связыванием, и его можно реализовать несколькими способами.

Для тестирования two-way binding нам понадобится для примера простой тестовый проект.

1. Создайте проект командами:

npm create vue@latest
cd two-way-bind-example/

npm install
npm run dev

Здесь "two-way-bind-example" это имя проекта, которое было указано при его создании. Оно совпадает с именем корневого каталога проекта.

В результате получится компонент приложения App.vue, удалите из него все лишее:

// App.vue
< script setup>< /script>
< template>< /template>
< style scoped>< /style>

2. Добавьте простейший компонент Input.vue и поместите его в папку src/components:

// Input.vue
< script setup>< /script>
< template> < input /> < /template>
< style scoped>< /style>

3. Добавьте в App.vue использование компонента Input.vue:

// App.vue
< script setup>
import Input from './components/Input.vue'; < /script>
< template> < Input placeholder="Введите город" /> < /template>

4. Добавьте в App.vue переменную city:

// App.vue
< script setup>
import Input from './components/Input.vue';

let city="Москва" < /script>

Итак, у нас есть компонент App.vue и компонент ввода Input.vue. Необходимо реализовать двустороннюю связь (two-way binding) значения этой переменной с тем, что вводится в Input.

[ref, defineEmits и defineProps]

Чтобы из компонента Input сообщить наверх, что произошли какие-то изменения при вводе, необходимо применить defineEmits. Передаваемое значение обычно обозначают в форме "событие:значение" следующим образом:

// Input.vue
< script setup>
const emit = defineEmits(['update:value']); < /script>

Эта запись говорит, что у нас есть некоторое значение (value), которое мы теперь обновили (update).

Для генерации события при вводе необходимо в шаблоне компонента Input сделать такую запись:

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

Здесь @input это событие, которое говорит о том, что произошел ввод текста. "emit('update:value', $event.target.value)" обозначает функцию, которая передает введенное значение.

Теперь при каждом нажатии кнопки будет передаваться событие о вводимом значении в родительский компонент App.vue.

Чтобы прочитать передаваемое значение, в родительском компоненте App.vue это значение надо принять:

// App.vue
< script setup>
import Input from './components/Input.vue';

let city="Москва"

function updateCity(newCity) {
console.log(newCity); } < /script>
< template> < Input placeholder="Введите город" @update:value="updateCity"/> < /template>
< style scoped>< /style>

Здесь @update:value="updateCity" обозначает прием генерируемого компонентом Input события, а updateCity это функция обработчика события. В ней вставлен вызов console.log, чтобы можно было в консоли браузера видеть вводимые изменения.

Vue two way binding fig01

Чтобы вывести вводимое значение в родительском компоненте App.vue, необходимо с помощью ref обозначить его состояние:

// App.vue
< script setup>
import { ref } from 'vue';
import Input from './components/Input.vue';

let city=ref("")

function updateCity(newCity) {
console.log(newCity);
city.value = newCity; } < /script>

Вывод значения состояния:

// App.vue
< template>
   {{ city }}
   < Input placeholder="Введите город"
     @update:value="updateCity"/>
< /template>

Вводимое значение будет сразу появляться на форме родительского компонента.

Теперь надо задать какое-то значение по умолчанию, например "Москва", и обеспечить его появление в поле ввода как начальное значение. Это и будет "two-way binding".

// App.vue
let city=ref("Москва")

Для этого в компоненте Input необходимо определить defineProps:

// Input.vue
< script setup>
const props = defineProps(["value"])
const emit = defineEmits(['update:value']); < /script>

Созданное свойство props необходимо привязать с помощью v-bind к полю ввода в шаблоне:

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

С одной стороны, мы теперь при каждом вводе эмитим событие @input и передаем наверх вводимое значение, а с другой стороны мы получаем props и кладем его в поле ввода как значение по умолчанию.

Для этого в родительском компоненте App.vue также надо обновить использование компонента Input:

// App.vue
< template>
   {{ city }}
   < Input placeholder="Введите город"
     :value="city"
     @update:value="updateCity" />
< /template>

Теперь мы в App.vue слушаем событие ввода (@update:value="updateCity"), и одновременно передаем в поле ввода некоторое хранимое значение (:value="city"). Это и есть так называемое "two-way binding", когда мы связали двусторонней связью не только чтение, но и запись какого-то сохраненного значения (let city=ref("Москва")). Мы можем как задавать значение, так и обновлять его.

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

[v-model]

Это более простой для использования способ двустороннего связывания. В шаблоне App.vue исправьте атрибуты компонента Input, заменив :value="city" и @update:value="updateCity" на один атрибут v-model="". Функцию updateCity теперь также можно удалить:

// App.vue
< script setup>
import { ref } from 'vue';
import Input from './components/Input.vue';

let city=ref("Москва") < /script>
< template> {{ city }} < Input placeholder="Введите город" v-model="city" /> < /template>

Также надо модифицировать компонент Input: удалить defineProps и defineEmits, и добавить определение модели с помощью defineModel.

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

После этого в шаблоне можно использовать model в качестве привязанного через v-bind значения, и событие @input также определить не через emit, а через model:

// Input.vue
< template>
    < input :value="model"
       @input="model=$event.target.value" />
< /template>

Это все что нужно для организации двусторонней связи.

Также v-model может автоматически связываться с поведением стандартных элементов интерфейса ввода: input, textarea, checkbox, radio, selected, multiselect, все это будет работать. Т. е. можно заменить :v6alue= и @input= на единственный атрибут v-model="model", код становится еще более лаконичным

// Input.vue
< template>
    < input v-model="model" />
< /template>

1. Для defineModel() можно и нужно в качестве параметра задать тип значения, например:

< script setup>
const model = defineModel({ type: String }); < /script>

2. Второй параметр, который мы можем передать в defineModel, это required:

const data = defineModel({ type: String, required: true });

Этот параметр обязывает использовать v-model, и полезен для случаев, когда разрабочик забыл его указать. В этом случае в консоль браузера будет выводиться предупреждение: [Vue warn]: Missing required prop: "modelValue".

3. Третьим параметром в defineModel можно передать значение по умолчанию, если мы его не передали, например:

const data = defineModel({ type: String, required: true, default: "Превед" });

[Именованные v-model]

4. Существует возможность использования именованных моделей, для этого в defineModel можно передать её произвольное пользовательское имя, например:

const data = defineModel({ type: String, required: true, default: "Превед" });
const mydata = defineModel({"mymodel", type: String });

Здесь "mymodel" задает имя модели. Теперь можно указывать в атрибутах несколько моделей с помощью синтаксиса v-model:имя_модели, например (здесь в vmodel:mymodel="city" вместо city может быть другая переменная):

< Input v-model="city" vmodel:mymodel="city" placeholder="Введите город" />

Тем самым мы можем задавать несколько моделей в нашем компоненте, просто задавая моделям понятные имена.

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

const handleEnter = () => { console.log('Текст из input:', model.value) } < /script>
< template> < input v-model="model" @keyup.enter="handleEnter" /> < /template>

В Vue двустороннее связывание с меткой (label) можно организовать несколькими способами. Вот основные подходы:

1. Использование v-model для двустороннего связывания

< template>
  < div>
    < !-- Метка, отображающая значение -->
    < p>Значение: {{ inputValue }}< /p>
< !-- Поле ввода с двусторонним связыванием --> < input v-model="inputValue" type="text" placeholder="Введите текст"> < /div> < /template>
< script>
export default {
data() {
return {
inputValue: '' // Начальное значение
}
} } < /script>

2. Связывание с конкретным элементом через `for` и `id`

< template>
  < div>
    < !-- Метка связана с полем ввода через id -->
    < label for="userInput">Введите имя:< /label>
< !-- Поле ввода --> < input id="userInput" v-model="userName" type="text" placeholder="Ваше имя" >
< !-- Отображение значения --> < p>Привет, {{ userName || 'гость' }}!< /p> < /div> < /template>
< script>
export default {
data() {
return {
userName: ''
}
} } < /script>

3. Кастомный компонент с более сложной логикой

< template>
  < div>
    
    < custom-label
      :value="textValue"
      label="Текст:"
      @update="textValue = $event"
    />
  < /div>
< /template>
< script>
export default {
data() {
return {
textValue: ''
}
},
components: {
'custom-label': {
template: `
< div class="custom-label">
< label>{{ label }} {{ value }}< /label>
< input
:value="value"
@input="$emit('update', $event.target.value)"
type="text"
>
< /div>
`,
props: ['value', 'label']
}
} } < /script>

4. Использование вычисляемых свойств для форматирования

< template>
  < div>
    < !-- Форматированное отображение -->
    < p>Отформатированное значение: {{ formattedValue }}< /p>
< !-- Поле ввода --> < input v-model="rawValue" type="text" placeholder="Введите число" >
< !-- Дополнительная метка --> < div :class="valueStatus"> Статус: {{ valueStatus === 'valid' ? '✓' : 'Введите значение' }} < /div> < /div> < /template>
< script>
export default {
data() {
return {
rawValue: ''
}
},
computed: {
// Форматирование значения для отображения
formattedValue() {
if (!this.rawValue) return 'не указано'
return this.rawValue.toUpperCase()
},

// Проверка валидности
valueStatus() {
return this.rawValue.trim().length > 0 ? 'valid' : 'empty'
}
} } < /script>
< style> .valid { color: green; } .empty { color: gray; } < /style>

5. Composition API (Vue 3)

< template>
  < div>
    < label>Счётчик: {{ count }}< /label>
    < input
      v-model.number="count"
      type="range"
      min="0"
      max="100"
    >
    < p>Текущее значение: {{ count }}< /p>
  < /div>
< /template>
< script setup>
import { ref, watch } from 'vue'

const count = ref(50)

// Можно добавить реактивность на изменения watch(count, (newValue) => {
console.log('Новое значение:', newValue) }) < /script>

Ключевые моменты:

1. v-model - основная директива для двустороннего связывания.
2. Реактивность - Vue автоматически обновляет DOM при изменении данных.
3. Методы и вычисляемые свойства - для дополнительной обработки значений.
4. События - можно использовать @input и @change для обработки изменений.

Выбор подхода зависит от конкретной задачи:

- Простое отображение: `v-model` + интерполяция `{{ }}`.
- Сложная логика: вычисляемые свойства.
- Кастомные компоненты: для повторного использования логики.

[Ссылки]

1Vue 3, быстрый старт.
2Vue: реактивность.
3Директивы шаблона Vue.