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

Чтение реестра напрямую (useRegistry) ​

useRegistry() читает тот реестр — на уровне приложения или поддерева — который сейчас в области видимости. FormRenderer вызывает его внутренне, чтобы разрешить компонент каждого поля; вызывать его самим стоит, только если вы строите собственный рендерер (альтернативу FormRenderer), которому нужно уважать ту же цепочку реестра.

ts
import { useRegistry } from '@macrulez/vue-form-schema'

// Внутри setup() кастомного компонента-рендерера
const registry = useRegistry() // ComponentMap, действующий в этой области видимости

Приоритет компонентов: field.component > пропс FormRenderer :components > реестр > встроенные значения по умолчанию.