Кастомные компоненты
Кастомные компоненты полей
Кастомный компонент — это чистый слой представления — он получает уже вычисленное состояние валидации как пропсы и сигнализирует об изменениях обратно в форму. Логика валидации не живёт внутри самого компонента.
Как проходит валидация
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 })Приоритет компонентов: field.component > пропс FormRenderer :components > реестр > встроенные значения по умолчанию.
Маскирование ввода
Маски форматируют пользовательский ввод в реальном времени. Применяются автоматически в FormRenderer; также доступны автономно.
Пресеты
| Пресет | Пример вывода |
|---|---|
phone-ru | +7 (916) 123-45-67 |
phone-eu | +49 (30) 123-45-67 |
date | 01.01.2024 |
inn | 123456789012 |
iban | GB29 NWBK 6016 1331 9268 19 |
{ type: 'text', name: 'phone', mask: { preset: 'phone-ru' } }Кастомные паттерны
# = цифра, A = буква (в верхнем регистре), всё остальное = литерал.
{ type: 'text', name: 'postcode', mask: { pattern: 'AA####' } } // AB1234Автономный API
import { applyMask, removeMask, bindMask } from '@macrulez/vue-form-schema'
applyMask('9161234567', { preset: 'phone-ru' }) // '+7 (916) 123-45-67'
removeMask('+7 (916) 123-45-67', { preset: 'phone-ru' }) // '9161234567'
const cleanup = bindMask(inputEl, { preset: 'date' })
onUnmounted(cleanup)