Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «V-model у Vue.js». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**v-model** - це директива Vue для двостороннього зв'язування між полем форми і станом компонента. ```vue <input v-model="text" /> <!-- Компілюється в --> <input :value="text" @input="text = $event.target.value" /> <!-- Кастомний компонент --> <CustomInput v-model="text" /> // Дочірній: defineProps(['modelValue']), defineEmits(['update:modelValue']) ``` **Ключове:** синтаксичний цукор над `:value` + `@input`. Для кастомних компонентів: проп `modelValue` і emit `update:modelValue`. Модифікатори: `.lazy`, `.number`, `.trim`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**v-model** - це директива Vue для двостороннього зв'язування (two-way binding) між полями форми і станом компонента через value prop та input event. ## Теорія ### TL;DR - `v-model` компілюється в `:value="x" @input="x = $event.target.value"` під капотом - Працює на нативних полях (`<input>`, `<textarea>`, `<select>`) і кастомних компонентах - Кастомні компоненти: потрібен проп `modelValue` і emit `update:modelValue` - Vue 3 підтримує кілька іменованих v-model на одному компоненті: `v-model:firstName`, `v-model:lastName` - Модифікатори `.lazy`, `.number`, `.trim` закривають найпоширеніші потреби при роботі з інпутами ### Швидкий приклад ```vue <template> <input v-model="message" placeholder="Введіть текст" /> <p>Live: {{ message }}</p> </template> <script setup> import { ref } from 'vue' const message = ref('Hello Vue') // Початкове значення відображається в інпуті </script> ``` Кожен символ в інпуті одразу оновлює `message`. Тег `<p>` ре-рендериться автоматично, бо `message` - реактивний ref. Жодного обробника подій не потрібно. ### Як це компілюється Компілятор шаблонів Vue трансформує `v-model="text"` під час збірки: ```vue <!-- Що пишеш ти --> <input v-model="text" /> <!-- Що генерує компілятор --> <input :value="text" @input="text = $event.target.value" /> ``` Для кастомних компонентів результат інший: ```vue <!-- Що пишеш ти --> <CustomInput v-model="text" /> <!-- Що генерує компілятор --> <CustomInput :modelValue="text" @update:modelValue="text = $event" /> ``` Система реактивності на основі Proxy (Vue 3) відстежує присвоєння і запускає ре-рендер для всього, що читає `text`. ### Коли використовувати - `<input>`, `<textarea>`, `<select>` - використовуй v-model напряму - Кастомні компоненти-інпути - v-model через `modelValue` + `update:modelValue` - Кілька полів на одному компоненті - іменовані v-model типу `v-model:firstName` - Кнопки та інші не-форм елементи - v-model тут не потрібен, використовуй `@click` - Складні форми зі спільним станом між багатьма компонентами - розглянь Pinia ### Модифікатори Три вбудовані модифікатори покривають найпоширеніші задачі обробки вводу: ```vue <input v-model.lazy="text" /> <!-- Оновлюється після blur, не при кожному символі --> <input v-model.number="age" /> <!-- Автоматично перетворює рядок на число --> <input v-model.trim="username" /> <!-- Обрізає пробіли на початку і в кінці --> <!-- Комбінація --> <input v-model.lazy.trim="email" /> ``` `.lazy` перемикає слухач з `@input` на `@change`. Зручно для пошукових полів, де не треба запускати запит при кожному натисканні. ### v-model у кастомних компонентах Батько передає значення, дитина емітить зміни назад. Це весь контракт. ```vue <!-- Батьківський компонент --> <CustomInput v-model="searchText" /> <!-- Дочірній: CustomInput.vue --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script> ``` Більш чистий патерн - computed з getter і setter. Це той підхід, який я найчастіше використовую в реальних компонентах: шаблон залишається простим, а потік даних очевидний. ```vue <!-- CustomInput.vue --> <script setup> import { computed } from 'vue' const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) const value = computed({ get: () => props.modelValue, set: (val) => emit('update:modelValue', val) }) </script> <template> <input v-model="value" /> </template> ``` ### Кілька v-model (Vue 3) Vue 3 прибрав обмеження Vue 2 на один v-model. Кожен `v-model:propName` відповідає пропу `propName` і еміту `update:propName`: ```vue <!-- Батьківський компонент --> <UserName v-model:firstName="firstName" v-model:lastName="lastName" /> <!-- Дочірній: UserName.vue --> <template> <input :value="firstName" @input="$emit('update:firstName', $event.target.value)" placeholder="Ім'я" /> <input :value="lastName" @input="$emit('update:lastName', $event.target.value)" placeholder="Прізвище" /> </template> <script setup> defineProps(['firstName', 'lastName']) defineEmits(['update:firstName', 'update:lastName']) </script> ``` ### Типові помилки **Пряма мутація пропа.** Це найпоширеніша помилка з v-model у компонентах Vue. ```vue <!-- Неправильно: мутація пропа --> <template> <input v-model="modelValue" /> </template> <script setup> defineProps(['modelValue']) // Vue видасть попередження: "Avoid mutating a prop directly" </script> ``` Пропи передаються в одному напрямку. Батько володіє даними, і будь-яка зміна має повернутися через emit. Виправлення через computed: ```vue <!-- Правильно: computed як проксі --> <script setup> import { computed } from 'vue' const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) const value = computed({ get: () => props.modelValue, set: (val) => emit('update:modelValue', val) }) </script> ``` **v-model на contenteditable.** Не дає жодного ефекту. ```vue <!-- Не працює --> <div contenteditable v-model="text"></div> ``` `v-model` розраховує на властивість `value`, якої у `contenteditable` елементів немає. Потрібне ручне підключення: ```vue <div contenteditable="true" :innerHTML="text" @input="text = $event.target.innerText" ></div> ``` **Неправильний тип ref для множинних checkbox.** Коли кілька чекбоксів прив'язані до одного ref, він має бути масивом, не рядком. ```vue <script setup> // Неправильно: рядок ламає логіку перемикання const hobbies = ref('') // Правильно: масив відстежує всі вибрані значення const hobbies = ref([]) </script> <template> <input type="checkbox" v-model="hobbies" value="coding" /> Coding <input type="checkbox" v-model="hobbies" value="skiing" /> Skiing <p>{{ hobbies }}</p> <!-- ['coding', 'skiing'] якщо обидва вибрані --> </template> ``` З масивом Vue додає і видаляє значення при переключенні. З рядком - поведінка непередбачувана. ### Де зустрічається в реальних проектах - Vuetify: `<v-text-field v-model="form.email">` в адмін-панелях - Quasar: `<q-input v-model="state.search">` для пошуку в мобільних застосунках - Element Plus: `<el-input v-model="query">` у таблицях із серверною фільтрацією - VeeValidate: обертає `v-model` щоб запустити валідацію перед емітом оновлення - Nuxt.js: `v-model` у формах профілю користувача разом з `useAsyncData` ### Питання на співбесіді **Q:** Що є скомпільованим виводом `<input v-model="msg" />`? **A:** `:value="msg" @input="msg = $event.target.value"`. Для кастомного компонента: `:modelValue="msg" @update:modelValue="msg = $event"`. **Q:** Чим v-model відрізняється у Vue 2 і Vue 3? **A:** Vue 2 використовує проп `value` і подію `input`, захардкоджено для всіх компонентів. Vue 3 перейшов на `modelValue` і `update:modelValue`, що дозволяє кілька іменованих v-model на одному компоненті. **Q:** Яку подію слухає v-model на `<select multiple>`? **A:** `change`, не `input`. Vue обробляє це автоматично і збирає вибрані опції у масив. **Q:** Як реалізувати lazy v-model у кастомному компоненті, щоб оновлення відбувалось тільки після blur? **A:** Зберігати проміжне значення в локальному `ref` при `@input`, а емітувати `update:modelValue` тільки в `@blur`. Це той самий ефект що `v-model.lazy`, але всередині кастомного компонента. **Q:** Два інпути мають `v-model="text"` на один ref. Що станеться? **A:** Race condition на `@input`. Перемагає той, хто спрацював останнім, тобто другий інпут завжди перезаписує перший. Використовуй окремі refs або computed з getter/setter для кожного поля. ## Приклади ### Базовий: рядок живого пошуку ```vue <template> <input v-model="query" placeholder="Пошук товарів..." /> <p>Шукаємо: {{ query }}</p> </template> <script setup> import { ref } from 'vue' const query = ref('') </script> ``` Кожне натискання клавіші оновлює `query`, параграф ре-рендериться. Жодного обробника подій не потрібно. ### Середній: форма todo (патерн TodoMVC) ```vue <template> <input v-model="newTodo" @keyup.enter="addTodo" placeholder="Додати завдання..." /> <ul> <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li> </ul> </template> <script setup> import { ref } from 'vue' const newTodo = ref('') const todos = ref([]) function addTodo() { if (!newTodo.value.trim()) return todos.value.push({ id: Date.now(), text: newTodo.value.trim() }) newTodo.value = '' // v-model очищає інпут коли ресетиш ref } </script> ``` Натискаєш Enter - завдання з'являється у списку, інпут очищається. Достатньо `newTodo.value = ''`, бо v-model тримає DOM синхронізованим із ref. ### Просунутий: кастомний email-інпут з валідацією і lazy-оновленням ```vue <!-- EmailInput.vue --> <template> <input :value="modelValue" @blur="handleBlur" @input="localValue = $event.target.value" :class="{ error: hasError }" /> <span v-if="hasError">Некоректний email</span> </template> <script setup> import { ref } from 'vue' const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) const localValue = ref(props.modelValue) const hasError = ref(false) function handleBlur() { hasError.value = !localValue.value.includes('@') if (!hasError.value) { emit('update:modelValue', localValue.value) // Емітить тільки при валідному blur } } </script> ``` Дані батька оновлюються тільки коли користувач покинув поле і значення валідне. Введення тексту залишається локальним до blur. Такий патерн часто зустрічається у бібліотеках форм, зокрема у VeeValidate.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.