Now that you have learned all the elements that allow you to build full-blown components, it's time to put all of this fresh knowledge into practice. In the next pages we are going to build a simple task list manager for our pomodoro application. In it, we will see a tasks table containing the to-do items we need to achieve:
We will also queue up tasks straight from the backlog of tasks available. This will help showing the time required to accomplish all the queued tasks and see how many pomodoros are defined in our working agenda.
Before building the actual component we need to set up our work environment first and in order to do so we will reuse the same HTML boilerplate file we used in the previous component. Please set aside the work you've done so far and keep the package.json
, tsconfig.json
, typings.json
and index.html
files we used in previous examples. Feel free to reinstall the modules required in case you need to, and replace the contents of the body tag in our index.html
template:
<nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <strong class="navbar-brand">My Pomodoro Tasks</strong> </div> </div> </nav> <pomodoro-tasks></pomodoro-tasks>
In a nutshell, we have just updated the title of the header layout above our new <pomodoro-tasks>
custom elements, which replaces the previous <pomodoro-timer>
. You might want to update the configuration of the System.import()
command to point to our new compiled component class:
System.import('built/pomodoro-tasks') .then(null, console.error.bind(console));
Create an empty pomodoro-tasks.ts
file. You might want to use this newly created file to build our new component from scratch and embed on it the definitions of all the accompanying pipes, directives, and components we will see later in this chapter.
Real-life projects are never implemented this way, since our code must conform to the "one class, one file" principle, taking advantage of ECMAScript modules for gluing things together. Chapter 5, Building an Application with Angular 2 Components will introduce you to a common set of good practices for building Angular 2 applications, including strategies for organizing your directory tree and your different elements (components, directives, pipes, services, and so on) in a sustainable way. This chapter, on the contrary, will leverage pomodoro-tasks.ts
to include all the code in a central location and then provide a bird's eye view of all the topics we will cover now without having to go switching across files. Bear in mind that this is in fact an anti-pattern, but for instructional purposes we will take this approach in this chapter for the last time. The order in which elements are declared within the file is important. Refer to the code repository in GitHub if exceptions rise.
Before moving on with our component, we need to import the dependencies required, formalize the data model we will use to populate the table, and then scaffold some data that will be served by a convenient service class.
Let's begin by adding to our pomodoro-tasks.ts
file the following code block, importing all the tokens we will require in this chapter. Pay special attention to the tokens we are importing from the Angular 2 library. We have covered Component
and Input
already, but all the rest will be explained later in this chapter:
import { Component, Input, Pipe, PipeTransform, Directive, OnInit, HostListener } from '@angular/core'; import { bootstrap } from '@angular/platform-browser-dynamic';
With the dependency tokens already imported, let's define the data model for our tasks, next to the block of imports:
/// Model interface interface Task { name: string; deadline: Date; queued: boolean; pomodorosRequired: number; }
The schema of a Task
model interface is pretty self-explanatory. Each task has a name, a deadline, a field informing how many pomodoros need to be shipped, and a Boolean field named queued
that defines if that task has been tagged to be done in our next pomodoro session.
You might be surprised that we define a model entity with an interface rather than a class, but this is perfectly fine when the entity model does not feature any business logic requiring implementation of methods or data transformation in a constructor or setter/getter function. When the latter is not required, an interface just suffices since it provides the static typing we require in a simple and more lightweight fashion.
Now, we need some data and a service wrapper class to deliver such data in the form of a collection of Task
objects. The TaskService
class defined here will do the trick, so append it to your code right after the Task
interface:
/// Local Data Service class TaskService { public taskStore: Array<Task> = []; constructor() { const tasks = [ { name: "Code an HTML Table", deadline: "Jun 23 2015", pomodorosRequired: 1 }, { name: "Sketch a wireframe for the new homepage", deadline: "Jun 24 2016", pomodorosRequired: 2 }, { name: "Style table with Bootstrap styles", deadline: "Jun 25 2016", pomodorosRequired: 1 }, { name: "Reinforce SEO with custom sitemap.xml", deadline: "Jun 26 2016", pomodorosRequired: 3 } ]; this.taskStore = tasks.map(task => { return { name: task.name, deadline: new Date(task.deadline), queued: false, pomodorosRequired: task.pomodorosRequired }; }); } }
This data store is pretty self-explanatory: it exposes a taskStore
property returning an array of objects conforming to the Task
interface (hence benefiting from static typing) with information about the name, deadline, and time estimate in pomodoros.
Now that we have a data store and a model class, we can begin building an Angular component which will consume this data source to render the tasks in our template view. Insert the following component implementation after the code you wrote before:
/// Component classes /// - Main Parent Component @Component({ selector: 'pomodoro-tasks', styleUrls: ['pomodoro-tasks.css'], templateUrl: 'pomodoro-tasks.html' }) class TasksComponent { today: Date; tasks: Task[]; constructor() { const TasksService: TasksService = new TasksService(); this.tasks = taskService.taskStore; this.today = new Date(); } }; bootstrap(TasksComponent);
As you can see, we have defined and instantiated through the bootstrap
function a new component named TasksComponent
with the selector <pomodoro-tasks>
(we already included it when we were populating the main index.html
file, remember?). This class exposes two properties: today's date and a tasks collection that will be rendered in a table contained in the component's view, as we will see shortly. To do so, it instantiates in its constructor the data source that we created previously, mapping it to the array of models typed as Task
objects represented by the tasks
field. We also initialize the today
property with an instance of the JavaScript built-in Date
object, which contains the current date.
As you have seen, the component selector does not match its controller class naming. We will delve deeper into naming conventions at the end of this chapter, as a preparation for Chapter 5, Building an Application with Angular 2 Components.
Let's create the stylesheet file now, whose implementation will be really simple and straightforward. Create a new file named pomodoro-tasks.css
at the same location where our component file lives. You can then populate it with the following styles ruleset:
h3, p { text-align: center; } table { margin: auto; max-width: 760px; }
This newly created stylesheet is so simple that it might seem a bit too much to have it as a standalone file. However, this comes as a good opportunity to showcase in our example the functionalities of the styleUrls
property of the component metadata.
Things are quite different in regards of our HTML template. This time we will not hardcode our HTML template in the component either, but we will point to an external HTML file to better manage our presentation code. Please create an HTML file and save it as pomodoro-tasks.html
in the same location where our main component's controller class exists. Once it is created, fill it in with the following HTML snippet:
<div class="container text-center"> <img src="assets/img/pomodoro.png" alt="Pomodoro"/> <div class="container"> <h4>Tasks backlog</h4> <table class="table"> <thead> <tr> <th>Task ID</th> <th>Task name</th> <th>Deliver by</th> <th>Pomodoros</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="let task of tasks; let i = index"> <th scope="row">{{i}}</th> <td>{{task.name | slice: 0:35 }} <span [hidden]="task.name.length < 35">...</span> </td> <td>{{task.deadline | date: 'fullDate' }} <span *ngIf="task.deadline < today" class="label label-danger"> Due </span> </td> <td class="text-center">{{task.pomodorosRequired}}</td> <td> [Future options...] </td> </tr> </tbody> </table> </div> </div>
We are basically creating a table that features a neat styling based on the Bootstrap framework. Then, we render all our tasks using the always convenient NgFor
directive, extracting and displaying the index
of each item in our collection as we explained while overviewing the NgFor
directive earlier in this chapter.
Please look at how we formatted the output of our task's name and deadline interpolations by means of pipes, and how conveniently we display (or not) an ellipsis to indicate when the text exceeds the maximum number of characters we allocated for the name by turning the HTML hidden
property into a property bound to an Angular expression. All this presentation logic is topped with a red label, indicating whether the given task is due whenever its end date is prior to this day. If you execute the preceding code, this page will show up on the screen:
You have probably noticed that those action buttons do not exist in our current implementation. We will fix this in the next section, playing around with state in our components. Back in Chapter 1, Creating Our Very First Component in Angular 2, we touched upon the click event handler for stopping and resuming the pomodoro countdown, and then delved deeper into the subject in Chapter 3, Implementing Properties and Events in Our Components, where we covered output properties. Let's continue on our research and see how we can hook up DOM event handlers with our component's public methods, adding a rich layer of interactivity to our components.
Add the following method to your TasksComponent
controller class. Its functionality is pretty basic; we just literally toggle the value of the queued
property for a given Task
object instance:
toggleTask(task: Task): void { task.queued = !task.queued; }
Now, we just need to hook it up with our view buttons. Update our view to include a click attribute (wrapped in braces so that it acts as an output property) in the button created within the NgFor
loop. Now that we will have different states in our Task
objects, let's reflect this in the button labels by implementing a NgSwitch
structure all together:
<table class="table"> <thead> <tr> <th>Task ID</th> <th>Task name</th> <th>Deliver by</th> <th>Pomodoros</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="#task of tasks; #i = index"> <th scope="row">{{i}} <span *ngIf="task.queued" class="label label-info"> Queued </span> </th> <td>{{task.name | slice: 0:35 }} <span [hidden]="task.name.length < 35">...</span> </td> <td>{{task.deadline | date: 'fullDate' }} <span *ngIf="task.deadline < today" class="label label-danger"> Due </span> </td> <td class="text-center">{{task.pomodorosRequired}}</td> <td> <button type="button" class="btn btn-default btn-xs" (click)="toggleTask(task)" [ngSwitch]="task.queued"> <template [ngSwitchWhen]="false"> <i class="glyphicon glyphicon-plus-sign"></i> Add </template> <template [ngSwitchWhen]="true"> <i class="glyphicon glyphicon-minus-sign"></i> Remove </template> <template ngSwitchDefault> <i class="glyphicon glyphicon-plus-sign"></i> Add </template> </button> </td> </tr> </tbody> </table>
Our brand new button can execute the toggleTask
method in our component class, passing as an argument the Task
object that corresponds to that iteration of NgFor
. On the other hand, the preceding NgSwitch
implementation allows us to display different button labels and icons depending on the state of the Task
object at any given time.
Execute the code as it is now and check out the results yourself. Neat, isn't it? But maybe we can get more juice from Angular 2 by adding more functionality to the task list.
Now that we can pick the tasks to be done from the table, it would be great to have some kind of visual hint of how many pomodoro sessions we are meant to achieve. The logic is as follows:
Task
object state changes and its Boolean queued
property is toggled.This functionality will have to react to the state changes of the set of Task
objects we're dealing with. The good news is that thanks to Angular 2's very own change detection system, making components fully aware of state changes is extremely easy.
Thus, our very first task will be to tweak our TasksComponent
class to include some way to compute and display how many tasks are queued up. We will use that information to render or not a block of markup in our component where we will inform how many pomodoros we have lined up and how much aggregated time it will take to accomplish them all.
The new queuedPomodoros
field of our class will provide such information, and we will want to insert a new method named updateQueuedPomodoros()
in our class that will update its numeric value upon instantiating the component or enqueueing tasks. On top of that, we will create a key/value mapping we can use later on to render a more expressive title header depending on the amount of queued pomodoros thanks to the I18nPlural
pipe:
class TasksComponent { today: Date; tasks: Task[]; queuedPomodoros: number; queueHeaderMapping: any = { '=0': 'No pomodoros', '=1': 'One pomodoro', 'other': '# pomodoros' }; constructor() { const TasksService: TasksService = new TasksService(); this.tasks = taskService.taskStore; this.today = new Date(); this.updateQueuedPomodoros(); } toggleTask(task: Task): void { task.queued = !task.queued; this.updateQueuedPomodoros(); } private updateQueuedPomodoros(): void { this.queuedPomodoros = this.tasks .filter((task: Task) => task.queued) .reduce((pomodoros: number, queuedTask: Task) => { return pomodoros + queuedTask.pomodorosRequired; }, 0); } };
The updateQueuedPomodoros()
method makes use of JavaScript's native Array.filter()
and Array.reduce()
methods to build a list of queued tasks out of the original
tasks
collection property. The reduce
method applied over the resulting array gives us the total number of pomodoros required. With a stateful computation of the number of queued pomodoros now available, it's time to update our template accordingly. Go to pomodoro-tasks.html
and inject the following chunk of HTML right before the <h4>Tasks backlog</h4>
element. The code is as follows:
<div> <h3> {{ queuedPomodoros | i18nPlural:queueHeaderMapping }} for today <span class="small" *ngIf="queuedPomodoros > 0">(Estimated time: {{ queuedPomodoros * 25 }})</span> </h3> </div> <h4>Tasks backlog</h4> <!-- rest of template remains the same -->
The preceding block renders an informative header title at all times, even when no pomodoros have been queued up. We also bind that value in the template and use it to estimate through an expression binding the amount of minutes required to go through each and every pomodoro session required.
Save your changes and reload the page, and then try to toggle some task items on the table to see how the information changes in real time. Exciting, isn't it?
Now, let's start building a tiny pomodoro icon component that will be nested inside the TasksComponent
component. This new component will display a smaller version of our big pomodoro icon, which we will use to display on the template the amount of pomodoros lined up to be done, as we described earlier in this chapter. Let's pave the way towards component trees, which we will analyze in detail in Chapter 5, Building an Application with Angular 2 Components. For now, just include the following component class before the TasksComponent
class you built earlier:
Our component will expose a public property named task
in which we can inject a Task
object. The component will use this Task
object binding to replicate the image rendered in the template as many times as pomodoro sessions are required by this task in its pomodorosRequired
property, all this by means of a NgFor
directive.
In our pomodoro-tasks.ts
file, inject the following block of code before our TasksComponent
:
@Component({ selector: 'pomodoro-task-icons', template: `<img *ngFor="let icon of icons" src="/assets/img/pomodoro.png" width="50">` }) class TaskIconsComponent implements OnInit { @Input() task: Task; icons: Object[] = []; ngOnInit() { this.icons.length = this.task.pomodorosRequired; this.icons.fill({ name: this.task.name }); } }
Our new TaskIconsComponent
features a pretty simple implementation, with a very intuitive selector matching its camel-cased class name and a template where we duplicate the given <img>
tag as many times as objects are populated in the icons
array property of the controller class, which is populated with the native fill method of the Array
object in the JavaScript API (the fill
method fills all the elements of an array with a static value passed as an argument), within ngOnInit()
. Wait, what is this? Shouldn't we implement the loop populating the icons
array member in the constructor instead?
This method is one of the lifecycle hooks we will overview in the next chapter, and probably the most important one. The reason why we populate the icons
array field here and not in the constructor method is because we need each and every data-bound properties to be properly initialized before proceeding to run the for
loop. Otherwise, it will be too soon to access the input value
task
since it will return an undefined
value.
The OnInit
interface demands an ngOnInit()
method to be integrated in the controller class that implements such -interface, and it will be executed once all input properties with a binding defined have been checked. We will take a bird's eye overview of component lifecycle hooks in Chapter 5, Building an Application with Angular 2 Components.
Still, our new component needs to find its way to its parent component. So, let's insert a reference to the component
class in the directives
property of the TasksComponent
decorator settings:
@Component({
selector: 'pomodoro-tasks',
directives: [TaskIconsComponent],
styleUrls: ['pomodoro-tasks.css'],
templateUrl: 'pomodoro-tasks.html'
})
Do you remember that we mentioned that components are basically directives with custom views? If so, then we will want to use the directives
property of each component every time we want to nest another component within. This explains the case for using the directives
property here.
Our next step will be to inject the <pomodoro-task-icons>
element in the TasksComponent
template. Go back to pomodoro-tasks.html
and update the code located inside the conditional block meant to be displayed when queuedPomodoros
is greater than zero. The code is as follows:
<div> <h3> {{ queuedPomodoros | i18nPlural:queueHeaderMapping }} for today <span class="small" *ngIf="queuedPomodoros > 0">(Estimated time: {{ queuedPomodoros * 25 }})</span> </h3> <p> <span *ngFor="let queuedTask of tasks"> <pomodoro-task-icons [task]="queuedTask" (mouseover)="tooltip.innerText = queuedTask.name" (mouseout)="tooltip.innerText = 'Mouseover for details'"> </pomodoro-task-icons> </span> </p> <p #tooltip *ngIf="queuedPomodoros > 0">Mouseover for details</p> </div> <h4>Tasks backlog</h4> <!-- rest of template remains the same -->
There is still some room for improvement though. Unfortunately, the icon size is hardcoded in the TaskIconsComponent
template and that makes it harder to reuse that component in other contexts where a different size might be required. Obviously, we could refactor the TaskIconsComponent
class to expose a size
input property and then bind the value received straight into the component template in order to resize the image accordingly:
@Component({ selector: 'pomodoro-task-icons', template: `<img *ngFor="let icon of icons" src="/assets/img/pomodoro.png" width="{{size}}">` }) class TaskIconsComponent implements OnInit { @Input() task: Task; icons: Object[] = []; @Input() size: number; ngOnInit() { ... } }
Then, we just need to update the implementation of pomodoro-tasks.html
to declare the value we need for the size:
<span *ngFor="let queuedTask of tasks">
<pomodoro-task-icons
[task]="queuedTask"
size="50"
(mouseover)="tooltip.innerText = queuedTask.name"
(mouseout)="tooltip.innerText = 'Mouseover for details'">
</pomodoro-task-icons>
</span>
Please note that the size attribute is not wrapped between brackets because we are binding a hardcoded value. If we wanted to bind a component variable, that attribute should be properly declared as [size]="{{mySizeVariable}}"
.
Let's summarize what we did:
queuedPomodoros
property in an H3 DOM element, plus a total estimation in minutes for accomplishing all of this contained in the {{ queuedPomodoros*25 }}
expression.NgFor
directive allows us to iterate through the tasks
array. In each iteration, we render a new <pomodoro-task-icons>
element.Task
model object of each iteration, represented by the queuedTask
reference, in the task
input property of the <pomodoro-task-icons>
in the loop template.<pomodoro-task-icons>
element to include additional mouse event handlers that point to the following paragraph, which has been flagged with the #tooltip
local reference. So, every time the user hovers the mouse over the pomodoro icon, the text beneath the icons row will display the respective pomodoro's task name.We ran the extra mile, turning the size of the icon rendered by <pomodoro-task-icons>
into a configurable property as part of the component API. We now have pomodoro icons that get updated in real time as we toggle the information on the table. New problems have arisen, however. Firstly, we are displaying pomodoro icon components matching the required pomodoros of each task, without filtering out those which are not queued. On the other hand, the overall estimation of time required to achieve all our queued pomodoros displays the gross number of minutes, and this information will make no sense as we add more and more pomodoros to the working plan.
Perhaps, it's time to amend this. It's a good thing that custom pipes have come to the rescue!