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:
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.
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.
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:
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:
ngModel
directive attribute, mapped to one of the properties of the Task
type represented by the task member of the controller class.(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']); }
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.
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.