Validation of an Angular form is very tricky. The validation is done in the code rather than with a template. This is a highly scalable approach. I am going to demonstrate an example using typescript.
Product class has three properties: Name, Category, and Price.
Product.ts
- exportclass Product {
- constructor(public id ? : number, public name ? : string, public category ? : string, public price ? : number) {}
- }
To enable model-based validation requires a new dependency to be declared in the application app.module.ts, as highlighted below, classed needs to be imported
- import { NgModule } from"@angular/core";
- import { FormsModule } from"@angular/forms";
- import { BrowserModule } from"@angular/platform-browser";
- import { ProductComponent } from"./component";
- import { ReactiveFormsModule } from"@angular/forms";
- @NgModule({
- declarations: [ProductComponent,],
- imports: [FormsModule, BrowserModule, ReactiveFormsModule],
- bootstrap: [ProductComponent]
- })
- exportclass AppModule { }
A new Class Form.model is defined to handle the form validation to keep the template simple. It does make sense to handle as much of the form as possible in the model and minimize the complexity of the form.
Form.model.ts
- import {
- FormControl,
- FormGroup,
- Validators
- } from "@angular/forms";
- exportclass ProductFormControl extends FormControl {
- label: string;
- modelProperty: string;
- constructor(label: string, property: string, value: any, validators: any) {
- super(value, validators);
- this.label = label;
- this.modelProperty = property;
- }
- getValidationMessages() {
- let message: string[] = [];
- if (this.errors) {
- for (let errorName inthis.errors) {
- switch (errorName) {
- case "required":
- message.push(`You must enter a ${this.label}`);
- break;
- case "minlength":
- message.push(`A ${this.label} must be at least ${this.errors['minlength'].requiredLength}`);
- break;
- case "pattern":
- message.push(`The ${this.label} contains illegal characters`);
- break;
- case "maxlength":
- message.push(`A ${this.label} must be no more than ${this.errors['maxlength'].requiredLength} charaters`);
- break;
- }
- }
- }
- return message;
- }
- }
- exportclass ProductFormGroup extends FormGroup {
- constructor() {
- super({
- name: new ProductFormControl("Name", "name", " ", Validators.required),
- price: new ProductFormControl("Price", "price", "", Validators.compose([Validators.required,
- Validators.pattern("^[0-9\.]+$")
- ])),
- category: new ProductFormControl("Category", "category", "", Validators.compose([Validators.required,
- Validators.pattern("^[A-Za-z ]+$"),
- Validators.minLength(3),
- Validators.maxLength(10)
- ]))
- });
- }
- get productControls(): ProductFormControl[] {
- return Object.keys(this.controls).map(k => this.controls[k] as ProductFormControl);
- }
- getFormValidationMessage(form: any) {
- let message: string[] = [];
- this.productControls.forEach(k => {
- k.getValidationMessages().forEach(m => message.push(m))
- });
- return message;
- }
- }
Now I have the form model class to handle the validation, and now I can use this class in my component.
Component.ts
- import {
- ApplicationRef,
- Component
- } from "@angular/core";
- import {
- NgForm
- } from "@angular/forms";
- import {
- Model
- } from "./repository";
- import {
- Product
- } from "./product";
- import {
- ProductFormGroup
- } from "./form.model";
- @Component({
- selector: "app",
- templateUrl: "app/template.html"
- })
- exportclass ProductComponent {
- model: Model = new Model();
- form: ProductFormGroup = new ProductFormGroup();
- constructor(ref: ApplicationRef) {
- ( < any > Window).appRef = ref;
- ( < any > Window).model = this.model;
- }
- getProductByPosition(position: number) {
- returnthis.model.getProdcts()[position];
- }
- getClassByPosition(position: number) {
- let product = this.getProductByPosition(position);
- return "p-a-1 " + (product.price < 50 ? "bg-info" : "bg-warning");
- }
- getClass(): string {
- return (this.model.getProdcts()).length == 5 ? "bg-success" : "bg-info";
- }
- getClassesByKey(key: number): string {
- let product = this.model.getProducts(key);
- return "p-a-1 " + (product.price < 50 ? "bg-info" : "bg-success");
- }
- getClassMap(key: number): object {
- let product = this.model.getProducts(key);
- return {
- "text-xs-center bg-danger": product.name == "Kayak",
- "bg-info": product.price < 50
- };
- }
- fontsizewithUnits: string = "30px";
- fontsizewithoutUnits: string = "30";
- getStyle(key: number): object {
- let product = this.model.getProducts(key);
- return {
- fontSize: "20px",
- "margin.px": 100,
- color: product.price > 50 ? "red" : "green"
- };
- }
- getProduct(key: number): Product {
- returnthis.model.getProducts(key);
- }
- getProducts(): Product[] {
- returnthis.model.getProdcts();
- }
- getProductNumber(): number {
- console.log("getProducts number is invoked");
- returnthis.model.getProdcts().length;
- }
- getRoundedPrice(key: number): number {
- return Math.floor(this.model.getProducts(key).price);
- }
- getSelected(product: Product): boolean {
- return product.name == this.selectedProduct;
- }
- targetName: string = "Kayak";
- counter: number = 1;
- selectedProduct: string;
- newproduct: Product = new Product();
- get jsonProduct() {
- return JSON.stringify(this.newproduct);
- }
- addProduct(p: Product) {
- console.log("New Product " + this.jsonProduct);
- }
- formSubmitted: boolean = false;
- submitForm(form: NgForm) {
- this.formSubmitted = true;
- if (form.valid) {
- this.addProduct(this.newproduct);
- this.newproduct = new Product();
- form.reset();
- this.formSubmitted = false;
- }
- }
- }
I have imported the ProductFormGroup class from the form.model module and used it to define a property called form, which I customize for use in the template.
Template.html
- <style>
- input.ng-dirty.ng-invalid {
- border: 2pxsolid#ff0000
- }
-
- input.ng-dirty.ng-valid {
- border: 2pxsolid#6bc502
- }
- </style>
- <divclass="bg-info p-a-1 m-b-1"> Model Data: {{jsonProduct}}</div>
- <formnovalidate[formGroup]="form" (ngSubmit)="submitForm(form)">
- <divclass="bg-danger p-a-1 m-b-1" *ngIf="formSubmitted && form.invalid"> There are problem with the form
- <ul>
- <li*ngFor="let error of form.getFormValidationMessage()">{{error}}</li>
- </ul>
- </div>
- <divclass="form-group"> <label>Name</label>
- <inputclass="form-control" formControlName="name" name="name" [(ngModel)]="newproduct.name" />
- <ulclass="text-danger list-unstyled" *ngIf="(formSubmitted || form.controls['name'].dirty) && form.controls['name'].invalid">
- <li*ngFor="let error of form.controls['name'].getValidationMessages()">{{error}}</li>
- </ul>
- </div>
- <divclass="form-group"> <label>Category</label>
- <inputclass="form-control" formControlName="category" name="category" [(ngModel)]="newproduct.category" />
- <ulclass="text-danger list-unstyled" *ngIf="(formSubmitted || form.controls['category'].dirty) && form.controls['category'].invalid">
- <li*ngFor="let error of form.controls['category'].getValidationMessages()">{{error}}</li>
- </ul>
- </div>
- <divclass="form-group"> <label>Price</label>
- <inputclass="form-control" name="price" formControlName="price" [(ngModel)]="newproduct.price" />
- <ulclass="text-danger list-unstyled" *ngIf="(formSubmitted || form.controls['price'].dirty) && form.controls['price'].invalid">
- <li*ngFor="let error of form.controls['price'].getValidationMessages()">{{error}}</li>
- </ul>
- </div>
- </form>
How to add a custom Validation on angular form.
In the previous blog I have shown model based validation on a form. I'm continuing that now if you want to add a custom validation on a form.
First you need to create a custom validation class.
Limit.Validators.ts
- import {
- FormControl
- } from "@angular/forms";
- exportclass LimitValidators {
- static Limit(limit: number) {
- return (control: FormControl): {
- [key: string]: any
- } => {
- let val = Number(control.value);
- if (val != NaN && val > limit) return {
- "limit": limit,
- "actualValue": val
- };
- else returnnull;
- }
- }
- }
As I have shown you earlier from.model class is handling all of the stuff related to form validation. To keep the template simple you add the below highlighted code to that class.
- import {
- FormControl,
- FormGroup,
- Validators
- } from "@angular/forms";
- import {
- LimitValidators
- } from "./limit.formvalidators";
- exportclass ProductFormControl extends FormControl {
- label: string;
- modelProperty: string;
- constructor(label: string, property: string, value: any, validators: any) {
- super(value, validators);
- this.label = label;
- this.modelProperty = property;
- }
- getValidationMessages() {
- let message: string[] = [];
- if (this.errors) {
- for (let errorName inthis.errors) {
- switch (errorName) {
- case "required":
- message.push(`You must enter a ${this.label}`);
- break;
- case "minlength":
- message.push(`A ${this.label} must be at least ${this.errors['minlength'].requiredLength}`);
- break;
- case "limit":
- message.push(`A ${this.label} can not be more than ${this.errors['limit']}`);
- break;
- case "pattern":
- message.push(`The ${this.label} contains illegal characters`);
- break;
- case "maxlength":
- message.push(`A ${this.label} must be no more than ${this.errors['maxlength'].requiredLength} charaters`);
- break;
- }
- }
- }
- return message;
- }
- }
- exportclass ProductFormGroup extends FormGroup {
- constructor() {
- super({
- name: new ProductFormControl("Name", "name", " ", Validators.required),
- price: new ProductFormControl("Price", "price", "", Validators.compose([Validators.required,
- Validators.pattern("^[0-9\.]+$"),
- LimitValidators.Limit(100)
- ])),
- category: new ProductFormControl("Category", "category", "", Validators.compose([Validators.required,
- Validators.pattern("^[A-Za-z ]+$"),
- Validators.minLength(3),
- Validators.maxLength(10)
- ]))
- });
- }
- get productControls(): ProductFormControl[] {
- return Object.keys(this.controls).map(k => this.controls[k] as ProductFormControl);
- }
- getFormValidationMessage(form: any) {
- let message: string[] = [];
- this.productControls.forEach(k => {
- k.getValidationMessages().forEach(m => message.push(m))
- });
- return message;
- }
- }