src/app/core/common-components/entity-form/dynamic-form-validators/dynamic-validators.service.ts

Metadata

Index

Methods

Methods

Public buildValidators
buildValidators(config: FormValidatorConfig, entity: Entity, fieldId?: string)

Builds all validator functions that are part of the configuration object. A validator function is a function that returns possible errors based on the state of a Form Field. If there is no Validator by a given name, issues a warning.

Example :
>>> buildValidators({ required: true, max: 5 })
[ Validators.required, Validators.max(5) ]
See ValidatorFn
Parameters :
Name Type Optional Description
config FormValidatorConfig No

The raw configuration object

entity Entity No

The entity that the form is editing

fieldId string Yes
Returns : FormControlOptions
import { Injectable, inject } from "@angular/core";
import { DynamicValidator, FormValidatorConfig } from "./form-validator-config";
import {
  AbstractControl,
  FormControl,
  FormControlOptions,
  ValidationErrors,
  ValidatorFn,
  Validators,
} from "@angular/forms";
import { Logging } from "../../../logging/logging.service";
import { uniquePropertyValidator } from "../unique-property-validator/unique-property-validator";
import { EntityMapperService } from "../../../entity/entity-mapper/entity-mapper.service";
import { buildReadonlyValidator } from "./readonly-after-set.validator";
import { Entity } from "../../../entity/model/entity";
import { AsyncPromiseValidatorFn } from "./validator-types";
import {
  descriptionForDateValidator,
  maxDateValidator,
  minDateValidator,
} from "./date-validators";
import {
  descriptionForAgeValidator,
  maxAgeValidator,
  minAgeValidator,
} from "./age-validators";

/**
 * creates a pattern validator that also carries a predefined
 * message
 * @param pattern The pattern to check
 * @param message The custom message to display when the pattern fails
 * @example
 * >>> validator = patternWithMessage(/foo/, "Can only be foo");
 * >>> validator(invalidFormField);
 * <pre>
 * {
 *   message: "Can only be foo",
 *   requiredPattern: "foo",
 *   actualValue: "bar"
 * }
 * </pre>
 */
export function patternWithMessage(
  pattern: string | RegExp,
  message: string,
): ValidatorFn {
  const patternValidator = Validators.pattern(pattern);

  return (control: AbstractControl) => {
    const errors = patternValidator(control);
    if (errors !== null) {
      Object.assign(errors.pattern, {
        message: message,
      });
    }
    return errors;
  };
}

@Injectable({
  providedIn: "root",
})
export class DynamicValidatorsService {
  private entityMapper = inject(EntityMapperService);

  /**
   * A map of all validators along with a factory that generates the validator function
   * given a value that serves as basis for the validation.
   * @private
   */
  private getValidator(
    key: DynamicValidator,
    value: any,
    entity: Entity,
    fieldId?: string,
    config?: FormValidatorConfig,
  ):
    | { async?: false; fn: ValidatorFn; errorName?: string }
    | {
        async: true;
        fn: AsyncPromiseValidatorFn;
        errorName?: string;
      }
    | null {
    switch (key) {
      case "min":
        return { fn: Validators.min(value as number) };
      case "max":
        return { fn: Validators.max(value as number) };
      case "minDate": {
        return { fn: minDateValidator(value) };
      }
      case "maxDate": {
        return { fn: maxDateValidator(value) };
      }
      case "minAge":
        return { fn: minAgeValidator(value, config?.maxAge) };
      case "maxAge":
        return { fn: maxAgeValidator(value, config?.minAge) };
      case "pattern":
        if (typeof value === "object") {
          return { fn: patternWithMessage(value.pattern, value.message) };
        } else {
          return { fn: Validators.pattern(value as string) };
        }
      case "uniqueId":
        if (!value) {
          return null;
        }

        if (!fieldId) {
          Logging.warn(
            "Trying to generate uniqueId validator without fieldId context",
          );
          return null;
        }

        return this.buildUniqueIdValidator(entity, fieldId);
      case "required":
        return value ? { fn: Validators.required } : null;
      case "readonlyAfterSet":
        return value ? buildReadonlyValidator(entity) : null;
      default:
        Logging.warn("Trying to generate a validator that does not exist", {
          validator: key,
        });
        return null;
    }
  }

  /**
   * Builds all validator functions that are part of the configuration object.
   * A validator function is a function that returns possible errors based
   * on the state of a Form Field.
   * If there is no Validator by a given name, issues a warning.
   * @param config The raw configuration object
   * @param entity The entity that the form is editing
   * @example
   * >>> buildValidators({ required: true, max: 5 })
   * [ Validators.required, Validators.max(5) ]
   * @see ValidatorFn
   */
  public buildValidators(
    config: FormValidatorConfig,
    entity: Entity,
    fieldId?: string,
  ): FormControlOptions {
    const formControlOptions = {
      validators: [],
      asyncValidators: [],
    };

    for (const key of Object.keys(config)) {
      const validatorFn = this.getValidator(
        key as DynamicValidator,
        config[key],
        entity,
        fieldId,
        config,
      );

      if (validatorFn?.async) {
        const effectiveName = validatorFn.errorName || key;
        const validatorFnWithReadableErrors = (control) =>
          validatorFn
            .fn(control)
            .then((res) => this.addHumanReadableError(effectiveName, res));
        formControlOptions.asyncValidators.push(validatorFnWithReadableErrors);
      } else if (validatorFn) {
        const effectiveName = validatorFn.errorName || key;
        const validatorFnWithReadableErrors = (control: FormControl) =>
          this.addHumanReadableError(effectiveName, validatorFn.fn(control));
        formControlOptions.validators.push(validatorFnWithReadableErrors);
      }

      // A validator function of `null` is a legal case, for which no validator function is added.
      // For example `{ required : false }` produces a `null` validator function
    }

    if (formControlOptions.asyncValidators.length > 0) {
      (formControlOptions as FormControlOptions).updateOn = "blur";
    }

    return formControlOptions;
  }

  private addHumanReadableError(
    validatorType: string,
    validationResult: ValidationErrors | null,
  ): ValidationErrors {
    if (!validationResult) {
      return validationResult;
    }

    // uniquePropertyValidator returns `uniqueProperty`, but this pipeline expects
    // the validator key (`uniqueId`). Normalize once so generic error rendering works.
    if (
      validatorType === "uniqueId" &&
      validationResult.uniqueId === undefined &&
      validationResult.uniqueProperty !== undefined
    ) {
      validationResult.uniqueId = validationResult.uniqueProperty;
      delete validationResult.uniqueProperty;
    }

    validationResult[validatorType] = {
      ...validationResult[validatorType],
      errorMessage: this.descriptionForValidator(
        validatorType,
        validationResult[validatorType],
      ),
    };

    return validationResult;
  }

  /**
   * returns a description for a validator given the value where it failed.
   * The value is specific for a certain validator. For example, the `min` validator
   * produces a value that could look something like `{ min: 5, current: 4 }`
   * @param validator The validator to get the description for
   * @param validationValue The value associated with the validator
   */
  private descriptionForValidator(
    validator: DynamicValidator | string,
    validationValue: any,
  ): string {
    switch (validator) {
      case "min":
        return $localize`Must be greater than ${validationValue.min}`;
      case "max":
        return $localize`Cannot be greater than ${validationValue.max}`;
      case "minDate":
        return descriptionForDateValidator("minDate", validationValue);
      case "maxDate":
        return descriptionForDateValidator("maxDate", validationValue);
      case "minAge":
        return descriptionForAgeValidator("minAge", validationValue);
      case "maxAge":
        return descriptionForAgeValidator("maxAge", validationValue);
      case "pattern":
        if (validationValue.message) {
          return validationValue.message;
        } else {
          return $localize`Please enter a valid pattern`;
        }
      case "required":
        return $localize`This field is required`;
      case "matDatepickerParse":
        return $localize`Please enter a valid date`;
      case "isNumber":
        return $localize`Please enter a valid number`;
      case "uniqueId":
        if (typeof validationValue === "string") {
          return validationValue;
        }
        return validationValue?.errorMessage;
      case "readonlyAfterSet":
        return validationValue;
      default:
        Logging.error("No description defined for validator", {
          validator,
          validationValue,
        });
        throw $localize`Invalid input`;
    }
  }

  private buildUniqueIdValidator(
    entity: Entity,
    fieldId: string,
  ): {
    async: true;
    fn: AsyncPromiseValidatorFn;
  } {
    return {
      fn: uniquePropertyValidator({
        getExistingValues: () =>
          this.entityMapper
            .loadType(entity.getType())
            .then((entities) =>
              entities
                .map((existingEntity) => Reflect.get(existingEntity, fieldId))
                .filter((existingValue) => existingValue !== undefined),
            ),
        normalize: false,
        fieldLabel: $localize`:field label:id`,
      }),
      async: true,
    };
  }
}

results matching ""

    No results matching ""