Angular 2 - Reactive form Validation messages

The way I see it, you need to create a nested loop inside the onValueChanged(data)-method. Since you have pretty many nested groups, I'm not going to replicate that. But the nested loop is generic, so it works for all your groups. But here is an example with just one nested group instead of several. I'm using the heroes example.

The nested group name is group, and the formcontrol inside that is called child.

formErrors that are used in the code should therefore have the child in a inside group:

formErrors = {
  'name': '',
  'power': '',
  'group':{
    'child': ''
  }
};

Therefore you must remember when you add the validation in the template, you need to use:

<div *ngIf="formErrors.group.child">
   {{ formErrors.group.child }}
</div>

Validation messages won't be inside group, but just like the other validation messages:

validationMessages = {
  'name': {
    'required': 'Name is required.',
  },
  'power': {
    'required': 'Power is required.'
  },
  'child': {
    'required': 'Child is required.',
  }
};

Lastly, the modified onValueChanges:

onValueChanged(data?: any) {
  if (!this.heroForm) { return; }
  const form = this.heroForm;

  // iterate toplevel of formErrors
  for (const field in this.formErrors) {
    // check if the field corresponds a formgroup (controls is present)
    if(form.get(field).controls ) {
      // if yes, iterate the inner formfields
      for(const subfield in form.get(field).controls) {
        // in this example corresponds = "child", reset the error messages
        this.formErrors[field][subfield] = '';
        // now access the actual formfield
        const control = form.get(field).controls[subfield];
        // validate and show appropriate error message
        if (control && control.dirty && !control.valid) {
          const messages = this.validationMessages[subfield];
          for (const key in control.errors) {
            this.formErrors[field][subfield] += messages[key] + ' ';
          }
        }
      }
    } 
    // does not contain a nested formgroup, so just iterate like before making changes to this method
    else {
      const control = form.get(field);
      this.formErrors[field] = '';
      if (control && control.dirty && !control.valid) {
        const messages = this.validationMessages[field];
        for (const key in control.errors) {
          this.formErrors[field] += messages[key] + ' ';
        }
      } 
    }
  }
}

Finally, a DEMO :)

Plunker

You'd have to remember though that in your case this works, but IF there would be nested groups inside the nested groups, this would not work, then you'd have to make yet another loop in the onValueChanges, but you don't have that problem here ;)


I wish Nehal's solution worked, but I couldn't get it to. Came up with my solution after working with @AJT_82 code a bit. My need really came from wanting to check passwords as a group and his solution didn't cover that. So, I have included the other peieces I used to create the full solution that worked for me.

I came accross this problem attempting to do validation of password confirmation in angular 4 after discovering that the method I had used in 2 no longer worked the same. The information on angular.io didn't really help much for this as a lot of the information is fragmented across different areas of their documention.

So, to clarify, this follows Angular's reactive form method. I wrote this to validate passwords in an instance where I also had other validation restrictions (password must be between 4 and 24 characters, is required, etc). This method should work just as well for email confirmation with a few small tweaks.

First, in order to compare validators for a group, the html form must have a subgroup identified using the formGroupName=" " identifier. This is within the primary [formGroup] identifier. Compared inputs must be inside this formGroupName labeled element. In my case it is just a div.

<div class="title">Hero Registration Form</div>
<form [formGroup]="regForm" id="regForm" (ngSubmit)="onSubmit()">
    <div class="input">
        <label for="heroname">Heroname</label> <input type="text"
            id="heroname" class="form-control" formControlName="heroname"
            required />
        <div *ngIf="formErrors.heroname" class="hero-reg-alert">{{
            formErrors.heroname }}</div>
    </div>
    <div class="input">
        <label for="email">Email</label> <input type="email" id="email"
            class="form-control" formControlName="email" required />
        <div *ngIf="formErrors.email" class="hero-reg-alert">{{
            formErrors.email }}</div>
    </div>
    <div formGroupName="password">
        <div class="input">
            <label for="password1">Password</label> <input type="password"
                id="password1" class="form-control" formControlName="password1"
                required />
            <div *ngIf="formErrors.password.password1" class="hero-reg-alert">
                {{ formErrors.password.password1 }}</div>
        </div>
        <div class="input">
            <label for="password2">Re-Enter Password</label> <input
                type="password" id="password2" class="form-control"
                formControlName="password2" required />
            <div
                *ngIf="formErrors.password.password2 || formErrors.password.password"
                class="hero-reg-alert">{{ formErrors.password.password }} {{
                formErrors.password.password2 }}</div>
        </div>
    </div>
    <button type="submit" [disabled]="!regForm.valid">
        <span id="right-btntxt">SUBMIT</span>
    </button>
</form>

You may notice that I have subvalues for formErrors as

formErrors.password.password
formErrors.password.password1
formErrors.password.password2

These are built in my code as so...

  formErrors = {
    'username': '',
    'email': '',
    'password': {
      'password': '',
      'password1': '',
      'password2': ''
    }
  };

I built it this way as 'password1' and 'password2' hold my formErrors for each field, while 'password' holds my error in the case of a mismatch (where the two entered passwords are not equal).

Here is may onValueChanged() formula. It checks if a field is an instanceof FormGroup. If so, it first checks the custom validation for that field group first (storing them in 'this.formErrors[field][field]' ), then proceeds to handle subfield validations. In the case of it not being an instanceof FieldGroup, validation is handled as per the example on angular.io's guideance doc.

  onValueChanged(data?: any) {
    if (!this.regForm) {return;}
    const form = this.regForm;

    for (const field in this.formErrors) {
      const formControl = form.get(field);
      if (formControl instanceof FormGroup) {
        this.formErrors[field][field] = '';
        // check for custom validation on field group
        const control = form.get(field);
        // handle validation for field group
        if (control && control.dirty && !control.valid) {
          const messages = this.validationMessages[field];
          for (const key in control.errors) {
            this.formErrors[field][field] += messages[key] + ' ';
          }
        }
        // handle validation for subfields
        for (const subfield in formControl.controls) {
          console.log('SUBFIELD', subfield);
          this.formErrors[field][subfield] = '';
          const control = formControl.controls[subfield];
          if (control && control.dirty && !control.valid) {
            const messages = this.validationMessages[subfield];
            for (const key in control.errors) {
              this.formErrors[field][subfield] += messages[key] + ' ';
            }
          }
        }
      } 
        // alternate validation handling for fields without subfields (AKA not in a group)
      else {
        const control = form.get(field);
        this.formErrors[field] = '';
        if (control && control.dirty && !control.valid) {
          const messages = this.validationMessages[field];
          for (const key in control.errors) {
            this.formErrors[field] += messages[key] + ' ';
          }
        }
      }
    }
  }

By giving the FieldGroup a subfield with it's own name, we can store the validations on the FieldGroup. Attempting to do this with the regular onValueChange code overwrites the subfields at the line...

    this.formErrors[field] = '';

Not providing a place to store the FieldGroup validation either overwrites the subfields or doesn't handle the FieldGroup.

Should you need it, this is how the form is built using buildFomr();

 buildForm(): void {
    this.regForm = this.formBuilder.group({
      'username': [this.registerUser.username, [
        Validators.required,
        Validators.minLength(4),
        Validators.maxLength(24)
      ]
      ],
      'email': [this.registerUser.email, [
        Validators.email,
        Validators.required
      ]
      ],
      'password': this.formBuilder.group({
        'password1': [this.registerUser.password, [
          Validators.required,
          Validators.minLength(8),
          Validators.maxLength(24)
        ]
        ],
        'password2': ['', [
          Validators.required
        ]
        ]
      }, {validator: this.passwordMatchValidator})
    }); // end buildForm

    this.regForm.valueChanges
      .subscribe(data => this.onValueChanged(data));

    this.onValueChanged(); // (re)set validation messages now
  }

and this is the custom validation function...

  passwordMatchValidator(control: FormGroup): {[key: string]: any} {
    return control.get('password1').value !== control.get('password2').value ? {mismatch: true} : null;
  }

You can also use the following alongside the original onValueChanged method:

formErrors = {
  'name': '',
  'power': '',
  'group.child':''
};

validationMessages = {
  'name': {
    'required': 'Name is required.',
  },
  'power': {
    'required': 'Power is required.'
  },
  'group.child': {
    'required': 'Child is required.',
  }
};

onValueChanged(data?: any) {
    if (!this.heroForm) { return; }
    const form = this.heroForm;

    for (const field in this.formErrors) {
      // clear previous error message (if any)
      this.formErrors[field] = '';
      const control = form.get(field);

      if (control && control.dirty && !control.valid) {
        const messages = this.validationMessages[field];
        for (const key in control.errors) {
          this.formErrors[field] += messages[key] + ' ';
        }
      }
    }
  }