Refactoring our application the Angular 2 way

In this section, we will split the code we created in Chapters 1, 3, and 4 into code units, following the single responsibility principle. So, do not expect many changes in the code, apart from allocating each module in its own dedicated file. This is why we will focus more on how to split things rather than explaining each module, whose purpose you should know already. In any event, we will take a minute to discuss changes if required.

Let's begin by creating in your work folder the same directory structure we saw in the previous section. We will populate each folder with files on the go.

The shared context

The shared context is where we store any module whose functionality is meant to be used by not one but many contexts at once, as it is agnostic to those contexts as well. A good example is the pomodoro bitmap we've been using to decorate our components, which should be stored in the app/shared/assets/img path (please do save it there, by the way).

Another good example is the interfaces that model data, mostly when their schema can be reused across a different context of functionality. For instance, when we defined the QueuedOnlyPipe in Chapter 3, Implementing Properties and Events in Our Components, we actioned only over the queued property of items in the recordset. We can then seriously consider implementing a Queued interface that we can use later on to provide type-checking for modules that feature that property. This will make our pipes more reusable and model-agnostic. The code is as follows:

app/shared/interfaces/queuable.ts

interface Queueable {
  queued: boolean;
}

export default Queueable;

Tip

Pay attention to this workflow: first we define the module corresponding to this code unit, and then we export it, flagging it as default so we can import it by name from elsewhere. Interfaces need to be exported this way, but for the rest of the book we will usually declare the module and export it in the same statement.

With this interface in place, we can now safely refactor the QueuedOnlyPipe to make it fully agnostic from the Task interface so that it is fully reusable on any context where a recordset, featuring items implementing the Queued interface, needs to be filtered, regardless of what they represent. The code is as follows:

app/shared/pipes/queued-only.pipe.ts

import { Pipe, PipeTransform } from 'angular/core';
import { Queueable } from '../shared';

@Pipe({
  name: 'pomodoroQueuedOnly',
  pure: false
})
export default class QueuedOnlyPipe implements PipeTransform {
  transform(
    queueableItems: Queueable[],
    ...args): Queueable[] {
    return queueableItems.filter((queueableItem: Queueable) => {
      return queueableItem.queued === args[0]
    });
  }
}

As you can see, each code unit contains a single module. This code unit conforms to the naming conventions set for Angular 2 filenames, clearly stating the module name in camel case, plus the type suffix (.pipe in this case). The implementation does not change either, apart from the fact that we have annotated all queue-able items with the Queuable type, instead of the Task annotation we had earlier. Now, our pipe can be reused wherever a model implementing the Queued interface is present.

However, there is something that should draw your attention: we're not importing the Queuable interface from its source location, but from a file named shared.ts located in the upper level. This is the facade file for the shared context, and we will expose all public shared modules from that file not only to the clients consuming the shared context modules, but to those inside the shared context as well. There is a case for this: if any module within the shared context changes its location, we need to update the facade so that any other element referring to that module within the same context remains unaffected, since it consumes it through the facade. This is actually a good moment to start beefing up our very first facade then:

app/shared/shared.ts

import Queueable from './interfaces/queueable';

export {
  Queueable
};

As you can see, facades have no business logic implementation and, in their simplest incarnation, are just a summarized block of imports publicly exposed in a single export. Now that we have a working Queuable interface and a facade, we can create the other interface we will require throughout the book, corresponding to the Task entity, along with the other pipe we required—both exposed through the facade as well:

app/shared/interfaces/task.ts

import { Queueable } from '../shared';

interface Task extends Queueable {
  name: string;
  deadline: Date;
  pomodorosRequired: number;
}

export default Task;

We implement an interface onto another interface in TypeScript by using extends (instead of implements). Now, for the FormattedTimePipe:

app/shared/pipes/formatted-time.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'pomodoroFormattedTime'
})
export default class FormattedTimePipe implements PipeTransform {
  transform(totalMinutes: number): string {
    let minutes: number = totalMinutes % 60;
    let hours: number = Math.floor(totalMinutes / 60);
    return `${hours}h:${minutes}m`;
  }
}

Obviously, both modules will be made available publicly from the facade, as we did previously.

Services in the shared context

There is no rule of thumb for services with regard to where they should go. Some schools of thought assume that services are mere data and logic providers and, as such, should be agnostic of what actually consumes them, irrespective of whether it is a component, directive, or any other service. Services become then first-class citizens that can easily be promoted to their own project workspace for greater reusability across different projects. Some other practitioners prefer to bind services to the feature context they belong to, if only a single context applies, favoring encapsulation. Ultimately, it will depend on the level of reusability versus encapsulation you aim to achieve in your application, and what entity actually makes use of the data and logic of those services.

We built a data service in the previous chapter to serve a tasks dataset to populate our data table with. As we will see later in this book, the data service will be consumed by other contexts of the application. So, we will allocate it in the shared context, exposing it through the facade as usual:

app/shared/services/task.service.ts

import { Injectable } from '@angular/core';
import { Task } from '../shared';

@Injectable()
export default class TaskService {
  public taskStore: 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
      };
    });
  }
}

Please pay attention to how we imported the Injectable() decorator and implemented it on our service. It does not require any dependency in its constructor, so other modules depending on this service will not have any issues anyway when declaring it in its constructors. The reason is simple: it is actually a good practice to apply the @Injectable() decorator in our services by default to ensure they keep being injected seamlessly as long as they begin depending on other providers, just in case we forget to decorate them then.

Configuring application settings from a central service

In the previous chapters, we hardcoded a lot of stuff in our components: labels, pomodoro durations, plural mappings, and so on. Sometimes, our contexts are meant to have a high level of specificity and and it's fine to have that information there. At other times, we might require more flexibility and a more convenient way to update these settings application-wide. For this example, we will make all the l18n pipes mappings and pomodoro settings available from a central service located in the shared context and exposed, as usual, from the shared.ts facade.

app/shared/services/settings.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export default class SettingsService {
  timerMinutes: number;
  labelsMap: any;
  pluralsMap: any;

  constructor() {
    this.timerMinutes = 25;
    this.labelsMap = {
      'timer': {
        'start': 'Start Timer',
        'pause': 'Pause Timer',
        'resume': 'Resume Countdown',
        'other': 'Unknown'
      }
    };
    this.pluralsMap = {
      'tasks': {
        '=0': 'No pomodoros',
        '=1': 'One pomodoro',
        'other': '# pomodoros'
      }
    }
  }
}

Please note how we expose context-agnostic mapping properties, which are actually namespaced, to better group the different mappings by context.

It would be perfectly fine to split this service into two specific services, one per context, and locate them inside their respective context folders, at least with regard to the l18n mappings. Keep in mind that data such as the time duration per pomodoro will be used across different contexts though, as we will see later in this chapter.

Creating a facade module including a custom providers barrel

With all the latest changes, our shared.ts facade should look like this:

app/shared/shared.ts

import Queueable from './interfaces/queueable';
import Task from './interfaces/task';

import FormattedTimePipe from './pipes/formatted-time.pipe';
import QueuedOnlyPipe from './pipes/queued-only.pipe';

import SettingsService from './services/settings.service';
import TaskService from './services/task.service';

export {
  Queueable,
  Task,

  FormattedTimePipe,
  QueuedOnlyPipe,

  SettingsService,
  TaskService
};

Our facade exposes interface typings, pipes, and service providers. As we will see when injecting our dependencies globally from the root component, it is actually quite common and convenient to group services (and directives as well, including components in the same group) into grouping alias tokens, usually named after the context name followed by the _PROVIDERS suffix, all in uppercase. With regard to the facade, we could introduce this block of code right above the export statement:

// import statements remain unchanged above
const SHARED_PIPES: any[] = [
  FormattedTimePipe,
  QueuedOnlyPipe
];

const SHARED_PROVIDERS: any[] = [
  SettingsService,
  TaskService
];

export {
  Queueable,
  Task,

  FormattedTimePipe,
  QueuedOnlyPipe,
  SHARED_PIPES,

  SettingsService,
  TaskService,
  SHARED_PROVIDERS
};

This way, we can register all our providers through a single token where required. The same applies to directives and components, where the rule of thumb is to export in the context facade a token with the name {CONTEXTNAME}_DIRECTIVES. This gives us the opportunity to inject support for all required components and directives from a context in another component by means of a single token. If such context winds up exposing more components and directives in the future or the names of its existing logical modules change, we will not need then to follow the trail of module tokens registered in the directives property of our components throughout the application. We will see all this in action further up in this chapter.

Creating our components

With our shared context sorted out, time has come to cater with our other two contexts: timer and tasks. Their names are self-descriptive enough of the scope of their functionalities. Each context folder will allocate the component, HTML view template, CSS, and directive files required to deliver their functionality, plus a facade file that exports the public components of this feature.

The timer context

Our first context is the one belonging to the timer functionality, which happens to be the simpler one as well. It comprises a unique component with the countdown timer we built in the previous chapters:

app/timer/timer-widget.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { SettingsService } from '../shared/shared';

@Component({
  selector: 'pomodoro-timer-widget',
  template: `
    <div class="text-center">
      <img src="/app/shared/assets/img/pomodoro.png">
      <h1> {{ minutes }}:{{ seconds  | number: '2.0' }} </h1>
      <p>
        <button (click)="togglePause()" class="btn btn-danger">
        {{ buttonLabelKey | i18nSelect: buttonLabelsMap }}
        </button>
      </p>
    </div>`
})
export default class TimerWidgetComponent {
  minutes: number;
  seconds: number;
  isPaused: boolean;
  buttonLabelKey: string;
  buttonLabelsMap: any;

  constructor(private settingsService: SettingsService) {
    this.buttonLabelsMap = settingsService.labelsMap.timer;
  }

  ngOnInit(): void {
    this.resetPomodoro();
    setInterval(() => this.tick(), 1000);
  }

  resetPomodoro(): void {
    this.isPaused = true;
    this.minutes = this.settingsService.timerMinutes - 1;
    this.seconds = 59;
    this.buttonLabelKey = 'start';
  }

  private tick(): void {
    if (!this.isPaused) {
      this.buttonLabelKey = 'pause';

      if (--this.seconds < 0) {
        this.seconds = 59;
        if (--this.minutes < 0) {
          this.resetPomodoro();
        }
      }
    }
  }

  togglePause(): void {
    this.isPaused = !this.isPaused;
    if (this.minutes < this.settingsService.timerMinutes || this.seconds < 59) {
      this.buttonLabelKey = this.isPaused ? 'resume' : 'pause';
    }
  }
}

As you can see, the implementation is pretty much the same we saw already back in Chapter 1, Creating Our Very First Component in Angular 2, with the exception of initializing the component at the init life cycle stage through the OnInit interface hook. We leverage the l18nSelect pipe to better handle the different labels required for each state of the timer, consuming the label information from the SettingsService provider, which is injected by the component injector through an annotated argument in the constructor. Later on in this chapter, we will see where to register that provider. The pomodoro duration in minutes is also consumed from the service, once the latter is bound to a class field.

The component is exported publicly through a facade, saving its client components from having to know the actual path and filename of the component. The code is as follows:

app/timer/timer.ts

import TimerWidgetComponent from './timer-widget.component';

const TIMER_DIRECTIVES: any[] = [
  TimerWidgetComponent
];

export {
  TIMER_DIRECTIVES,
  TimerWidgetComponent
};

Please note that the TIMER_DIRECTIVES alias token is included for our convenience should the component range grow in the future and we wish to take advantage of a single entry point to all components and directives publicly available.

The tasks context

The tasks context encompasses some more logic, since it entails two components and a directive. Let's begin by creating the core unit required by TaskTooltipDirective:

app/tasks/task-tooltip.directive.ts

import { Task } from '../shared/shared';
import { Input, Directive, HostListener } from '@angular/core';

@Directive({
  selector: '[task]'
})
export default class TaskTooltipDirective {
  private defaultTooltipText: string;
  @Input() task: Task;
  @Input() taskTooltip: any;

  @HostListener('mouseover')
  onMouseOver() {
    if(!this.defaultTooltipText && this.taskTooltip) {
      this.defaultTooltipText = this.taskTooltip.innerText;
    }
    this.taskTooltip.innerText = this.task.name;
  }
  @HostListener('mouseout')
  onMouseOut() {
    if(this.taskTooltip) {
      this.taskTooltip.innerText = this.defaultTooltipText;
    }
  }
}

The directive keeps all the original functionality in place and just imports the Angular 2 core types and task-typing it requires. Let's look at the TaskIconsComponent now:

app/tasks/task-icons.component.ts

import { Component, Input, OnInit } from '@angular/core';
import { Task } from '../shared/shared';

@Component({
  selector: 'pomodoro-task-icons',
  template: `<img *ngFor="let icon of icons"
                  src="/app/shared/assets/img/pomodoro.png"
                  width="{{size}}">`
})
export default class TaskIconsComponent implements OnInit {
  @Input() task: Task;
  @Input() size: number;
  icons: Object[] = [];

  ngOnInit() {
    this.icons.length = this.task.pomodorosRequired;
    this.icons.fill({ name: this.task.name });
  }
}

So far so good. Now, let's jump to TasksComponent. This component will require some more overhead, since it features external HTML templates and style sheets, which are basically the same we already built back in Chapter 4, Enhancing our Components with Pipes and Directives:

app/tasks/tasks.component.css

h3, p {
    text-align: center;
}
.table {
    margin: auto;
    max-width: 860px;
}

app/tasks/tasks.component.html

<div class="container text-center">
  <h3>
    {{ queuedPomodoros | i18nPlural: queueHeaderMapping }} for today
    <span class="small" *ngIf="queuedPomodoros > 0">(Estimated time:
    {{ queuedPomodoros * timerMinutes | pomodoroFormattedTime }})
    </span>
  </h3>
  <p>
    <span*ngFor="let queuedTask of tasks | pomodoroQueuedOnly: true">
      <pomodoro-task-icons
        [task]="queuedTask"
        [taskTooltip]="tooltip"
        size="50">
      </pomodoro-task-icons>
    </span>
  </p>
  <p #tooltip [hidden]="queuedPomodoros === 0">Mouseover for details</p>

  <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}}
          <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"
            [ngSwitch]="task.queued"
            (click)="toggleTask(task)">
            <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>
</div>

Please take a moment to check out the naming convention applied to the external component files, whose filename matches the component's own to identify which file belongs to what in flat structures inside a context folder. Also, please note how we removed the main pomodoro bitmap from the template and replaced the hardcoded pomodoro time durations with a variable named timerMinutes in the binding expression that computes the time estimation to accomplish all queued tasks. We will see how that variable is populated in the following component class:

app/tasks/tasks.component.ts

import { Component, OnInit } from '@angular/core';
import TaskIconsComponent from './task-icons.component';
import TaskTooltipDirective from './task-tooltip.directive';
import {
  TaskService,
  SettingsService,
  Task,
  SHARED_PIPES
} from '../shared/shared';

@Component({
  selector: 'pomodoro-tasks',
  directives: [TaskIconsComponent, TaskTooltipDirective],
  pipes: [SHARED_PIPES],
  styleUrls: ['app/tasks/tasks.component.css'],
  templateUrl: 'app/tasks/tasks.component.html'
})
export default class TasksComponent implements OnInit {
  today: Date;
  tasks: Task[];
  queuedPomodoros: number;
  queueHeaderMapping: any;
  timerMinutes: number;

  constructor(
    private taskService: TaskService,
    private settingsService: SettingsService) {

    this.tasks = this.taskService.taskStore;
    this.today = new Date();
    this.queueHeaderMapping = settingsService.pluralsMap.tasks;
    this.timerMinutes = settingsService.timerMinutes;
  }

  ngOnInit(): void {
    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);
  }
};

Several aspects of the TasksComponent implementation are worth highlighting:

  • We import the component's required child component and directives relatively. If we tried to bring them from the facade, we would get an undefined value because of a circular reference.
  • We do not import all the required pipes, just its SHARED_PIPES alias token, registering it in the pipe's component decorator property.
  • We inject the TaskService and SettingsService providers in the component, leveraging Angular's DI system. The dependencies are injected with accessors right from the constructor, becoming private class members on the spot.
  • The tasks dataset and the pomodoro time duration are then populated from the bound services.

Our last step is to expose the facade required for this feature context. In all fairness, we are not meant to export everything on every context. We can simply get away with exporting only the main context component, while leaving any other sub component or directive outside the scope of the context facade. That is actually what we will do in here, since it is quite unlikely that any host component would ever need to include in its own view the TaskIconsComponent. We will nevertheless export the TaskTooltipDirective, since its implementation might be reused in the future by some other component dealing with the [task] input properties.

app/tasks/tasks.ts

import TasksComponent from './tasks.component';
import TaskTooltipDirective from './task-tooltip.directive';

const TASKS_DIRECTIVES: any[] = [
  TasksComponent,
  TaskTooltipDirective
];

export {
  TASKS_DIRECTIVES,
  TasksComponent,
  TaskTooltipDirective
};

Please check how we conform to the naming convention for alias tokens when grouping components and directives in their own alias token.

Defining the top root component

With all our feature contexts ready, time has come to define the top root component, which will kickstart the whole application as a cluster of components laid out in a tree hierarchy. The root component usually has a minimum implementation. Basically, its goal is to register the dependency providers the application will require as singletons at different levels of the component hierarchy, and instantiate in its view template the main child components that will eventually evolve into branches of child components.

app/app.component.ts

import { Component } from '@angular/core';
import { TIMER_DIRECTIVES } from './timer/timer';
import { TASKS_DIRECTIVES } from './tasks/tasks';
import { SHARED_PROVIDERS } from './shared/shared';

@Component({
  selector: 'pomodoro-app',
  directives: [TIMER_DIRECTIVES, TASKS_DIRECTIVES],
  providers: [SHARED_PROVIDERS],
  template: `
    <nav class="navbar navbar-default navbar-static-top">
      <div class="container">
        <div class="navbar-header">
          <strong class="navbar-brand">My Pomodoro App</strong>
        </div>
      </div>
    </nav>
    
    <pomodoro-timer-widget></pomodoro-timer-widget>
    <pomodoro-tasks></pomodoro-tasks>
    `
})
export default class AppComponent {}

Please check how we conveniently import the alias tokens and register them in the providers and directives properties of the component decorator. The SHARED_PROVIDERS token deserves a special mention. All the providers grouped by it are now available down the tree of components that hangs from this top root component, so there's no need to register it again and the state of each provider will remain consistent across the application domain.

The only exception to this would be to have one component at some level registering, for argument's sake, the TaskService as a provider. That would turn into a new instance of the service at that component level and for all its child components as well.

Bootstrapping the application

We now have a full-blown application featuring different functionality contexts, wrapped by a top root component. The last step of our endeavor will be to bootstrap the application, by importing the main top root component and passing it over to the bootstrap() function:

app/main.ts

import { bootstrap } from '@angular/platform-browser-dynamic';
import AppComponent from './app.component';

bootstrap(AppComponent);

This file must be imported from the main index.html file in order to trigger the whole process, by using a standard module loader such as SystemJS or WebPack. In the book repository, we use SystemJS so please refer to the chapter code there for further reference. In the index.html file, we will expect a custom element matching the top root component selector, as shown in the following index.html transcription:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My Angular 2 Pomodoro Application</title>

    <script src="node_modules/es6-shim/es6-shim.min.js"></script>

    <script src="node_modules/zone.js/dist/zone.js"></script>
    <script src="node_modules/reflect-metadata/Reflect.js"></script>
    <script src="node_modules/systemjs/dist/system.js"></script>
    <script src="node_modules/rxjs/bundles/Rx.js"></script>

    <script src="systemjs.config.js"></script>
    <script>
      System.import ('built/app/main')
      .then(null, console.error.bind(console));
    </script>
    <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">

    <base href="/">
  </head>

  <body>
    <pomodoro-app>Loading...</pomodoro-app>
  </body>

</html>

With all the files in place, we can safely compile the project and see the results served by a web server in a browser window. If you have downloaded package.json (and related JSON files) from the book repo, please run npm start from your terminal window and enjoy. You made a fantastic pomodoro application by yourself!

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

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