/**
* @module module:lib/engine/ValidatorFactory
* @description Factory for generating Angular ValidatorFn from Decaf validation metadata.
* @summary ValidatorFactory maps validation keys defined by the Decaf validation system
* into Angular ValidatorFn instances. It supports type-based resolution and comparison
* validators and provides helpers to create proxies for nested control validation.
*
* @link {@link ValidatorFactory}
*/
import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
import {
ComparisonValidationKeys,
DEFAULT_PATTERNS,
PathProxy,
PathProxyEngine,
Primitives,
Validation,
ValidationKeys,
Validator,
} from '@decaf-ts/decorator-validation';
import { FieldProperties, HTML5InputTypes, parseValueByType } from '@decaf-ts/ui-decorators';
import { AngularEngineKeys } from './constants';
import { KeyValue } from './types';
import { NgxRenderingEngine } from './NgxRenderingEngine';
type ComparisonValidationKey = typeof ComparisonValidationKeys[keyof typeof ComparisonValidationKeys];
/**
*
* Resolves the correct validator key and its associated properties based on the input key and type.
*
* When the validation key is TYPE, it's necessary to resolve the actual validator based on the
* field's type (e.g., 'password', 'email', 'url') instead of using the generic getValidator("type") logic.
* This allows directly invoking specific validators like getValidator('password'), ensuring the correct
* behavior for type-based validation.
*
* @param key - The validation key (e.g., 'type', 'required', etc.).
* @param value - The value that needs be provided to the validator.
* @param type - The field's declared type.
* @returns An object containing the resolved validator key and its corresponding props.
*/
const resolveValidatorKeyProps = (key: string, value: unknown, type: string): {
validatorKey: string;
props: Record<string, unknown>;
} => {
const patternValidators: Record<string, unknown> = {
[ValidationKeys.PASSWORD]: DEFAULT_PATTERNS.PASSWORD.CHAR8_ONE_OF_EACH,
[ValidationKeys.EMAIL]: DEFAULT_PATTERNS.EMAIL,
[ValidationKeys.URL]: DEFAULT_PATTERNS.URL,
};
const isTypeBased = key === ValidationKeys.TYPE && Object.keys(patternValidators).includes(type);
const validatorKey = isTypeBased ? type : key;
if (key === ValidationKeys.TYPE && HTML5InputTypes.CHECKBOX && value !== type )
value = type;
const props: Record<string, unknown> = {
// [validatorKey]: (!isTypeBased && key === 'type') ? parseType(type) : value,
[validatorKey]: (!isTypeBased && validatorKey === ValidationKeys.TYPE) ? NgxRenderingEngine.get().translate(value as string, false) : value,
// Email, Password, and URL are validated using the "pattern" key
...(isTypeBased && { [ValidationKeys.PATTERN] : patternValidators[type] }),
};
return { validatorKey, props };
};
export class ValidatorFactory {
static spawn(fieldProps: FieldProperties, key: string): ValidatorFn {
if (!Validation.keys().includes(key))
throw new Error('Unsupported custom validation');
const validatorFn: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const { type, customTypes, options } = fieldProps;
let fieldType = (customTypes || type) as string;
if ((fieldType === HTML5InputTypes.CHECKBOX || fieldType === Array.name) && Array.isArray(options))
fieldType = Primitives.STRING;
const { validatorKey, props } = resolveValidatorKeyProps(key, fieldProps[key as keyof FieldProperties], fieldType);
const validator = Validation.get(validatorKey) as Validator;
// parseValueByType does not support undefined values
const value = typeof control.value !== 'undefined'
? parseValueByType(fieldType, control.value, fieldProps)
: undefined;
// Create a proxy to enable access to parent and child values
let proxy: PathProxy<unknown> = ValidatorFactory.createProxy({} as AbstractControl);
if (Object.values(ComparisonValidationKeys).includes(key as ComparisonValidationKey)) {
const parent: FormGroup = control instanceof FormGroup ? control : (control as KeyValue)[AngularEngineKeys.PARENT];
proxy = ValidatorFactory.createProxy(parent) as PathProxy<unknown>;
}
let errs: string | undefined;
try {
errs = validator.hasErrors(value, props, proxy);
} catch (e: unknown) {
errs = `${key} validator failed to validate: ${e}`;
console.error(errs);
}
return errs ? { [validatorKey]: true } : null;
};
Object.defineProperty(validatorFn, 'name', {
value: `${key}Validator`,
});
return validatorFn;
}
/**
* @summary Creates a proxy wrapper for an Angular AbstractControl to assist with custom validation logic.
* @description Returns a structured proxy object that simulates a hierarchical tree of form values.
* Enables Validators handling method to access parent and child properties using consistent dot-notation in Angular forms.
*
* @param {AbstractControl} control - The control to wrap in a proxy.
* @returns {PathProxy<unknown>} A proxy object exposing form values and enabling recursive parent access.
*/
static createProxy(control: AbstractControl): PathProxy<unknown> {
return PathProxyEngine.create(control, {
getValue(target: AbstractControl, prop: string): unknown {
if (target instanceof FormControl)
return target.value;
if (target instanceof FormGroup) {
const control = target.controls[prop];
return control instanceof FormControl ? control.value : control;
}
// const value = target[prop];
// if (value instanceof FormControl)
// return value.value;
//
// if (value instanceof FormGroup) {
// const control = value.controls[prop];
// return control instanceof FormControl ? control.value : control;
// }
return (target as KeyValue)?.[prop];
},
getParent: function(target: AbstractControl) {
return target?.['_parent'];
},
ignoreUndefined: true,
ignoreNull: true,
});
}
}
Source