From 3d4a8b56ff79b241aee6d9109604bb9f4893ec07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Eduardo=20Garc=C3=ADa=20M=C3=A1rquez?= Date: Thu, 8 Jan 2026 00:30:20 -0500 Subject: [PATCH] feat(frontend): add reusable form validators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create emailValidator with stricter pattern than Angular's built-in - Create nameValidator with configurable minLength, maxLength, allowNumbers - Support for Spanish characters (accents, ñ, ü) - Export validators from shared module - Add JSDoc documentation with usage examples --- src/frontend/src/app/shared/index.ts | 4 ++ .../app/shared/validators/email.validator.ts | 30 +++++++++ .../src/app/shared/validators/index.ts | 2 + .../app/shared/validators/name.validator.ts | 64 +++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 src/frontend/src/app/shared/validators/email.validator.ts create mode 100644 src/frontend/src/app/shared/validators/index.ts create mode 100644 src/frontend/src/app/shared/validators/name.validator.ts diff --git a/src/frontend/src/app/shared/index.ts b/src/frontend/src/app/shared/index.ts index b93d7e5..4dd3953 100644 --- a/src/frontend/src/app/shared/index.ts +++ b/src/frontend/src/app/shared/index.ts @@ -7,3 +7,7 @@ export * from './components/ui/connectivity-overlay/connectivity-overlay.compone // Pipes export * from './pipes/credits.pipe'; export * from './pipes/initials.pipe'; + +// Validators +export * from './validators/email.validator'; +export * from './validators/name.validator'; diff --git a/src/frontend/src/app/shared/validators/email.validator.ts b/src/frontend/src/app/shared/validators/email.validator.ts new file mode 100644 index 0000000..1dedd52 --- /dev/null +++ b/src/frontend/src/app/shared/validators/email.validator.ts @@ -0,0 +1,30 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +/** + * Email validation pattern that matches standard email format. + * More strict than HTML5 default email validation. + */ +const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + +/** + * Custom email validator that provides stricter validation than Angular's built-in. + * Validates format: local@domain.tld + * + * @returns ValidatorFn that returns null if valid, or { email: true } if invalid + * + * @example + * ```typescript + * this.form = this.fb.group({ + * email: ['', [Validators.required, emailValidator()]] + * }); + * ``` + */ +export function emailValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; // Let required validator handle empty values + } + const isValid = EMAIL_PATTERN.test(control.value); + return isValid ? null : { email: true }; + }; +} diff --git a/src/frontend/src/app/shared/validators/index.ts b/src/frontend/src/app/shared/validators/index.ts new file mode 100644 index 0000000..5fd0d4c --- /dev/null +++ b/src/frontend/src/app/shared/validators/index.ts @@ -0,0 +1,2 @@ +export * from './email.validator'; +export * from './name.validator'; diff --git a/src/frontend/src/app/shared/validators/name.validator.ts b/src/frontend/src/app/shared/validators/name.validator.ts new file mode 100644 index 0000000..40a92ac --- /dev/null +++ b/src/frontend/src/app/shared/validators/name.validator.ts @@ -0,0 +1,64 @@ +import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; + +/** + * Name validation options. + */ +export interface NameValidatorOptions { + /** Minimum length required (default: 3) */ + minLength?: number; + /** Maximum length allowed (default: 100) */ + maxLength?: number; + /** Whether to allow numbers (default: false) */ + allowNumbers?: boolean; +} + +const DEFAULT_OPTIONS: Required = { + minLength: 3, + maxLength: 100, + allowNumbers: false, +}; + +/** + * Custom name validator with configurable options. + * Validates that name contains only letters, spaces, and common punctuation. + * + * @param options - Validation options + * @returns ValidatorFn that returns validation errors or null + * + * @example + * ```typescript + * this.form = this.fb.group({ + * name: ['', [Validators.required, nameValidator({ minLength: 2 })]] + * }); + * ``` + */ +export function nameValidator(options: NameValidatorOptions = {}): ValidatorFn { + const opts = { ...DEFAULT_OPTIONS, ...options }; + + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; // Let required validator handle empty values + } + + const value = control.value.trim(); + + if (value.length < opts.minLength) { + return { minlength: { requiredLength: opts.minLength, actualLength: value.length } }; + } + + if (value.length > opts.maxLength) { + return { maxlength: { requiredLength: opts.maxLength, actualLength: value.length } }; + } + + // Pattern: letters (including accented), spaces, hyphens, apostrophes + const pattern = opts.allowNumbers + ? /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ0-9\s'-]+$/ + : /^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\s'-]+$/; + + if (!pattern.test(value)) { + return { pattern: true }; + } + + return null; + }; +}