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

Vue предоставляет реактивную модель данных и механизм композиции логики через Composition API, что делает его удобной средой для интеграции схем валидации, построенных на чистых функциях. Vest хорошо ложится на этот подход благодаря декларативному описанию правил и отсутствию жёсткой привязки к UI-слою.

Ключевая идея интеграции заключается в разделении трёх уровней:

  • реактивное состояние формы (Vue)
  • правила валидации (Vest)
  • отображение ошибок (UI-компоненты)

Такое разделение позволяет избегать смешивания логики ввода и бизнес-валидации.


Базовая интеграция через Composition API

Наиболее прямой способ подключения Vest в Vue — использование computed и watch для запуска проверок при изменении состояния формы.

import { reactive, computed, watch } from 'vue';
import { create, test, enforce } from 'vest';

const validationSuite = create((data) => {
  test('username', 'Имя обязательно', () => {
    enforce(data.username).isNotEmpty();
  });

  test('password', 'Пароль слишком короткий', () => {
    enforce(data.password).longerThan(5);
  });
});

export function useLoginForm() {
  const form = reactive({
    username: '',
    password: ''
  });

  const result = computed(() => validationSuite(form));

  const errors = computed(() => result.value.getErrors());

  watch(form, () => {
    validationSuite(form);
  }, { deep: true });

  return {
    form,
    errors
  };
}

В этом подходе Vue отвечает за реактивность, а Vest — за вычисление состояния валидации.


Разделение состояния: data vs validation state

При интеграции важно разделять:

  • form state — данные пользователя
  • validation state — результат выполнения suite

Пример структуры:

const state = reactive({
  form: {
    email: '',
    password: ''
  },
  validation: null
});

Инициализация валидации через watcher:

watch(
  () => state.form,
  (newValue) => {
    state.validation = validationSuite(newValue);
  },
  { deep: true, immediate: true }
);

Такой подход упрощает синхронизацию состояния без лишних вычислений в шаблоне.


Использование computed для ленивой валидации

Vue позволяет избегать лишних вызовов через computed, но в случае Vest важно понимать: suite может быть тяжёлой операцией при сложных формах.

Оптимизированный вариант:

const validation = computed(() => {
  return validationSuite(form);
});

При таком подходе пересчёт происходит только при изменении зависимостей.


Интеграция с полями формы

Каждое поле формы может получать ошибки через вспомогательные функции.

function getFieldErrors(result, field) {
  return result.getErrors(field) || [];
}

Использование в компоненте:

const emailErrors = computed(() =>
  getFieldErrors(validation.value, 'email')
);

Шаблон Vue:

<input v-model="form.email" />
<div v-if="emailErrors.length">
  <p v-for="err in emailErrors" :key="err">
    {{ err }}
  </p>
</div>

Асинхронная валидация

Vest поддерживает асинхронные проверки, что критично при интеграции с API.

test('email', 'Email уже занят', async () => {
  const response = await fetch(`/api/check-email?value=${data.email}`);
  const result = await response.json();

  enforce(result.available).isTruthy();
});

Во Vue важно учитывать состояние загрузки:

const state = reactive({
  loading: false
});

watch(
  () => form.email,
  async () => {
    state.loading = true;
    await validationSuite(form);
    state.loading = false;
  }
);

Композиционный подход: useValidation

Для масштабируемых приложений логично вынести логику в отдельный composable.

import { reactive, watch } from 'vue';

export function useValidation(suite, form) {
  const state = reactive({
    result: null,
    errors: {}
  });

  watch(
    form,
    () => {
      state.result = suite(form);
      state.errors = state.result.getErrors();
    },
    { deep: true, immediate: true }
  );

  return state;
}

Использование:

const { result, errors } = useValidation(validationSuite, form);

Работа с группами полей

Vest позволяет группировать проверки, что удобно для сложных форм.

test('password', 'Пароли не совпадают', () => {
  enforce(data.password)
    .equals(data.confirmPassword);
});

Во Vue это часто связывается с computed-логикой:

const passwordsMatch = computed(() =>
  form.password === form.confirmPassword
);

Однако предпочтительнее держать правило в Vest, чтобы не дублировать логику.


Оптимизация производительности

При больших формах важно учитывать:

1. Изоляция изменений

Использование watch на конкретные поля:

watch(() => form.email, () => {
  validationSuite(form);
});

2. Debounce проверок

import { debounce } from 'lodash-es';

const runValidation = debounce(() => {
  validationSuite(form);
}, 300);

watch(form, runValidation, { deep: true });

3. Кэширование результатов Vest

Vest уже кэширует результаты внутри suite, что снижает нагрузку при повторных вызовах.


Интеграция с v-model и кастомными компонентами

При использовании кастомных компонентов Vue:

<BaseInput v-model="form.username" />

Ошибки передаются через props:

<BaseInput
  v-model="form.username"
  :errors="getFieldErrors(validation, 'username')"
/>

Компонент:

export default {
  props: {
    modelValue: String,
    errors: Array
  }
};

Обработка состояния touched и dirty

Vest не управляет UI-состоянием взаимодействия, поэтому это реализуется во Vue:

const state = reactive({
  touched: {}
});

function markTouched(field) {
  state.touched[field] = true;
}

Использование:

<input
  v-model="form.email"
  @blur="markTouched('email')"
/>

Отображение ошибок:

const showError = (field) =>
  state.touched[field] && errors[field]?.length;

Интеграция с TypeScript-типизацией

При использовании TypeScript в Vue можно типизировать форму и suite:

type Form = {
  email: string;
  password: string;
};

const form = reactive<Form>({
  email: '',
  password: ''
});

И типизированная валидация:

const suite = create((data: Form) => {
  test('email', () => {
    enforce(data.email).isNotEmpty();
  });
});

Масштабируемая структура проекта

Типичная организация интеграции:

/validation
  loginSuite.js
  registerSuite.js

/composables
  useValidation.js
  useLoginForm.js

/components
  BaseInput.vue
  LoginForm.vue

Такой подход снижает связанность между UI и правилами проверки и упрощает повторное использование логики валидации между компонентами Vue.