Source

lib/engine/ValidatorFactory.ts

import { AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
import {
  ComparisonValidationKeys,
  DEFAULT_PATTERNS,
  PathProxy,
  PathProxyEngine,
  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;
  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 { name, type } = fieldProps;
      const { validatorKey, props } = resolveValidatorKeyProps(key, fieldProps[key as keyof FieldProperties], type);
      const validator = Validation.get(validatorKey) as Validator;

      // parseValueByType does not support undefined values
      const value = typeof control.value !== 'undefined'
        ? parseValueByType(type, type === HTML5InputTypes.CHECKBOX ? name : 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 {
        if(!props['types'] && !props['customTypes'])
          props['types'] = props['type'];
        errs = validator.hasErrors(value, props, proxy);
      } catch (e: unknown) {
        errs = `${key} validator failed to validate: ${e}`;
        console.warn(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,
    });
  }
}