Using Angular's forms module

Now, let's continue with the implementation of the application. For the next step, we'll work on the AddDeveloper and Home components. You can continue your implementation by extending what you currently have in ch6/ts/step-0, or if you haven't reached step 1 yet, you can keep working on the files in ch6/ts/step-1.

Angular offers two ways of developing forms with validation:

  • A template-driven approach: This provides a declarative API where we declare the validations into the template of the component.
  • A model-driven approach (also known as reactive forms): This provides an imperative, reactive API.

Let's start with the template-driven approach for now and explore the model-driven approach in the next chapter.

Developing template-driven forms

Forms are essential for each CRUD (Create Retrieve Update and Delete) application. In our case, we want to build a form for entering the details of the developers we want to store.

By the end of this section, we'll have a form that allows us to enter the real name of a given developer, to add their preferred technology, enter their e-mail, and declare whether they are popular in the community or not yet. The end result will look as follows:

Developing template-driven forms

Figure 4

Add the following import to app.ts:

import {FormsModule} from '@angular/forms';

The next thing we need to do is import FormsModule in our AppModule. The FormsModule contains a set of predefined directives for managing Angular forms, such as the form and ngModel directives. The FormsModule also declares an array with a predefined set of form-related providers that we can use in our application.

After the import of the FormsModule, our app.ts will look like:

// ch6/ts/step-2/add_developer.ts

@NgModule({
  imports: [BrowserModule, FormsModule, routingModule],
  declarations: [App, Home, AddDeveloper, ControlErrors],
  providers: [{
    provide: LocationStrategy,
    useClass: HashLocationStrategy
  }],
  bootstrap: [App]
})
class AppModule {}

Now, update the AddDeveloper implementation to the following:

// ch6/ts/step-2/add_developer.ts

@Component({ 
  selector: 'dev-add', 
  templateUrl: './add_developer.html', 
  styles: [...]
}) 
export class AddDeveloper { 
  developer = new Developer(); 
  errorMessage: string; 
  successMessage: string; 
  submitted = false; 
  // ...
  constructor(private developers: DeveloperCollection) {} 
  addDeveloper() {} 
} 

The developer property contains the information associated with the current developer that we're adding with the form. The last two properties, errorMessage and successMessage, will be used respectively to display the current form's error or success messages once the developer has been successfully added to the developers collection, or when an error has occurred.

Digging into the template-driven form's markup

As the next step, let's create the template for the AddDeveloper component (step-1/add_developer.html). Add the following content to the file:

<span *ngIf="errorMessage" 
       class="alert alert-danger">{{errorMessage}}</span> 
<span *ngIf="successMessage" 
       class="alert alert-success">{{successMessage}}</span> 

These two elements are intended to display the error and success messages when you add a new developer. They will be visible when errorMessage or successMessage have non-falsy values (that is, something different from the empty string, false, undefined, 0, NaN, or null).

Now, let's develop the actual form:

<form #f="ngForm" class="form col-md-4" [hidden]="submitted"
   (ngSubmit)="addDeveloper()">
  <div class="form-group">
    <label class="control-label" for="realNameInput">Real name</label>
    <div>
      <input id="realNameInput" class="form-control" 
          type="text" name="realName"
          [(ngModel)]="developer.realName" required>
    </div>
  </div>
  <!-- MORE CODE TO BE ADDED --> 
  <button class="btn btn-default" type="submit">Add</button> 
</form>  

We declare a new form using the HTML form tag. Once Angular finds such tags in a template with an included form directive in the parent component, it will automatically enhance its functionality in order to be used as an Angular form. Once the form is processed by Angular, we can apply form validation and data bindings. After this, using #f="ngForm", we define a local variable in template, which allows us to reference to the form using the identifier f. The last thing left from the form element is the submit event handler. We use a syntax that we're already familiar with, (ngSubmit)="expr"; in this case, the value of the expression is the call of the addDeveloper method defined in the component's controller.

Now, let's take a look at the div element with the class name control-group.

Note

Note that this is not an Angular-specific class; it is a CSS class defined by Bootstrap that we use in order to provide a better look and feel to the form.

Inside the div element, we can find a label element that doesn't have any Angular-specific markup and an input element that allows us to set the real name of the current developer. We set the control to be of a type text and declare its identifier and name equal to realNameInput. The required attribute is defined by the HTML5 specification and is used for validation. By using it on the element, we declare that this element is required to have a value. Although the required attribute is not Angular-specific, Angular will extend its semantics by including an Angular-specific validation behavior. This behavior includes setting specific CSS classes on the control when its status changes and managing its state which the framework keeps internally.

The behavior of the form controls will be enhanced by running validation over them when their values change, and applying specific classes during the controls' life cycles. You may be familiar with this from AngularJS, where the form controls are decorated with the ng-pristine, ng-invalid, and ng-valid classes, and so on.

The following table summarizes the CSS classes that the framework adds to the form controls during their life cycle:

Classes

Description

ng-untouched

The control hasn't been visited

ng-touched

The control has been visited

ng-pristine

The control's value hasn't been changed

ng-dirty

The control's value has been changed

ng-valid

All the validators attached to the control have returned true

ng-invalid

Any of the validators attached to the control has a false value

According to this table, we can define that we want all the input controls with an invalid value to have a red border in the following way:

input.ng-dirty.ng-invalid { 
  border: 1px solid red; 
} 

The exact semantics behind the preceding CSS in the context of Angular is that we use a red border for all the input elements whose values were changed and are invalid according to the validators attached to them.

Now, let's explore how we can attach validation behavior to our controls.

Using the built-in validators

We have already seen that we can alter validation behavior to any control using the required attribute. Angular provides two more built-in validators, as follows:

  • minlength: This allows us to specify the minimum length of value that a given control should have.
  • maxlength: This allows us to specify the maximum length of value that a given control should have.

These validators are defined with Angular directives and can be used in the following way:

<input id="realNameInput" class="form-control" 
       type="text" minlength="2" maxlength="30"> 

This way, we specify that we want the value of the input to be between 2 and 30 characters.

Defining custom validators

Another data property defined in the Developer class is email. Let's add an input field for it. Above the Add button in the preceding form, add the following markup:

<div class="form-group"> 
  <label class="control-label" for="emailInput">Email</label> 
  <div> 
    <input type="text" id="emailInput" class="form-control" name="emailInput"
           [(ngModel)]="developer.email"> 
  </div> 
</div> 

We can think of the [(ngModel)] as an alternative to the ng-model directive from AngularJS. We will explain it in detail in the Two-way data binding with Angular section.

Although Angular provides a set of predefined validators, they are not enough for all the various formats our data can live in. Sometimes, we'll need custom validation logic for our application-specific data. For instance, in this case, we want to define an e-mail validator. A typical regular expression, which works in general cases (but does not cover the entire specification that defines the format of the e-mail addresses), looks as follows: /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/.

In ch6/ts/step-1/email_validator.ts, define a function that accepts an instance of Angular control as an argument and returns null if the control's value is empty or matches the regular expression mentioned earlier, and { 'invalidEmail': true } otherwise:

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 }; 
  } 
} 

Now, from the @angular/common and @angular/core modules, import NG_VALIDATORS and Directive, and wrap this validation function within the following directive:

@Directive({ 
  selector: '[email-input]', 
  providers: [{
    provide: NG_VALIDATORS,
    multi: true,
    useValue: validateEmail
  }]
}) 
class EmailValidator {} 

In the preceding code, we defined a multiprovider for the token NG_VALIDATORS. Once we inject the value associated with this token, we'll get an array with all the validators attached to the given control (for reference, take a look at the section for multiproviders in Chapter 5, Dependency Injection in Angular).

There are only two steps left in order to make our custom validation work. First, add the email-input attribute to the e-mail control:

<input type="text" id="emailInput" class="form-control" email-input
       [(ngModel)]="developer.email"> 

Next, add the directive to the declarations in the AppModule:

// ...
import {EmailValidator} from './email_validator';
// ...

@NgModule({
  // ...
  declarations: [..., EmailValidator],
  // ...
})
class AppModule {}

Note

We're using an external template for the AddDeveloper control. There's no ultimate answer to whether a given template should be externalized or inlined. Best practice states that we should inline the short templates and externalize the longer ones. However, there's no specific definition as to which templates are considered short and which are considered long. The decision of whether the template should be used inline or put into an external file depends on the developer's personal preferences or common conventions within the organization.

Using select inputs with Angular

As the next step, we should allow the user of the application to enter the technology into which the input developer has the most proficiency. We can define a list of technologies and show them in the form as a select input.

In the AddDeveloper class, add the technologies property:

class AddDeveloper { 
  ... 
  technologies: string[] = [ 
    'JavaScript', 
    'C', 
    'C#', 
    'Clojure' 
  ]; 
  ... 
} 

Now, in the template, just above the Add button, add the following markup:

<div class="form-group"> 
  <label class="control-label" 
         for="technologyInput">Technology</label> 
  <div> 
    <select class="form-control" name="technology" required 
            [(ngModel)]="developer.technology"> 
        <option *ngFor="let technology of technologies" [value]="technology">
          {{technology}}
        </option> 
    </select> 
  </div> 
</div> 

Just like for the input elements we declared earlier, Angular will add the same classes depending on the state of the select input. In order to show a red border around the select element when its value is invalid, we will need to alter the CSS rules:

@Component({ 
  ... 
  styles: [ 
    `input.ng-touched.ng-invalid, 
     select.ng-touched.ng-invalid { 
      border: 1px solid red; 
    }` 
  ], 
  ... 
}) 
class AddDeveloper {...} 

Note

Note that inlining all the styles in our components' declaration could be a bad practice because, this way, they won't be reusable. What we can do is extract all the common styles across our components into separate files. The @Component decorator has a property called styleUrls of type string[] where we can add a reference to the extracted styles used by the given component. This way, we can inline only the component-specific styles, if required.

Right after this, we declare the name of the control to be equal to "technology" using name="technology". Using the required attribute, we declare that the user of the application must specify the technology in which the current developer is proficient. Let's skip the [(ngModel)] attribute for now and see how we can define the options of the select element.

Inside the select element, we define the different options using:

<option *ngFor="let technology of technologies" [value]="technology">
  {{technology}}
</option> 

This is a syntax we're already familiar with. We simply iterate over all the technologies defined within the AddDeveloper class, and for each technology we show an option element with a value for the technology name.

Using the NgForm directive

We have already mentioned that the form directive enhances the HTML5 form's behavior by adding some additional Angular-specific logic. Now, let's take a step back and take a look at the form that surrounds the input elements:

<form #f="ngForm" (ngSubmit)="addDeveloper()" 
      class="form col-md-4" [hidden]="submitted"> 
  ... 
</form> 

In the preceding snippet, we define a new identifier called f, which references to the form. We can think of the form as a composition of controls; we can access the individual controls through the form's controls property. On top of this, the form has the touched, untouched, pristine, dirty, invalid, and valid properties, which depend on the individual controls defined within the form. For example, if none of the controls within the form have been touched, then the form itself will show untouched as its status. However, if any of the controls in the form have been touched at least once, the form will show as touched. Similarly, the form will be valid only if all its controls are valid.

In order to illustrate the usage of the form element, let's define a component with the control-errors selector, which shows the current errors for a given control. We can use it in the following way:

<label class="control-label" for="realNameInput">Real name</label> 
<div> 
  <input id="realNameInput" class="form-control" type="text" 
         [(ngModel)]="developer.realName" 
         required maxlength="50"> 
  <control-errors control="realName" 
    [errors]="{ 
      'required': 'Real name is required', 
      'maxlength': 'The maximum length of the real name is 50 characters' 
    }" 
   /> 
</div> 

Note that we've also added the maxlength validator to the realName control.

The control-errors element has the following attributes:

  • control: This declares the name of the control we want to show errors for.
  • errors: This creates a mapping between control error and an error message.

Now, create a new file called control_errors.ts and add the following imports in it:

import {Component, Host, Input} from '@angular/core';
import {NgForm} from '@angular/forms';

In these imports, NgForm represents the Angular forms, and Host is a parameter decorator related to the DI mechanism, which we have already covered in Chapter 5, Dependency Injection in Angular.

Here is a part of the component's definition:

@Component({ 
  template: '<div>{{currentError}}</div>', 
  selector: 'control-errors',
}) 
class ControlErrors { 
  @Input() errors: Object; 
  @Input() control: string; 
  constructor(@Host() private formDir: NgForm) {} 
  get currentError() {...} 
} 

The ControlErrors component defines two inputs: control, the name of the control (the value of the name attribute) and errors, the mapping between an error identifier and an error message. They can be specified, respectively, by the control and the errors attributes of the control-errors element.

For instance, lets suppose we have the following input:

<input type="text" name="foobar" required> 

We can declare its associated control-errors component using the following markup:

<control-errors control="foobar" 
      [errors]="{ 
       'required': 'The value of foobar is required' 
      }"></control-errors> 

Inside the currentError getter, in the declaration of the ControlErrors class above, we need to do the following two things:

  • Find a reference to the component declared with the control attribute.
  • Return the error message associated with any of the errors that make the current control invalid.

Here is a snippet that implements this behavior:

@Component(...) 
class ControlErrors { 
  ... 
  get currentError() {
    let control = this.formDir.controls[this.control];
    let errorMessages = [];
    if (control && control.touched) {
      errorMessages = Object.keys(this.errors)
        .map(k => control.hasError(k) ? this.errors[k] : null)
        .filter(error => !!error);
    }
    return errorMessages.pop();
  }
} 

In the first line of the implementation of currentError, we get the target control using the controls property of the injected form. The controls property is of the type {[key: string]: AbstractControl}, where the key is the name of the control we've declared with the name attribute. Once we have a reference to the instance of the target control, we can check whether its status is touched (that is, whether it has been focused), and if it is, we can loop over all the errors within the errors property of the instance of ControlErrors. The map function will return an array with either an error message or a null value. The only thing left to do is to filter all the null values and get only the error messages. Once we get the error messages for each error, we will return the last one by popping it from the errorMessages array.

The end result should look as follows:

Using the NgForm directive

Figure 5

Tip

If you experience any problems during the implementation of the ControlErrors component, you can take a look at its implementation at ch6/ts/step-2/control_errors.ts.

The hasError method of every control accepts as an argument an error message identifier, which is defined by the corresponding validator. For instance, in the preceding example, where we defined the custom e-mail validator, we return the { 'invalidEmail': true } object literal when the input control has an invalid value. If we apply the ControlErrors component to the e-mail control, its declaration should look as follows:

  <control-errors control="email" 
    [errors]="{
      'invalidEmail': 'Invalid email address'
    }"></control-errors> 
..................Content has been hidden....................

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