Home » Создание пользовательской валидации в Angular
Создание пользовательской валидации в Angular

Создание пользовательской валидации в Angular

Каждый фронтенд-разработчик рано или поздно сталкивается с необходимостью создать валидацию, которая выходит за рамки стандартных решений Angular. Встроенные валидаторы хороши для базовых проверок, но когда нужно проверить уникальность email’а на сервере, валидировать сложные бизнес-правила или создать специфичную логику для конкретного проекта — тут без пользовательских валидаторов не обойтись. Особенно это актуально для серверных приложений, где часто требуется интеграция с различными API и сложная логика обработки форм.

Как работает пользовательская валидация в Angular

Валидатор в Angular — это простая функция, которая принимает AbstractControl и возвращает либо null (если валидация прошла успешно), либо объект с описанием ошибки. Звучит просто, но дьявол кроется в деталях.

Существует два типа пользовательских валидаторов:

  • Синхронные валидаторы — выполняются мгновенно и возвращают результат сразу
  • Асинхронные валидаторы — возвращают Promise или Observable, идеально подходят для проверки данных на сервере

Базовая структура синхронного валидатора:


export function customValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;

if (!value || someValidationLogic(value)) {
return null; // валидация прошла успешно
}

return { 'customError': { value: value } }; // ошибка валидации
}

Пошаговая настройка пользовательской валидации

Начнем с простого примера — создадим валидатор для проверки силы пароля:


// validators/password-strength.validator.ts
import { AbstractControl, ValidationErrors } from '@angular/forms';

export function passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;

if (!value) {
return null; // не валидируем пустые значения
}

const hasNumber = /[0-9]/.test(value);
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(value);
const minLength = value.length >= 8;

const passwordValid = hasNumber && hasUpperCase && hasLowerCase && hasSpecialChar && minLength;

if (!passwordValid) {
return {
'passwordStrength': {
hasNumber: hasNumber,
hasUpperCase: hasUpperCase,
hasLowerCase: hasLowerCase,
hasSpecialChar: hasSpecialChar,
minLength: minLength
}
};
}

return null;
}

Теперь подключим этот валидатор к форме:


// app.component.ts
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { passwordStrengthValidator } from './validators/password-strength.validator';

@Component({
selector: 'app-root',
template: `

Пароль должен содержать:

  • Цифры
  • Заглавные буквы
  • Строчные буквы
  • Спецсимволы
  • Минимум 8 символов

`
})
export class AppComponent {
userForm: FormGroup;

constructor(private fb: FormBuilder) {
this.userForm = this.fb.group({
password: ['', [Validators.required, passwordStrengthValidator]]
});
}
}

Асинхронная валидация: проверка на сервере

Для серверных приложений часто нужна проверка уникальности данных. Вот пример асинхронного валидатора для проверки email’а:


// validators/email-unique.validator.ts
import { Injectable } from '@angular/core';
import { AbstractControl, AsyncValidator, ValidationErrors } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
})
export class EmailUniqueValidator implements AsyncValidator {

constructor(private http: HttpClient) {}

validate(control: AbstractControl): Observable {
if (!control.value) {
return of(null);
}

return of(control.value).pipe(
debounceTime(300), // задержка для уменьшения количества запросов
switchMap(email =>
this.http.get<{exists: boolean}>(`/api/check-email?email=${email}`)
),
map(response => response.exists ? { 'emailTaken': true } : null),
catchError(() => of(null)) // игнорируем ошибки сети
);
}
}

Использование асинхронного валидатора:


// app.component.ts
export class AppComponent {
userForm: FormGroup;

constructor(
private fb: FormBuilder,
private emailValidator: EmailUniqueValidator
) {
this.userForm = this.fb.group({
email: ['',
[Validators.required, Validators.email], // синхронные валидаторы
[this.emailValidator.validate.bind(this.emailValidator)] // асинхронный валидатор
]
});
}
}

Практические примеры и кейсы

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

1. Валидатор для проверки доменного имени


export function domainValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;

if (!value) return null;

const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;

if (!domainRegex.test(value)) {
return { 'invalidDomain': true };
}

// Проверка длины доменного имени
if (value.length > 253) {
return { 'domainTooLong': true };
}

return null;
}

2. Валидатор для проверки IP-адреса


export function ipAddressValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;

if (!value) return null;

const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;

if (!ipv4Regex.test(value) && !ipv6Regex.test(value)) {
return { 'invalidIp': true };
}

return null;
}

3. Валидатор для проверки порта


export function portValidator(control: AbstractControl): ValidationErrors | null {
const value = control.value;

if (!value) return null;

const port = parseInt(value, 10);

if (isNaN(port) || port < 1 || port > 65535) {
return { 'invalidPort': true };
}

// Проверка на зарезервированные порты
const reservedPorts = [22, 23, 25, 53, 80, 110, 143, 443, 993, 995];
if (reservedPorts.includes(port)) {
return { 'reservedPort': true };
}

return null;
}

Сравнение подходов к валидации

Подход Производительность Сложность реализации Переиспользование Тестирование
Встроенные валидаторы Отличная Низкая Высокое Простое
Пользовательские синхронные Хорошая Средняя Высокое Средней сложности
Пользовательские асинхронные Зависит от сервера Высокая Высокое Сложное
Директивы валидации Хорошая Высокая Среднее Сложное

Продвинутые техники и оптимизация

Для высоконагруженных приложений важно оптимизировать валидацию. Вот несколько полезных техник:

1. Кеширование результатов асинхронной валидации


@Injectable({
providedIn: 'root'
})
export class CachedEmailValidator implements AsyncValidator {
private cache = new Map();

constructor(private http: HttpClient) {}

validate(control: AbstractControl): Observable {
const email = control.value;

if (!email) return of(null);

// Проверяем кеш
if (this.cache.has(email)) {
const exists = this.cache.get(email);
return of(exists ? { 'emailTaken': true } : null);
}

return this.http.get<{exists: boolean}>(`/api/check-email?email=${email}`).pipe(
map(response => {
this.cache.set(email, response.exists); // сохраняем в кеш
return response.exists ? { 'emailTaken': true } : null;
}),
catchError(() => of(null))
);
}
}

2. Композиция валидаторов


export function createServerConfigValidator() {
return function(control: AbstractControl): ValidationErrors | null {
const errors: ValidationErrors = {};

// Проверяем каждое поле конфигурации
const domainError = domainValidator(control.get('domain')!);
const ipError = ipAddressValidator(control.get('ip')!);
const portError = portValidator(control.get('port')!);

if (domainError) errors['domain'] = domainError;
if (ipError) errors['ip'] = ipError;
if (portError) errors['port'] = portError;

return Object.keys(errors).length ? errors : null;
};
}

Интеграция с другими инструментами

Пользовательские валидаторы отлично интегрируются с популярными библиотеками:

С библиотекой Yup


import * as yup from 'yup';

export function yupValidator(schema: yup.Schema) {
return function(control: AbstractControl): ValidationErrors | null {
try {
schema.validateSync(control.value);
return null;
} catch (error) {
return { 'yupError': error.message };
}
};
}

// Использование
const serverSchema = yup.object().shape({
hostname: yup.string().required().min(3).max(253),
port: yup.number().required().min(1).max(65535),
ssl: yup.boolean().required()
});

this.serverForm = this.fb.group({
config: ['', yupValidator(serverSchema)]
});

С RxJS операторами


export function debounceValidator(validator: AsyncValidatorFn, delay: number = 300): AsyncValidatorFn {
return function(control: AbstractControl): Observable {
return timer(delay).pipe(
switchMap(() => validator(control))
);
};
}

Автоматизация и скрипты

Для автоматизации создания валидаторов можно использовать Angular CLI schematics:


// Создание схемы для генерации валидаторов
ng generate schematic validator --name=custom-validator

// Генерация валидатора из схемы
ng generate validator:custom-validator --name=domain-validator

Также полезно создать скрипт для автоматического тестирования валидаторов:


// test-validators.spec.ts
describe('Custom Validators', () => {
const testCases = [
{ validator: domainValidator, valid: ['example.com', 'sub.example.com'], invalid: ['', 'invalid..domain'] },
{ validator: portValidator, valid: [8080, 3000], invalid: [0, 70000, 'abc'] }
];

testCases.forEach(({ validator, valid, invalid }) => {
valid.forEach(value => {
it(`should validate ${value} as valid`, () => {
expect(validator({ value } as AbstractControl)).toBeNull();
});
});

invalid.forEach(value => {
it(`should validate ${value} as invalid`, () => {
expect(validator({ value } as AbstractControl)).toBeTruthy();
});
});
});
});

Производительность и мониторинг

Для мониторинга производительности валидаторов можно использовать декораторы:


function measureValidation(name: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;

descriptor.value = function(...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();

console.log(`Validator ${name} took ${end - start} milliseconds`);
return result;
};
};
}

// Использование
@measureValidation('passwordStrength')
export function passwordStrengthValidator(control: AbstractControl): ValidationErrors | null {
// логика валидации
}

Интересные факты и статистика

Согласно исследованиям Google, формы с хорошей валидацией увеличивают конверсию на 25-40%. При этом:

  • Пользователи покидают форму в 67% случаев, если валидация работает медленно
  • Асинхронная валидация должна срабатывать не позднее 300ms для комфортного UX
  • Кеширование результатов валидации может снизить нагрузку на сервер до 80%

Для развертывания Angular-приложений с продвинутой валидацией рекомендую использовать VPS-серверы с предустановленным Node.js, а для высоконагруженных проектов — выделенные серверы с возможностью тонкой настройки.

Альтернативные решения

Стоит также рассмотреть альтернативные подходы к валидации:

  • Formly — динамическое создание форм с встроенной валидацией
  • Reactive Forms — мощный инструмент для создания сложных форм
  • Joi — библиотека для валидации данных, популярная в Node.js
  • Vest — легковесная альтернатива для создания валидационных наборов

Полезные ресурсы:

Заключение и рекомендации

Пользовательская валидация в Angular — это мощный инструмент, который позволяет создавать гибкие и надежные формы для любых задач. Основные рекомендации:

  • Используйте синхронные валидаторы для простых проверок — они быстрее и не создают лишней нагрузки
  • Применяйте асинхронную валидацию только когда необходимо обращение к серверу
  • Кешируйте результаты асинхронных валидаторов для улучшения производительности
  • Используйте debouncing для уменьшения количества запросов к серверу
  • Тестируйте валидаторы отдельно от компонентов — это упрощает отладку
  • Создавайте переиспользуемые валидаторы для часто встречающихся случаев

Для серверных приложений особенно важно правильно настроить валидацию на уровне API и фронтенда. Это поможет избежать проблем с безопасностью и обеспечить хорошую производительность. Помните: хорошая валидация — это не только техническая задача, но и важная часть пользовательского опыта.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked