Chapter 8. Forms and Authentication Handling in Angular 2

In the previous chapter, we covered routing and this led to security concerns when it came to providing different tiers of content in our application. Enabling user authentication is the first step for introducing relevant features such as publishing forms in our application. However, if we want to build these brand new functionalities, we will need to discover how to set a foundation first. In this chapter, we will see how to build forms and then move on to cover how to leverage those forms to allow users to login and create new content.

As a word of caution about this chapter, we will overview different ways of building forms. All of them are valid, and its use will depend on the goals you're aiming on every moment to fulfill each project requirement.

In this chapter, we will:

  • Learn how to create responsive input controls in our forms with directives
  • Discuss two-way data binding support in Angular 2
  • Bind data models and interface types for forms and input controls
  • Design control sets both declaratively and imperatively
  • Dive into the alternatives for input validation
  • Build our own custom validators
  • Develop our own login forms
  • Implement general-purpose authorization providers
  • Secure areas of our site by requesting user login upfront

Two-way data binding in Angular 2

We mentioned in previous chapters that one of the main differences between Angular 2 and the previous incarnations of the framework is that it does not favor two-way data binding as the core pattern of data management. Well, this is not exactly true. While most of the data management processes in Angular 2 are one way only, form management provides room for two-way data binding by means of the NgModel directive.

Let's see all this through an actual example. In the previous chapter, we introduced a new component so we could expand the range of components available in our app in order to have more options for navigating the site, and thus we could better test our router's implementation. This new component, named TaskEditorComponent, had no implementation yet and its template featured this layout:

app/tasks/task-editor.component.html

<form class="container">
  <h3>Task Editor:</h3>
  <div class="form-group">
    <input type="text"
      class="form-control"
      placeholder="Task name"
      required>
  </div>

  <div class="form-group">
    <input type="Date"
      class="form-control"
      required>
  </div>

  <div class="form-group">
    <input type="number"
      class="form-control"
      placeholder="Pomodoros required"
      min="1"
      max="4"
      required>
  </div>

  <div class="form-group">
    <input type="checkbox" name="queued">
    <label for="queued"> this task by default?</label>
  </div>

  <p>
    <input type="submit" class="btn btn-success" value="Save">
    <a [routerLink]="['TasksComponent']" class="btn btn-danger">Cancel</a>
  </p>
</form>

This is a tiny but nifty web form indeed. The component included support for some routing lifecycle hooks in order to serve as a proof-of-concept for the different stages a component goes through in its journey through the navigation pipeline. Apart from that, the form had no life whatsoever—it was just an unanimated creature in the middle of nowhere.

Note

You will see several classnames decorating our forms through the different examples included in this chapter. Unless pointed otherwise, all classnames contained in this chapter are borrowed from the Bootstrap style sheet for styling up our form, for example, container, form-group, form-control and so on. Angular has no relationship with these and they are indeed not required when coding against the framework.

The NgModel directive

Let's infuse some life into it then! One of the good things about implementing two-way data binding support in our form elements is that we do not need to import anything upfront. Angular 2 is smart enough to detect what it needs and the only directive we will need is already supplied out-of-the-box. We are obviously referring to the NgModel directive. According to the Angular 2 official documentation:

"ngModel binds an existing domain model to a form control. For a two-way binding, use [(ngModel)] to ensure the model updates in both directions."

In a nutshell, in the very moment we bind an ngModel attribute to a form control, the control will watch the value stored at the component class property it is bound to and will update itself as soon as the value changes in the model. You might think: this is already done by Angular without any real fanfare. Yes, but the main difference here is that such surveillance is performed in both ways. This means that the class model will update its state as soon as the form control value is updated.

Enough said! It's time for some action. Let's update our task editing component to try this out. Bring up the code of our task editing component and, first of all, please note that it features a CanActivate decorator that posed a passthrough question to the user. Let's remove it, since we will encounter more secure and elegant ways to provide such functionality later in this chapter. Now, let's add a new member named taskName, which will obviously represent a task name!

app/tasks/task-editor.component.ts

export default class TaskEditorComponent implements OnActivate, CanDeactivate, OnDeactivate {
    taskName: string;

    constructor(private title: Title) {}

    // Rest of the component remains unchanged
}

Open the associated template and update the first input block to look like this. We will explain all this in a minute:

app/tasks/task-editor.component.html

<p>Your task name is {{taskName}}</p>
<div class="form-group">
  <input type="text"
    class="form-control"
    placeholder="Task name"
    [(ngModel)]="taskName">
</div>

As you can see, we have attached a [(ngModel)] attribute directive into our input control pointing to the string property we just created in the component class, which is also shown on the screen right above the input. Execute the code and change the text field values. You will see how the text entered is updated in real-time on screen.

The syntax of the ngModel gives a very good hint to what is it all about. We are blending in a single attribute an event handler and a property binding (hence the combination of brackets plus braces), so we can inject a value into the target control while listening to changes made on the value at the same time. In other words, it is two-way data binding.

Obviously, this is a very simplistic example and we aim to build something more ambitious, so let's leverage this recently gained experience to build something more useful. In the following section, we are going to:

  • Link the form to a newly instantiated task object acting as a model
  • Populate the model with the values entered in the form
  • Persist the changes after validating the data entered
  • Redirect the user to the Pomodoros table to see the task just created there

Binding a type to a form with NgModel

Remove the code just added and import the Task interface type into our form along with the TaskService manager, by appending the following import statement to the top:

app/tasks/task-editor.component.ts

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import {
  Router,
  ROUTER_DIRECTIVES,
  ComponentInstruction,
  CanActivate,
  OnActivate,
  CanDeactivate,
  OnDeactivate } from '@angular/router-deprecated';
import { 
  Task,
  TaskService } from '../shared/shared';

You might have noticed that we also imported the Router type from angular2/router. We will need it to redirect the user back to the Pomodoro list page once the new task has been successfully created.

Now, we need to append a new Task-annotated member to our class and declare TaskService as a dependency in the constructor, so we can persist the newly created task later. Remove the taskName string field we created earlier and update the class with these changes:

app/tasks/task-editor.component.ts

export default class TaskEditorComponent implements OnActivate, CanDeactivate, OnDeactivate {
  task: Task;

  constructor(
    private title: Title,
    private router: Router,
  private taskService: TaskService) {
    this.task = <Task>{};
  }

  // Rest of the component remains unchanged
}

We have added a new field to the class representing the Task model our form will be bound to. Since Task is an interface type, we cannot instantiate it by using the new keyword, since interfaces have no constructor. However, we can take advantage of generics and typecast an empty object to enforce the Task interface, as we did in the preceding code.

On the other hand, the types declared in the constructor ensure that the Angular injector will make them available as class fields for the rest of the component members once it is instantiated.

Ideally, we would just need to link the form data to the object represented by the task member of our component class, persist it throughout the application by using the methods already created in the TaskService class, and then proceed to the task list right after that. Let's begin by updating our HTML template with the required ngModel attributes, including a Submit button and a submit handler in the form wrapper tag:

app/tasks/task-editor.component.html

<form class="container" (submit)="saveTask()">
  <h3>Task Editor:</h3>
  <div class="form-group">
    <input type="text" 
      class="form-control"
      placeholder="Task name" 
      [(ngModel)]="task.name">
  </div>

  <div class="form-group">
    <input type="date" 
      class="form-control"
      [(ngModel)]="task.deadline">
  </div>

  <div class="form-group">
    <input type="number" 
      class="form-control"
      placeholder="Pomodoros required"
      min="1"
      max="4"
      [(ngModel)]="task.pomodorosRequired">
  </div>

  <div class="form-group">
    <input type="checkbox" 
      name="queued"
      [(ngModel)]="task.queued">
    <label for="queued"> this task by default?</label>
  </div>

  <p>
    <input type="submit" class="btn btn-success" value="Save">
    <a [routerLink]="['TaskList']" class="btn btn-danger">
      Cancel
    </a>
  </p>
</form>

There are two remarkable elements in this piece of code:

  • Now each input control features an ngModel directive attribute, mapped to one of the properties of the Task type represented by the task member of the controller class.
  • We have included a Submit button in our form, although the form tag does not feature any action attribute, so where are we submitting our form to? The (submit) event listener takes care of handling the event by binding an event handler to it.

Our three input fields now benefit from two-way data binding functionality, being each input control pointing to a property exposed by the model object. When submitted, the form will execute the saveTask() method located in the body of our component. Let's take a look into this method then. It has not been added already to the class though so please extend our component with a method featuring such a name and append it anywhere in the class right after its constructor:

app/tasks/task-editor.component.ts

saveTask() {
  this.task.deadline = new Date(this.task.deadline.toString());
  this.taskService.addTask(this.task);
  this.router.navigate(['TaskList']);
}

Note

You have probably raised an eyebrow after watching the first line of code in the saveTask() method. Yes, that is weird. We grab the value of the deadline property just to convert it to a string (it was a Date object already) and then we turn it into a Date object again. There is a reason for this. The DatePipe (like the one we use in the TasksComponent template) will only take the Date objects and these need to be properly formatted since no localization transformation is provided at the time of this writing. The date input field does not supply such localization functionality so we need to ensure data consistency across the board by repurposing the data format before saving it. There are better workarounds for this but all of them are basically more verbose and definitely sit outside the scope of our topic here, so we will stick to this quick fix for the rest of the chapter.

Bypassing the CanDeactivate router hook upon submitting forms

Now our component has everything we need in order to reflect the changes made on our model by the form. However, if we attempt to fill out the form with the details of our next task and proceed to save it, we will be confronted with that pesky alert popup we set up in the previous chapter for inviting the users to fill out the form, and that's what we just did now! It's time for a last-minute change then. Let's insert a beacon variable informing whether the form has been updated and successfully saved or not, and use it to skip the popup later where required. The code is as follows:

app/tasks/task-editor.component.ts

export default class TaskEditorComponent implements OnActivate, CanDeactivate, OnDeactivate {
  task: Task;
  changesSaved: boolean;

  constructor(
    private title: Title,
    private router: Router,
    private taskService: TaskService) {
      this.task = <Task>{};
  }

  saveTask() {
    this.task.deadline = new Date(this.task.deadline.toString());
    this.taskService.addTask(this.task);
    this.changesSaved = true;
    this.router.navigate(['TaskList']);
  }

  routerOnActivate() {
    this.title.setTitle('Welcome to the Task Form!');
  }

  routerCanDeactivate() {
    return this.changesSaved || confirm('Are you sure you want to leave?');
  }

  routerOnDeactivate() {
    this.title.setTitle('My Angular 2 Pomodoro Timer');
  }
}

Basically, the changesSaved field represents a boolean flag that will take a truth value right after persisting the Task typed data through the TaskService API. This allows the routerCanDeactivate() method to either return true as soon as it sees whether the changes have been saved or just throw the confirm popup.

So far so good, but now it's time to get fancy and beautify our form logic a little bit. Validating our input fields is definitely a good starting point and that will lead us to the next stage in our journey through the exciting world of Angular 2 forms.

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

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