Monday 8 June 2020

Angular 8/9 Form control

Angular has a new helper Class called FormBuilderFormBuilder allows us to explicitly declare forms in our components. This allows us to also explicitly list each form control’s validators.
In our example we are going to build a small form with three inputs, user name, email and profile description.
A simple user form
We will start with looking at our app.module.ts file.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { ControlMessagesComponent } from './control-messages.component';
import { ValidationService } from './validation.service';

@NgModule({
  imports: [BrowserModule, ReactiveFormsModule],
  declarations: [ControlMessagesComponent, AppComponent],
  providers: [ValidationService],
  bootstrap: [AppComponent]
})
export class AppModule {}
In our AppModule we are registering our components and services for our application. Once registered we can bootstrap our AppModule for our application. For us to use the form features in this post we will use the ReactiveFormsModule. To read more about @NgModule check out the documentation. Now lets take a look at our AppComponent.
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { ValidationService } from 'app/validation.service';

@Component({
  selector: 'demo-app',
  templateUrl: 'app/app.component.html'
})
export class AppComponent {
  userForm: any;

  constructor(private formBuilder: FormBuilder) {
    this.userForm = this.formBuilder.group({
      name: ['', Validators.required],
      email: ['', [Validators.required, ValidationService.emailValidator]],
      profile: ['', [Validators.required, Validators.minLength(10)]]
    });
  }

  saveUser() {
    if (this.userForm.dirty && this.userForm.valid) {
      alert(
        `Name: ${this.userForm.value.name} Email: ${this.userForm.value.email}`
      );
    }
  }
}
First we import the FormBuilder class. We inject it through our App component constructor. In our constructor is the following:
this.userForm = this.formBuilder.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, ValidationService.emailValidator]],
  profile: ['', Validators.required]
});
This creates a new form with our desired controls. The first parameter in the control we leave empty as this lets you initialize your form control with a value. The second parameter can be a list of Validators. Angular has some built in validators such as required and minLength. In this example we have our own Validation Service with a few more custom Validators as well. Now that we have our userForm created lets take a look at our form.Angular Form Essentials

Angular Form Essentials

GET THE E-BOOK NOW!

<form [formGroup]="userForm" (submit)="saveUser()">
  <label for="name">Name</label>
  <input formControlName="name" id="name" #name="ngControl" />
  <div *ngIf="name.touched && name.hasError('required')">Required</div>

  <label for="email">Email</label>
  <input formControlName="email" id="email" #email="ngControl" />
  <div *ngIf="email.touched && email.hasError('email')">Invalid</div>

  <label for="profile">Profile Description</label>
  <input formControlName="email" id="profile" #profile="ngControl" />
  <div *ngIf="profile.touched && profile.hasError('required')">Invalid</div>

  <button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
We could do something like this example where we show and hide based on input properties. We can create template variables with the # syntax. This would work fine but what if our form grows with more controls? Or what if we have multiple validators on our form controls like our email example? Our template will continue to grow and become more and more complex.
So lets look at building a custom component that helps abstract our validation logic out of our forms. Here is the same form but with our new component.
<form [formGroup]="userForm" (submit)="saveUser()">
  <label for="name">Name</label>
  <input formControlName="name" id="name" />
  <control-messages [control]="userForm.get('name')"></control-messages>

  <label for="email">Email</label>
  <input formControlName="email" id="email" />
  <control-messages [control]="userForm.get('email')"></control-messages>

  <label for="profile">Profile Description</label>
  <textarea formControlName="profile" id="profile"></textarea>
  <control-messages [control]="userForm.get('profile')"></control-messages>

  <button type="submit" [disabled]="!userForm.valid">Submit</button>
</form>
Here our control-messages component takes in a reference of the control input to check its validation. This is what the rendered form looks like with our validation.
Form with validation triggered
Here is the example code for our control-messages component.
import { Component, Input } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { ValidationService } from './validation.service';

@Component({
  selector: 'control-messages',
  template: `
    <div *ngIf="errorMessage !== null"></div>
  `
})
export class ControlMessages {
  errorMessage: string;
  @Input() control: FormControl;
  constructor() {}

  get errorMessage() {
    for (let propertyName in this.control.errors) {
      if (
        this.control.errors.hasOwnProperty(propertyName) &&
        this.control.touched
      ) {
        return ValidationService.getValidatorErrorMessage(
          propertyName,
          this.control.errors[propertyName]
        );
      }
    }

    return null;
  }
}
Our control-messages component takes in an input property named control which passes us a reference to a formControl. If an error does exist on the form control it looks for that error in our validation service. We store what messages we would like to show in a central location in the validation service so all validation messages are consistent application wide. Our validation service takes in a name of the error and an optional value parameter for more complex error messages.
Here is an example of our validation service:
export class ValidationService {
  static getValidatorErrorMessage(validatorName: string, validatorValue?: any) {
    let config = {
      required: 'Required',
      invalidCreditCard: 'Is invalid credit card number',
      invalidEmailAddress: 'Invalid email address',
      invalidPassword:
        'Invalid password. Password must be at least 6 characters long, and contain a number.',
      minlength: `Minimum length ${validatorValue.requiredLength}`
    };

    return config[validatorName];
  }

  static creditCardValidator(control) {
    // Visa, MasterCard, American Express, Diners Club, Discover, JCB
    if (
      control.value.match(
        /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/
      )
    ) {
      return null;
    } else {
      return { invalidCreditCard: true };
    }
  }

  static emailValidator(control) {
    // RFC 2822 compliant regex
    if (
      control.value.match(
        /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
      )
    ) {
      return null;
    } else {
      return { invalidEmailAddress: true };
    }
  }

  static passwordValidator(control) {
    // {6,100}           - Assert password is between 6 and 100 characters
    // (?=.*[0-9])       - Assert a string has at least one number
    if (control.value.match(/^(?=.*[0-9])[a-zA-Z0-9!@#$%^&*]{6,100}$/)) {
      return null;
    } else {
      return { invalidPassword: true };
    }
  }
}
In our service we have our custom validators and a list of error messages with corresponding text that should be shown in given use case.
Our control-messages component can now be used across our application and help prevent us from writing extra markup and template logic for validation messages. Read more about Angular forms in the documentation.



No comments:

Post a Comment