@sakhnovkrg/vue-form-validator
v0.2.2
Published
Type-safe Vue 3 form validation library
Maintainers
Readme
Vue Form Validator
Современная, типобезопасная библиотека валидации форм для Vue 3 с интуитивным API и мощными возможностями.
⚠️ Библиотека находится в разработке
Демо
✨ Основные возможности
- 🚀 Без зависимостей - Легкая и быстрая (~6kB gzipped)
- 🦄 Типобезопасность - Полная поддержка TypeScript с автодополнением
- 📋 Декларативность - Вся форма определяется в одном месте: структура, валидация и обработка
- 🎯 Интуитивный API - Простые и понятные методы для работы с формами
- 🔄 Реактивность - Валидация в реальном времени с реактивностью Vue
- 🌍 Интернационализация - Поддержка реактивных сообщений об ошибках
- 📂 Загрузка файлов - Встроенная валидация файлов с хелперами
- 📝 Динамические массивы - Поддержка валидации массивов объектов
- ⚡ Асинхронная валидация - Удаленная валидация с debouncing
- 🔗 Кросс-полевая валидация - Зависимости полей и сравнения
- 🎨 Гибкость - Кастомные правила и условная валидация
🧬 Умная типизация
Библиотека спроектирована с особым вниманием к типобезопасности. createForm поддерживает как простые, так и вложенные структуры данных.
TypeScript контролирует корректность имен полей на всех уровнях:
Для основных полей формы (строгая типизация):
const form = createForm({
email: '',
password: ''
}, ...)
form.error('email') // ✅ Корректно - поле существует
form.error('invalid') // ❌ Ошибка TypeScript - поле не существует
form.hasError('password') // ✅ Корректно с автодополнениемДля вложенных полей массивов и объектов (продвинутая типизация):
const form = createForm({
contacts: [{ name: '', email: '' }],
address: { street: '', city: '' }
}, ...)
// ✅ TypeScript автоматически выводит допустимые пути:
form.hasError('contacts.0.name') // contacts.${number}.name
form.hasError('contacts.0.email') // contacts.${number}.email
form.hasError('address.street') // address.street
form.hasError('address.city') // address.city
// ❌ TypeScript не позволит указать несуществующие пути:
form.hasError('contacts.0.invalid') // Ошибка компиляции!
form.hasError('address.invalid') // Ошибка компиляции!
// ✅ Для автодополнения используйте helper'ы:
form.hasError(form.arrayPath('contacts', 0, 'name')) // автодополнение
form.hasError(form.objectPath('address', 'street')) // автодополнениеТипы автоматически выводятся из начальных значений, обеспечивая полную типобезопасность на всех уровнях API.
⚡ Поддерживаемые структуры данных
createForm поддерживает все типы структур данных:
- ✅ Простые поля -
string,number,boolean,File,File[] - ✅ Массивы объектов - динамические списки с валидацией элементов
- ✅ Вложенные объекты - многоуровневые структуры данных
- ✅ Смешанные структуры - комбинации простых полей, массивов и объектов
📦 Установка
npm install @sakhnovkrg/vue-form-validator🚀 Быстрый старт
<script setup lang="ts">
import { createForm } from '@sakhnovkrg/vue-form-validator'
const {
values,
isDirty,
isValid,
isSubmitting,
error,
hasError,
touch,
submit,
} = createForm(
{
email: '',
password: '',
},
(r, define) =>
define({
email: r.required().email(),
password: r.required().minLength(8),
}),
{
async onSubmit(values) {
console.log('Форма отправлена:', values)
},
}
)
</script>
<template>
<form @submit.prevent="submit">
<div>
<input
v-model="values.email"
@blur="touch('email')"
placeholder="Email"
/>
<span v-if="hasError('email')" class="error">
{{ error('email') }}
</span>
</div>
<div>
<input
v-model="values.password"
@blur="touch('password')"
type="password"
placeholder="Пароль"
/>
<span v-if="hasError('password')" class="error">
{{ error('password') }}
</span>
</div>
<button type="submit" :disabled="!isDirty || !isValid || isSubmitting">
{{ isSubmitting ? 'Отправка...' : 'Отправить' }}
</button>
</form>
</template>🎯 Декларативный подход
Вся логика формы определяется в одном вызове createForm():
import { createForm } from '@sakhnovkrg/vue-form-validator'
const form = createForm(
// 1. Структура данных
{ email: '', password: '' },
// 2. Правила валидации
(r, define) =>
define({
email: r.required().email(),
password: r.required().minLength(8),
}),
// 3. Обработчики событий
{
onSubmit: values => {
/* отправка формы */
},
}
)Преимущества:
- ✅ Никаких отдельных схем или разбросанной логики
- ✅ TypeScript автоматически выводит типы из определения
- ✅ Вся форма видна в одном месте - легко понимать и поддерживать
- ✅ Меньше boilerplate кода
🌍 Интернационализация (i18n)
Для интернационализации вам понадобится реактивный подход с computed(), который автоматически обновляет сообщения об ошибках при смене языка.
Обычный подход (без i18n)
// Простой и быстрый - для форм с фиксированными сообщениями
const form = createForm(initialValues, (r, define) =>
define({
email: r.required('Email обязателен').email('Неверный формат'),
})
)Реактивный подход (с i18n)
// Реактивные сообщения - обновляются при смене языка
const form = createForm(
initialValues,
computed(() => {
const r = createRules()
return {
email: r.required(t('validation.required')).email(t('validation.email')),
}
})
)Полный пример с vue-i18n:
<script setup lang="ts">
import { createForm, createRules } from '@sakhnovkrg/vue-form-validator'
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
const { t } = useI18n()
const form = createForm(
{ username: '', email: '', password: '' },
// вместо (r, define) => ...
computed(() => {
const r = createRules()
return {
username: r.required(t('validation.required')),
email: r.required(t('validation.required')).email(t('validation.email')),
password: r
.required(t('validation.required'))
.minLength(6, t('validation.minLength', { count: 6 })),
}
}),
{
onSubmit: async values => {
console.log('Form submitted:', values)
},
}
)
</script>
<template>
<form @submit.prevent="form.submit">
<!-- .values и .val — взаимозаменяемы, .val удобнее в script -->
<input v-model="form.values.username" @blur="form.touch('username')" />
<span v-if="form.hasError('username')">{{ form.error('username') }}</span>
<input v-model="form.values.email" @blur="form.touch('email')" />
<span v-if="form.hasError('email')">{{ form.error('email') }}</span>
<!-- Остальные поля... -->
</form>
</template>📖 Справочник API
createForm(initialValues, rulesBuilder, options?)
Создает реактивную форму с валидацией.
Параметры:
initialValues- Начальные значения формы (поддерживает вывод типов)rulesBuilder- Функция-строитель правил(r, define) => define({...})или реактивный computedcomputed(() => { const r = createRules(); return {...} })для i18noptions- Дополнительные настройки
Настройки:
onSubmit?- Обработчик отправки формыonClear?- Обработчик очистки формы
Возвращает: Экземпляр формы с реактивными свойствами и методами
Поддерживаемые возможности:
- Поддержка вложенных путей типа
'contacts.0.email' - Методы
arrayPath()иobjectPath()для типобезопасного построения путей - Управление массивами:
addArrayItem(),removeArrayItem(),toggleArrayItem() - Автоматическая оптимизация в зависимости от структуры данных
Свойства и методы формы
Реактивное состояние
| Свойство | Тип | Описание |
| --------------- | ------------------------------- | -------------------------------------------------- |
| values | Ref<T> | Текущие значения формы (реактивный ref) |
| val | T | Геттер для удобного доступа к значениям (в script) |
| errors | Ref<Record<string, string[]>> | Ошибки валидации по полям |
| touched | Ref<Record<string, boolean>> | Состояние "тронутости" полей |
| dirty | Ref<Record<string, boolean>> | Измененные поля |
| isValidating | Ref<Record<string, boolean>> | Поля в процессе валидации |
| isSubmitting | Ref<boolean> | Статус отправки формы |
| isValid | ComputedRef<boolean> | Валидность всей формы |
| isDirty | ComputedRef<boolean> | Наличие несохраненных изменений |
| hasAnyErrors | ComputedRef<boolean> | Наличие ошибок в форме |
| touchedFields | ComputedRef<string[]> | Список "тронутых" полей |
| dirtyFields | ComputedRef<string[]> | Список измененных полей |
Методы валидации
| Метод | Описание |
| --------------------- | ---------------------------------------------------- |
| setRules(rules) | Установить правила валидации |
| validateField(name) | Валидировать поле (обычное или вложенное) |
| validateForm() | Валидировать всю форму |
| submit() | Отправить форму после валидации |
| touch(field) | Отметить поле как "тронутое" (обычное или вложенное) |
Управление состоянием
| Метод | Описание |
| -------------------- | ------------------------------------ |
| setValues(values) | Обновить значения полей |
| getValues() | Получить копию текущих значений |
| clear(useInitial?) | Очистить форму |
| reset(newValues?) | Сбросить форму к начальным значениям |
| resetState() | Сбросить состояние валидации |
| setErrors(errors) | Установить ошибки для полей |
| resetErrors() | Очистить все ошибки |
Проверка состояния полей
Унифицированные методы (работают с обычными и вложенными полями):
| Метод | Возврат | Описание |
| ----------------------- | ---------------- | ---------------------------------- |
| hasError(field) | boolean | Есть ли ошибки в поле |
| error(field) | string \| null | Первая ошибка поля |
| allErrors(field) | string[] | Все ошибки поля |
| isTouched(field) | boolean | Было ли поле "тронуто" |
| validating(field) | boolean | Валидируется ли поле |
| isFieldDirty(field) | boolean | Изменено ли поле |
| getFieldStatus(field) | FieldStatus | Полная информация о состоянии поля |
Примеры использования:
// Обычные поля
form.hasError('email')
form.error('name')
// Вложенные пути
form.hasError('contacts.0.email')
form.error('address.street')
// С автодополнением через helper'ы
form.hasError(form.arrayPath('contacts', 0, 'email'))
form.error(form.objectPath('address', 'street'))Работа с вложенными структурами
| Метод | Описание |
| ---------------------------------------- | ------------------------------------------------ |
| addArrayItem(arrayPath, item) | Добавить элемент в массив |
| removeArrayItem(arrayPath, index) | Удалить элемент из массива |
| toggleArrayItem(arrayPath, item) | Переключить элемент в массиве (добавить/удалить) |
| arrayIncludes(arrayPath, item) | Проверить содержится ли элемент в массиве |
| arrayPath(arrayField, index, property) | Построить типобезопасный путь к элементу массива |
| objectPath(objectField, property) | Построить типобезопасный путь к свойству объекта |
Файловые утилиты
| Свойство | Описание |
| --------------------------- | --------------------------------------------------- |
| file.{fieldName}.files | ComputedRef<File[]> - Список файлов |
| file.{fieldName}.fileInfo | ComputedRef<FileInfo[]> - Информация о файлах |
| file.{fieldName}.handler | (event: Event) => void - Обработчик выбора файлов |
| file.{fieldName}.clear | () => void - Очистить выбранные файлы и DOM input |
Note: Helpers создаются лениво при первом обращении. Для множественного выбора установите multiple на <input type="file"> — библиотека определит это автоматически по событию ввода.
Важно: Метод clear() полностью очищает файловые поля - как значение в форме, так и визуальное отображение в DOM input элементе. Это предотвращает ситуацию, когда после clear() файл исчезает из формы, но остается отображаться в input.
Продвинутые методы
| Метод | Описание |
| -------------------- | --------------------------------------------------------- |
| clearCache(field?) | Очистить кэш валидации (поля или весь кэш) |
| dispose() | Остановить watchers и очистить ресурсы (авто при unmount) |
🛠️ Встроенные правила валидации
Базовые правила
r.required('Кастомное сообщение') // Обязательное поле
r.email() // Валидный email
r.minLength(5) // Минимальная длина
r.maxLength(100) // Максимальная длина
r.numeric() // Только цифры
r.regex(/pattern/, 'сообщение') // Кастомный regex
r.oneOf(['a', 'b', 'c']) // Должно быть одним из значенийЧисловые правила
r.minValue(0) // Минимальное значение
r.maxValue(100) // Максимальное значение
r.between(0, 100) // Диапазон значенийКросс-полевые правила
r.sameAs('password') // Должно совпадать с другим полем
r.dateAfter('startDate') // Дата должна быть после другого поля
r.requiredIf('type', 'business') // Обязательно при условииПравила файлов
r.fileRequired() // Выбор файла обязателен
r.fileSize(5 * 1024 * 1024) // Максимальный размер файла (5MB)
r.fileType(['.jpg', '.png']) // Разрешенные типы файлов
r.fileCount(1, 5) // Диапазон количества файловПравила массивов
r.arrayRequired() // Проверяет, что значение — массив и в нём есть хотя бы один элемент
r.arrayMinLength(1) // Минимальная длина массива
r.arrayMaxLength(10) // Максимальная длина массиваПримечание: arrayRequired() и arrayMinLength(1) работают одинаково, но arrayRequired() предоставляет более семантичное название для обязательных массивов.
Продвинутые правила
// Удаленная валидация с debouncing
r.remote(
async username => {
const response = await fetch(`/api/check-username/${username}`)
return response.ok
},
'Имя пользователя уже занято',
500
)
// Кастомная валидация
r.custom((value, allValues) => {
return value.includes(allValues.domain)
}, 'Неверный формат')📂 Загрузка файлов
Конфигурация
import { createForm } from '@sakhnovkrg/vue-form-validator'
const form = createForm(
{
avatar: null as File | null,
documents: null as File[] | null,
},
(r, define) =>
define({
avatar: r
.fileRequired()
.fileType(['.jpg', '.jpeg', '.png'])
.fileSize(3 * 1024 * 1024),
documents: r.fileRequired().fileCount(1, 5),
})
)Использование
<template>
<!-- Один файл -->
<input type="file" @change="form.file.avatar.handler" />
<div v-if="form.file.avatar.files.value.length">
Выбран: {{ form.file.avatar.fileInfo.value[0]?.name }}
<button @click="form.file.avatar.clear()">Удалить</button>
</div>
<!-- Множественные файлы -->
<input type="file" multiple @change="form.file.documents.handler" />
<div v-if="form.file.documents.files.value.length">
<p>Файлов: {{ form.file.documents.files.value.length }}</p>
<ul>
<li v-for="file in form.file.documents.fileInfo.value" :key="file.name">
{{ file.name }} ({{ file.formattedSize }})
</li>
</ul>
<button @click="form.file.documents.clear()">Очистить все</button>
</div>
</template>📝 Вложенные структуры данных
Библиотека поддерживает валидацию динамических массивов и вложенных объектов с типобезопасным API.
Динамические массивы
interface Contact {
name: string
email: string
role: string
}
const form = createForm(
{
teamName: '',
contacts: [] as Contact[],
},
r => ({
teamName: r.required(),
contacts: r.arrayMinLength(1),
'contacts.*.name': r.required(),
'contacts.*.email': r.required().email(),
'contacts.*.role': r.required(),
})
)
// Управление массивом
form.addArrayItem('contacts', { name: '', email: '', role: '' })
form.removeArrayItem('contacts', index)Пример компонента:
<template>
<div v-for="(contact, index) in form.values.contacts" :key="index">
<input
v-model="contact.name"
@blur="form.touch(form.arrayPath('contacts', index, 'name'))"
/>
<span v-if="form.hasError(form.arrayPath('contacts', index, 'name'))">
{{ form.error(form.arrayPath('contacts', index, 'name')) }}
</span>
<button @click="form.removeArrayItem('contacts', index)">Удалить</button>
</div>
<button
@click="form.addArrayItem('contacts', { name: '', email: '', role: '' })"
>
Добавить контакт
</button>
</template>Вложенные объекты
const form = createForm(
{
name: '',
address: { street: '', city: '', zipCode: '' },
profile: { bio: '', website: '' },
},
r => ({
name: r.required(),
'address.street': r.required(),
'address.city': r.required(),
'address.zipCode': r.required().regex(/^\d{5}$/, 'ZIP: 5 цифр'),
'profile.bio': r.maxLength(200),
'profile.website': r.regex(/^https?:\/\/.+/, 'Начните с http://'),
})
)Пример компонента:
<template>
<fieldset>
<legend>Адрес</legend>
<!-- Строковые пути — просто и наглядно -->
<input
v-model="form.values.address.street"
@blur="form.touch('address.street')"
/>
<span v-if="form.hasError('address.street')">{{
form.error('address.street')
}}</span>
<!-- objectPath() — с автодополнением TypeScript -->
<input
v-model="form.values.address.city"
@blur="form.touch(form.objectPath('address', 'city'))"
/>
<span v-if="form.hasError(form.objectPath('address', 'city'))">
{{ form.error(form.objectPath('address', 'city')) }}
</span>
</fieldset>
</template>🎯 Продвинутые примеры
Условная валидация
createForm({ type: '', companyName: '' }, (r, define) =>
define({
type: r.required().oneOf(['personal', 'business']),
companyName: r.requiredIf('type', 'business'),
})
)Асинхронная проверка имени пользователя
createForm({ username: '' }, (r, define) =>
define({
username: r
.required()
.minLength(3)
.remote(
async name => !(await fetch(`/api/users/${name}`)).ok,
'Имя пользователя уже занято'
),
})
)Валидация диапазона дат
createForm({ startDate: '', endDate: '' }, (r, define) =>
define({
startDate: r.required(),
endDate: r.required().dateAfter('startDate'),
})
)Универсальная форма для создания и редактирования
Одна и та же форма для создания и редактирования. Ключевой момент — при загрузке данных используйте reset(), а не setValues(), чтобы обновить baseline и isDirty оставался false.
<script setup lang="ts">
import { onMounted, computed } from 'vue'
import { createForm } from '@sakhnovkrg/vue-form-validator'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const userId = computed(() =>
route.params.id ? Number(route.params.id) : null
)
const isEditMode = computed(() => !!userId.value)
const form = createForm(
{
name: '',
email: '',
avatar: null as File | null,
},
(r, define) =>
define({
name: r.required().minLength(2),
email: r.required().email(),
avatar: [
r.fileType(['.jpg', '.jpeg', '.png']),
r.fileSize(3 * 1024 * 1024),
],
}),
{
async onSubmit(values) {
const formData = new FormData()
formData.append('name', values.name)
formData.append('email', values.email)
if (values.avatar) formData.append('avatar', values.avatar)
const url = isEditMode.value ? `/api/users/${userId.value}` : '/api/users'
const method = isEditMode.value ? 'PUT' : 'POST'
const response = await fetch(url, { method, body: formData })
if (!response.ok) {
const data = await response.json()
form.setErrors(data.fieldErrors)
return
}
const userData = await response.json()
if (!isEditMode.value) {
await router.push(`/users/${userData.id}/edit`)
}
},
}
)
// Загрузка данных: reset() обновляет baseline, форма остаётся чистой
onMounted(async () => {
if (userId.value) {
const { name, email } = await fetch(`/api/users/${userId.value}`).then(r =>
r.json()
)
form.reset({ name, email })
}
})
</script>
<template>
<form @submit.prevent="form.submit">
<input
v-model="form.values.name"
@blur="form.touch('name')"
placeholder="Имя"
/>
<span v-if="form.hasError('name')">{{ form.error('name') }}</span>
<input
v-model="form.values.email"
@blur="form.touch('email')"
placeholder="Email"
/>
<span v-if="form.hasError('email')">{{ form.error('email') }}</span>
<input type="file" @change="form.file.avatar.handler" />
<button
type="submit"
:disabled="!form.isDirty || !form.isValid || form.isSubmitting"
>
{{
form.isSubmitting
? 'Сохранение...'
: isEditMode
? 'Сохранить'
: 'Создать'
}}
</button>
</form>
</template>Установка ошибок полям
const form = createForm({ username: '', email: '' }, (r, define) =>
define({
username: r.required().minLength(3),
email: r.required().email(),
})
)
// Установить ошибку для одного поля
form.setErrors({ username: ['Это имя пользователя уже занято'] })
// Установить ошибки для нескольких полей
form.setErrors({
username: ['Недопустимые символы в имени'],
email: ['Email уже зарегистрирован', 'Неверный формат email'],
})
// Очистить все ошибки
form.resetErrors()
// Проверить наличие ошибки
if (form.hasError('username')) {
console.log(form.error('username')) // Первая ошибка
console.log(form.allErrors('username')) // Все ошибки поля
}Типичный паттерн обработки серверных ошибок — внутри onSubmit:
const form = createForm(
{ email: '', username: '' },
(r, define) =>
define({ email: r.required().email(), username: r.required() }),
{
async onSubmit(values) {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
})
if (!res.ok) {
// Сервер возвращает: { fieldErrors: { email: ['Уже существует'] } }
const { fieldErrors } = await res.json()
if (fieldErrors) form.setErrors(fieldErrors)
return
}
console.log('Создан:', await res.json())
},
}
)⚡ Кэширование валидации
Библиотека автоматически кэширует результаты валидации для повышения производительности. Кэш очищается автоматически при:
- Изменении значения поля
- Отметке поля как затронутого (
touch) - Вызове
clearCache(fieldName)
Автоматическая очистка кэша
Кэш очищается автоматически в этих методах:
setValues()- для всех изменяемых полейtoggleArrayItem(),addArrayItem(),removeArrayItem()- для массивов- При изменении значений через
v-model
Когда нужно очищать кэш вручную
В большинстве случаев кэш очищается автоматически. Ручная очистка нужна только при:
// Прямых манипуляциях с реактивными данными (не рекомендуется)
form.val.tags.push('newItem') // вместо этого используйте addArrayItem
form.clearCache('tags') // в таких случаях нужна ручная очистка
// Крайне редких случаях отладки
form.clearCache() // очистить весь кэшРекомендация: Используйте встроенные методы (setValues, addArrayItem, etc.) - они автоматически управляют кэшем.
Пример реальной проблемы: При удалении всех элементов из массива через splice() напрямую, кэш может содержать старый результат валидации. Решение - использовать removeArrayItem() или очистить кэш вручную.
🧪 Разработка
Запуск playground
npm run devОткрывает development playground с живыми примерами на http://localhost:3000
Тесты
npm test # Запустить тесты
npm run test -- --coverage # Запустить тесты с покрытиемСборка
npm run build # Собрать библиотеку и типы
npm run build:playground # Собрать playground для деплоя
npm run preview # Предпросмотр собранного playground