Chapter 7. Explaining Pipes and Communicating with RESTful Services

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:

  • Develop model-driven forms.
  • Define parameterized routes.
  • Define child routes.
  • Use the HTTP module for communication with RESTful APIs.
  • Transform data with custom pipes.

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.

Developing model-driven forms in Angular

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:

Developing model-driven forms in Angular

Figure 1

In the preceding screenshot, there are two forms:

  • A form that contains the following controls for importing existing users from GitHub:
    • The input for the GitHub handle.
    • A checkbox that points out whether we want to import the developer from GitHub or enter it manually.

  • A form for entering new users manually.

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.

Using composition of control validators

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:

  • When the Fetch from GitHub checkbox is checked, disable the form for entering a new developer and enable the form for importing a developer from GitHub.
  • When the current active (or enabled) form is invalid, disable the submit button.

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.

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

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