Putting it all together in the Pomodoro task list

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:

Putting it all together in the Pomodoro task list

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.

Setting up our main HTML container

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

Building our task list table with Angular directives

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.

Note

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.

Tip

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.

Tip

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:

Building our task list table with Angular directives

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.

Toggling tasks in our task list

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.

Tip

We are decorating the newly created buttons with font icons fetched from the Glyphicons font family. The icons are part of the Bootstrap CSS bundle we installed previously and are in no means related to Angular 2. Feel free to skip its use or to replace it by another icon font family.

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.

Displaying state changes in our templates

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:

  • The user reviews the tasks on the table and picks the ones to be done by clicking on each one.
  • Every time a row is clicked, the underlying Task object state changes and its Boolean queued property is toggled.
  • The state change is reflected immediately on the surface by displaying a "queued" label on the related task item.
  • The user gets prompt information of the amount of pomodoro sessions required and a time estimation to deliver them all.
  • We see how a row of pomodoro icons are displayed above the table, displaying the sum of pomodoros from all the tasks set to be done.

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.

Tip

We are hardcoding the duration of each pomodoro in the template. Ideally, such constant value should be bound from an application variable or a centralized setting. Don't worry, we will see how we can improve this implementation in the next chapters.

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?

Embedding child components

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.

Tip

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:

  • We inserted a new DOM element that will show up only when we have pomodoros queued up.
  • We displayed an actual header telling us how many pomodoros we are meant to achieve, by binding the queuedPomodoros property in an H3 DOM element, plus a total estimation in minutes for accomplishing all of this contained in the {{ queuedPomodoros*25 }} expression.
  • The NgFor directive allows us to iterate through the tasks array. In each iteration, we render a new <pomodoro-task-icons> element.
  • We bound the 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.
  • We took advantage of the <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!

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

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