Сложные формы: Quasar, VeeValidate и schema-driven validation

↑ 5.3 Quasar · ← Предыдущая

Обязательно~4 мин чтенияУровень: middle

Зачем это на собесе

Для больших форм (вложенные структуры, зависимые поля, массивы) нужна схема, а не десятки inline-правил.

Объяснение

Схема-first: правила описываются в одной схеме (Zod/Valibot/Yup), а форма получает валидацию, типы и значения по умолчанию.

import { useForm, useFieldArray } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";
 
const schema = toTypedSchema(z.object({
  customer: z.object({ name: z.string().min(2, "Минимум 2 символа"), email: z.string().email("Неверный email") }),
  delivery: z.enum(["pickup", "courier"]),
  address: z.string().optional(),
  items: z.array(z.object({ sku: z.string().min(1), qty: z.number().int().min(1) })).min(1, "Добавьте позицию"),
}).superRefine((v, ctx) => {                                        // кросс-полевые проверки
  if (v.delivery === "courier" && !v.address) ctx.addIssue({ code: "custom", path: ["address"], message: "Укажите адрес" });
}));
 
const { handleSubmit, errors, defineField, setErrors, meta, isSubmitting, resetForm } = useForm({ validationSchema: schema, initialValues: { items: [{ sku: "", qty: 1 }] } });
const [name, nameAttrs] = defineField("customer.name", { validateOnModelUpdate: false });
const { fields: items, push, remove } = useFieldArray<{ sku: string; qty: number }>("items");
 
const onSubmit = handleSubmit(async (values) => {                    // values типизированы по схеме
  try { await api.createOrder(values); } catch (e) { setErrors(mapServerErrors(e)); }
});
<q-form @submit.prevent="onSubmit">
  <q-input v-model="name" v-bind="nameAttrs" label="Имя" :error="!!errors['customer.name']" :error-message="errors['customer.name']" />
  <div v-for="(f, i) in items" :key="f.key">
    <q-input v-model.number="f.value.qty" type="number" :error-message="errors[`items[${i}].qty`]" :error="!!errors[`items[${i}].qty`]" />
    <q-btn icon="delete" @click="remove(i)" />
  </div>
  <q-btn label="Добавить" @click="push({ sku: '', qty: 1 })" />
  <q-btn type="submit" color="primary" :loading="isSubmitting" :disable="!meta.dirty" />
</q-form>

Возможности: состояния (dirty, touched, valid, pending), вложенные объекты и массивы, зависимые поля, серверные ошибки (setErrors), сброс/восстановление, defineField с компонентными атрибутами.

Практики:

  • Одна схема на форму, повторно используемая на клиенте и (по возможности) синхронизированная с бэкендом (OpenAPI → Zod).
  • Значения по умолчанию — часть модели; нормализация данных до отправки (z.coerce, .transform).
  • Защита от потери данных: предупреждение при уходе (onBeforeRouteLeave и meta.dirty).
  • Автосохранение черновика (debounce) в стор/localStorage.
  • Доступность: связывание ошибок с полями (aria-describedby), фокус на первом ошибочном поле после submit.
  • Мастер-формы (stepper): валидация по шагам через validate({ mode })/отдельные схемы.

Альтернативы: FormKit, Vuelidate (валидация без схем), TanStack Form, @formkit/....

Нюансы и подводные камни

  • Расхождение схемы клиента и правил сервера — согласуйте контракт.
  • Слишком частая валидация при каждом вводе раздражает: validateOnModelUpdate: false, валидация при blur.
  • Массивы: используйте стабильные key (f.key), не индекс.
  • Большие схемы замедляют ввод: разбивайте на части.
  • Тестируйте форму через Testing Library (заполнение → submit → ошибки).

Практика

  1. Соберите форму заказа с вложенным объектом, массивом позиций и зависимым полем адреса.
  2. Подключите отображение ошибок сервера по полям.
  3. Добавьте защиту от потери несохранённых данных и автосохранение черновика.

Вопросы с ответами

Связанные темы