Chapter 7. Working with forms

This chapter covers

  • Understanding the Angular Forms API (NgModel, FormControl, FormGroup, form directives, FormBuilder)
  • Working with template-driven forms
  • Working with reactive forms
  • Understanding form validation

Angular offers rich support for handling forms. It goes beyond regular data-binding by treating form fields as first-class citizens and providing fine-grained control over form data.

This chapter will start by demonstrating how you can implement a sample user registration form in pure HTML. While working on this form, we’ll briefly discuss the standard HTML forms and their shortcomings. Then you’ll see what the Angular Forms API brings to the table, and we’ll cover the template-driven and reactive approaches to creating forms in Angular.

After covering the basics, you’ll refactor the original version of the user registration form to use the template-driven approach, and we’ll discuss its pros and cons. Then we’ll do the same with the reactive approach. After that, we’ll discuss form validation. At the end of the chapter, you’ll apply this new knowledge to the online auction application and start implementing its search form component.

Template-driven vs. reactive approaches

In a template-driven approach, forms are fully programmed in the component’s template. The template defines the structure of the form, the format of its fields, and the validation rules.

In contrast, in a reactive approach, you create the form model programmatically in the code (in TypeScript, in this case). The template can be either statically defined and bound to an existing form model or dynamically generated based on the model.

By the end of the chapter, you’ll be familiar with the Angular Forms API and the various ways of working with forms and applying data validation.

7.1. Overview of HTML forms

HTML provides basic features for displaying forms, validating entered values, and submitting the data to the server. But HTML forms may not be good enough for real-world business applications, which need a way to programmatically process the entered data, apply custom validation rules, display user-friendly error messages, transform the format of the entered data, and choose the way data is submitted to the server. For business applications, one of the most important considerations when choosing a web framework is how well it handles forms.

In this section, we’ll evaluate standard HTML form features using a sample user registration form, and we’ll define a set of requirements that a modern web application needs to fulfill users’ expectations. We’ll also look at the form features provided by Angular.

7.1.1. Standard browser features

You may be wondering what you need from an application framework other than data-binding, if HTML already allows you to validate and submit forms. To answer this question, let’s review an HTML form that uses only standard browser features.

Listing 7.1. Plain HTML user registration form
<form action="/register" method="POST">
  <div>Username:         <input type="text"></div>
  <div>SSN:              <input type="text"></div>
  <div>Password:         <input type="password"></div>
  <div>Confirm password: <input type="password"></div>
  <button type="submit">Submit</button>
</form>

The form contains a button and four input fields: username, SSN, password, and password confirmation. Users can enter whatever values they want: no input validation is applied here. When the user clicks the Submit button, the form’s values are submitted to the server’s /register endpoint using HTTP POST, and the page is refreshed.

The default HTML form behavior isn’t a good fit for a SPA, which typically needs the following functionality:

  • Validation rules should be applied to individual input fields.
  • Error messages should be displayed next to the input fields that cause the problems.
  • Dependent fields should be validated all together. This form has password and password-confirmation fields, so whenever either of them is changed, both fields should be revalidated.
  • The application should be in control of the values submitted to the server. When the user clicks the Submit button, the application should invoke an event-handler function to pass the form values. The application can validate the values or change their format before sending the submit request.
  • The application should decide how the data is submitted to the server, whether it’s a regular HTTP request, an AJAX request, or a WebSocket message.

HTML’s validation attributes and semantic input types partially satisfy the first two requirements.

HTML validation attributes

Several standard validation attributes allow you to validate individual input fields: required, pattern, maxlength, min, max, step, and so on. For example, you can request that username be a required field and its value should contain only letters and numbers:

<input id="username" type="text" required pattern="[a-zA-Z0-9]+">

Here you use a regular expression, [a-zA-Z0-9]+, to restrict what can be entered in this field. When the user clicks the Submit button, the form will be validated before the submit request is sent. In figure 7.1, you can see the default error message displayed in the Chrome browser when username doesn’t conform to the specified pattern.

Figure 7.1. Validation error message

There are a number of problems with this message:

  • It’s too vague and doesn’t help the user identify and fix the problem.
  • As soon as the input field loses focus, the error message disappears.
  • This message format likely won’t match other styles in the application.

This input field prevents users from submitting invalid values, but it doesn’t let you provide a decent user experience by helping the user with friendly client-side validation.

Semantic input types

HTML supports multiple types of input elements: text, number, url, email, and so on. Choosing the right type for a form field may prevent users from entering an invalid value. But although this provides a better user experience, it’s still not enough to satisfy application-specific validation needs.

Let’s consider a ZIP code field (a U.S. postal code). It might be tempting to use the number input element, because a ZIP code is represented by a numeric value (at least, in the United States). To keep the values within a certain range, you could use the min and max attributes. For example, for a five-digit ZIP code, you could use following markup:

<input id="zipcode" type="number" min="10000" max="99999">

But not every five-digit number is a valid ZIP code. In a more complex example, you might also want to allow only a subset of ZIP codes from the user’s state.

To support all these real-world scenarios, you need more advanced form support, and this is what an application framework can provide. Let’s see what Angular has to offer.

7.1.2. Angular’s Forms API

There are two approaches to working with forms in Angular: template-driven and reactive. These two approaches are exposed as two different APIs (sets of directives and TypeScript classes) in Angular.

With the template-driven approach, the form model is defined in the component’s template using directives. Because you’re limited to the HTML syntax while defining the form model, the template-driven approach suits only simple scenarios.

For complex forms, the reactive approach is a better option. With the reactive approach, you create an underlying data structure in the code (not in the template). After the model is created, you link the HTML template elements to the model using special directives prefixed with form*. Unlike template-driven forms, reactive forms can be tested without a web browser.

Let’s highlight several important concepts to further clarify the difference between template-driven and reactive forms:

  • Both types of forms have a model, which is an underlying data structure that stores the form’s data. In the template-driven approach, the model is created implicitly by Angular based on the directives you attach to the template’s elements. In the reactive approach, you create the model explicitly and then link the HTML template elements to that model.
  • The model is not an arbitrary object. It’s an object constructed using classes defined in the @angular/forms module: FormControl, FormGroup, and Form-Array. In the template-driven approach, you don’t access these classes directly, whereas in the reactive approach you explicitly create instances of these classes.
  • The reactive approach doesn’t spare you from writing an HTML template. The view won’t be generated for you by Angular.
Enabling Forms API support

Both types of forms—template-driven and reactive—need to be explicitly enabled before you start using them. To enable template-driven forms, add FormsModule from @angular/forms to the imports list of the NgModule that uses the Forms API. For reactive forms, use ReactiveFormsModule. Here’s how to do it:

We won’t repeat this code for each example in this chapter, but all of them assume the modules are imported. All the downloadable code samples for the book import the modules in AppModule.

7.2. Template-driven forms

As we mentioned earlier, you can use only directives to define a model in the template-driven approach. But what directives can you use? These directives come with FormsModule: NgModel, NgModelGroup, and NgForm.

In chapter 5, we discussed how the NgModel directive can be used for two-way data binding. But in the Forms API, it plays a different role: it marks the HTML element that should become a part of the form model. Although these two roles are separate, they don’t conflict and can be safely used at the same time on a single HTML element. You’ll see examples later in this section. Let’s briefly look at these directives and then apply the template-driven approach to the sample registration form.

7.2.1. Directives overview

Here we’ll briefly describe the three main directives from FormsModule: NgModel, NgModelGroup, and NgForm. We’ll show how they can be used in the template and highlight their most important features.

NgForm

NgForm is the directive that represents the entire form. It’s automatically attached to every <form> element. NgForm implicitly creates an instance of the FormGroup class that represents the model and stores the form’s data (more on FormGroup later in this chapter). NgForm automatically discovers all child HTML elements marked with the NgModel directive and adds their values to the form model.

The NgForm directive has multiple selectors that you can use to attach NgForm to non-<form> elements:

This syntax comes in handy if you’re using a CSS framework that requires a certain structure for the HTML elements, and you can’t use the <form> element.

If you want to exclude a particular <form> from being handled by Angular, use the ngNoForm attribute:

<form ngNoForm></form>

The ngNoForm attribute prevents Angular from creating an instance of NgForm and attaching it to the <form> element.

NgForm has an exportAs property declared on its @Directive annotation, which allows you to use the value of this property to create a local template variable that references the instance of NgForm:

<form #f="ngForm"></form>
<pre>{{ f.value | json }}</pre>

First, you specify ngForm as the value of the exportAs property of NgForm; f points at the instance of NgForm attached to the <form>. Then you can use the f variable to access instance members of the NgForm object. One of them is value, which represents the current value of all form fields as a JavaScript object. You can pass it through the standard json pipe to display the form’s value on the page.

NgForm intercepts the standard HTML form’s submit event and prevents automatic form submission. Instead, it emits the custom ngSubmit event:

<form #f="ngForm" (ngSubmit)="onSubmit(f.value)"></form>

The code subscribes to the ngSubmit event using the event-binding syntax. onSubmit is an arbitrary name for the method defined in the component, and it’s invoked when the ngSubmit event is fired. To pass all of the form’s values as an argument to this method, use the f variable to access NgForm’s value property.

NgModel

In the context of the Forms API, NgModel represents a single field on the form. It implicitly creates an instance of the FormControl class that represents the model and stores the fields’ data (more on FormControl later in this chapter).

You attach the FormControl object to an HTML element using the ngModel attribute. Note that the Forms API doesn’t require either a value assigned to ngModel or any kind of brackets around the attribute:

The NgForm.value property points at the JavaScript object that holds the values of all form fields. The value of the field’s name attribute becomes the property name of the corresponding property in the JavaScript object in NgForm.value.

Like NgForm, the NgModel directive has an exportAs property, so you can create a variable in the template that will reference an instance of NgModel and its value property:

NgModelGroup

NgModelGroup represents a part of the form and allows you to group form fields together. Like NgForm, it implicitly creates an instance of the FormGroup class. Basically, NgModelGroup creates a nested object in the object stored in NgForm.value. All the child fields of NgModelGroup become properties on the nested object.

Here’s how you can use it:

7.2.2. Enriching the HTML form

Let’s refactor the sample user registration form from listing 7.1. There, it was a plain HTML form that didn’t use any Angular features. Now you’ll wrap it into an Angular component, add validation logic, and enable programmatic handling of the submit event. Let’s start by refactoring the template and then move on to the TypeScript part. First, modify the <form> element.

Listing 7.2. Angular-aware form
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
        <!-- Form fields go here -->
</form>

You declare a local template variable f that points at the NgForm object attached to the <form> element. You need this variable to access the form’s properties, such as value and valid, and to check whether the form has errors of a specific type.

You also configure the event handler for the ngSubmit event emitted by NgForm. You don’t want to listen to the standard submit event, and NgForm intercepts the submit event and stops its propagation. This prevents the form from being automatically submitted to the server, resulting in a page reload. Instead, NgForm emits its own ngSubmit event.

The onSubmit() method is the event handler. It’s defined as the component’s instance method. In template-driven forms, onSubmit() takes one argument: the form’s value, which is a plain JavaScript object that keeps the values of all the fields on the form. Next, you change the username and ssn fields.

Listing 7.3. Modified username and ssn fields

Now let’s change the password fields. Because these fields are related and represent the same value, it’s natural to combine them into a group. It will also be convenient to deal with both passwords as a single object when you implement form validation later in this chapter.

Listing 7.4. Modified password fields

The Submit button is the only HTML element left in the template, but it remains the same as in the plain HTML version of the form:

<button type="submit">Submit</button>

Now that you’re done with the template refactoring, let’s wrap it into a component. Here’s the code of the component:

Listing 7.5. HTML form component
@Component({
  selector: 'app',
  template: `...`
})
class AppComponent {
  onSubmit(formValue: any) {
    console.log(formValue);
  }
}

We haven’t included the content of the template, to keep the listing terse, but the refactored version described earlier should be inlined here.

The onSubmit() event handler takes a single argument: the form’s value. As you can see, the handler doesn’t use any Angular-specific API. Depending on the validity flag, you can decide whether to post the formValue to the server. In this example, you print it to the console.

Figure 7.2 displays the sample registration form with the form directives applied to it. Each form directive is circled so you can see what the form is made up of. The complete running application that illustrates how to use form directives is located in the 01_template-driven.ts file, in the code that comes with the book.

Figure 7.2. Form directives on the registration form

7.3. Reactive forms

Unlike in the template-driven approach, creating a reactive form is a two-step process. First you need to create a model programmatically in the code, and then you link HTML elements to that model using directives in the template. Let’s start with the first step, creating a model.

7.3.1. Form model

The form model is an underlying data structure that keeps the form’s data. It’s constructed out of special classes defined in the @angular/forms module: FormControl, FormGroup, and FormArray.

FormControl

FormControl is an atomic form unit. Usually it corresponds to a single <input> element, but it can also represent a more complex UI component like a calendar or a slider. FormControl keeps the current value of the HTML element it corresponds to, the element’s validity status, and whether it’s been modified.

Here’s how you can create a control:

FormGroup

FormGroup usually represents a part of the form and is a collection of FormControls. FormGroup aggregates the values and the statuses of each FormControl in the group. If one of the controls in a group is invalid, the entire group becomes invalid. It’s convenient for managing related fields on the form. FormGroup is also used to represent the entire form. For example, if a date range is represented by two date input fields, they can be combined into a single group to obtain the date range as a single value and display an error if either of the entered dates is invalid.

Here’s how you can create a control group combining the from and to controls:

let formModel = new FormGroup({
  from: new FormControl(),
  to  : new FormControl()
});
FormArray

FormArray is similar to FormGroup, but it has a variable length. Whereas FormGroup represents an entire form or a fixed subset of a form’s fields, FormArray usually represents a growable collection of fields. For example, you could use FormArray to allow users to enter an arbitrary number of emails. Here’s a model that would back such a form:

7.3.2. Form directives

The reactive approach uses a completely different set of directives than template-driven forms. The directives for reactive forms come with ReactiveFormsModule (see section 7.2).

All reactive directives are prefixed with the form* string, so you can easily distinguish the reactive from the template-driven approach just by looking at the template. Reactive directives aren’t exportable, which means you can’t create a variable in the template that references an instance of a directive. This is done intentionally to clearly separate the two approaches. In template-driven forms, you don’t access the model classes; and in reactive forms, you can’t operate the model in the template.

Table 7.1 shows how model classes correspond to form directives. The first column lists the model classes covered in the previous section. In the second column are the directives that bind a DOM element to an instance of a model class using the property-binding syntax. As you can see, FormArray can’t be used with the property binding. The third column lists directives that link a DOM element to a model class by name. They must only be used in the formGroup directive.

Table 7.1. Correspondence of model classes to form directives

Model class

Form directives

  Bind Link
FormGroup formGroup formGroupName
FormControl formControl formControlName
FormArray formArrayName

Let’s look at the form directives.

formGroup

formGroup often binds an instance of the FormGroup class that represents the entire form model to a top-level form’s DOM element, usually a <form>. All directives attached to the child DOM elements will be in the scope of formGroup and can link model instances by name.

To use the formGroup directive, first create a FormGroup in the component:

@Component(...)
class FormComponent {
  formModel: FormGroup = new FormGroup({});
}

Then add the formGroup attribute to an HTML element. The value of the formGroup attribute references a component’s property that keeps an instance of the FormGroup class:

<form [formGroup]="formModel"></form>
formGroupName

formGroupName can be used to link nested groups in a form. It needs to be in the scope of a parent formGroup directive to link one of its child FormGroup instances. Here’s how you’d define a form model that can be used with formGroupName.

Listing 7.6. Form model to use with formGroupName

Now let’s look at the template.

Listing 7.7. formGroup template

In the formGroup scope, you can use formGroupName to link child model classes by names defined in the parent FormGroup. The value you assign to the formGroupName attribute must match the name you chose for the child FormGroup in listing 7.7 (in this case, it’s dateRange).

Property-binding shorthand syntax

Because the value you assign to the *Name directive is a string literal, you can use a shorthand syntax and omit the square brackets around the attribute name. The long version would look like this:

<div [formGroupName]="'dateRange'">...</div>

Note the square brackets around the attribute name and single quotes around the attribute value.

formControlName

formControlName must be used in the scope of the formGroup directive. It links one of its child FormControl instances to a DOM element.

Let’s continue the example of the date-range model introduced when we explained the formGroupName directive. The component and form model remain the same. You only need to complete the template.

Listing 7.8. Completed formGroup template
<form [formGroup]="formModel">
  <div formGroupName="dateRange">
    <input type="date" formControlName="from">
    <input type="date" formControlName="to">
  </div>
</form>

As in the formGroupName directive, you just specify the name of a FormControl you want to link to the DOM element. Again, these are the names you chose while defining the form model.

formControl

formControl can be used for single-field forms, when you don’t want to create a form model with FormGroup but still want to use Forms API features like validation and the reactive behavior provided by the FormControl.valueChanges property. You saw an example in chapter 5 when we discussed observables. Here’s the essence of that example.

Listing 7.9. FormControl

You could use ngModel to sync the value entered by user with the component’s property; but because you’re using the Forms API, you can use its reactive features. In the preceding example, you apply several RxJS operators to the observable returned by the valueChanges property to improve the user experience. More details on this example can be found in chapter 5.

Here’s the template of the FormComponent from listing 7.9:

<input type="text" [formControl]="weatherControl">

Because you’re working with a standalone FormControl that’s not a part of a FormGroup, you can’t use the formControlName directive to link it by name. Instead you use formControl with the property-binding syntax.

formArrayName

formArrayName must be used in the scope of a formGroup directive. It links one of its child FormArray instances to a DOM element. Because form controls in FormArray don’t have names, you can link them to DOM elements only by index. Usually you render them in a loop, using the ngFor directive.

Let’s look at an example that allows users to enter an arbitrary number of emails. We’ll highlight the key parts of the code here, but you can find the full runnable example in 02_growable-items-form.ts in the code distributed with the book. First you define the model.

Listing 7.10. 02_growable-items-form.ts file: defining the model

In the template, email fields are rendered in the loop using the ngFor directive.

Listing 7.11. 02_growable-items-form.ts file: template

The let i notation in the *ngFor loop allows you to automatically bind the value of the array’s index to the i variable available in the loop. The formControlName directive links the FormControl in FormArray to a DOM element; but instead of specifying a name, it uses the i variable that references the index of the current control. When users click the Add Email button, you push a new FormControl instance to the FormArray: this.formModel.get('emails').push(new FormControl()).

Figure 7.3 shows the form with two email fields; an animated version, available at https://www.manning.com/books/angular-2-development-with-typescript, shows how it works. Every time the user clicks the Add Email button, a new FormControl instance is pushed to the emails FormArray, and through data-binding a new input field is rendered on the page. The form’s value is updated below the form in real time via data-binding as well.

Figure 7.3. Form with a growable email collection

7.3.3. Refactoring the sample form

Now let’s refactor the sample registration form from listing 7.1. Originally it was a plain HTML form, and then you applied a template-driven approach. Now it’s time for a reactive version. You start reactive forms by defining a form model.

Listing 7.12. Defining a form model

The formModel property keeps an instance of the FormGroup type that defines the structure of the form. You’ll use this property in the template to bind the model to the DOM element with the formGroup directive. It’s initialized programmatically in the constructor by instantiating model classes. The names you give to form controls in the parent FormGroup are used in the template to link the model to the DOM elements with the formControlName and formGroupName directives.

passwordsGroup is a nested FormGroup to group the password and password-confirmation fields. It will be convenient to manage their values as a single object when you add form validation.

Because reactive form directives aren’t exportable, you can’t access them in the template and pass the form’s value directly to the onSubmit() method as an argument. Instead, you access the value using the component’s property that holds the form’s model.

Now that the model is defined, you can write the HTML markup that binds to the model.

Listing 7.13. HTML binding to the model

The behavior of this reactive version of the registration form is identical to the template-driven version, but the internal implementation differs. The complete application that illustrates how to create reactive forms is located in the 03_reactive.ts file, in the code that comes with the book.

7.3.4. Using FormBuilder

FormBuilder simplifies the creation of reactive forms. It doesn’t provide any unique features compared to the direct use of the FormControl, FormGroup, and FormArray classes, but its API is more terse and saves you from the repetitive typing of class names.

Let’s refactor the component class from the previous section to use FormBuilder. The template will remain exactly the same, but you’ll change the way the formModel is constructed. Here’s what it should look like.

Listing 7.14. Refactoring formModel with FormBuilder

Unlike FormGroup, FormBuilder allows you to instantiate FormControls using an array. Each item of the array has a special meaning. The first item is FormControl’s initial value. The second is a validator function. It can also accept a third argument, which is an async validator function. The rest of the array’s items are ignored.

As you can see, configuring a form model with FormBuilder is less verbose and is based on the configuration objects rather than requiring explicit instantiation of the control’s classes. The complete application that illustrates how to use FormBuilder is located in the 04_form-builder.ts file in the code that comes with the book.

7.4. Form validation

One of the advantages of using the Forms API, compared to regular data binding, is that forms have validation capabilities. Validation is available for both types of forms: template-driven and reactive. You create validators as plain TypeScript functions. In the reactive approach, you use functions directly, and in the template-driven approach you wrap them into custom directives.

Let’s start by validating reactive forms and then move to template-driven ones. We’ll cover the basics and apply validation to the sample registration form.

7.4.1. Validating reactive forms

Validators are just functions that conform to the following interface:

The validator function should declare a single parameter of type AbstractControl and return an object literal. There are no restrictions on the implementation of the function—it’s up to the validator’s author. AbstractControl is the superclass for FormControl, FormGroup, and FormArray, which means validators can be created for all model classes.

A number of predefined validators ship with Angular: required, minLength, maxLength, and pattern. They’re defined as static methods of the Validators class declared in the @angular/forms module, and they match standard HTML5 validation attributes.

Once you have a validator, you need to configure the model to use it. In the reactive approach, you provide validators as arguments to the constructors of the model classes. Here’s an example:

You can also provide a list of validators as the second argument:

let usernameControl = new FormControl('', [Validators.required,
 Validators.minLength(5)]);

To test the control’s validity, use the valid property, which returns either true or false:

If any of the validation rules fails, you can get error objects generated by the validator functions:

let errors: {[key: string]: any} = usernameControl.errors;
The error object

The error returned by a validator is represented by a JavaScript object that has a property with the same name as the validator. Whether it’s an object literal or an object with a complex prototypal chain doesn’t matter for the validator.

The property’s value can be of any type and may provide additional error details. For example, the standard Validators.minLength() validator returns the following error object:

{
  minlength: {
    requiredLength: 7,
    actualLength: 5
  }
}

The object has a top-level property that matches the validator’s name, minlength. Its value is also an object with two fields: requiredLength and actualLength. These error details can be used to display a user-friendly error message.

Not all validators provide the error details. Sometimes the top-level property just indicates that the error has occurred. In this case, the property is initialized with the value true. Here’s an example of the standard Validators.required() error object:

{
  required: true
}
Custom validators

Standard validators are good for validating basic data types, like strings and numbers. If you need to validate a more complex data type or application-specific logic, you may need to create a custom validator. Because validators in Angular are just functions with a certain signature, they’re fairly easy to create. You need to declare a function that accepts an instance of one of the control types—FormControl, FormGroup, or FormArray—and returns an object that represents the validation error (see the sidebar “The error object”).

Here’s an example of a custom validator that checks whether the control’s value is a valid Social Security number (SSN), which is a unique ID given to each U.S. citizen:

Custom validators are used the same way as the standard ones:

let ssnControl = new FormControl('', ssnValidator);

The complete running application that illustrates how to create custom validators is located in the 05_custom-validator.ts file in the code that comes with the book.

Group validators

You may want to validate not only individual fields but also groups of fields. Angular allows you to define validator functions for FormGroups as well.

Let’s create a validator that will make sure the password and password-confirmation fields on the sample registration form have the same value. Here’s one possible implementation:

The signature of the function conforms to the ValidatorFn interface : the first parameter is of type FormGroup, which is a subclass of AbstractControl, and the return type is an object literal. Note that you use an ECMAScript feature called destructuring (see the “Destructuring” section in appendix A). You extract the value property from the instance of the FormGroup class that will be passed as an argument. This makes sense here because you never access any other FormGroup property in the validator’s code.

Next you get the names of all properties in the value object and save them in two variables, first and rest. first is the name of a property that will be used as the reference value—values of all other properties must be equal to it to make validation pass. rest stores the names of all the other properties. Again, you’re using the destructuring feature to extract references to the array items (see the section “Destructuring of arrays” in appendix A). Finally, you return either null if the values in the group are valid or an object that indicates the error state otherwise.

Validating the sample registration form

Now that we’ve covered the basics, let’s add validation to the sample registration form. You’ll use the ssnValidator and equalValidator implemented earlier in this section. Here’s the modified form model.

Listing 7.15. Modified form model

Before printing the form’s model to the console in the onSubmit() method, you check whether the form is valid:

onSubmit() {
  if (this.formModel.valid) {
    console.log(this.formModel.value);
  }
}

In the model-driven approach, configuring validators requires changes only in the code, but you still want to make some changes in the template. You want to display validation errors when the user enters an invalid value. Here’s the modified version of the template.

Listing 7.16. Modified template

Note how you access the hasError() method available on the form model when you conditionally show error messages. It takes two parameters: the name of the validation error you want to check, and the path to the field you’re interested in in the form model. In the case of username, it’s a direct child of the top-level FormGroup that represents the form model, so you specify just the name of the control. But the password field is a child of the nested FormGroup, so the path to the control is specified as an array of strings. The first element is the name of the nested group, and the second element is the name of the password field itself. Like the username field, passwordsGroup specifies the path as a string because it’s a direct child of the top-level FormGroup.

The complete running application that illustrates how to use validator functions with reactive forms is located in the 09_reactive-with-validation.ts file in the code that comes with the book. In this example, you hardcoded the error messages in the template, but they can be provided by the validators. For the example that dynamically provides error messages, see the 07_custom-validator-error-message.ts file.

Configuring validators with FormBuilder

Validators can also be configured when you’re using FormBuilder to define form models. Here’s a modified version of the model for the sample registration form that uses FormBuilder:

Asynchronous validators

The Forms API supports asynchronous validators. Async validators can be used to check form values against a remote server, which involves sending an HTTP request. Like regular validators, async validators are functions. The only difference is that async validators should return either an Observable or a Promise object. Here’s an async version of the SSN validator.

Listing 7.17. Async SSN validator

Async validators are passed as the third argument to constructors of model classes:

let ssnControl = new FormControl('', null, asyncSsnValidator);

The complete running application that illustrates how to use async validators is located in the 08_async-validator.ts file in the code that comes with the book.

Checking a field’s status and validity

You’re already familiar with control properties such as valid, invalid, and errors for checking field statuses. In this section, we’ll look at a number of other properties that help improve the user experience:

  • Touched and untouched fields— In addition to checking a control’s validity, you can also use the touched and untouched properties to check whether a field was visited by the user. If the user puts the focus into a field using the keyboard or mouse, the field becomes touched; otherwise it’s untouched. This can be useful when displaying error messages—if the value in a field is invalid but it was never visited by the user, you can choose not to highlight it with red, because it’s not a user mistake. Here’s an example:

    Note

    All the properties discussed here are available for the model classes FormControl, FormGroup, and FormArray, as well as for the template-driven directives NgModel, NgModelGroup, and NgForm.

    Note the CSS class binding example on the last line. It conditionally applies the hasError CSS class to the element if the expression on the right side is true. If you used only c.invalid, the border would be highlighted as soon as the page was rendered; but this can confuse users, especially if the page has a lot of fields. Instead, you add one more condition: the field must be touched. Now the field is highlighted only after a user visits this field.
  • Pristine and dirty fields— Another useful pair of properties are pristine and its counterpart dirty. dirty indicates that the field was modified after it was initialized with its original value. These properties can be used to prompt the user to save changed data before leaving the page or closing the dialog window.
    Note

    All of the preceding properties have corresponding CSS classes (ng-touched and ng-untouched, ng-dirty and ng-pristine, ng-valid and ng-invalid) that are automatically added to HTML elements when the property is true. These can be useful to style elements in a certain state.

  • Pending fields— If you have async validators configured for a control, you may also find the Boolean property pending to be useful. It indicates whether the validity status is currently unknown. This happens when an async validator is still in progress and you need to wait for the results. This property can be used for displaying a progress indicator.

For reactive forms, the statusChanges property of type Observable can be more convenient. It emits one of three values: VALID, INVALID, and PENDING.

Validating template-driven forms

Directives are all you can use when you create template-driven forms, so you can wrap validator functions into directives to use them in the template. Let’s create a directive that wraps the SSN validator implemented in listing 7.17.

Listing 7.18. SsnValidatorDirective

The square brackets around the ssn selector denote that the directive can be used as an attribute. This is convenient, because you can add the attribute to any <input> element or to an Angular component represented as a custom HTML element.

In this example, you register the validator function using the predefined NG_VALIDATORS Angular token. This token is in turn injected by the NgModel directive, and NgModel gets the list of all validators attached to the HTML element. Then NgModel passes validators to the FormControl instance it implicitly creates internally. The same mechanism is responsible for running validators; directives are just a different way to configure them. The multi property lets you associate multiple values with the same token. When the token is injected into the NgModel directive, NgModel gets a list of values instead of a single value. This enables you to pass multiple validators.

Here’s how you can use SsnValidatorDirective:

<input type="text" name="my-ssn" ngModel ssn>

You can find the complete running application that illustrates directive validators in the 06_custom-validator-directive.ts file in the code that comes with the book.

Validating the sample registration form

Now you can add form validation to the sample registration form. Let’s start with the template.

Listing 7.19. Registration form validation template

In the template-driven approach, you don’t have a model in the component. Only the template can inform the form’s handler whether the form is valid, and that’s why you pass the form’s value and validity status as arguments to the onSubmit() method. You also add the novalidate attribute to prevent standard browser validation from interfering with the Angular validation.

Validation directives are added as attributes. The required directive is provided by Angular and is available once you register Forms API support with FormsModule. Similarly, you can use the minlength directive to validate the password field.

To conditionally show and hide validation errors, you use the same hasError() method you used in the reactive version. But to access this method, you need to use a form property of type FormGroup, available on the f variable that references an instance of the formGroup directive.

In the onSubmit() method, you check whether the form is valid before printing the value to the console.

Listing 7.20. Checking form validation

Now for the last step: you need to add custom validator directives to the declarations list of the NgModule where you define AppComponent.

Listing 7.21. Adding validator directives

The complete running application that illustrates how to use validator directives with template-driven forms is located in the 10_template-driven-with-validation.ts file in the code that comes with the book.

7.5. Hands-on: adding validation to the search form

This hands-on exercise will start where you left off in chapter 6. You’ll need to modify the code of the SearchComponent to enable form validation and collect the data entered in the form. When the search form is submitted, you’ll print the form’s value on the browser’s console. Chapter 8 is about communication with the server, and in that chapter you’ll refactor the code so the search form will make a real HTTP request.

In this section you’ll perform the following steps:

  1. Add a new method to the ProductService class that returns an array of all available product categories.
  2. Create a model representing the search form using FormBuilder.
  3. Configure validation rules for the model.
  4. Refactor the template to properly bind to the model created in the previous step.
  5. Implement the onSearch() method to handle the form’s submit event.

Figure 7.4 shows what the search form will look like after you complete this hands-on exercise. It illustrates the validators in action.

Figure 7.4. Search form with validators

If you prefer to see the final version of this project, browse the source code in the auction folder from chapter 7. Otherwise, copy the auction folder from chapter 6 to a separate location, and follow the instructions in this section.

7.5.1. Modifying the root module to add Forms API support

Update the app.module.ts file to enable reactive forms support for the application. Import ReactiveFormsModule from @angular/forms, and add it to the list of imported modules in the main application NgModule.

Listing 7.22. Updated app.module.ts file
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule.forRoot([ ... ])
  ],

7.5.2. Adding a list of categories to the SearchComponent

Each product has the categories property, represented by an array of strings, and a single product can relate to multiple categories. The form should allow users to select a category while searching for products; you need a way to provide a list of all available categories to the form so it can display them to users. In a real-world application, the categories would likely come from the server. In this online auction example, you’ll add a method to the ProductService class that will return hardcoded categories:

  1. Open the app/services/product-service.ts file, and add a getAllCategories() method that accepts no parameters and returns a list of strings:
    getAllCategories(): string[] {
      return ['Books', 'Electronics', 'Hardware'];
    }
  2. Open the app/components/search/search.ts file, and add an import statement for ProductService:
    import {ProductService} from '../../services/product-service';
  3. Configure this service as a provider for SearchComponent:
    @Component({
      selector: 'auction-search',
      providers: [ProductService],
      //...
    })
  4. Declare a categories: string[] class property as a reference to the list of categories. You’ll use it for the data binding:
    export default class SearchComponent {
      categories: string[];
    }
  5. Declare a constructor() with one parameter: ProductService. Angular will inject it when the component is instantiated. Initialize the categories property using the getAllCategories() method:

7.5.3. Creating a form model

Now let’s define the model that will handle the search form:

  1. Open the app/components/search/search.ts file, and add the Forms API-related imports. The import statement at the beginning of the file should look like this:
    import {Component} from '@angular/core';
    import {FormControl, FormGroup, FormBuilder, Validators} from
     '@angular/forms';
  2. Declare a formModel class property of the FormGroup type:
    export default class SearchComponent {
      formModel: FormGroup;
      //...
    }
  3. In the constructor, define the formModel using the FormBuilder class:
    const fb = new FormBuilder();
    this.formModel = fb.group({
      'title': [null, Validators.minLength(3)],
      'price': [null, positiveNumberValidator],
      'category': [-1]
    })
  4. Add a positiveNumberValidator function:
    function positiveNumberValidator(control: FormControl): any {
      if (!control.value) return null;
      const price = parseInt(control.value);
      return price === null || typeof price === 'number' && price > 0
          ? null : {positivenumber: true};
    }
    positiveNumberValidator() attempts to parse an integer value from the FormControl’s value using the standard parseInt() function. If the parsed value is a valid positive number, the function returns null, meaning there are no errors. Otherwise the function returns an error object.

7.5.4. Refactoring the template

Let’s add form directives to the template to bind the model defined in the previous step to the HTML elements:

  1. You defined the form model in the code implementing the reactive approach, so in the template you should attach the NgFormModel directive to the <form> element:

  2. Define the validation rules, and conditionally display error messages for the title field:
    <div class="form-group"
         [class.has-error]="formModel.hasError('minlength', 'title')">
      <label for="title">Product title:</label>
      <input id="title"
             placeholder="Title"
             class="form-control"
             type="text"
             formControlName="title"
             minlength="3">
      <span class="help-block"
           [class.hidden]="!formModel.hasError('minlength', 'title')">
        Type at least 3 characters
      </span>
    </div>
    Here you use the form-group, form-control, has-error, and help-block CSS classes defined in the Twitter Bootstrap library. They’re required to properly render the form and highlight the field with the red border in the case of a validation error. You can read more about these classes in the Bootstrap documentation, in the “Forms” section: http://getbootstrap.com/css/#forms.
  3. Do the same for the product price field:
    <div class="form-group"
          [class.has-error]="formModel.hasError('positivenumber', 'price')">
      <label for="price">Product price:</label>
      <input id="price"
              placeholder="Price"
              class="form-control"
              type="number"
              step="any"
              min="0"
              formControlName="price">
      <span class="help-block"
            [class.hidden]="!formModel.hasError('positivenumber', 'price')">
        Price is not a positive number
      </span>
    </div>
  4. Add validation rules and an error message for the product category field:
    <div class="form-group">
      <label for="category">Product category:</label>
      <select id="category"
              class="form-control"
              formControlName="category">
        <option value="-1">All categories</option>
        <option *ngFor="let c of categories"
                [value]="c">{{c}}</option>
      </select>
    </div>
    The Submit button remains unchanged.

7.5.5. Implementing the onSearch() method

Add the following onSearch() method:

onSearch() {
  if (this.formModel.valid) {
    console.log(this.formModel.value);
  }
}

7.5.6. Launching the online auction

To launch the application, open a command window and start http-server in the root directory of the project. Enter http://localhost:8080 in a web browser, and you should see a Home page that includes the search form shown in 7.4. This version of the application illustrates form creation and validation without performing a search. You’ll implement the search functionality in chapter 8, when we discuss communication with servers.

7.6. Summary

In this chapter, you’ve learned how to work with forms in Angular. These are the main takeaways from this chapter:

  • There are two approaches to working with forms: template-driven and reactive. The template-driven approach is easier and quicker to configure, but the reactive one is easier to test, enables more flexibility, and provides more control over the form.
  • The reactive approach offers advantages for applications that use not only the DOM renderer but another one (such as one from NativeScript) targeting non-browser environments. Reactive forms are programmed once and can be reused by more than one renderer.
  • A number of standard validators ship with Angular, but you can also create custom ones. You should validate the user’s input, but client-side validation isn’t a replacement for performing additional validation on the server. Consider client-side validation as a way to provide instant feedback to the user, minimizing server requests involving invalid data.
..................Content has been hidden....................

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