Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 - Reactive form Validation messages

My objective is to put all my validation messages in component instead of html file

I have a sign up page and below is the fields:

public buildRegisterForm() {
this.userForm = this.fb.group({
  firstName: ['', [Validators.required, Validators.minLength(3)]],
  lastName: ['', [Validators.required, Validators.maxLength(50)]],
  emailGroup: this.fb.group({
    email: ['', [Validators.required, Validators.pattern(this.emailPattern)]],
    retypeEmail: ['', Validators.required],
  }, { validator: formMatcherValidator('email', 'retypeEmail') }),
  passwordGroup: this.fb.group({
    password: ['', [Validators.required, strongPasswordValidator()]],
    retypePassword: ['', Validators.required],
  }, { validator: formMatcherValidator('password', 'retypePassword')}),
});
}

I'm following this tutorial link to achieve what I want which is to put all my validation messages in component file instead of html file.

export const validationMessages = {
'firstName': {
'required': 'Your first name is required.',
'minlength': 'Your first name must be at least 3 characters long.'
},
'lastName': {
'required': 'Your last name is required.',
'minlength': 'Your last name must be less than 50 characters long.'
},
'emailGroup': {
  'email': {
      'required': 'Your email is required',
      'pattern': 'Your login email does not seem to be a valid email address.'
     },
 'retypeEmail': {
      'required': 'Your retype email is required',
      'match': 'The email provided do not match.'
   },
},
'passwordGroup':{
     'password': {
         'required': 'Your password is required',
         'strongPassword': 'Your password must be between 8 - 15 characters and must contain at least three of the following: upper case letter, lower case letter, number, symbol.'
     },
   'retypePassword': {
       'required': 'Your retype password is required',
        'match': 'The password provided do not match.'
  }
}


onValueChanged method

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

// tslint:disable-next-line:forin
for (const field in this.formErrors) {
  // clear previous error message (if any)
  this.formErrors[field] = '';
  let control = form.get(field);
  // console.log("control", control.dirty);

  console.log("controlEmail", control);
  if (control && (control.dirty || control.touched) && control.invalid) {
    let messages = validationMessages[field];
    // tslint:disable-next-line:forin
    for (const key in control.errors) {
      this.formErrors[field] += messages[key] + ' ';
    }
  }
 }
}

And this method is not working when I have multi formBuider Group or nested Object. Any tips for this 1?
similar to this How to validate reactive forms with nested form groups?

like image 344
weikian Avatar asked Apr 13 '17 04:04

weikian


2 Answers

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 ;)

like image 158
AT82 Avatar answered Oct 20 '22 18:10

AT82


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;
  }
like image 36
Jonathan Borgia Avatar answered Oct 20 '22 20:10

Jonathan Borgia