4
votes

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?

3

3 Answers

22
votes

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.

3
votes

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>
0
votes

Michael Oryl's answer has two bugs in my case, maybe I use different version (Angular 2.3.0):

1. passwordConfirm = new FormControl("", [Validators.required, this.validatePasswordConfirmation]);

should just be:

passwordConfirm = new FormControl("", [Validators.required]);

because than:

validatePasswordConfirmation(group: FormGroup)

fires with child element of FormGroup. I have no idea why but it happens. So the second validator for whole group is enough because that fires with whole FormGroup.

  1. When original field is edited after repeat field and both fields are equal Michaels code stays in error state. This code does the job in this case in more complicated validation you'll need something more complex:

    if (pw.value != pw2.value) {
      pw2.setErrors({validatePasswordConfirmation: true});
    }
    else{
      pw2.setErrors(null);
    }
    

Full code:

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

  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});
    }else{
      pw2.setErrors(null);
    }

    return null; 
  }
}