Библиотека валидации Vest строится на концепции декларативных тестовых «сьютов», которые выполняются синхронно или асинхронно и возвращают структурированный результат валидации. В связке с Angular основной задачей становится корректное отображение результатов Vest в модель реактивных форм и механизм Angular Validators.
Ключевая сложность интеграции заключается в различии подходов:
ValidationErrors | nullЭти различия требуют создания адаптера между системами.
Angular Reactive Forms используют структуру FormGroup,
FormControl, FormArray. Каждый контрол может
принимать валидаторы синхронного и асинхронного типа.
Vest, в свою очередь, строит проверки следующим образом:
import { suite, test, enforce } from 'vest';
const userValidation = suite((data = {}) => {
test('email', 'Некорректный email', () => {
enforce(data.email).isEmail();
});
test('password', 'Слишком короткий пароль', () => {
enforce(data.password).longerThan(6);
});
});
Результат выполнения содержит:
Angular требует преобразования этого результата в формат:
{
email?: { message: string },
password?: { message: string }
}
Основной подход заключается в создании функции-обёртки, которая
выполняет сьют Vest и преобразует результат в
ValidationErrors.
import { AbstractControl, ValidationErrors } from '@angular/forms';
export function vestValidator(suite: any) {
return (control: AbstractControl): ValidationErrors | null => {
const result = suite(control.value);
if (result.hasErrors()) {
return mapVestErrors(result.getErrors());
}
return null;
};
}
Функция mapVestErrors преобразует структуру Vest:
function mapVestErrors(errors: any): ValidationErrors {
const mapped: ValidationErrors = {};
Object.keys(errors).forEach((field) => {
mapped[field] = {
vestError: errors[field][0],
};
});
return mapped;
}
При работе с группами полей важно передавать весь объект формы в Vest, а не отдельные контролы.
this.form = new FormGroup({
email: new FormControl(''),
password: new FormControl(''),
});
Подписка на изменения:
this.form.valueChanges.subscribe((value) => {
const result = userValidation(value);
this.applyVestResult(result);
});
Angular не принимает ошибки напрямую на уровне FormGroup
как структурированные поля Vest, поэтому требуется ручное
распределение.
private applyVestResult(result: any) {
Object.keys(this.form.controls).forEach((key) => {
const control = this.form.get(key);
if (!control) return;
const fieldErrors = result.getErrors(key);
if (fieldErrors) {
control.setErrors({
vest: fieldErrors[0],
});
} else {
control.setErrors(null);
}
});
}
Vest поддерживает асинхронные проверки через test с
Promise:
test('username', 'Имя занято', async () => {
await delay(300);
enforce(await isUsernameTaken(data.username)).equals(false);
});
Angular требует использование AsyncValidatorFn.
import { AbstractControl, AsyncValidatorFn } from '@angular/forms';
export function vestAsyncValidator(suite: any): AsyncValidatorFn {
return (control: AbstractControl) => {
return new Promise((resolve) => {
const result = suite(control.value);
if (result.hasErrors()) {
resolve(mapVestErrors(result.getErrors()));
} else {
resolve(null);
}
});
};
}
Важно учитывать, что Angular ожидает Observable или Promise, поэтому синхронизация с Vest должна учитывать debounce на уровне формы.
Проблема частых вызовов Vest при каждом valueChanges
требует оптимизации.
Используется комбинация RxJS операторов:
this.form.valueChanges
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe((value) => {
const result = userValidation(value);
this.applyVestResult(result);
});
Такой подход снижает нагрузку при сложных сьютах.
В больших формах целесообразно разделять проверку:
test('passwordMatch', 'Пароли не совпадают', () => {
enforce(data.password).equals(data.confirmPassword);
});
В Angular это применяется на уровне FormGroup:
this.form.setErrors({
vestGroup: 'passwordMismatch',
});
Для масштабируемых приложений логика обёртки выносится в сервис.
@Injectable({ providedIn: 'root' })
export class VestValidationService {
run(suite: any, data: any) {
return suite(data);
}
map(result: any) {
return mapVestErrors(result.getErrors());
}
}
Использование:
const result = this.vestService.run(userValidation, this.form.value);
this.errors = this.vestService.map(result);
Angular позволяет комбинировать валидаторы:
this.form = new FormGroup({
email: new FormControl('', [
Validators.required,
vestValidator(userValidation)
]),
});
Проблема заключается в конфликте приоритетов: Angular встроенные валидаторы могут перезаписывать ошибки Vest. Решение — объединение ошибок:
const existingErrors = control.errors || {};
const vestErrors = mapVestErrors(result.getErrors());
control.setErrors({
...existingErrors,
...vestErrors,
});
Если данные формы обновляются извне (например, через API), необходимо повторно запускать Vest:
patchUser(data: any) {
this.form.patchValue(data);
const result = userValidation(this.form.value);
this.applyVestResult(result);
}
Это предотвращает рассинхронизацию UI и состояния валидации.
При большом количестве полей возникает необходимость централизованного хранения ошибок.
Подход:
vestErrors$ = new BehaviorSubject<any>(null);
this.form.valueChanges.subscribe((value) => {
const result = userValidation(value);
this.vestErrors$.next(result.getErrors());
});
Angular поддерживает динамическое создание контролов, Vest требует адаптации структуры данных.
addField(name: string) {
this.form.addControl(name, new FormControl(''));
}
Сьют должен учитывать динамические ключи:
test('dynamicField', () => {
enforce(data[name]).isNotEmpty();
});
Для строгой типизации используется обобщённая модель:
type VestFieldErrors<T> = {
[K in keyof T]?: {
vestError: string;
};
};
Это позволяет согласовать Angular FormModel и результат проверки.
В некоторых случаях Vest возвращает несколько ошибок на одно поле. Angular же хранит одну активную ошибку.
Стратегии обработки:
Пример агрегации:
mapped[field] = {
vestError: errors[field].join(', '),
};
При формах с сотнями контролов выполнение полного Vest-сьюта становится узким местом.
Используются подходы:
const memoizedSuite = memoize(userValidation);
Также применяется условное выполнение тестов:
onlyIf(data.email);
Интеграция может быть инкапсулирована в директиву:
@Directive({
selector: '[vestValidate]',
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => VestDirective),
multi: true,
}]
})
export class VestDirective implements Validator {
validate(control: AbstractControl) {
const result = userValidation(control.value);
return result.hasErrors()
? mapVestErrors(result.getErrors())
: null;
}
}
Такой подход позволяет использовать Vest декларативно в шаблонах.
Angular управляет жизненным циклом компонентов через hooks:
ngOnInitngOnChangesngOnDestroyVest не имеет собственного жизненного цикла, поэтому важно:
ngOnDestroy() {
this.sub.unsubscribe();
}
Часто сервер возвращает ошибки, которые должны отображаться вместе с клиентской валидацией.
this.form.setErrors({
server: 'Email already exists',
});
При этом Vest-ошибки и серверные объединяются:
control.setErrors({
...serverErrors,
...vestErrors,
});
В крупных приложениях интеграция Vest и Angular требует архитектурного разделения:
Такое разделение снижает связность и позволяет переиспользовать сьюты вне Angular-контекста, включая Node.js или тестирование.