Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Performing cross-field validation on Angular 2 Reactive Forms

I'm trying to build out a registration form in Angular 2 using the Reactive Forms module. As such, I have a FormGroup defined for the form, and I can then list validators for each FormControl therein.

Consider this partial class:

export class TestFormComponent implements OnInit {
  form: FormGroup;
  password = new FormControl("", [Validators.required]);
  passwordConfirm = new FormControl("", [Validators.required, this.validatePasswordConfirmation]);

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    this.form = this.fb.group({
      "password": this.password,
      "passwordConfirm": this.passwordConfirm
    });
  }

  validatePasswordConfirmation(fc: FormControl) {
    var pw2 = fc.value;
    var pw = // how do I get this value properly????

    if (pw === '') {
      return {err:"Password is blank"};
    }

    if (pw2 === '') {
      return {err:"Confirmation password is blank"};
    }

    if (pw !== pw2) {
      return {err:"Passwords do not match"}
    }

    return null;
  }
}

You can see I have a validator created for the passwordConfirm field, but I don't know how to get the value of the main password field (for use as pw in the validator) to do the comparison.

I can't just reference this.form.value.password because this in the validator doesn't refer to the main class that contains the form.

Any ideas?

like image 783
Michael Oryl Avatar asked Nov 28 '22 00:11

Michael Oryl


2 Answers

So the answer turns out to be putting a new validator on the form as a whole, and then using the FormGroup object that is passed to the validator as a way to compare the field values. That much I had suspected. What I was missing, however, was how to set the error state properly on the individual passwordConfirm field. This code shows how to do it:

export class TestFormComponent implements OnInit {
  form: FormGroup;
  password = new FormControl("", [Validators.required]);
  passwordConfirm = new FormControl("", [Validators.required, this.validatePasswordConfirmation]);

  constructor(private fb: FormBuilder) {
  }

  ngOnInit() {
    this.form = this.fb.group({
      "password": this.password,
      "passwordConfirm": this.passwordConfirm
    },
    {
      validator: this.validatePasswordConfirmation
    });
  }

  validatePasswordConfirmation(group: FormGroup) {
    var pw = group.controls['password'];
    var pw2 = group.controls['passwordConfirm'];

    if (pw.value !== pw2.value) { // this is the trick
      pw2.setErrors({validatePasswordConfirmation: true});
    }

    // even though there was an error, we still return null
    // since the new error state was set on the individual field
    return null; 
  }
}

The trick, as mentioned in the comment in the code above, is that you can set error states on individual FormControl fields with the setErrors() method. So now, with this code in place, the confirmation field gets the proper valid/invalid state set based upon the regular validators it has, like Validators.required, as well as from the custom form based validator we added.

With this method, you could create complex form-based validators that can check the states of many different form fields and set validation states on each individually based on any business logic you can come up with. This makes cross-field validation with Angular 2 Reactive forms quite simple.

like image 97
Michael Oryl Avatar answered Dec 15 '22 16:12

Michael Oryl


pw2.setErrors(null); causes problems if the pw2 field has validators on itself, by itself, such as minLength:

  ngOnInit() {
    this.form = this.fb.group({
      "password": [this.password, Validators.minLength(6)],
      "passwordConfirm": [this.passwordConfirm, Validators.minLength(6)]
    },
    {
      validator: this.validatePasswordConfirmation
    });
  }

The setErrors(null) will destroy the minLength warning.

It's best if the cross-field validator validatePasswordConfirmation() returns the error instead, because where the error appears in the HTML -- beside an individual field, or above/below the form as a whole -- is totally in our control anyway.

<div *ngIf="myNgForm.submitted" class="text-error">
    <span *ngIf="form.errors?.validatePasswordConfirmation">This field must match the first field</span>
    <span *ngIf="form.controls.passwordConfirm.errors?.minlength">must be at least 6 chars</span>
</div>
like image 41
Ron Newcomb Avatar answered Dec 15 '22 18:12

Ron Newcomb