← Курс/VeeValidate: валидация форм#254 из 257+25 XP

VeeValidate: валидация форм

Зачем нужен VeeValidate

Валидация форм — повторяющаяся и трудоёмкая задача. **VeeValidate** — наиболее популярная библиотека валидации для Vue 3. Она предоставляет два подхода: компонентный (через <Field> и <Form>) и composable-API. Современный подход — через Composition API.

npm install vee-validate
# Опционально — схемы валидации:
npm install yup
# или
npm install zod

Базовое использование: useField и useForm

import { useForm, useField } from 'vee-validate'

const { handleSubmit, errors, isSubmitting } = useForm({
  validationSchema: {
    email: (val) => {
      if (!val) return 'Email обязателен'
      if (!/^[^@]+@[^@]+.[^@]+$/.test(val)) return 'Некорректный email'
      return true  // валидация прошла
    },
    password: (val) => {
      if (!val) return 'Пароль обязателен'
      if (val.length < 8) return 'Минимум 8 символов'
      return true
    },
  },
})

const { value: email, errorMessage: emailError } = useField('email')
const { value: password, errorMessage: passwordError } = useField('password')

const onSubmit = handleSubmit((values) => {
  console.log('Форма валидна:', values)
  // Отправляем данные
})

Шаблон

<template>
  <form @submit="onSubmit">
    <div>
      <input v-model="email" type="email" placeholder="Email" />
      <span class="error">{{ emailError }}</span>
    </div>
    <div>
      <input v-model="password" type="password" placeholder="Пароль" />
      <span class="error">{{ passwordError }}</span>
    </div>
    <button :disabled="isSubmitting" type="submit">
      {{ isSubmitting ? 'Загрузка...' : 'Войти' }}
    </button>
  </form>
</template>

Интеграция с Yup

**Yup** позволяет описывать схемы валидации декларативно:

import { useForm } from 'vee-validate'
import * as yup from 'yup'

const schema = yup.object({
  name: yup.string().required('Имя обязательно').min(2, 'Минимум 2 символа'),
  email: yup.string().required('Email обязателен').email('Некорректный email'),
  age: yup.number().min(18, 'Минимальный возраст 18 лет').max(120),
  password: yup.string()
    .required('Пароль обязателен')
    .min(8, 'Минимум 8 символов')
    .matches(/[A-Z]/, 'Нужна хотя бы одна заглавная буква'),
  confirmPassword: yup.string()
    .oneOf([yup.ref('password')], 'Пароли не совпадают'),
})

const { handleSubmit, errors } = useForm({ validationSchema: schema })

Интеграция с Zod

import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'

const schema = toTypedSchema(z.object({
  email: z.string().email('Некорректный email'),
  password: z.string().min(8, 'Минимум 8 символов'),
}))

const { handleSubmit } = useForm({ validationSchema: schema })

defineRule — глобальные правила

import { defineRule, configure } from 'vee-validate'
import { required, email, min } from '@vee-validate/rules'

// Регистрируем глобальные правила
defineRule('required', required)
defineRule('email', email)
defineRule('min', min)

// Настраиваем сообщения об ошибках
configure({
  generateMessage: (context) => {
    const messages = {
      required: `Поле "${context.field}" обязательно`,
      email: `Некорректный email`,
      min: `Минимум ${context.rule.params[0]} символов`,
    }
    return messages[context.rule.name] || 'Недопустимое значение'
  },
})

Полезные возможности

const {
  handleSubmit,
  errors,        // { field: 'сообщение об ошибке' }
  values,        // текущие значения всех полей
  resetForm,     // очистить форму и ошибки
  setFieldValue, // программно установить значение
  setErrors,     // установить серверные ошибки
  isSubmitting,  // true пока выполняется handleSubmit
  meta,          // { valid, dirty, touched, pending }
} = useForm()

// Серверные ошибки (например, 'email уже занят')
setErrors({ email: 'Этот email уже зарегистрирован' })

Примеры

Реализация движка валидации форм с поддержкой правил, схем и сообщений об ошибках — аналог ядра VeeValidate

// ============================================
// Движок валидации форм
// (упрощённая модель VeeValidate)
// ============================================

// Встроенные правила валидации
const rules = {
  required: (value) => {
    if (value === null || value === undefined || value === '') {
      return 'Это поле обязательно'
    }
    return true
  },

  email: (value) => {
    if (!value) return true  // required обрабатывает пустоту
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    return re.test(value) ? true : 'Некорректный email'
  },

  min: (value, [minLength]) => {
    if (!value) return true
    return String(value).length >= Number(minLength)
      ? true
      : `Минимум ${minLength} символов`
  },

  max: (value, [maxLength]) => {
    if (!value) return true
    return String(value).length <= Number(maxLength)
      ? true
      : `Максимум ${maxLength} символов`
  },

  matches: (value, [pattern, message]) => {
    if (!value) return true
    const re = new RegExp(pattern)
    return re.test(value) ? true : (message || 'Некорректный формат')
  },

  minValue: (value, [min]) => {
    if (value === '' || value === null) return true
    return Number(value) >= Number(min)
      ? true
      : `Минимальное значение: ${min}`
  },
}

// ============================================
// Валидация одного поля
// ============================================

function validateField(value, fieldRules) {
  if (!fieldRules) return null

  // fieldRules может быть функцией или массивом правил
  if (typeof fieldRules === 'function') {
    const result = fieldRules(value)
    return result === true ? null : result
  }

  // Обрабатываем массив правил: ['required', 'email', ['min', 8]]
  for (const rule of fieldRules) {
    let ruleName, params
    if (Array.isArray(rule)) {
      [ruleName, ...params] = rule
    } else {
      ruleName = rule
      params = []
    }

    const validator = rules[ruleName]
    if (!validator) {
      console.warn(`Правило не найдено: "${ruleName}"`)
      continue
    }

    const result = validator(value, params)
    if (result !== true) return result  // первая ошибка
  }

  return null  // валидация прошла
}

// ============================================
// Класс формы (аналог useForm)
// ============================================

class Form {
  constructor(schema) {
    this.schema = schema
    this.values = {}
    this.errors = {}
    this.touched = {}

    // Инициализируем значения по умолчанию
    for (const field of Object.keys(schema)) {
      this.values[field] = ''
      this.errors[field] = null
      this.touched[field] = false
    }
  }

  setValue(field, value) {
    this.values[field] = value
    this.touched[field] = true
    // Валидируем поле при изменении
    this.errors[field] = validateField(value, this.schema[field])
  }

  validateAll() {
    let isValid = true
    for (const [field, rules] of Object.entries(this.schema)) {
      this.errors[field] = validateField(this.values[field], rules)
      this.touched[field] = true
      if (this.errors[field]) isValid = false
    }
    return isValid
  }

  submit(onSuccess) {
    const isValid = this.validateAll()
    if (isValid) {
      onSuccess({ ...this.values })
    } else {
      console.log('  Форма содержит ошибки, отправка отменена')
    }
    return isValid
  }

  reset() {
    for (const field of Object.keys(this.schema)) {
      this.values[field] = ''
      this.errors[field] = null
      this.touched[field] = false
    }
  }

  get isValid() {
    return Object.values(this.errors).every(e => e === null)
  }
}

// ============================================
// Демонстрация
// ============================================

console.log('=== Форма регистрации ===')

const form = new Form({
  name: ['required', ['min', 2], ['max', 50]],
  email: ['required', 'email'],
  password: [
    'required',
    ['min', 8],
    ['matches', '[A-Z]', 'Нужна хотя бы одна заглавная буква'],
  ],
  age: ['required', ['minValue', 18]],
})

// Симуляция пользовательского ввода
console.log('\nПользователь вводит данные:')

form.setValue('name', 'A')  // слишком короткое
console.log('name error:', form.errors.name)

form.setValue('name', 'Alice')
console.log('name error:', form.errors.name)  // null

form.setValue('email', 'не-email')
console.log('email error:', form.errors.email)

form.setValue('email', 'alice@example.com')
console.log('email error:', form.errors.email)  // null

form.setValue('password', 'weak')
console.log('password error:', form.errors.password)

form.setValue('password', 'StrongPass1')
console.log('password error:', form.errors.password)  // null

form.setValue('age', '15')
console.log('age error:', form.errors.age)

form.setValue('age', '25')
console.log('age error:', form.errors.age)  // null

console.log('\nОтправка формы:')
form.submit((values) => {
  console.log('Форма отправлена!', values)
})

console.log('\nПопытка отправить с ошибкой:')
form.setValue('email', 'bad-email')
form.submit((values) => {
  console.log('Это не должно вывестись')
})
console.log('Ошибки:', form.errors)