Angular - form validation in real application

Viewed 657

The purpose of my question is to find out if it has to be complicated or maybe I'm doing something wrong.

It's quite easy to show how powerful the form validation is for the purpose of a tutorial. But when it comes to a real life application then things get complicated. I started with a simple registration form with

  • username
  • email
  • password
  • repeated password

If I used tutorial-like approach then I'd use *ngIf to display all kind of errors that appear above the corresponding input. The problem is that it leads to terrible user experience:

  • error are shown and hidden that makes inputs jumping
  • if I have many validation conditions for password then user gets quite a long list of errors
  • the form starts to look like one big warning and is not concise
  • repeated password shouts that it's not the same as password although user hasn't typed anything yet

Moreover in order to stop displaying errors if the form is not touched one need to add code like this all over the place in templates

myForm.get('controlName').invalid && (myForm.get('controlName').dirty || myForm.get('controlName').touched)

what starts to resemble PHP spaghetti code.

What I want to say it that even for very simple form the logic starts to be very complicated if one wants to crate a nice user experience. And what I've finished with so far I a logic in component that scans errors for particular controls and returns just one error at a time.

What is your experience? Do you have some advice or an example of best practices?

3 Answers

There are two types of forms provided in Angular 2+.

  1. Template-driven form, very similar with Angularjs 1.x
  2. Reactive Forms, providing programmatic form handling for complex logic, like custom validation, and validation between elements.

For the validation of password and repeated password in your case, when using Reactive Forms, you can create a wrapper FormGroup to group these two form elements.

this.email = new FormControl('', [Validators.required, this.validateEmail]);
    this.username = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]);
    this.password = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]);
    this.passwordConfirm = new FormControl('', [Validators.required, Validators.minLength(6), Validators.maxLength(20)]);

    this.passwordGroup = fb.group(
      {
        password: this.password,
        passwordConfirm: this.passwordConfirm
      },
      { validator: this.passwordMatchValidator }
    );

    this.signupForm = fb.group({
      email: this.email,
      username: this.username,
      passwordGroup: this.passwordGroup
    });

And use a local function to validate the password group or use a custom directive.

passwordMatchValidator(g: FormGroup) {
    return g.get('password').value === g.get('passwordConfirm').value
      ? null : { 'mismatch': true };
  }

And add an indicator in the template file if the validation is failed.

<div class="col-md-12" [class.has-danger]="passwordGroup.invalid">
              <div class="form-control-feedback" *ngIf="passwordGroup.invalid">
                <p *ngIf="passwordGroup.hasError('mismatch')">Passwords are mismatched.</p>
              </div>
            </div>

Check the sample codes from my Github. And read the pages of Handling forms and processing auth for details.

You can use angular material input which has mat-error option which displays the errors very concisely. You can checkout official documentation at https://material.angular.io for detailed usage of mat-error and mat-hint. You simple have to use something like following to show error: <form [formGroup]="form" (ngSubmit)="onSubmit()"> <mat-form-field> <input matInput type="text" formControlName="username" name="email" placeholder="E-Mail" #email required> <mat-error id="repeat-help" class="text-warning" *ngIf="!(form.controls.username.valid || form.controls.username.pristine)"> Please enter a valid e-mail ID </mat-error> </mat-form-field>

You can use reactive forms to make a good UI.

I understand your problem. If a form has many validated controls setting the error messages in the template could be quite confusing. I suggest you move all the error checking to the component file. Check this Stackblitz I made: https://stackblitz.com/edit/angular-dugfod. It is based on Deborah Kurata's excelente course about reactive forms at Pluralsight (at the time of writing this answer the course is available for free).

The main points in the Stackblitz example are these:

1) Create an array of error messages for every validated control in the form:

  private validationMessages = [
    {
      controlName: 'name',
      messages: { required: 'Name is required', minlength: 'Min length 3' }, 
      message: '' // for the actual message to be shown in the form
    },
    {
      controlName: 'country',
      messages: { countryNot: "Country can't be England" },
      message: ''
    },

2) Create a function that checks if a control has errors and set the related error message in the array:

  setMessage(controlName: string) {
    let control = this.form.get(controlName);
    const val = this.validationMessages.filter(x => x.controlName === controlName)[0];
    val.message = '';

    if (control.errors) {
      val.message = Object.keys(control.errors)
        .map(key => val.messages[key]).join(' ');
    }
  }

3) In the ngOnInit method set an Observable to track the changes of the validated controls and for every change of value call the setMessage function:

    const fields = ['name', 'country', 'numberOfPlayers', 'coach', 'playerLimitsGroup.minPlayers',
      'playerLimitsGroup.maxPlayers', 'playerLimitsGroup'];
    fields.forEach(field => {
      const control = this.form.get(field);
      control.valueChanges.subscribe(
        () => this.setMessage(field)
      );
      this.setMessage(field); // set the initial error messages for demonstration
    });

4) Create a method to get the current error message of a control. Note that here you can decide if you want to show the error messages always or only when the control is dirty or touched:

  getMessage(controlName: string) {
    let control = this.form.get(controlName);
    let hideCleanErrors = this.form.get('hideCleanErrors').value;
    if (control.dirty || control.touched || !hideCleanErrors) {
      return this.validationMessages.filter(x => x.controlName === controlName)[0].message;
    }
    return '';
  }

5) Now you have a neat way to show the error messages for every control in the template:

    <label>Name:
      <input type="text" formControlName="name">
    </label>
    <span>{{getMessage('name')}}</span>

Additionally my Stackblitz example shows several ways to set up custom validators with and without parameters, cross-field validation and setting a validator at runtime.

Related