У формах повторюється той самий набір: мітка, поле, підказка, помилка, атрибути доступності. Його виносять у компонент поля.
defineModel (Vue 3.4+) - найкоротший спосіб підтримати v-model на власному компоненті:
<!-- TextField.vue -->
<script setup lang="ts">
import { useId } from 'vue'
defineOptions({ inheritAttrs: false })
const model = defineModel<string>({ required: true })
const props = defineProps<{ label: string; error?: string; hint?: string }>()
const id = useId()
</script>
<template>
<div class="field">
<label :for="id">{{ props.label }}</label>
<input
:id="id"
v-model="model"
v-bind="$attrs"
:aria-invalid="props.error ? 'true' : undefined"
:aria-describedby="props.error ? `${id}-error` : undefined"
/>
<p v-if="props.hint && !props.error">{{ props.hint }}</p>
<p v-if="props.error" :id="`${id}-error`" class="error">{{ props.error }}</p>
</div>
</template>
<TextField v-model="form.email" label="Email" type="email" autocomplete="email" :error="errors.email?.[0]" />
Що тут важливо:
defineModelстворює ref, синхронізований з батьком: читання - значенняmodelValue, запис - подіяupdate:modelValue. Без нього довелося б оголошувати проп і подію вручну;inheritAttrs: false+v-bind="$attrs"- атрибути, передані компоненту (type,autocomplete,placeholder,required), потрапляють на<input>, а не на обгорткуdiv. Інакшеtype="email"опиниться наdivі не працюватиме;- модифікатори теж підтримуються:
const [model, modifiers] = defineModel({ set(v) { return modifiers.trim ? v.trim() : v } }); - кілька моделей на одному компоненті:
defineModel('from')іdefineModel('to')для діапазону дат -v-model:fromіv-model:to.
Типові помилки:
- мутувати проп напряму (
props.modelValue = ...) - Vue попередить, а батько не дізнається про зміну; - локальна копія значення без синхронізації - поле перестає реагувати на скидання форми батьком;
- об'єкт у
defineModelі зміна його властивостей (model.value.city = ...) мутує об'єкт батька в обхід події - для об'єктів присвоюйте нове значення (model.value = { ...model.value, city }).
Межа абстракції: компонент поля відповідає за розмітку й доступність, а не за валідацію. Правила й стан форми лишаються у формі чи бібліотеці валідації - тоді той самий TextField працює з будь-яким підходом.