How to use Built-in Angular validator within custom ControlValueAccessor

723 views Asked by At

I have a custom component inheriting ControlValueAccessor on my application that integrate ng-select

The goal of this component is to have one central place where ng-select is integrated so I can re-use it across the application.

This is how I call the validation method:

  constructor(
    @Optional() @Self() public controlDir: NgControl
  ) {
    controlDir.valueAccessor = this;
  }

  ngOnInit() {
    const control = this.controlDir.control;
    if (control.validator) {
      control.setValidators([this.validate.bind(this, [control.validator])]);
    }
    control.updateValueAndValidity({ emitEvent: false });
  }

  validate(validators: ValidatorFn[], c: FormControl) {
    const allErrors: any = {};
    for (const validator of validators) {
      const hasError = validator(c);
      if (hasError) {
        Object.entries(hasError).forEach(([errorName, isOnError]) => {
          allErrors[errorName] = isOnError;
        });
      }
    }

    if (Object.keys(allErrors).length > 0) {
      return allErrors;
    }

    return null;
  }

And this is how I would typically instantiate the formControl on the parent component:

const control = this.formBuilder.control(['[email protected]'], [Validators.email]);

this.form = this.formBuilder.group({ test: control });

I want to give the parent the option to use Built-in angular validators on my custom component. (required, email, min, max...).

The problem is that the control value of my custom component is an array of string, for instance the value will be:

[ '[email protected]', '[email protected]' ]

the component looks like this

component screenshot

Problem: in my validate function, I have access to a ValidatorFn that will check if my control is a valid email. if my control value was a string, it would work as expected but it's an array of string, so it's not working.

So my guess is that i need to re-implement the email validator for my custom component (as it makes sense that angular can't figure out magically the structure of my data within my custom component).

But I can't figure out how to identify that the validator defined is Validator.email.

this.controlDir.control.validator is a function and I have no clue how to identify that it's the email validator so I can add my custom validation for emails.

Question: How can I know from my custom validate function which Validator was set from the parent? Was it Validators.required, Validators.email ...etc

2

There are 2 answers

1
Michael Doye On BEST ANSWER

How can I know from my custom validate function which Validator was set from the parent?

Unfortunately there is no way to get the validators for a given control (details).

In theory, you could iterate over your control value (if it is an array) and create a new FormControl to validate each string in the array.

As an example you could do it like this:

isControlValid(controlValue: string[], validator: ValidatorFn) {
  let emailHasError = false;
  for (value of controlValue) {
    const ctrl = new FormControl(value, validator);
    if (Object.keys(validator(ctrl)).length) {
      emailHasError = true; // if any of the values are invalid
    }
  }
  return emailHasError; // or return whatever you need to.
}

Use it like this perhaps

validate(validators: ValidatorFn[], c: FormControl) {
  const allErrors: any = {};
  for (const validator of validators) {
    if (Array.isArray(c.value)) {
      if (this.isControlValid(c.value, validator)) {
        // maybe update `allErrors` here or something

You can implement it however you want, but the idea is simply to validate each string using it's own form control.

There might be some obscure way to achieve this differently using NG_VALIDATORS but I have not looked into it.

5
Akash On

How can I know from my custom validate function which Validator was set from the parent? Was it Validators.required, Validators.email

Every validator function returns an error object. If you see the Validators.email() from angular, it returns { 'email': true } object. So if you loop through control.errors (or hasError in your example), you can check if the key for any object matches email -

static email(control) {
        if (isEmptyInputValue(control.value)) {
            return null; // don't validate empty values to allow optional controls
        }
        return EMAIL_REGEXP.test(control.value) ? null : { 'email': true };
    }

Below is how your validate() function will look like -

validate(validators: ValidatorFn[], c: FormControl) {
    const allErrors: any = {};
    for (const validator of validators) {
      const hasError = validator(c);
      if (hasError) {
        Object.entries(hasError).forEach(([errorName, isOnError]) => {
          if(errorName === 'email' ) {
            console.log('Validators.email was set');
          }
          allErrors[errorName] = isOnError;
        });
      }
    }

    if (Object.keys(allErrors).length > 0) {
      return allErrors;
    }

    return null;
  }