← Курс/Работа с формами и валидация#219 из 257+25 XP

Работа с формами и валидация

Базовая структура формы

В Vue форма строится на связке v-model + реактивное состояние + обработчик отправки. Модификатор @submit.prevent блокирует стандартное поведение браузера (перезагрузку страницы):

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model.trim="form.name" type="text" placeholder="Имя">
    <input v-model.trim="form.email" type="email" placeholder="Email">
    <input v-model.number="form.age" type="number" placeholder="Возраст">
    <button type="submit">Отправить</button>
  </form>
</template>

<script setup>
import { reactive } from 'vue'

const form = reactive({ name: '', email: '', age: 0 })

function handleSubmit() {
  console.log('Данные формы:', { ...form })
}
</script>

Реактивные ошибки валидации

Храним ошибки в отдельном реактивном объекте:

<script setup>
import { reactive, computed } from 'vue'

const form = reactive({ name: '', email: '', password: '' })
const errors = reactive({ name: '', email: '', password: '' })
const touched = reactive({ name: false, email: false, password: false })
</script>

Computed-правила валидации

Валидацию удобно описывать через computed-свойства:

<script setup>
import { reactive, computed } from 'vue'

const form = reactive({ name: '', email: '', password: '' })

const validationRules = computed(() => ({
  name: [
    { test: form.name.length >= 2, message: 'Имя должно быть не менее 2 символов' },
    { test: /^[а-яёА-ЯЁa-zA-Z\s]+$/.test(form.name), message: 'Только буквы' },
  ],
  email: [
    { test: form.email.includes('@'), message: 'Введите корректный email' },
    { test: form.email.length > 0, message: 'Email обязателен' },
  ],
  password: [
    { test: form.password.length >= 8, message: 'Минимум 8 символов' },
    { test: /[0-9]/.test(form.password), message: 'Должна содержать цифры' },
  ],
}))

const fieldErrors = computed(() => {
  const result = {}
  for (const [field, rules] of Object.entries(validationRules.value)) {
    const failed = rules.find(rule => !rule.test)
    result[field] = failed ? failed.message : ''
  }
  return result
})

const isFormValid = computed(() =>
  Object.values(fieldErrors.value).every(err => err === '')
)
</script>

Показ ошибок только после взаимодействия

Не стоит показывать ошибки сразу при открытии формы. Используем флаг touched:

<template>
  <input
    v-model.trim="form.name"
    @blur="touched.name = true"
    :class="{ 'is-error': touched.name && fieldErrors.name }"
  >
  <p v-if="touched.name && fieldErrors.name" class="error-msg">
    {{ fieldErrors.name }}
  </p>
</template>

Сброс формы

function resetForm() {
  Object.assign(form, { name: '', email: '', password: '' })
  Object.assign(touched, { name: false, email: false, password: false })
}

Обработка отправки

const isSubmitting = ref(false)
const submitError = ref('')

async function handleSubmit() {
  // Помечаем все поля как touched для показа ошибок
  Object.keys(touched).forEach(key => { touched[key] = true })

  if (!isFormValid.value) return

  isSubmitting.value = true
  submitError.value = ''

  try {
    await api.submitForm({ ...form })
    resetForm()
    console.log('Форма успешно отправлена!')
  } catch (err) {
    submitError.value = err.message
  } finally {
    isSubmitting.value = false
  }
}

Примеры

Полная система валидации форм: правила, ошибки, touched-флаги и обработка отправки

// Реализуем систему валидации форм — аналог того что делает Vue через reactive + computed

// Движок валидации — принимает данные и описание правил
function createValidator(rules) {
  return function validate(data) {
    const errors = {}
    let isValid = true

    for (const [field, fieldRules] of Object.entries(rules)) {
      const value = data[field] ?? ''
      let fieldError = ''

      for (const rule of fieldRules) {
        const passed = rule.test(value, data)
        if (!passed) {
          fieldError = rule.message
          break  // первая ошибка поля
        }
      }

      errors[field] = fieldError
      if (fieldError) isValid = false
    }

    return { errors, isValid }
  }
}

// Создаём правила для формы регистрации
const registrationRules = {
  username: [
    { test: v => v.length >= 3, message: 'Имя пользователя: минимум 3 символа' },
    { test: v => v.length <= 20, message: 'Имя пользователя: максимум 20 символов' },
    { test: v => /^[a-zA-Z0-9_]+$/.test(v), message: 'Только латиница, цифры и _' },
  ],
  email: [
    { test: v => v.length > 0, message: 'Email обязателен' },
    { test: v => v.includes('@') && v.includes('.'), message: 'Некорректный email' },
  ],
  password: [
    { test: v => v.length >= 8, message: 'Пароль: минимум 8 символов' },
    { test: v => /[A-Z]/.test(v), message: 'Пароль должен содержать заглавную букву' },
    { test: v => /[0-9]/.test(v), message: 'Пароль должен содержать цифру' },
  ],
  passwordConfirm: [
    { test: v => v.length > 0, message: 'Подтвердите пароль' },
    { test: (v, data) => v === data.password, message: 'Пароли не совпадают' },
  ],
}

const validate = createValidator(registrationRules)

// Эмуляция состояния формы и touched-флагов
let formData = { username: '', email: '', password: '', passwordConfirm: '' }
let touched = { username: false, email: false, password: false, passwordConfirm: false }

function getVisibleErrors(formData, touched) {
  const { errors } = validate(formData)
  const visible = {}
  for (const field of Object.keys(errors)) {
    visible[field] = touched[field] ? errors[field] : ''
  }
  return visible
}

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

console.log('1. Пустая форма (пользователь ещё ничего не трогал):')
console.log('   Видимые ошибки:', getVisibleErrors(formData, touched))
// Все пустые — touched = false

console.log('\n2. Пользователь тронул поле username и ничего не ввёл:')
touched.username = true
console.log('   Видимые ошибки:', getVisibleErrors(formData, touched))
// username: 'Имя пользователя: минимум 3 символа'

console.log('\n3. Ввёл короткое имя "ab":')
formData.username = 'ab'
console.log('   Видимые ошибки:', getVisibleErrors(formData, touched))

console.log('\n4. Ввёл корректное имя "alexey_123":')
formData.username = 'alexey_123'
console.log('   Видимые ошибки:', getVisibleErrors(formData, touched))
// username: '' (нет ошибки)

console.log('\n5. Полностью заполненная корректная форма:')
formData = {
  username: 'alexey_123',
  email: 'alexey@example.com',
  password: 'SecurePass1',
  passwordConfirm: 'SecurePass1',
}
touched = { username: true, email: true, password: true, passwordConfirm: true }

const { errors, isValid } = validate(formData)
console.log('   Ошибки:', errors)
console.log('   Форма валидна:', isValid)

console.log('\n6. Пароли не совпадают:')
formData.passwordConfirm = 'WrongPass'
const result = validate(formData)
console.log('   passwordConfirm error:', result.errors.passwordConfirm)
console.log('   Форма валидна:', result.isValid)