2

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 3

3

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.

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

2 Comments

In your example you start to display error even if user hasn't had a chance to repeat password, right? And how would you display errors if you had multiple validators for password: min length, max length, required, must contain special char, mast contain capital letter, must contain digit? All of the errors would appear from the very beginning.
The min,max,requried are standard validators, others can be added in a custom validator.
1

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.

1 Comment

Email is an easy example. Please see my comment to Hantsy's answer.
1

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.

2 Comments

@ Kari F. Thank you for your answer. My current solution if quite similar to your concept. What I wanted to avoid is mixing presentation (template) with logic (controller) but maybe it's a more concise solution... And thank you for using subscriber to value changes - it's better than setting it in every input:)
You have to check and set the error messages somewhere. I think it's better to do that in the code file. In my example the template file is clean of all logic and error checking.

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.