In the last chapter, we covered some very powerful features of the framework. However, we can go even deeper into the functionality of Angular's forms module and router. In the next sections, we'll explain how we can:
We will explore all these concepts in the process of extending the functionality of the "Coders repository" application. At the beginning of the preceding chapter, we mentioned that we will allow the import of developers from GitHub. However, before we implement this feature, let's extend the functionality of the form.
These will be the last steps for finishing the "Coders repository". You can build on top of the code available at ch6/ts/step-1/
(or ch6/ts/step-2
, depending on your previous work), in order to extend the application's functionality with the new concepts we will cover. The complete example is located at ch7/ts/multi-page-model-driven
.
This is the result that we will achieve by the end of this section:
Figure 1
In the preceding screenshot, there are two forms:
The second form looks exactly the way we left it in the last chapter. However, this time, its definition looks a little bit different:
<form class="form col-md-4" [formGroup]="addDevForm" [hidden]="submitted"> <!-- TODO --> </form>
Note that, this time, we don't have the submit
handler or the #f="ngForm"
attribute. Instead, we bind the [formGroup]
property to addDevForm
defined inside the component's controller. Using this attribute, we can bind to something called FormGroup
. As its name states, the FormGroup
class consists of a list of controls grouped together with the sets of validation rules associated with them.
We need to use a similar declaration in the form used for importing a developer. However, this time, we will provide a different value of the [formGroup]
property, as we will define a different form group in the component's controller. Place the following snippet above the form we introduced earlier:
<form class="form col-md-4" [formGroup]="importDevForm" [hidden]="submitted"> <!-- TODO --> </form>
Now, let's declare the importDevForm
and addDevForm
properties in the component's controller:
import {FormGroup} from '@angular/forms'; @Component(...) export class AddDeveloper { importDevForm: FormGroup; addDevForm: FormGroup; ... constructor(private developers: DeveloperCollection, fb: FormBuilder) {...} addDeveloper() {...} }
Initially, we import the FormGroup
class from the @angular/forms
module and, later, declare the required properties in the controller. Note that we have one additional parameter of the constructor of AddDeveloper
called fb
of the FormBuilder
type.
FormBuilder
provides a programmable API for the definition of FormGroup
where we can attach validation behavior to each control in the group. Let's use the FormBuilder
instance for the initialization of the importDevForm
and addDevForm
properties:
... constructor(private developers: DeveloperCollection, fb: FormBuilder) { this.importDevForm = fb.group({ githubHandle: ['', Validators.required], fetchFromGitHub: [false] }); this.addDevForm = fb.group({ realName: ['', Validators.required], email: ['', validateEmail], technology: ['', Validators.required], popular: [false] }); } ...
The FormBuilder
instance has a method called group
that allows us to define properties, such as the default values and the validators for the individual controls in a given form.
According to the previous snippet, importDevForm
has two fields: githubHandle
and fetchFromGitHub
. We declare that the value of the githubHandle
control is required, and set the default value of the control fetchFromGitHub
to false
.
In the second form, addDevForm
, we declare four controls. For the realName
control as the default value, we set the empty string and use Validators.requred
in order to introduce validation behavior (which is exactly what we did for the githubHandle
control). As a validator for the e-mail input, we will use the validateEmail
function and set the control's initial value to an empty string. The validateEmail
function used for validation is the one we defined in the last chapter:
function validateEmail(emailControl) { if (!emailControl.value || /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/.test(emailControl.value)) { return null; } else { return { 'invalidEmail': true }; } }
The last two controls we define here are the technology
control, for which a value is required and has an empty string as its initial value, and the popular
control, with its initial value set to false
.
We took a look at how we can apply a single validator to form controls. Using model-driven approach, we applied the Validators.required
validator in a way equivalent to what we did in the preceding chapter, where we used template-driven forms and added the required
attribute. However, in some applications, the domain may require a more complex validation logic. For example, if we want to apply both the required and the validateEmail
validators to the e-mail control, we should do the following:
this.addDevForm = fb.group({ ... email: ['', Validators.compose([ Validators.required, validateEmail] )], ... });
The compose
method of the Validators
object accepts an array of validators as an argument and returns a new validator. The new validator's behavior will be a composition of the logic defined in the individual validators passed as an argument, and they will be applied in the same order as they were introduced in the array.
The property names in the object literal passed to the group
method, of the FormBuilder
, should match with the values that we set to the formControlName
attributes of the inputs in the template. This is the complete template of importDevForm
:
<form class="form col-md-4" [formGroup]="importDevForm" [hidden]="submitted"> <div class="form-group"> <label class="control-label" for="githubHandleInput">GitHub handle</label> <div> <input id="githubHandleInput" class="form-control" type="text" formControlName="githubHandle"> <control-errors control="githubHandle" [errors]="{ 'required': 'The GitHub handle is required' }"></control-errors> </div> </div> <div class="form-group"> <label class="control-label" for="fetchFromGitHubCheckbox"> Fetch from GitHub </label> <input class="checkbox-inline" id="fetchFromGitHubCheckbox" type="checkbox" formControlName="fetchFromGitHub"> </div> </form>
In the preceding template, we can note that, once the submitted
flag has the value true
, the form will be hidden from the user. Next to the first input element, we will set the value of the formControlName
attribute to githubHandle
. The formControlName
attribute associates an existing form input in the template with one declared in the FormGroup
, corresponding to the form element where HTML input resides. This means that the key associated with the controls' definition inside the object literal, which we pass to the group
method of the FormBuilder
, must match with the name of the corresponding control in the template, set with formControlName
.
Now we want to implement the following behavior:
We'll explore how we can achieve this functionality using Angular's reactive forms (also known as model-driven forms) API.
Inside the AddDeveloper
class, add the following methods definitions:
... export class AddDeveloper { //... ngOnInit() { this.toggleControls(this.importDevForm.controls['fetchFromGitHub'].value); this.subscription = this.importDevForm.controls['fetchFromGitHub'] .valueChanges.subscribe(this.toggleControls.bind(this)); } ngOnDestroy() { this.subscription.unsubscribe(); } private toggleControls(importEnabled: boolean) { const addDevControls = this.addDevForm.controls; if (importEnabled) { this.importDevForm.controls['githubHandle'].enable(); Object.keys(addDevControls).forEach((c: string) => addDevControls[c].disable()); } else { this.importDevForm.controls['githubHandle'].disable(); Object.keys(addDevControls).forEach((c: string) => addDevControls[c].enable()); } } } ...
Note that in ngOnInit
, we invoke the toggleControls
method with the current value of the fetchFromGitHub
checkbox. We can get reference to the AbstractControl
, which represents the checkbox, by getting the fetchFromGitHub
property of the controls
within the importDevForm
.
After that, we subscribe to the valueChange
event of the checkbox by passing a callback to its subscribe
method. Each time the value of the checkbox is changed, the callback we've passed to subscribe
will be invoked.
Later, in ngOnDestroy
, we unsubscribe from the valueChange
subscription in order to prevent our code from memory leaks.
Finally, the most interesting thing happens in toggleControls
. To this method, we pass a flag that indicates whether we want the importDevForm
to be enabled or not. If we want the form to be enabled, all we need to do is to invoke the enable
method of the githubHandle
control and disable all the controls in the addDevForm
. We can disable all the controls in addDevForm
by iterating over the control names (that is, the keys of the controls
property of the addDevForm
), getting the corresponding control instance for each individual name, and invoking its disable method. In case the importEnabled
flag has value false
, we do the exact opposite, by invoking the enable
method of the controls from the addDevForm
and the disable
method of the control from importDevForm
.