Skip to content

Кастомные компоненты

Кастомные компоненты полей

Кастомный компонент — это чистый слой представления — он получает уже вычисленное состояние валидации как пропсы и сигнализирует об изменениях обратно в форму. Логика валидации не живёт внутри самого компонента.

Как проходит валидация

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'а:

ts
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: []
}>()

Полный пример: кастомный ввод телефона

vue
<!-- 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

ts
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 напрямую:

vue
<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: вычисляемые сокращения

ts
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)

ts
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')

На уровне поддерева

ts
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
date01.01.2024
inn123456789012
ibanGB29 NWBK 6016 1331 9268 19
ts
{ type: 'text', name: 'phone', mask: { preset: 'phone-ru' } }

Кастомные паттерны

# = цифра, A = буква (в верхнем регистре), всё остальное = литерал.

ts
{ type: 'text', name: 'postcode', mask: { pattern: 'AA####' } }  // AB1234

Автономный API

ts
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)