MrD
MrD

Reputation: 5084

Angular Custom Validator On Multiple Fields

I have the following snippet of code used to validate password and password again fields in an Angular template driven form. The idea is that if the two fields are different an error is attached to both. If they're the same, no errors are removed from both.

validate(control: AbstractControl): ValidationErrors | null {

    // The current value
    let currentValue = control.value;

    // Control.root exposes the parent element of the directory
    let otherField : AbstractControl = control.root.get(this.compareTo);


    if(currentValue && otherField && currentValue !== otherField.value) {

      otherField.setErrors({"fieldsMismatch": true});
      otherField.markAsTouched();

      setTimeout(() => {
        control.setErrors({"fieldsMismatch" : true});
        control.markAsTouched()}
        )


    } else {
      control.setErrors(null)
      otherField.setErrors(null)
    }


    return null;
  }

1) If I remove the logic to set the current element's (control) error from the setTimeout, it stops working. Why?

2) If I remove errors using

control.setError({"fieldsMismatch": null});
otherField.setError({"fieldsMismatch": null});

The error is removed from both. But, for the current field (control), the errors key is set to null, meaning that .ng-invalid is removed from the input tag. For otherField, errors is set to an empty object, meaning the input is still marked as invalid. Why? I can just set errors to null explicitly, but then if I had other validation that would also be removed.

Both objects are of type AbstractControl, so I don't get what drives the difference.

Upvotes: 5

Views: 20142

Answers (3)

Marco Fernandes
Marco Fernandes

Reputation: 1

Hope this helps someone: //sync

    export const multipleFieldsValidator = (): ValidatorFn => {
  return (control: AbstractControl): ValidationErrors | null => {
    return control.value > control.parent?.get('another-field-id')?.value ? null : { fieldIsInvalid: true }
  };
};

//async

    export const multipleFieldsValidator = (
  simService: SimulationService
): AsyncValidatorFn => {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    return simService.generic(control.value, control.parent?.get('another-field-id')?.value).pipe(
      map((response: any) => {
        
        return !response.isValid ? { fieldIsInvalid: true } : null;
      })
    );
  };
};

Upvotes: 0

Eliseo
Eliseo

Reputation: 58029

In angular Template driven form, you can use a directive that implements validators (if we needn't pass an argument it's not necesary), see the docs

In this case, we can use some like

@Directive({
  selector: '[repeatControl]',
  providers: [{ provide: NG_VALIDATORS, useExisting: RepeatNameDirective, multi: true }]
})
export class RepeatNameDirective implements Validator {
  @Input('repeatControl') control: NgControl;

  validate(control: AbstractControl): { [key: string]: any } | null {
    let invalid: boolean = this.control.value && control.value && 
            this.control.value != control.value;

    if (this.control.invalid != invalid)
      setTimeout(() => {
        this.control.control.updateValueAndValidity({ emitEvent: false })
      })

    return invalid ? { error: "Must be equal" }
      : null;
  }
}

See that we need pass as argument the control witch we want compare, to allow us to make an updateValueAndValidity (else one control will be invalid, and the other valid or viceverse) and we need put this instruction under a setTimeOut

Our form becomes like

<input id="password" name="password" class="form-control"
      [repeatControl]="repeatPassword"
      [(ngModel)]="data.password" #password="ngModel"  >

<input id="repeatPassword" name="repeatPassword" class="form-control"
      [repeatControl]="password"
      [(ngModel)]="data.repeatPassword" #repeatPassword="ngModel" >

<div *ngIf="repeatPassword.errors || password.errors">
  Password and repeat password must be equals
</div>

See that the inputs is the referenceVariable See the stackblitz (and take account how Angular add the class ng-invalid automatically to both inputs)

Upvotes: 2

Muhammed Albarmavi
Muhammed Albarmavi

Reputation: 24464

a better way is to create a form group level for compare two fields at that level you access to the form group and set the error to form it self

check this validator

export function checkMatchValidator(field1: string, field2: string) {
  return function (frm) {
    let field1Value = frm.get(field1).value;
    let field2Value = frm.get(field2).value;

    if (field1Value !== '' && field1Value !== field2Value) {
      return { 'match': `value ${field1Value} is not equal to ${field2Value}` }
    }
    return null;
  }
}

password /confirmPassword form

 form: FormGroup
  constructor(formBuilder: FormBuilder) {

    this.form = formBuilder.group({
      password: ['', Validators.required],
      confirmPassword: ['', Validators.required],
    },
      {
        validator: checkMatchValidator('password', 'confirmPassword')
      }
    );
  }
}

style invalid input base on the form state

.login-form.ng-invalid.ng-touched input {
  border:1px solid red;
  background:rgba(255,0,0,0.2);
}

Upvotes: 4

Related Questions