© Adam Freeman 2020
A. FreemanPro Angular 9https://doi.org/10.1007/978-1-4842-5998-6_14

14. Using Events and Forms

Adam Freeman1 
(1)
London, UK
 
In this chapter, I continue describing the basic Angular functionality, focusing on features that respond to user interaction. I explain how to create event bindings and how to use two-way bindings to manage the flow of data between the model and the template. One of the main forms of user interaction in a web application is the use of HTML forms, and I explain how event bindings and two-way data bindings are used to support them and validate the content that the user provides. Table 14-1 puts events and forms in context.
Table 14-1.

Putting Event Bindings and Forms in Context

Question

Answer

What are they?

Event bindings evaluate an expression when an event is triggered, such as a user pressing a key, moving the mouse, or submitting a form. The broader form-related features build on this foundation to create forms that are automatically validated to ensure that the user provides useful data.

Why are they useful?

These features allow the user to change the state of the application, changing or adding to the data in the model.

How are they used?

Each feature is used in a different way. See the examples for details.

Are there any pitfalls or limitations?

In common with all Angular bindings, the main pitfall is using the wrong kind of bracket to denote a binding. Pay close attention to the examples in this chapter and check the way you have applied bindings when you don’t get the results you expect.

Are there any alternatives?

No. These features are a core part of Angular.

Table 14-2 summarizes the chapter.
Table 14-2.

Chapter Summary

Problem

Solution

Listing

Enabling forms support

Add the @angular/forms module to the application

1–3

Responding to an event

Use an event binding

4–6

Getting details of an event

Use the $event object

7

Referring to elements in the template

Define template variables

8

Enabling the flow of data in both directions between the element and the component

Use a two-way data binding

9, 10

Capturing user input

Use an HTML form

11, 12

Validating the data provided by the user

Perform form validation

13–22

Defining validation information using JavaScript code

Use a model-based form

23–28

Extending the built-in form validation features

Define a custom form validation class

29–30

Preparing the Example Project

For this chapter, I will continue using the example project that I created in Chapter 11 and have been modifying in the chapters since.

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/pro-angular-9. See Chapter 1 for how to get help if you have problems running the examples.

Importing the Forms Module

The features demonstrated in this chapter rely on the Angular forms module, which must be imported to the Angular module, as shown in Listing 14-1.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ProductComponent } from "./component";
import { FormsModule } from "@angular/forms";
@NgModule({
  declarations: [ProductComponent],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [ProductComponent]
})
export class AppModule { }
Listing 14-1.

Declaring a Dependency in the app.module.ts File in the src/app Folder

The imports property of the NgModule decorator specifies the dependencies of the application. Adding FormsModule to the list of dependencies enables the form features and makes them available for use throughout the application.

Preparing the Component and Template

Listing 14-2 removes the constructor and some of the methods from the component class to keep the code as simple as possible and adds a new property, named selectedProduct.
import { ApplicationRef, Component } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    selectedProduct: Product;
}
Listing 14-2.

Simplifying the Component in the component.ts File in the src/app Folder

Listing 14-3 simplifies the component’s template, leaving just a table that is populated using the ngFor directive.
<div class="m-2">
  <table class="table table-sm table-bordered">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index">
      <td>{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
</div>
Listing 14-3.

Simplifying the Template in the template.html File in the src/app Folder

To start the development server, open a command prompt, navigate to the example folder, and run the following command:
ng serve
Open a new browser window and navigate to http://localhost:4200 to see the table shown in Figure 14-1.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig1_HTML.jpg
Figure 14-1.

Running the example application

Using the Event Binding

The event binding is used to respond to the events sent by the host element. Listing 14-4 demonstrates the event binding, which allows a user to interact with an Angular application.
<div class="m-2">
  <div class="bg-info text-white p-2">
    Selected Product: {{selectedProduct || '(None)'}}
  </div>
  <table class="table table-sm table-bordered m-2">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index">
      <td (mouseover)="selectedProduct=item.name">{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
</div>
Listing 14-4.

Using the Event Binding in the template.html File in the src/app Folder

When you save the changes to the template, you can test the binding by moving the mouse pointer over the first column in the HTML table, which displays a series of numbers. As the mouse moves from row to row, the name of the product displayed in that row is displayed at the top of the page, as shown in Figure 14-2.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig2_HTML.jpg
Figure 14-2.

Using an event binding

This is a simple example, but it shows the structure of an event binding, which is illustrated in Figure 14-3.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig3_HTML.jpg
Figure 14-3.

The anatomy of an event binding

An event binding has these four parts:
  • The host element is the source of events for the binding.

  • The round brackets tell Angular that this is an event binding, which is a form of one-way binding where data flows from the element to the rest of the application.

  • The event specifies which event the binding is for.

  • The expression is evaluated when the event is triggered.

Looking at the binding in Listing 14-4, you can see that the host element is a td element, meaning that this is the element that will be the source of events. The binding specifies the mouseover event, which is triggered when the mouse pointer moves over the part of the screen occupied by the host element.

Unlike one-way bindings, the expressions in event bindings can make changes to the state of the application and can contain assignment operators, such as =. The expression for the binding assigns a value of item.name to a variable called selectedProduct. The selectedProduct variable is used in a string interpolation binding at the top of the template, like this:
...
<div class="bg-info text-white p-2">
    Selected Product: {{selectedProduct || '(None)'}}
</div>
...

The value displayed by the string interpolation binding is updated when the value of the selectedProduct variable is changed by the event binding. Manually starting the change detection process using the ApplicationRef.tick method is no longer required because the bindings and directives in this chapter take care of the process automatically.

Working with DOM Events

If you are unfamiliar with the events that an HTML element can send, then there is a good summary available at developer.mozilla.org/en-US/docs/Web/Events. There are a lot of events, however, and not all of them are supported widely or consistently in all browsers. A good place to start is the “DOM Events” and “HTML DOM Events” sections of the mozilla.org page, which define the basic interactions that a user has with an element (clicking, moving the pointer, submitting forms, and so on) and that can be relied on to work in most browsers.

If you use the less common events, then you should make sure they are available and work as expected in your target browsers. The excellent http://caniuse.com provides details of which features are implemented by different browsers, but you should also perform thorough testing.

The expression that displays the selected product uses the null coalescing operator to ensure that the user always sees a message, even when no product is selected. A neater approach is to define a method that performs this check, as shown in Listing 14-5.
import { ApplicationRef, Component } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    selectedProduct: string;
    getSelected(product: Product): boolean {
        return product.name == this.selectedProduct;
    }
}
Listing 14-5.

Enhancing the Component in the component.ts File in the src/app Folder

I have defined a method called getSelected that accepts a Product object and compares its name to the selectedProduct property. In Listing 14-6, the getSelected method is used by a class binding to control membership of the bg-info class, which is a Bootstrap class that assigns a background color to an element.
<div class="m-2">
  <div class="bg-info text-white p-2">
    Selected Product: {{selectedProduct || '(None)'}}
  </div>
  <table class="table table-sm table-bordered m-2">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index"
        [class.bg-info]="getSelected(item)">
      <td (mouseover)="selectedProduct=item.name">{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
</div>
Listing 14-6.

Setting Class Membership in the template.html File in the src/app Folder

The result is that tr elements are added to the bg-info class when the selectedProduct property value matches the name property of the Product object used to create them, which is changed by the event binding when the mouseover event is triggered, as shown in Figure 14-4.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig4_HTML.jpg
Figure 14-4.

Highlighting table rows through an event binding

This example shows how user interaction drives new data into the application and starts the change-detection process, causing Angular to reevaluate the expressions used by the string interpolation and class bindings. This flow of data is what brings Angular applications to life: the bindings and directives described in Chapters 12 and 13 respond dynamically to changes in the application state, creating content generated and managed entirely within the browser.

What Happened to Dynamically Created Properties?

Earlier versions of Angular allowed templates to use properties that were created at runtime and not defined in the component. This technique took advantage of the dynamic nature of JavaScript, although it was flagged as an error when the application was compiled for production. Angular 9 has introduced new build tools that prevent this kind of trick, ensuring that the facilities used by the template must be defined by the component.

Using Event Data

The previous example used the event binding to connect two pieces of data provided by the component: when the mouseevent is triggered, the binding’s expression sets the selectedProduct property using a data value that was provided to the ngfor directive by the component’s getProducts method.

The event binding can also be used to introduce new data into the application from the event itself, using details that are provided by the browser. Listing 14-7 adds an input element to the template and uses the event binding to listen for the input event, which is triggered when the content of the input element changes.
<div class="m-2">
  <div class="bg-info text-white p-2">
    Selected Product: {{selectedProduct || '(None)'}}
  </div>
  <table class="table table-sm table-bordered m-2">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index"
        [class.bg-info]="getSelected(item)">
      <td (mouseover)="selectedProduct=item.name">{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
  <div class="form-group">
    <label>Product Name</label>
    <input class="form-control" (input)="selectedProduct=$event.target.value" />
  </div>
</div>
Listing 14-7.

Using an Event Object in the template.html File in the src/app Folder

When the browser triggers an event, it provides an object that describes it. There are different types of event objects for different categories of events (mouse events, keyboard events, form events, and so on), but all events share the three properties described in Table 14-3.
Table 14-3.

The Properties Common to All DOM Event Objects

Name

Description

type

This property returns a string that identifies the type of event that has been triggered.

target

This property returns the object that triggered the event, which will generally be the object that represents the HTML element in the DOM.

timeStamp

This property returns a number that contains the time that the event was triggered, expressed as milliseconds since January 1, 1970.

The event object is assigned to a template variable called $event, and the binding expression in Listing 14-7 uses this variable to access the event object’s target property.

The input element is represented in the DOM by an HTMLInputElement object, which defines a value property that can be used to get and set the contents of the input element. The binding expression responds to the input event by setting the value of the component’s selectedProduct property to the value of the input element’s value property, like this:
...
<input class="form-control" (input)="selectedProduct=$event.target.value" />
...

The input event is triggered when the user edits the contents of the input element, so the component’s selectedProduct property is updated with the contents of the input element after each keystroke. As the user types into the input element, the text that has been entered is displayed at the top of the browser window using the string interpolation binding.

The ngClass binding applied to the tr elements sets the background color of the table rows when the selectedProduct property matches the name of the product they represent. And, now that the value of the selectedProduct property is driven by the contents of the input element, typing the name of a product will cause the appropriate row to be highlighted, as shown in Figure 14-5.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig5_HTML.jpg
Figure 14-5.

Using event data

Using different bindings to work together is at the heart of effective Angular development and makes it possible to create applications that respond immediately to user interaction and to changes in the data model.

Using Template Reference Variables

In Chapter 13, I explained how template variables are used to pass data around within a template, such as defining a variable for the current object when using the ngFor directive. Template reference variables are a form of template variable that can be used to refer to elements within the template, as shown in Listing 14-8.
<div class="m-2">
  <div class="bg-info text-white p-2">
    Selected Product: {{product.value || '(None)'}}
  </div>
  <table class="table table-sm table-bordered m-2">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index"
        (mouseover)="product.value=item.name"
        [class.bg-info]="product.value==item.name">
      <td (mouseover)="selectedProduct=item.name">{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
  <div class="form-group">
    <label>Product Name</label>
    <input #product class="form-control" (input)="false" />
  </div>
</div>
Listing 14-8.

Using a Template Variable in the template.html File in the src/app Folder

Reference variables are defined using the # character, followed by the variable name. In the listing, I defined a variable called product like this:
...
<input #product class="form-control" (input)="false" />
...
When Angular encounters a reference variable in a template, it sets its value to the element to which it has been applied. For this example, the product reference variable is assigned the object that represents the input element in the DOM, the HTMLInputElement object. Reference variables can be used by other bindings in the same template. This is demonstrated by the string interpolation binding, which also uses the product variable, like this:
...
Selected Product: {{product.value || '(None)'}}
...
This binding displays the value property defined by the HTMLInputElement that has been assigned to the product variable or the string (None) if the value property returns null. Template variables can also be used to change the state of the element, as shown in this binding:
...
<tr *ngFor="let item of getProducts(); let i = index"
    (mouseover)="product.value=item.name"
    [class.bg-info]="product.value==item.name">
...

The event binding responds to the mouseover event by setting the value property on the HTMLInputElement that has been assigned to the product variable. The result is that moving the mouse over one of the tr elements will update the contents of the input element.

There is one awkward aspect to this example, which is the binding for the input event on the input element.
...
<input #product class="form-control" (input)="false" />
...

Angular won’t update the data bindings in the template when the user edits the contents of the input element unless there is an event binding on that element. Setting the binding to false gives Angular something to evaluate just so the update process will begin and distribute the current contents of the input element throughout the template. This is a quirk of stretching the role of a template reference variable a little too far and isn’t something you will need to do in most real projects. As you will see in later examples—and later chapters—most data bindings rely on variables defined by the template’s component.

Filtering Key Events

The input event is triggered every time the content in the input element is changed. This provides an immediate and responsive set of changes, but it isn’t what every application requires, especially if updating the application state involves expensive operations.

The event binding has built-in support to be more selective when binding to keyboard events, which means that updates will be performed only when a specific key is pressed. Here is a binding that responds to every keystroke:
...
<input #product class="form-control" (keyup)="selectedProduct=product.value" />
...
The keyup event is a standard DOM event, and the result is that application is updated as the user releases each key while typing in the input element. I can be more specific about which key I am interested in by specifying its name as part of the event binding, like this:
...
<input #product class="form-control"
    (keyup.enter)="selectedProduct=product.value" />
...

The key that the binding will respond to is specified by appending a period after the DOM event name, followed by the name of the key. This binding is for the Enter key, and the result is that the changes in the input element won’t be pushed into the rest of the application until that key is pressed.

Using Two-Way Data Bindings

Bindings can be combined to create a two-way flow of data for a single element, allowing the HTML document to respond when the application model changes and also allowing the application to respond when the element emits an event, as shown in Listing 14-9.
<div class="m-2">
  <div class="bg-info text-white p-2">
    Selected Product: {{selectedProduct || '(None)'}}
  </div>
  <table class="table table-sm table-bordered m-2">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index"
        [class.bg-info]="getSelected(item)">
      <td (mouseover)="selectedProduct=item.name">{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
  <div class="form-group">
    <label>Product Name</label>
    <input class="form-control" (input)="selectedProduct=$event.target.value"
           [value]="selectedProduct || ''" />
  </div>
  <div class="form-group">
    <label>Product Name</label>
    <input class="form-control" (input)="selectedProduct=$event.target.value"
           [value]="selectedProduct || ''" />
  </div>
</div>
Listing 14-9.

Creating a Two-Way Binding in the template.html File in the src/app Folder

Each of the input elements has an event binding and a property binding. The event binding responds to the input event by updating the component’s selectedProduct property. The property binding ties the value of the selectedProduct property to the element’s value property.

The result is that the contents of the two input elements are synchronized, and editing one causes the other to be updated as well. And, since there are other bindings in the template that depend on the selectedProduct property, editing the contents of an input element also changes the data displayed by the string interpolation binding and changes the highlighted table row, as shown in Figure 14-6.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig6_HTML.jpg
Figure 14-6.

Creating a two-way data binding

This is an example that makes the most sense when you experiment with it in the browser. Enter some text into one of the input elements, and you will see the same text displayed in the other input element and in the div element whose content is managed by the string interpolation binding. If you enter the name of a product into one of the input elements, such as Kayak or Lifejacket, then you will also see the corresponding row in the table highlighted.

The event binding for the mouseover event still takes effect, which means as you move the mouse pointer over the first row in the table, the changes to the selectedProduct value will cause the input elements to display the product name.

Using the ngModel Directive

The ngModel directive is used to simplify two-way bindings so that you don’t have to apply both an event and a property binding to the same element. Listing 14-10 shows how to replace the separate bindings with the ngModel directive.
<div class="m-2">
  <div class="bg-info text-white p-2">
    Selected Product: {{selectedProduct || '(None)'}}
  </div>
  <table class="table table-sm table-bordered m-2">
    <tr><th></th><th>Name</th><th>Category</th><th>Price</th></tr>
    <tr *ngFor="let item of getProducts(); let i = index"
        [class.bg-info]="getSelected(item)">
      <td (mouseover)="selectedProduct=item.name">{{i + 1}}</td>
      <td>{{item.name}}</td>
      <td>{{item.category}}</td>
      <td>{{item.price}}</td>
    </tr>
  </table>
  <div class="form-group">
    <label>Product Name</label>
    <input class="form-control" [(ngModel)]="selectedProduct" />
  </div>
  <div class="form-group">
    <label>Product Name</label>
    <input class="form-control" [(ngModel)]="selectedProduct" />
  </div>
</div>
Listing 14-10.

Using the ngModel Directive in the template.html File in the src/app Folder

Using the ngModel directive requires combining the syntax of the property and event bindings, as illustrated in Figure 14-7.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig7_HTML.jpg
Figure 14-7.

The anatomy of a two-way data binding

A combination of square and round brackets is used to denote a two-way data binding, with the round brackets placed inside the square ones: [( and )]. The Angular development team refers to this as the banana-in-a-box binding because that’s what the brackets and parentheses look like when placed like this [()]. Well, sort of.

The target for the binding is the ngModel directive, which is included in Angular to simplify creating two-way data bindings on form elements, such as the input elements used in the example.

The expression for a two-way data binding is the name of a property, which is used to set up the individual bindings behind the scenes. When the contents of the input element change, the new content will be used to update the value of the selectedProduct property. Equally, when the value of the selectedProduct value changes, it will be used to update the contents of the element.

The ngModel directive knows the combination of events and properties that the standard HTML elements define. Behind the scenes, an event binding is applied to the input event, and a property binding is applied to the value property.

Tip

It is important that you remember to use both brackets and parentheses with the ngModel binding. If you use just parentheses—(ngModel)—then you are setting an event binding for an event called ngModel, which doesn’t exist. The result is an element that won’t be updated or won’t update the rest of the application. You can use the ngModel directive with just square brackets—[ngModel]—and Angular will set the initial value of the element but won’t listen for events, which means that changes made by the user won’t be automatically reflected in the application model.

Working with Forms

Most web applications rely on forms for receiving data from users, and the two-way ngModel binding described in the previous section provides the foundation for using forms in Angular applications. In this section, I create a form that allows new products to be created and added to the application’s data model and then describe some of the more advanced form features that Angular provides.

Adding a Form to the Example Application

Listing 14-11 shows some enhancements to the component that will be used when the form is created and removes some features that are no longer required.
import { ApplicationRef, Component } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    newProduct: Product = new Product();
    get jsonProduct() {
        return JSON.stringify(this.newProduct);
    }
    addProduct(p: Product) {
        console.log("New Product: " + this.jsonProduct);
    }
}
Listing 14-11.

Enhancing the Component in the component.ts File in the src/app Folder

The listing adds a new property called newProduct, which will be used to store the data entered into the form by the user. There is also a jsonProduct property with a getter that returns a JSON representation of the newProduct property and that will be used in the template to show the effect of the two-way bindings. (I can’t create a JSON representation of an object directly in the template because the JSON object is defined in the global namespace, which, as I explained in Chapter 13, cannot be accessed directly from template expressions.)

The final addition is an addProduct method that writes out the value of the jsonProduct method to the console; this will let me demonstrate some basic form-related features before adding support for updating the data model later in the chapter.

In Listing 14-12, the template content has been replaced with a series of input elements for each of the properties defined by the Product class.
<div class="m-2">
  <div class="bg-info text-white mb-2 p-2">Model Data: {{jsonProduct}}</div>
  <div class="form-group">
    <label>Name</label>
    <input class="form-control" [(ngModel)]="newProduct.name" />
  </div>
  <div class="form-group">
    <label>Category</label>
    <input class="form-control" [(ngModel)]="newProduct.category" />
  </div>
  <div class="form-group">
    <label>Price</label>
    <input class="form-control" [(ngModel)]="newProduct.price" />
  </div>
  <button class="btn btn-primary" (click)="addProduct(newProduct)">Create</button>
</div>
Listing 14-12.

Adding Input Elements in the template.html File in the src/app Folder

Each input element is grouped together with a label and contained in a div element, which is styled using the Bootstrap form-group class. Individual input elements are assigned to the Bootstrap form-control class to manage the layout and style.

The ngModel binding has been applied to each input element to create a two-way binding with the corresponding property on the component’s newProduct object, like this:
...
<input class="form-control" [(ngModel)]="newProduct.name" />
...
There is also a button element, which has a binding for the click event that calls the component’s addProduct method, passing in the newProduct value as an argument.
...
<button class="btn btn-primary" (click)="addProduct(newProduct)">Create</button>
...
Finally, a string interpolation binding is used to display a JSON representation of the component’s newProduct property at the top of the template, like this:
...
<div class="bg-info text-white mb-2 p-2">Model Data: {{jsonProduct}}</div>
...
The overall result, illustrated in Figure 14-8, is a set of input elements that update the properties of a Product object managed by the component, which are reflected immediately in the JSON data.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig8_HTML.jpg
Figure 14-8.

Using the form elements to create a new object in the data model

When the Create button is clicked, the JSON representation of the component’s newProduct property is written to the browser’s JavaScript console, producing a result like this:
New Product: {"name":"Running Shoes","category":"Running","price":"120.23"}

Adding Form Data Validation

At the moment, any data can be entered into the input elements in the form. Data validation is essential in web applications because users will enter a surprising range of data values, either in error or because they want to get to the end of the process as quickly as possible and enter garbage values to proceed.

Angular provides an extensible system for validating the content of form elements, based on the approach used by the HTML5 standard. There are four attributes that you can add to input elements, each of which defines a validation rule, as described in Table 14-4.
Table 14-4.

The Built-in Angular Validation Attributes

Attribute

Description

required

This attribute is used to specify a value that must be provided.

minlength

This attribute is used to specify a minimum number of characters.

maxlength

This attribute is used to specify a maximum number of characters. This type of validation cannot be applied directly to form elements because it conflicts with the HTML5 attribute of the same name. It can be used with model-based forms, which are described later in the chapter.

pattern

This attribute is used to specify a regular expression that the value provided by the user must match.

You may be familiar with these attributes because they are part of the HTML specification, but Angular builds on these properties with some additional features. Listing 14-13 removes all but one of the input elements to demonstrate the process of adding validation to the form as simply as possible. (I restore the missing elements later in the chapter.)
<div class="m-2">
  <div class="bg-info p-2 mb-2">Model Data: {{jsonProduct}}</div>
  <form novalidate (ngSubmit)="addProduct(newProduct)">
    <div class="form-group">
      <label>Name</label>
      <input class="form-control"
             name="name"
             [(ngModel)]="newProduct.name"
             required
             minlength="5"
             pattern="^[A-Za-z ]+$" />
    </div>
    <button class="btn btn-primary" type="submit">
      Create
    </button>
  </form>
</div>
Listing 14-13.

Adding Form Validation in the template.html File in the src/app Folder

Angular requires elements being validated to define the name attribute, which is used to identify the element in the validation system. Since this input element is being used to capture the value of the Product.name property, the name attribute on the element has been set to name.

This listing adds three of the four validation attributes to the input element. The required attribute specifies that the user must provide a value, the minlength attribute specifies that there should be at least three characters, and the pattern attribute specifies that only alphabetic characters and spaces are allowed.

The validation attributes that Angular uses are the same ones used by the HTML 5 specification, so I have added the novalidate attribute to the form element, which tells the browser not to use its native validation features, which are inconsistently implemented by different browsers and generally get in the way. Since Angular will be providing the validation, the browser’s own implementation of these features is not required.

Finally, notice that a form element has been added to the template. Although you can use input elements independently, the Angular validation features work only when there is a form element present, and Angular will report an error if you add the ngControl directive to an element that is not contained in a form.

When using a form element, the convention is to use an event binding for a special event called ngSubmit like this:
...
<form novalidate (ngSubmit)="addProduct(newProduct)">
...

The ngSubmit binding handles the form element’s submit event. You can achieve the same effect binding to the click event on individual button elements within the form if you prefer.

Styling Elements Using Validation Classes

Once you have saved the template changes in Listing 14-13 and the browser has reloaded the HTML, right-click the input element in the browser window and select Inspect or Inspect Element from the pop-up window. The browser will display the HTML representation of the element in the Developer Tools window, and you will see that the input element has been added to three classes, like this:
...
<input class="form-control ng-pristine ng-invalid ng-touched" minlength="5"
    name="name" pattern="^[A-Za-z ]+$" required="" ng-reflect-name="name">
...
The classes to which an input element is assigned provide details of its validation state. There are three pairs of validation classes, which are described in Table 14-5. Elements will always be members of one class from each pair, for a total of three classes. The same classes are applied to the form element to show the overall validation status of all the elements it contains. As the status of the input element changes, the ngControl directive switches the classes automatically for both the individual elements and the form element.
Table 14-5.

The Angular Form Validation Classes

Name

Description

ng-untouchedng-touched

An element is assigned to the ng-untouched class if it has not been visited by the user, which is typically done by tabbing through the form fields. Once the user has visited an element, it is added to the ng-touched class.

ng-pristineng-dirty

An element is assigned to the ng-pristine class if its contents have not been changed by the user and to the ng-dirty class otherwise. Once the contents have been edited, an element remains in the ng-dirty class, even if the user then returns to the previous contents.

ng-validng-invalid

An element is assigned to the ng-valid class if its contents meet the criteria defined by the validation rules that have been applied to it and to the ng-invalid class otherwise.

These classes can be used to style form elements to provide the user with validation feedback. Listing 14-14 adds a style element to the template and defines styles that indicate when the user has entered invalid or valid data.

Tip

In real applications, styles should be defined in separate stylesheets and included in the application through the index.html file or using a component’s decorator settings (which I describe in Chapter 17). I have included the styles directly in the template for simplicity, but this makes real applications harder to maintain because it makes it difficult to figure out where styles are coming from when there are multiple templates in use.

<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<div class="m-2">
  <div class="bg-info p-2 mb-2">Model Data: {{jsonProduct}}</div>
  <form novalidate (ngSubmit)="addProduct(newProduct)">
    <div class="form-group">
      <label>Name</label>
      <input class="form-control"
             name="name"
             [(ngModel)]="newProduct.name"
             required
             minlength="5"
             pattern="^[A-Za-z ]+$" />
    </div>
    <button class="btn btn-primary" type="submit">
      Create
    </button>
  </form>
</div>
Listing 14-14.

Providing Validation Feedback in the template.html File in the src/app Folder

These styles set green and red borders for input elements whose content has been edited and is valid (and so belong to both the ng-dirty and ng-valid classes) and whose content is invalid (and so belong to the ng-dirty and ng-invalid classes). Using the ng-dirty class means that the appearance of the elements won’t be changed until after the user has entered some content.

Angular validates the contents and changes the class memberships of the input elements after each keystroke or focus change. The browser detects the changes to the elements and applies the styles dynamically, which provides users with validation feedback as they enter data into the form, as shown in Figure 14-9.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig9_HTML.jpg
Figure 14-9.

Providing validation feedback

As I start to type, the input element is shown as invalid because there are not enough characters to satisfy the minlength attribute. Once there are five characters, the border is green, indicating that the data is valid. When I type the character 2, the border turns red again because the pattern attribute is set to allow only letters and spaces.

Tip

If you look at the JSON data at the top of the page in Figure 14-9, you will see that the data bindings are still being updated, even when the data values are not valid. Validation runs alongside data bindings, and you should not act on form data without checking that the overall form is valid, as described in the “Validating the Entire Form” section.

Displaying Field-Level Validation Messages

Using colors to provide validation feedback tells the user that something is wrong but doesn’t provide any indication of what the user should do about it. The ngModel directive provides access to the validation status of the elements it is applied to, which can be used to display guidance to the user. Listing 14-15 adds validation messages for each of the attributes applied to the input element using the support provided by the ngModel directive.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<div class="m-2">
  <div class="bg-info p-2 mb-2">Model Data: {{jsonProduct}}</div>
  <form novalidate (ngSubmit)="addProduct(newProduct)">
    <div class="form-group">
      <label>Name</label>
      <input class="form-control"
             name="name"
             [(ngModel)]="newProduct.name"
             #name="ngModel"
             required
             minlength="5"
             pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstyled" *ngIf="name.dirty && name.invalid">
        <li *ngIf="name.errors.required">
          You must enter a product name
        </li>
        <li *ngIf="name.errors.pattern">
          Product names can only contain letters and spaces
        </li>
        <li *ngIf="name.errors.minlength">
          Product names must be at least
          {{name.errors.minlength.requiredLength}} characters
        </li>
      </ul>
    </div>
    <button class="btn btn-primary" type="submit">
      Create
    </button>
  </form>
</div>
Listing 14-15.

Adding Validation Messages in the template.html File in the src/app Folder

To get validation working, I have to create a template reference variable to access the validation state in expressions, which I do like this:
...
<input class="form-control" name="name" [(ngModel)]="newProduct.name"
    #name="ngModel" required minlength="5" pattern="^[A-Za-z ]+$"/>
...
I create a template reference variable called name and set its value to ngModel. This use of an ngModel value is a little confusing: it is a feature provided by the ngModel directive to give access to the validation status. This will make more sense once you have read Chapters 15 and 16, in which I explain how to create custom directives and you see how they provide access to their features. For this chapter, it is enough to know that in order to display validation messages, you need to create a template reference variable and assign it ngModel to access the validation data for the input element. The object that is assigned to the template reference variable defines the properties that are described in Table 14-6.
Table 14-6.

The Validation Object Properties

Name

Description

path

This property returns the name of the element.

valid

This property returns true if the element’s contents are valid and false otherwise.

invalid

This property returns true if the element’s contents are invalid and false otherwise.

pristine

This property returns true if the element’s contents have not been changed.

dirty

This property returns true if the element’s contents have been changed.

touched

This property returns true if the user has visited the element.

untouched

This property returns true if the user has not visited the element.

errors

This property returns an object whose properties correspond to each attribute for which there is a validation error.

value

This property returns the value of the element, which is used when defining custom validation rules, as described in the “Creating Custom Form Validators” section.

Listing 14-15 displays the validation messages in a list. The list should be shown only if there is at least one validation error, so I applied the ngIf directive to the ul element, with an expression that uses the dirty and invalid properties, like this:
...
<ul class="text-danger list-unstyled" *ngIf="name.dirty && name.invalid">
...
Within the ul element, there is an li element that corresponds to each validation error that can occur. Each li element has an ngIf directive that uses the errors property described in Table 14-6, like this:
...
<li *ngIf="name.errors.required">You must enter a product name</li>
...

The errors.required property will be defined only if the element’s contents have failed the required validation check, which ties the visibility of the li element to the outcome of that validation check.

Using the Safe Navigation Property With Forms
The errors property is created only when there are validation errors, which is why I check the value of the invalid property in the expression on the ul element. An alternative approach is to use the safe navigation property, which is used in templates to navigate through a series of properties without generating an error if one of them returns null. Here is an alternative approach to defining the template in Listing 14-15 that doesn’t check the valid property and relies on the safe navigation property instead:
...
<ul class="text-danger list-unstyled" *ngIf="name.dirty">
    <li *ngIf="name.errors?.required">
        You must enter a product name
    </li>
    <li *ngIf="name.errors?.pattern">
        Product names can only contain letters and spaces
    </li>
    <li *ngIf="name.errors?.minlength">
        Product names must be at least
        {{name.errors.minlength.requiredLength}} characters
    </li>
</ul>
...

Appending a ? character after a property name tells Angular not to try to access any subsequent properties or methods if the property is null or undefined. In this example, I have applied the ? character after the errors property, which means Angular won’t try to read the required, pattern, or minlength properties if the error property hasn’t been defined.

Each property defined by the errors object returns an object whose properties provide details of why the content has failed the validation check for its attribute, which can be used to make the validation messages more helpful to the user. Table 14-7 describes the error properties provided for each attribute.
Table 14-7.

The Angular Form Validation Error Description Properties

Name

Description

required

This property returns true if the required attribute has been applied to the input element. This is not especially useful because this can be deduced from the fact that the required property exists.

minlength.requiredLength

This property returns the number of characters required to satisfy the minlength attribute.

minlength.actualLength

This property returns the number of characters entered by the user.

pattern.requiredPattern

This property returns the regular expression that has been specified using the pattern attribute.

pattern.actualValue

This property returns the contents of the element.

These properties are not displayed directly to the user, who is unlikely to understand an error message that includes a regular expression, although they can be useful during development to figure out validation problems. The exception is the minlength.requiredLength property, which can be useful for avoiding the duplication of the value assigned to the minlength attribute on the element, like this:
...
<li *ngIf="name.errors.minlength">
  Product names must be at least {{name.errors.minlength.requiredLength}} characters
</li>
...
The overall result is a set of validation messages that are shown as soon as the user starts editing the input element and that change to reflect each new keystroke, as illustrated in Figure 14-10.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig10_HTML.jpg
Figure 14-10.

Displaying validation messages

Using the Component to Display Validation Messages

Including separate elements for all possible validation errors quickly becomes verbose in complex forms. A better approach is to add some logic to the component to prepare the validation messages in a method, which can then be displayed to the user through the ngFor directive in the template. Listing 14-16 shows the addition of a component method that accepts the validation state for an input element and produces an array of validation messages.
import { ApplicationRef, Component } from "@angular/core";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    newProduct: Product = new Product();
    get jsonProduct() {
        return JSON.stringify(this.newProduct);
    }
    addProduct(p: Product) {
        console.log("New Product: " + this.jsonProduct);
    }
    getValidationMessages(state: any, thingName?: string) {
        let thing: string = state.path || thingName;
        let messages: string[] = [];
        if (state.errors) {
            for (let errorName in state.errors) {
                switch (errorName) {
                    case "required":
                        messages.push(`You must enter a ${thing}`);
                        break;
                    case "minlength":
                        messages.push(`A ${thing} must be at least
                            ${state.errors['minlength'].requiredLength}
                            characters`);
                        break;
                    case "pattern":
                        messages.push(`The ${thing} contains
                             illegal characters`);
                        break;
                }
            }
        }
        return messages;
    }
}
Listing 14-16.

Generating Validation Messages in the component.ts File in the src/app Folder

The getValidationMessages method uses the properties described in Table 14-6 to produce validation messages for each error, returning them in a string array. To make this code as widely applicable as possible, the method accepts a value that describes the data item that an input element is intended to collect from the user, which is then used to generate error messages, like this:
...
messages.push(`You must enter a ${thing}`);
...
This is an example of the JavaScript string interpolation feature, which allows strings to be defined like templates, without having to use the + operator to include data values. Note that the template string is denoted with backtick characters (the ` character and not the regular JavaScript ' character). The getValidationMessages method defaults to using the path property as the descriptive string if an argument isn’t received when the method is invoked, like this:
...
let thing: string = state.path || thingName;
...
Listing 14-17 shows how the getValidationMessages can be used in the template to generate validation error messages for the user without needing to define separate elements and bindings for each one.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<div class="m-2">
  <div class="bg-info p-2 mb-2">Model Data: {{jsonProduct}}</div>
  <form novalidate (ngSubmit)="addProduct(newProduct)">
    <div class="form-group">
      <label>Name</label>
      <input class="form-control"
             name="name"
             [(ngModel)]="newProduct.name"
             #name="ngModel"
             required
             minlength="5"
             pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstyled" *ngIf="name.dirty && name.invalid">
        <li *ngFor="let error of getValidationMessages(name)">
          {{error}}
        </li>
      </ul>
    </div>
    <button class="btn btn-primary" type="submit">
      Create
    </button>
  </form>
</div>
Listing 14-17.

Getting Validation Messages in the template.html File in the src/app Folder

There is no visual change, but the same method can be used to produce validation messages for multiple elements, which results in a simpler template that is easier to read and maintain.

Validating the Entire Form

Displaying validation error messages for individual fields is useful because it helps emphasize where problems need to be fixed. But it can also be useful to validate the entire form. Care must be taken not to overwhelm the user with error messages until they try to submit the form, at which point a summary of any problems can be useful. In preparation, Listing 14-18 adds two new members to the component.
import { ApplicationRef, Component } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    // ...other methods omitted for brevity...
    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;
        }
    }
}
Listing 14-18.

Enhancing the Component in the component.ts File in the src/app Folder

The formSubmitted property will be used to indicate whether the form has been submitted and will be used to prevent validation of the entire form until the user has tried to submit.

The submitForm method will be invoked when the user submits the form and receives an NgForm object as its argument. This object represents the form and defines the set of validation properties; these properties are used to describe the overall validation status of the form so that, for example, the invalid property will be true if there are validation errors on any of the elements contained by the form. In addition to the validation property, NgForm provides the reset method, which resets the validation status of the form and returns it to its original and pristine state.

The effect is that the whole form will be validated when the user performs a submit, and if there are no validation errors, a new object will be added to the data model before the form is reset so that it can be used again. Listing 14-19 shows the changes required to the template to take advantage of these new features and implement form-wide validation.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<div class="m-2">
  <form novalidate #form="ngForm" (ngSubmit)="submitForm(form)">
    <div class="bg-danger text-white p-2 mb-2"
         *ngIf="formSubmitted && form.invalid">
      There are problems with the form
    </div>
    <div class="form-group">
      <label>Name</label>
      <input class="form-control"
             name="name"
             [(ngModel)]="newProduct.name"
             #name="ngModel"
             required
             minlength="5"
             pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstyled"
          *ngIf="(formSubmitted || name.dirty) && name.invalid">
        <li *ngFor="let error of getValidationMessages(name)">
          {{error}}
        </li>
      </ul>
    </div>
    <button class="btn btn-primary" type="submit">
      Create
    </button>
  </form>
</div>
Listing 14-19.

Performing Form-Wide Validation in the template.html File in the src/app Folder

The form element now defines a reference variable called form, which has been assigned to ngForm. This is how the ngForm directive provides access to its functionality, through a process that I describe in Chapter 15. For now, however, it is important to know that the validation information for the entire form can be accessed through the form reference variable.

The listing also changes the expression for the ngSubmit binding so that it calls the submitForm method defined by the controller, passing in the template variable, like this:
...
<form novalidate ngForm="productForm" #form="ngForm" (ngSubmit)="submitForm(form)">
...

It is this object that is received as the argument of the submitForm method and that is used to check the validation status of the form and to reset the form so that it can be used again.

Listing 14-19 also adds a div element that uses the formSubmitted property from the component along with the valid property (provided by the form template variable) to show a warning message when the form contains invalid data, but only after the form has been submitted.

In addition, the ngIf binding has been updated to display the field-level validation messages so that they will be shown when the form has been submitted, even if the element itself hasn’t been edited. The result is a validation summary that is shown only when the user submits the form with invalid data, as illustrated by Figure 14-11.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig11_HTML.jpg
Figure 14-11.

Displaying a validation summary message

Displaying Summary Validation Messages

In a complex form, it can be helpful to provide the user with a summary of all the validation errors that have to be resolved. The NgForm object assigned to the form template reference variable provides access to the individual elements through a property named controls. This property returns an object that has properties for each of the individual elements in the form. For example, there is a name property that represents the input element in the example, which is assigned an object that represents that element and defines the same validation properties that are available for individual elements. In Listing 14-20, I have added a method to the component that receives the object assigned to the form element’s template reference variables and uses its controls property to generate a list of error messages for the entire form.
import { ApplicationRef, Component } from "@angular/core";
import { NgForm } from "@angular/forms";
import { Model } from "./repository.model";
import { Product } from "./product.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    // ...other methods omitted for brevity...
    getFormValidationMessages(form: NgForm): string[] {
        let messages: string[] = [];
        Object.keys(form.controls).forEach(k => {
            this.getValidationMessages(form.controls[k], k)
                .forEach(m => messages.push(m));
        });
        return messages;
    }
}
Listing 14-20.

Generating Form-Wide Validation Messages in the component.ts File in the src/app Folder

The getFormValidationMessages method builds its list of messages by calling the getValidationMessages method defined in Listing 14-16 for each control in the form. The Object.keys method creates an array from the properties defined by the object returned by the controls property, which is enumerated using the forEach method.

In Listing 14-21, I have used this method to include the individual messages at the top of the form, which will be visible once the user clicks the Create button.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<div class="m-2">
  <form novalidate #form="ngForm" (ngSubmit)="submitForm(form)">
    <div class="bg-danger text-white p-2 mb-2"
         *ngIf="formSubmitted && form.invalid">
      There are problems with the form
      <ul>
        <li *ngFor="let error of getFormValidationMessages(form)">
          {{error}}
        </li>
      </ul>
    </div>
    <div class="form-group">
      <label>Name</label>
      <input class="form-control"
             name="name"
             [(ngModel)]="newProduct.name"
             #name="ngModel"
             required
             minlength="5"
             pattern="^[A-Za-z ]+$" />
      <ul class="text-danger list-unstyled"
          *ngIf="(formSubmitted || name.dirty) && name.invalid">
          <li *ngFor="let error of getValidationMessages(name)">
            {{error}}
          </li>
        </ul>
    </div>
    <button class="btn btn-primary" type="submit">
      Create
    </button>
  </form>
</div>
Listing 14-21.

Displaying Form-Wide Validation Messages in the template.html File in the src/app Folder

The result is that validation messages are displayed alongside the input element and collected at the top of the form once it has been submitted, as shown in Figure 14-12.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig12_HTML.jpg
Figure 14-12.

Displaying an overall validation summary

Disabling the Submit Button

The final adjustment in this section is to disable the button once the user has submitted the form, preventing the user from clicking it again until all the validation errors have been resolved. This is a commonly used technique even though it has little bearing on the application, which won’t accept the data from the form while it contains invalid values but provides useful reinforcement to the user that they cannot proceed until the validation problems have been resolved.

In Listing 14-22, I have used the property binding on the button element and added an input element for the price property to show how the approach scales up with multiple elements in the form.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<div class="m-2">
    <form novalidate #form="ngForm" (ngSubmit)="submitForm(form)">
        <div class="bg-danger text-white p-2 mb-2"
                 *ngIf="formSubmitted && form.invalid">
            There are problems with the form
            <ul>
                <li *ngFor="let error of getFormValidationMessages(form)">
                    {{error}}
                </li>
            </ul>
        </div>
        <div class="form-group">
            <label>Name</label>
            <input class="form-control"
                   name="name"
                   [(ngModel)]="newProduct.name"
                   #name="ngModel"
                   required
                   minlength="5"
                   pattern="^[A-Za-z ]+$" />
            <ul class="text-danger list-unstyled"
                *ngIf="(formSubmitted || name.dirty) && name.invalid">
                <li *ngFor="let error of getValidationMessages(name)">
                    {{error}}
                </li>
            </ul>
        </div>
        <div class="form-group">
            <label>Price</label>
            <input class="form-control" name="price" [(ngModel)]="newProduct.price"
                   #price="ngModel" required pattern="^[0-9.]+$" />
            <ul class="text-danger list-unstyled"
                *ngIf="(formSubmitted || price.dirty) && price.invalid">
                <li *ngFor="let error of getValidationMessages(price)">
                    {{error}}
                </li>
            </ul>
        </div>
        <button class="btn btn-primary" type="submit"
                [disabled]="formSubmitted && form.invalid"
                [class.btn-secondary]="formSubmitted && form.invalid">
            Create
        </button>
    </form>
</div>
Listing 14-22.

Disabling the Button and Adding an Input Element in the template.html File in the src/app Folder

For extra emphasis, I used the class binding to add the button element to the btn-secondary class when the form has been submitted and has invalid data. This class applies a Bootstrap CSS style, as shown in Figure 14-13.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig13_HTML.jpg
Figure 14-13.

Disabling the submit button

Using Model-Based Forms

The form in the previous section relies on HTML elements and attributes to define the fields that make up the form and also to apply the validation constraints. The advantage of this approach is that it is familiar and simple. The disadvantage is that large forms become complex and difficult to maintain, with each field demanding its own block of content to manage its layout and its validation requirements and to display any validation messages.

Angular provides another approach, known as model-based forms, in which the details of the form and its validation are defined in code rather than a template. This approach scales up better, but it requires some up-front effort, and the results are not as natural as defining everything in the template. In the sections that follow, I set up and apply a model that describes the form and the validation it requires.

Enabling Model-Based Forms Feature

The support for model-based forms requires a new dependency to be declared in the application’s Angular module, as shown in Listing 14-23.
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { ProductComponent } from "./component";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
@NgModule({
    imports: [BrowserModule, FormsModule, ReactiveFormsModule],
    declarations: [ProductComponent],
    bootstrap: [ProductComponent]
})
export class AppModule {}
Listing 14-23.

Enabling Model-Based Forms in the app.module.ts File in the src/app Folder

The model-based forms feature is defined in a module called ReactiveFormsModule, which is defined in the @angular/forms JavaScript module, which was added to the project at the start of the chapter.

Defining the Form Model Classes

I am going to start by defining classes that will describe the form so that I can keep the template as simple as possible. You don’t have to follow this approach completely, but if you are going to adopt model-based forms, it makes sense to handle as much of the form as possible in the model and minimize the complexity of the template. I added a file called form.model.ts to the src/app folder and added the code shown in Listing 14-24.
import { FormControl, FormGroup, Validators } from "@angular/forms";
export class ProductFormControl extends FormControl {
    label: string;
    modelProperty: string;
    constructor(label:string, property:string, value: any, validator: any) {
        super(value, validator);
        this.label = label;
        this.modelProperty = property;
    }
}
export class ProductFormGroup extends FormGroup {
    constructor() {
        super({
            name: new ProductFormControl("Name", "name", "", Validators.required),
            category: new ProductFormControl("Category", "category", "",
                Validators.compose([Validators.required,
                    Validators.pattern("^[A-Za-z ]+$"),
                    Validators.minLength(3),
                    Validators.maxLength(10)])),
            price: new ProductFormControl("Price", "price", "",
                Validators.compose([Validators.required,
                    Validators.pattern("^[0-9.]+$")]))
        });
    }
}
Listing 14-24.

The Contents of the form.model.ts File in the src/app Folder

The two classes defined in the listing extend the ones that Angular uses to manage forms and their contents behind the scenes. The FormControl class is used to represent a single element in a form, such as input element, and the FormGroup class is used to manage a form element and its contents.

The new subclasses add features that will make it easier to generate the HTML form programmatically. The ProductFormControl class extends the FormControl class with properties that specify the text for the label element associated with an input element and with the name of the Product class property that the input element will represent.

The ProductFormGroup class extends FormGroup. The important part of this class is the constructor for the ProductFormGroup class, which is responsible for setting up the model that will be used to create and validate the form. The constructor for the FormGroup class, which is the superclass for ProductFormGroup, accepts an object whose property names correspond to the names of the input elements in the template, each of which is assigned a ProductFormControl object that will represent it and that specifies the validation checks that are required. The first property in the object passed to the super constructor is the simplest.
...
name: new ProductFormControl("Name", "name", "", Validators.required),
...
The property is called name, which tells Angular that it corresponds to an input element called name in the template. The arguments for the ProductFormControl constructor specify the content for the label element that will be associated with the input element (Name), the name of the Product class property that the input element will be bound to (name), the initial value for the data binding (the empty string), and the validation checks that are required. Angular defines a class called Validators in the @angular/forms module that has properties for each of the built-in validation checks, as described in Table 14-8.
Table 14-8.

The Validator Properties

Name

Description

Validators.required

This property corresponds to the required attribute and ensures that a value is entered.

Validators.minLength

This property corresponds to the minlength attribute and ensures a minimum number of characters.

Validators.maxLength

This property corresponds to the maxlength attribute and ensures a maximum number of characters.

Validators.pattern

This property corresponds to the pattern attribute and matches a regular expression.

Validators can be combined using the Validators.compose method so that several checks are performed on a single element, like this:
...
category: new ProductFormControl("Category", "category", "",
    Validators.compose([Validators.required,
        Validators.pattern("^[A-Za-z ]+$"),
        Validators.minLength(3),
        Validators.maxLength(10)])),
...

The Validators.compose method accepts an array of validators. The constructor arguments defined by the pattern, minLength, and maxLength validators correspond to the attribute values. The overall effect for this element is that values are required, must contain only alphabet characters and spaces, and must be between three and ten characters.

The next step is to move the methods that generate validation error messages from the component into the new form model classes, as shown in Listing 14-25. This keeps all the form-related code together and helps keep the component as simple as possible. (I have also added validation message support for the maxLength validator in the ProductFormControl class’s getValidationMessages method.)
import { FormControl, FormGroup, Validators } from "@angular/forms";
export class ProductFormControl extends FormControl {
    label: string;
    modelProperty: string;
    constructor(label:string, property:string, value: any, validator: any) {
        super(value, validator);
        this.label = label;
        this.modelProperty = property;
    }
    getValidationMessages() {
        let messages: string[] = [];
        if (this.errors) {
            for (let errorName in this.errors) {
                switch (errorName) {
                    case "required":
                        messages.push(`You must enter a ${this.label}`);
                        break;
                    case "minlength":
                        messages.push(`A ${this.label} must be at least
                            ${this.errors['minlength'].requiredLength}
                            characters`);
                        break;
                    case "maxlength":
                        messages.push(`A ${this.label} must be no more than
                            ${this.errors['maxlength'].requiredLength}
                            characters`);
                        break;
                    case "pattern":
                        messages.push(`The ${this.label} contains
                             illegal characters`);
                        break;
                }
            }
        }
        return messages;
    }
}
export class ProductFormGroup extends FormGroup {
    constructor() {
        super({
            name: new ProductFormControl("Name", "name", "", Validators.required),
            category: new ProductFormControl("Category", "category", "",
                Validators.compose([Validators.required,
                    Validators.pattern("^[A-Za-z ]+$"),
                    Validators.minLength(3),
                    Validators.maxLength(10)])),
            price: new ProductFormControl("Price", "price", "",
                Validators.compose([Validators.required,
                    Validators.pattern("^[0-9.]+$")]))
        });
    }
    get productControls(): ProductFormControl[] {
        return Object.keys(this.controls)
            .map(k => this.controls[k] as ProductFormControl);
    }
    getValidationMessages(name: string): string[] {
        return (this.controls['name'] as ProductFormControl).getValidationMessages();
    }
    getFormValidationMessages() : string[] {
        let messages: string[] = [];
        Object.values(this.controls).forEach(c =>
            messages.push(...(c as ProductFormControl).getValidationMessages()));
        return messages;
    }
}
Listing 14-25.

Moving the Validation Message Methods in the form.model.ts File in the src/app Folder

The validation messages are generated in the same way as they were previously, with minor adjustments to reflect the fact that the code is now part of the form model rather than the component.

Using the Model for Validation

Now that I have a form model, I can use it to validate the form. Listing 14-26 shows how the component class has been updated to enable model-based forms and to make the form model classes available to the template. It also removes the methods that generate the validation error messages, which were moved into the form model classes in Listing 14-25.
import { ApplicationRef, Component } from "@angular/core";
import { NgForm, FormGroup } from "@angular/forms";
import { Model } from "./repository.model";
import { Product } from "./product.model";
import { ProductFormGroup, ProductFormControl } from "./form.model";
@Component({
    selector: "app",
    templateUrl: "template.html"
})
export class ProductComponent {
    model: Model = new Model();
    formGroup: ProductFormGroup = new ProductFormGroup();
    getProduct(key: number): Product {
        return this.model.getProduct(key);
    }
    getProducts(): Product[] {
        return this.model.getProducts();
    }
    newProduct: Product = new Product();
    get jsonProduct() {
        return JSON.stringify(this.newProduct);
    }
    addProduct(p: Product) {
        console.log("New Product: " + this.jsonProduct);
    }
    formSubmitted: boolean = false;
    submitForm() {
        Object.keys(this.formGroup.controls)
            .forEach(c => this.newProduct[c] = this.formGroup.controls[c].value);
        this.formSubmitted = true;
        if (this.formGroup.valid) {
            this.addProduct(this.newProduct);
            this.newProduct = new Product();
            this.formGroup.reset();
            this.formSubmitted = false;
        }
    }
}
Listing 14-26.

Using a Form Model in the component.ts File in the src/app Folder

The listing imports the ProductFormGroup class from the form.model module and uses it to define a property called form, which makes the custom form model class available for use in the template.

Listing 14-27 updates the template to use the model-based features to handle validation, replacing the attribute-based validation configuration defined in the template.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
    <div class="bg-danger text-white p-2 mb-2"
            *ngIf="formSubmitted && formGroup.invalid">
        There are problems with the form
        <ul>
            <li *ngFor="let error of formGroup.getFormValidationMessages()">
                {{error}}
            </li>
        </ul>
    </div>
    <div class="form-group">
        <label>Name</label>
        <input class="form-control" name="name" formControlName="name" />
        <ul class="text-danger list-unstyled"
                *ngIf="(formSubmitted || formGroup.controls['name'].dirty) &&
                    formGroup.controls['name'].invalid">
            <li *ngFor="let error of formGroup.getValidationMessages('name')">
                {{error}}
            </li>
        </ul>
    </div>
    <div class="form-group">
        <label>Category</label>
        <input class="form-control" name="name" formControlName="category" />
        <ul class="text-danger list-unstyled"
                *ngIf="(formSubmitted || formGroup.controls['category'].dirty) &&
                    formGroup.controls['category'].invalid">
            <li *ngFor="let error of formGroup.getValidationMessages('category')">
                {{error}}
            </li>
        </ul>
    </div>
    <div class="form-group">
        <label>Price</label>
        <input class="form-control" name="price" formControlName="price" />
        <ul class="text-danger list-unstyled"
                *ngIf="(formSubmitted || formGroup.controls['price'].dirty) &&
                formGroup.controls['price'].invalid">
            <li *ngFor="let error of formGroup.getValidationMessages('price')">
                {{error}}
            </li>
        </ul>
    </div>
    <button class="btn btn-primary" type="submit"
        [disabled]="formSubmitted && formGroup.invalid"
        [class.btn-secondary]="formSubmitted && formGroup.invalid">
            Create
    </button>
</form>
Listing 14-27.

Using a Form Model in the template.html File in the src/app Folder

The first changes are to the form element. Using model-based validation requires the formGroup directive, like this:
...
<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
...

The value assigned to the formGroup directive is the component’s form property, which returns the ProductFormGroup object, which is the source of validation information about the form.

The next changes are to the input elements. The individual validation attributes and the template variable that was assigned the special ngForm value have been removed. A new forControlName attribute has been added, which identifies the input element to the model-based form system, using the name used in the ProductFormGroup in Listing 14-24.
...
<input class="form-control" name="name" formControlName="name" />
...
This attribute allows Angular to add and remove the validation classes for the input element. In this case, the formControlName attribute has been set to name, which tells Angular that this element should be validated using specific validators.
...
name: new ProductFormControl("Name", "name", "", Validators.required),
...

The FormGroup class provides a controls property that returns a collection of the FormControl objects that it is managing, indexed by name. Individual FormControl objects can be retrieved from the collection and either inspected to get the validation state or used to generate validation messages.

As part of the changed in Listing 14-27, I have added all three input elements required to get the data to create new Product objects, each of which is checked using the validation model, as shown in Figure 14-14.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig14_HTML.jpg
Figure 14-14.

Using model-based form validation

Generating the Elements from the Model

There is a lot of duplication in Listing 14-27. The validation attributes have been moved into the code, but each input element still requires a supporting framework of content to handle its layout and to display its validation messages to the user.

The next step is to simplify the template by using the form model to generate the elements in the form and not just validate them. Listing 14-28 shows how the standard Angular directives can be combined with the form model to generate the form programmatically.
<style>
    input.ng-dirty.ng-invalid { border: 2px solid #ff0000 }
    input.ng-dirty.ng-valid { border: 2px solid #6bc502 }
</style>
<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
    <div class="bg-danger text-white p-2 mb-2"
            *ngIf="formSubmitted && formGroup.invalid">
        There are problems with the form
        <ul>
            <li *ngFor="let error of formGroup.getFormValidationMessages()">
                {{error}}
            </li>
        </ul>
    </div>
    <div class="form-group" *ngFor="let control of formGroup.productControls">
        <label>{{control.label}}</label>
        <input class="form-control"
            name="{{control.modelProperty}}"
            formControlName="{{control.modelProperty}}" />
        <ul class="text-danger list-unstyled"
                *ngIf="(formSubmitted || control.dirty) && control.invalid">
            <li *ngFor="let error of control.getValidationMessages()">
                {{error}}
            </li>
        </ul>
    </div>
    <button class="btn btn-primary" type="submit"
        [disabled]="formSubmitted && formGroup.invalid"
        [class.btn-secondary]="formSubmitted && formGroup.invalid">
            Create
    </button>
</form>
Listing 14-28.

Using the Model to Generate the Form in the template.html File in the src/app Folder

This listing uses the ngFor directive to create form elements using the description provided by the ProductFormControl and ProductFormGroup model classes. Each element is configured with the same attributes as in Listing 14-27, but their values are taken from the model descriptions, which allows the template to be simplified and rely on the model for both the definition of the form elements and their validation.

Once you have a basic form model in place, you can expand it and extend it to reflect the needs of the application. For example, you can add new elements, extend the FormControl subclass to contain additional information (such as values for the type attribute of the input element), generate select elements for fields, and provide placeholder values to help guide the user.

Creating Custom Form Validators

Angular supports custom form validators, which can be used to enforce a validation policy that is specific to the application, rather than the general-purpose validation that the built-in validators provide. To demonstrate, I added a file called limit.formvalidator.ts to the src/app folder and used it to define the class shown in Listing 14-29.
import { FormControl } from "@angular/forms";
export class LimitValidator {
    static Limit(limit:number) {
        return (control:FormControl) : {[key: string]: any} => {
            let val = Number(control.value);
            if (val != NaN && val > limit) {
                return {"limit": {"limit": limit, "actualValue": val}};
            } else {
                return null;
            }
        }
    }
}
Listing 14-29.

The Contents of the limit.formvalidator.ts File in the src/app Folder

Custom validators are factories that create functions used to perform validation. In this case, the LimitValidator class defines the Limit method, which is static and is the factory that returns the validation function. The argument to the Limit method is the largest value that should be allowed to pass validation.

When Angular invokes the validation function returned by the Limit method, it provides a FormControl method as the argument. The custom validation function in the listing uses the value property to get the value entered by the user, convert it to a number, and compare it to the allowed limit.

Validation functions return null for valid values and return an object that contains details of the error for invalid values. To describe a validation error, the object defines a property that specifies which validation rule has failed, which is limit in this case, and assigns the property another object that provides details. The limit property returns an object that has a limit property that is set to the validation limit and an actualValue property that is set to the value entered by the user.

Applying a Custom Validator

Listing 14-30 shows how the form model has been extended to support the new custom validator class and apply it to the input element for the product’s price property.
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { LimitValidator } from "./limit.formvalidator";
export class ProductFormControl extends FormControl {
    label: string;
    modelProperty: string;
    constructor(label:string, property:string, value: any, validator: any) {
        super(value, validator);
        this.label = label;
        this.modelProperty = property;
    }
    getValidationMessages() {
        let messages: string[] = [];
        if (this.errors) {
            for (let errorName in this.errors) {
                switch (errorName) {
                    case "required":
                        messages.push(`You must enter a ${this.label}`);
                        break;
                    case "minlength":
                        messages.push(`A ${this.label} must be at least
                            ${this.errors['minlength'].requiredLength}
                            characters`);
                        break;
                    case "maxlength":
                        messages.push(`A ${this.label} must be no more than
                            ${this.errors['maxlength'].requiredLength}
                            characters`);
                        break;
                    case "pattern":
                        messages.push(`The ${this.label} contains
                             illegal characters`);
                        break;
                    case "limit":
                        messages.push(`A ${this.label} cannot be more
                            than ${this.errors['limit'].limit}`);
                        break;
                }
            }
        }
        return messages;
    }
}
export class ProductFormGroup extends FormGroup {
    constructor() {
        super({
            name: new ProductFormControl("Name", "name", "", Validators.required),
            category: new ProductFormControl("Category", "category", "",
                Validators.compose([Validators.required,
                    Validators.pattern("^[A-Za-z ]+$"),
                    Validators.minLength(3),
                    Validators.maxLength(10)])),
            price: new ProductFormControl("Price", "price", "",
                Validators.compose([Validators.required,
                    LimitValidator.Limit(100),
                    Validators.pattern("^[0-9.]+$")]))
        });
    }
    get productControls(): ProductFormControl[] {
        return Object.keys(this.controls)
            .map(k => this.controls[k] as ProductFormControl);
    }
    getValidationMessages(name: string): string[] {
        return (this.controls['name'] as ProductFormControl).getValidationMessages();
    }
    getFormValidationMessages() : string[] {
        let messages: string[] = [];
        Object.values(this.controls).forEach(c =>
            messages.push(...(c as ProductFormControl).getValidationMessages()));
        return messages;
    }
}
Listing 14-30.

Applying a Custom Validator in the form.model.ts File in the src/app Folder

The result is that the value entered into the Price field has a limit of 100, and larger values display the validation error message shown in Figure 14-15.
../images/421542_4_En_14_Chapter/421542_4_En_14_Fig15_HTML.jpg
Figure 14-15.

A custom validation message

Summary

In this chapter, I introduced the way that Angular supports user interaction using events and forms. I explained how to create event bindings, how to create two-way bindings, and how they can be simplified using the ngModel directive. I also described the support that Angular provides for managing and validating HTML forms. In the next chapter, I explain how to create custom directives.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset