Кастомные компоненты полей
Кастомный компонент — это чистый слой представления — он получает уже вычисленное состояние валидации как пропсы и сигнализирует об изменениях обратно в форму. Логика валидации не живёт внутри самого компонента.
Как проходит валидация
useForm
├─ validators / asyncValidators / required ← заданы в схеме
├─ errors.value['fieldName'] = ['Too short'] ← вычисляется внутренне
└─ передаёт в ваш компонент через пропсы:
error: string[] — список сообщений об ошибках
touched: boolean — терял ли фокус этот полеЕдинственная задача вашего компонента:
| Что | Как |
|---|---|
| Сообщить об изменении значения | emit('update:modelValue', newValue) |
| Запустить валидацию | emit('blur') — запускает валидацию, когда validateOn равен 'blur' или 'eager' |
| Показать ошибки | Читать props.error / props.touched (или использовать useFormField) |
Контракт FormFieldProps
Каждый компонент, подключаемый к библиотеке, должен объявить эти пропсы и два emit'а:
import type { FormFieldProps } from '@macrulez/vue-form-schema'
// props
const props = defineProps<FormFieldProps>()
// {
// field: FieldDefinition — полная конфигурация поля (валидаторы, label, …)
// modelValue: unknown — текущее значение из состояния формы
// error: string[] — ошибки валидации (пусто, если валидно)
// touched: boolean — true после первого blur
// }
// emits
const emit = defineEmits<{
'update:modelValue': [value: unknown]
blur: []
}>()Полный пример: кастомный ввод телефона
<!-- MyPhoneInput.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useFormField } from '@macrulez/vue-form-schema'
import type { FormFieldProps } from '@macrulez/vue-form-schema'
const props = defineProps<FormFieldProps>()
const emit = defineEmits<{
'update:modelValue': [value: string]
blur: []
}>()
const { hasError, errorMessage, isRequired } = useFormField(props)
// убираем не-цифры для хранения, отображаем отформатированным
const display = computed(() =>
String(props.modelValue ?? '')
.replace(/\D/g, '')
.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3'),
)
</script>
<template>
<div class="field">
<label :for="field.name">
{{ field.label }}
<span v-if="isRequired" aria-hidden="true">*</span>
</label>
<input
:id="field.name"
type="tel"
:value="display"
:aria-invalid="hasError ? 'true' : 'false'"
:aria-describedby="hasError ? `${field.name}-error` : undefined"
@input="
emit('update:modelValue', ($event.target as HTMLInputElement).value.replace(/\D/g, ''))
"
@blur="emit('blur')"
/>
<p v-if="hasError" :id="`${field.name}-error`" role="alert">
{{ errorMessage }}
</p>
</div>
</template>Подключение к полю через field.component
import MyPhoneInput from './MyPhoneInput.vue'
import { minLength, pattern } from '@macrulez/vue-form-schema'
const schema: FieldDefinition[] = [
{
type: 'text',
name: 'phone',
label: 'Phone number',
component: MyPhoneInput, // ← ваш компонент рендерится вместо TextField
required: true,
validators: [
minLength(10, 'Enter a full phone number'),
pattern(/^\d{10}$/, 'Digits only, 10 characters'),
],
},
]Использование без FormRenderer (ручное связывание)
Если вы рендерите поля сами — без FormRenderer — подключайте ошибки и обработчик touch напрямую:
<script setup lang="ts">
import { useForm } from '@macrulez/vue-form-schema'
import MyPhoneInput from './MyPhoneInput.vue'
const form = useForm({ schema, validateOn: 'blur' })
const touchField = (form as any).touchField // экспонируется внутренне
</script>
<template>
<form @submit.prevent="form.submit()">
<MyPhoneInput
:field="form.fields.value[0]"
:model-value="form.values.value.phone"
:error="form.errors.value.phone ?? []"
:touched="form.touched.value.phone ?? false"
@update:model-value="form.setField('phone', $event)"
@blur="touchField('phone')"
/>
<button type="submit">Save</button>
</form>
</template>Хелпер useFormField: вычисляемые сокращения
import { useFormField } from '@macrulez/vue-form-schema'
const props = defineProps<FormFieldProps>()
const {
hasError, // ComputedRef<boolean> — touched && error.length > 0
errorMessage, // ComputedRef<string | null> — первая ошибка либо null
allErrors, // ComputedRef<string[]> — все ошибки, когда touched, иначе []
isRequired, // ComputedRef<boolean>
isDisabled, // ComputedRef<boolean>
} = useFormField(props)Реестр компонентов
Заменить все экземпляры типа поля в поддереве — полезно для интеграции UI-библиотек.
На уровне приложения (плагин Vue)
import { createApp } from 'vue'
import { createFormRegistry } from '@macrulez/vue-form-schema'
import { ElInput, ElSelect } from 'element-plus'
createApp(App)
.use(createFormRegistry({ text: ElInput, select: ElSelect }))
.mount('#app')На уровне поддерева
import { provideRegistry } from '@macrulez/vue-form-schema'
// Внутри setup() компонента
provideRegistry({ checkbox: MyToggle })Чтение реестра напрямую (useRegistry)
useRegistry() читает тот реестр — на уровне приложения или поддерева — который сейчас в области видимости. FormRenderer вызывает его внутренне, чтобы разрешить компонент каждого поля; вызывать его самим стоит, только если вы строите собственный рендерер (альтернативу FormRenderer), которому нужно уважать ту же цепочку реестра.
import { useRegistry } from '@macrulez/vue-form-schema'
// Внутри setup() кастомного компонента-рендерера
const registry = useRegistry() // ComponentMap, действующий в этой области видимостиПриоритет компонентов: field.component > пропс FormRenderer :components > реестр > встроенные значения по умолчанию.