13

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?

3
  • 1
    Please always include the code as code, not pictures. If someone wants to help you and test your code, they are not going to type all that code longhand, including me ;) Commented Apr 13, 2017 at 12:12
  • Thanks for the feedback. I have updated the picture to code. Commented Apr 13, 2017 at 13:36
  • Hello @weikian, It is really fascinating that I am not alone. I opened an issue in the Angular repo and talked about this specific problem. Please contribute in it if possible Commented Jun 17, 2022 at 5:56

4 Answers 4

8

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

Sign up to request clarification or add additional context in comments.

10 Comments

Hi @AJT_82, thank you so much for the answer. I learnt a few thing today. You are such a genius. Anyway, just a small error when I try to run in my localhost. The error shows Property 'controls' does not exist on type 'AbstractControl'.. The rest are working fine
Genius, I wouldn't go that far, lol :P Okay, where do you get the error? You could fork the plunker I made and reproduce it, and I'd be happy to take a look :)
In onValueChanged method if(form.get(field).controls ) {, for(const subfield in form.get(field).controls) {, const control = form.get(field).controls[subfield]; These are the 'controls' that cause error
Could you just fork the plunker with your code, can't really help otherwise... Pointing out where the error is doesn't really help since I don't have your code ;)
Well that is nice, but I meant that you would actually integrate your code so that it would be a working sample using your code ;) I'd like to have something to debug instead of first making your code work so that it showcases the issue and then trying to locate the problem. Help me help you :)
|
3

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] + ' ';
        }
      }
    }
  }

Comments

1

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

1 Comment

thanks for sharing :) Hope it will benefits to other developers too
0
 public morningForm:FormGroup;

 constructor(public fb: FormBuilder) { }
 ngOnInit() {
 this.morningForm = this.fb.group({
      'country': [''],
      'city' : [''],
      'phone': ['']
    });
    this.setValidators();
}
private setValidators() {
    let me = this;
    let status = false;
    me.morningForm.valueChanges.subscribe(fieldObj => {
      for (const fieldName in fieldObj) {
      // IF ENTER OR FIND THE VALUE THEN ALL FIELD SET AUTO REQUIRED VALIDATION.
        if (fieldObj[fieldName] != '') {
          status = true;
          break;
        }
      }
    });
    this.setRequiredField(status)
  }

  private setRequiredField(status:any) {
    let me = this;
    let country = me.morningForm.get('country');
    let city = me.morningForm.get('city');
    let phone = me.morningForm.get('phone');

    country.setValidators(null);
    city.setValidators(null);
    phone.setValidators(null);
    if (status) {
      country.setValidators([Validators.required]);
      city.setValidators([Validators.required]);
      phone.setValidators([Validators.required]);
    }
    country.updateValueAndValidity();
    city.updateValueAndValidity();
    phone.updateValueAndValidity();
  }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.