vuevaldi
v0.14.0
Published
Reactive form validation library for Vue 3 with yup
Readme
vuevaldi
Библиотека валидации форм для Vue 3.
TypeScript-first, агностик к валидатору (yup работает из коробки), реактивная, событийно-ориентированная.
🌐 Language / Язык: English · Русский
Возможности
- Агностик к валидатору: подходит любой валидатор, реализующий интерфейс
FormValidator; адаптер для yup включён. - Ошибки структурно повторяют модель формы (вложенные объекты), внутри — плоские пути.
- Метки (labels) и значения по умолчанию берутся из самой схемы (yup
.label()/.default()). - Перевалидация при вводе (
validateOnInput) — до submit показываются ошибки только изменённых полей. - События submit:
success,error,finished. - Маппинг серверных ошибок через
errorHandler+ violations. - Написано на TypeScript со строгой типизацией полей, ошибок, меток и дефолтов.
Установка
npm install vuevaldi yupvue и yup — peer-зависимости. Пакет только ESM, типы включены.
Быстрый старт
Объявляем yup-схему, создаём валидатор через useYupValidator и форму через useForm.
<script setup lang="ts">
import * as yup from 'yup';
import { useForm, useYupValidator } from 'vuevaldi';
const schema = yup.object({
name: yup.string().required('Имя обязательно').label('Имя'),
email: yup.string().email('Некорректный email').required('Email обязателен').label('Email'),
age: yup.number().min(18, 'Должно быть 18+').label('Возраст'),
});
const validator = useYupValidator(schema);
const { model, error, errors, labels, isSubmitting, submit } = useForm({
validator,
validateOnInput: true,
submitHandler: async values => {
await api.save(values);
},
});
</script>
<template>
<form @submit.prevent="submit()">
<p v-if="error" class="form-error">{{ error }}</p>
<div>
<label>{{ labels.name }}</label>
<input v-model="model.name" />
<span v-if="errors.name" class="field-error">{{ errors.name[0] }}</span>
</div>
<div>
<label>{{ labels.email }}</label>
<input v-model="model.email" />
<span v-if="errors.email" class="field-error">{{ errors.email[0] }}</span>
</div>
<div>
<label>{{ labels.age }}</label>
<input v-model.number="model.age" />
<span v-if="errors.age" class="field-error">{{ errors.age[0] }}</span>
</div>
<button type="submit" :disabled="isSubmitting">Отправить</button>
</form>
</template>model, errors, labels и остальное — Vue-рефы / computed-рефы. При деструктуризации в <script setup> они автоматически разворачиваются в шаблоне. Если держать весь объект целиком (const form = useForm(...)), обращайтесь через .value: form.model.value.name.
submit() сначала валидирует; при успехе вызывает submitHandler(values) и возвращает true, иначе — false.
API — useForm(opt)
Поля FormContextOptions (все опциональны, кроме validator и submitHandler):
| Опция | Тип | Описание |
|---|---|---|
| values | PartialObjectDeep<TFields> | Реальные начальные данные (например, загруженная запись). Используются как есть, без merge со schema-defaults. |
| defaultValues | PartialObjectDeep<TFields> | Шаблон начального/сброшенного состояния. Глубоко сливается поверх schema-defaults (schema дозаполняет, эти переопределяют). |
| extraData | TExtraData | Произвольные дополнительные данные, доступные через getExtraData(). |
| submitHandler | (values: TFields) => TResp \| Promise<TResp> | Обязателен. Вызывается при успешной валидации. |
| errorHandler | (error: TErr) => { message: string; violations?: Violation[] } | Преобразует ошибку submit в сообщение и ошибки полей. |
| resetAfterSubmit | boolean | Сбросить форму после успешного submit. |
| validateOnInput | boolean | Перевалидировать при изменении модели; до первого submit показываются ошибки только изменённых полей. |
| validator | FormValidator<TFields> | Обязателен. Реализация валидатора. |
type Violation = { message: string; propertyPath: string };Приоритет начальной модели:
values ?? merge(schemaDefaults, defaultValues) ?? {}valuesвыигрывают целиком (schema не подмешивается — реальные данные не трогаются).- Иначе база — schema-defaults, поверх глубоко сливается
defaultValues. - Массивы заменяются целиком, не сливаются по индексу.
API — возвращаемый контекст
| Поле | Тип | Описание |
|---|---|---|
| model | Ref<PartialObjectDeep<TFields>> | Реактивное состояние формы; биндьте инпуты через v-model. |
| error | ComputedRef<string> | Глобальная ошибка формы (submit / сервер). |
| errors | ComputedRef<ValidationErrors<TFields>> | Ошибки полей, структурно повторяющие модель. |
| labels | ComputedRef<FormLabels<TFields>> | Метки полей из схемы, структурно повторяющие модель. |
| isSubmitting | ComputedRef<boolean> | true, пока идёт submit(). |
| isDirty | ComputedRef<boolean> | true, если модель отличается от исходного состояния (на момент создания формы или последнего reset()). Сравнение всегда идёт по текущим данным, поэтому ручной возврат значений к исходным (в т.ч. по отдельному полю) тоже сбрасывает флаг в false — не только reset(). |
| submit | (throwError?: boolean) => Promise<boolean> | Валидация → вызов submitHandler. Возвращает успех. С throwError бросает Error(error) при неудаче. |
| reset | (newOpt?: { values?, defaultValues? }) => void | Сброс ошибок и модели (с merge schema-defaults как выше). |
| validate | () => Promise<false \| TFields> | Валидирует текущую модель; возвращает распарсенные значения или false. |
| addEventListener | (event, listener) => void | Подписка на success / error / finished. |
| getExtraData | () => TExtraData | Возвращает extraData; бросает ошибку, если не задано. |
AnyFormContext — слаботипизированная версия того же контракта (для передачи контекста куда-либо без типов).
Валидатор
Интерфейс FormValidator
Любой валидатор для useForm должен реализовать этот контракт:
interface FormValidator<TFields extends Recordable = Recordable> {
isValid: (values: PartialObjectDeep<TFields>) => Promise<boolean>;
parse: (values: PartialObjectDeep<TFields>) => Promise<
| { isError: false; values: TFields; errors: undefined }
| { isError: true; values: undefined; errors: FlattenedErrors }
>;
describe?: () => FormSchemaInfo<TFields>;
}errors— плоская карта:Record<string, string[]>(пути вида'address.city'). Форма разворачивает её во вложенный рефerrors.describeопционален и питаетlabels+ schema-defaults.FlattenedErrors = Record<string, string[]>.
useYupValidator(schema, options?)
Оборачивает yup ObjectSchema в FormValidator.
import { useYupValidator } from 'vuevaldi';
const schema = yup.object({
name: yup.string().required().label('Имя'),
age: yup.number().min(18).label('Возраст'),
});
const validator = useYupValidator(schema, {
// ValidateOptions (yup): strict, abortEarly, stripUnknown, recursive...
});Дефолты: strict: false, abortEarly: false, stripUnknown: false, recursive: true, disableStackTrace: true.
Работает со вложенными объектами, массивами, кортежами, .when() и т.д. — ошибки раскладываются по путям полей и затем собираются обратно в структуру модели.
const schema = yup.object({
profile: yup.object({
firstName: yup.string().required().label('Имя'),
lastName: yup.string().required().label('Фамилия'),
}),
tags: yup.array().of(yup.string()),
items: yup.array().of(yup.object({
sku: yup.string().required().label('Артикул'),
qty: yup.number().min(1).label('Количество'),
})),
});errors.profile.firstName → string[], errors.items → [{ sku?: string[]; qty?: string[] }].
Метки и дефолты из схемы
useYupValidator отдаёт describe(), построенный из yup schema.describe(), который наполняет две вещи:
labels— метки из.label(), структурно какerrors/модель:- поля-листья →
string | undefined; - поля-объекты → вложенный объект меток; собственный label объекта доступен по зарезервированному ключу
$label(labels.money.$label); - массивы примитивов → метка элемента (с фолбэком на метку массива);
- массивы объектов → массив с одним элементом = метки элемента (
labels.items[0].sku); метка самого массива лежит на массиве (labels.items.$label), метка объекта-элемента —labels.items[0].$label.
- поля-листья →
defaults— значения из.default(), база для начальной модели иreset()(сливаются подdefaultValues, см. выше).
const schema = yup.object({
name: yup.string().default('Иван').label('Имя'),
address: yup.object({
city: yup.string().default('Москва').label('Город'),
}).label('Адрес'),
});
const { labels, model } = useForm({
validator: useYupValidator(schema),
submitHandler: async () => {},
});
// labels: { name: 'Имя', address: { $label: 'Адрес', city: 'Город' } }
// labels.address.$label → 'Адрес'
// model: { name: 'Иван', address: { city: 'Москва' } }Кастомные валидаторы могут реализовать describe?: () => FormSchemaInfo<TFields>, чтобы тоже отдавать labels и defaults.
События
Подписка через addEventListener; каждый вызов регистрирует ещё один слушатель.
form.addEventListener('success', data => console.log('Сохранено', data));
form.addEventListener('error', err => console.error('Ошибка', err));
form.addEventListener('finished', () => console.log('Готово'));| Событие | Payload | Когда |
|---|---|---|
| success | TResp — возвращаемое значение submitHandler | После успешного submit |
| error | TErr — ошибка валидации ('Validation failed') или ошибка из submitHandler | При неудачной валидации или submit |
| finished | — | Всегда после попытки submit |
Ошибки при submit
submit() сначала валидирует. При неудачной валидации error получает 'Validation failed', срабатывает событие error, submit() возвращает false. Если валидация прошла — ожидается submitHandler(values):
- при успехе — событие
success(+ сброс, еслиresetAfterSubmit), возвращаетсяtrue; - при ошибке — вызывается
errorHandler(error)(если передан), иначеerror.value = error.messageдля инстансовError.
errorHandler преобразует ошибку в сообщение и ошибки полей, обычно из ответа бэкенда:
import type { AxiosError } from 'axios';
const schema = yup.object({
name: yup.string().required().label('Имя'),
email: yup.string().required().label('Email'),
});
type UserForm = yup.InferType<typeof schema>;
interface ApiError {
message: string;
errors: { field: string; message: string }[];
}
const form = useForm<UserForm, void, AxiosError<ApiError>>({
validator: useYupValidator(schema),
submitHandler: values => api.save(values),
errorHandler: e => ({
message: e.response?.data.message ?? 'Запрос не удался',
violations: e.response?.data.errors.map(v => ({
message: v.message,
propertyPath: v.field, // например 'name' или 'address.city'
})),
}),
});Violations с propertyPath попадают в ту же вложенную структуру errors, что и ошибки клиентской валидации.
Опции подробнее
validateOnInput
При включении форма глубоко следит за моделью, перевалидирует при каждом изменении и до первого submit показывает ошибки только изменённых полей. После неудачного submit показываются все ошибки.
resetAfterSubmit
Сбрасывает форму (ошибки + модель к дефолтам) после успешного вызова submitHandler.
extraData / getExtraData
Произвольные метаданные, переданные при создании и читаемые через getExtraData() (типизированы как TExtraData). Бросает ошибку, если extraData не передан.
values vs defaultValues
values— реальные данные, используются как есть, без merge со schema-defaults.defaultValues— шаблон, глубоко сливается поверх schema-defaults.- Не передавать ничего — форма инициализируется только из schema-defaults.
Кастомный валидатор
Подойдёт любой объект, соответствующий FormValidator. Минимальный ручной валидатор:
import type { FormValidator } from 'vuevaldi';
type UserForm = {
name: string;
age?: number;
};
const customValidator: FormValidator<UserForm> = {
isValid: async values => Boolean(values.name && values.name.length >= 2),
parse: async values => {
const errors: Record<string, string[]> = {};
if (!values.name) {
errors.name = ['Имя обязательно'];
} else if (values.name.length < 2) {
errors.name = ['Имя должно быть не короче 2 символов'];
}
if (values.age !== undefined && values.age < 18) {
errors.age = ['Должно быть 18+'];
}
if (Object.keys(errors).length > 0) {
return { isError: true, values: undefined, errors };
}
return { isError: false, values: values as UserForm, errors: undefined };
},
};
const form = useForm<UserForm>({
validator: customValidator,
submitHandler: values => api.save(values),
});Опционально реализуйте describe(), чтобы тоже отдавать labels и schema-defaults:
import type { FormSchemaInfo } from 'vuevaldi';
const customValidator: FormValidator<UserForm> = {
// isValid, parse ...
describe: (): FormSchemaInfo<UserForm> => ({
labels: { name: 'Имя', age: 'Возраст' },
defaults: { name: 'Иван' },
}),
};Экспортируемые типы
import type {
AnyFormContext, // слаботипизированный контекст
FormContext, // типизированный контекст
FormContextOptions, // опции useForm
FormSchemaInfo, // { labels?, defaults? } из validator.describe
FormValidator, // контракт валидатора
FormLabels, // структура меток
PartialObjectDeep, // глубокий partial полей (массивы не рекурсятся)
ValidationErrors, // структура ошибок
} from 'vuevaldi';Лицензия
MIT © ТОО «ADEGARA».
