Angular's component architecture

 In this article by Gion Kunz, author of the book Mastering Angular 2 Components, has explained the concept of directives from the first version of Angular changed the game in frontend UI frameworks. This was the first time that I felt that there was a simple yet powerful concept that allowed the creation of reusable UI components. Directives could communicate with DOM events or messaging services. They allowed you to follow the principle of composition, and you could nest directives and create larger directives that solely consisted of smaller directives arranged together. Actually, directives were a very nice implementation of components for the browser.

(For more resources related to this topic, see here.)

In this section, we'll look into the component-based architecture of Angular 2 and how the previous topic about general UI components will fit into Angular.

Everything is a component

As an early adopter of Angular 2 and while talking to other people about it, I got frequently asked what the biggest difference is to the first version. My answer to this question was always the same. Everything is a component.

For me, this paradigm shift was the most relevant change that both simplified and enriched the framework. Of course, there are a lot of other changes with Angular 2. However, as an advocate of component-based user interfaces, I've found that this change is the most interesting one. Of course, this change also came with a lot of architectural changes.

Angular 2 supports the idea of looking at the user interface holistically and supporting composition with components. However, the biggest difference to its first version is that now your pages are no longer global views, but they are simply components that are assembled from other components. If you've been following this chapter, you'll notice that this is exactly what a holistic approach to user interfaces demands. No more pages but systems of components.

Angular 2 still uses the concept of directives, although directives are now really what the name suggests. They are orders for the browser to attach a given behavior to an element. Components are a special kind of directives that come with a view.

Creating a tabbed interface component

Let's introduce a new UI component in our ui folder in the project that will provide us with a tabbed interface that we can use for composition. We use what we learned about content projection in order to make this component reusable.

We'll actually create two components, one for Tabs, which itself holds individual Tab components.

First, let's create the component class within a new tabs/tab folder in a file called tab.js:

import {Component, Input, ViewEncapsulation, HostBinding} from '@angular/core';
import template from './tab.html!text';

@Component({
selector: 'ngc-tab',
host: {
   class: 'tabs__tab'
},
template,
encapsulation: ViewEncapsulation.None
})
export class Tab {
@Input() name;
@HostBinding('class.tabs__tab--active') active = false;
}

The only state that we store in our Tab component is whether the tab is active or not. The name that is displayed on the tab will be available through an input property.

We use a class property binding to make a tab visible. Based on the active flag we set a class; without this, our tabs are hidden.

Let's take a look at the tab.html template file of this component:

<ng-content></ng-content>

This is it already? Actually, yes it is! The Tab component is only responsible for the storage of its name and active state, as well as the insertion of the host element content in the content projection point. There's no additional templating that is needed.

Now, we'll move one level up and create the Tabs component that will be responsible for the grouping all the Tab components. As we won't include Tab components directly when we want to create a tabbed interface but use the Tabs component instead, this needs to forward content that we put into the Tabs host element. Let's look at how we can achieve this.

In the tabs folder, we will create a tabs.js file that contains our Tabs component code, as follows:

import {Component, ViewEncapsulation, ContentChildren} from '@angular/core';
import template from './tabs.html!text';
// We rely on the Tab component
import {Tab} from './tab/tab';

@Component({
selector: 'ngc-tabs',
host: {
   class: 'tabs'
},
template,
encapsulation: ViewEncapsulation.None,
directives: [Tab]
})
export class Tabs {
// This queries the content inside <ng-content> and stores a
// query list that will be updated if the content changes
@ContentChildren(Tab) tabs;

// The ngAfterContentInit lifecycle hook will be called once the
// content inside <ng-content> was initialized
ngAfterContentInit() {
   this.activateTab(this.tabs.first);
}

activateTab(tab) {
   // To activate a tab we first convert the live list to an
   // array and deactivate all tabs before we set the new
   // tab active
   this.tabs.toArray().forEach((t) => t.active = false);
   tab.active = true;
}
}

Let's observe what's happening here. We used a new @ContentChildren annotation, in order to query our inserted content for directives that match the type that we pass to the decorator. The tabs property will contain an object of the QueryList type, which is an observable list type that will be updated if the content projection changes. You need to remember that content projection is a dynamic process as the content in the host element can actually change, for example, using the NgFor or NgIf directives.

We use the AfterContentInit lifecycle hook, which we've already briefly discussed in the Custom UI elements section of Chapter 2, Ready, Set, Go! This lifecycle hook is called after Angular has completed content projection on the component. Only then we have the guarantee that our QueryList object will be initialized, and we can start working with child directives that were projected as content.

The activateTab function will set the Tab components active flag, deactivating any previous active tab. As the observable QueryList object is not a native array, we first need to convert it using toArray() before we start working with it.

Let's now look at the template of the Tabs component that we created in a file called tabs.html in the tabs directory:

<ul class="tabs__tab-list">
<li *ngFor="let tab of tabs">
   <button class="tabs__tab-button"
           [class.tabs__tab-button--active]="tab.active"
           (click)="activateTab(tab)">{{tab.name}}</button>
</li>
</ul>
<div class="tabs__l-container">
<ng-content select="ngc-tab"></ng-content>
</div>

The structure of our Tabs component is as follows.

  • First we render all the tab buttons in an unordered list.
  • After the unordered list, we have a tabs container that will contain all our Tab components that are inserted using content projection and the <ng-content> element. Note that the selector that we use is actually the selector we use for our Tab component.
  • Tabs that are not active will not be visible because we control this using CSS on our Tab component class attribute binding (refer to the Tab component code).

This is all that we need to create a flexible and well-encapsulated tabbed interface component. Now, we can go ahead and use this component in our Project component to provide a segregation of our project detail information.

We will create three tabs for now where the first one will embed our task list. We will address the content of the other two tabs in a later chapter.

Let's modify our Project component template in the project.html file as a first step.

Instead of including our TaskList component directly, we now use the Tabs and Tab components to nest the task list into our tabbed interface:

<ngc-tabs>
<ngc-tab name="Tasks">
   <ngc-task-list [tasks]="tasks"
                   (tasksUpdated)="updateTasks($event)">
   </ngc-task-list>
</ngc-tab>
<ngc-tab name="Comments"></ngc-tab>
<ngc-tab name="Activities"></ngc-tab>
</ngc-tabs>

You should have noticed by now that we are actually nesting two components within this template code using content projection, as follows:

  • First, the Tabs component uses content projection to select all the <ngc-tab> elements. As these elements happen to be components too (our Tab component will attach to elements with this name), they will be recognized as such within the Tabs component once they are inserted.
  • In the <ngc-tab> element, we then nest our TaskList component. If we go back to our Task component template, which will be attached to elements with the name ngc-tab, we will have a generic projection point that inserts any content that is present in the host element. Our task list will effectively be passed through the Tabs component into the Tab component.

The visual efforts timeline

Although the components that we created so far to manage efforts provide a good way to edit and display effort and time durations, we can still improve this with some visual indication.

In this section, we will create a visual efforts timeline using SVG. This timeline should display the following information:

  • The total estimated duration as a grey background bar
  • The total effective duration as a green bar that overlays on the total estimated duration bar
  • A yellow bar that shows any overtime (if the effective duration is greater than the estimated duration)

The following two figures illustrate the different visual states of our efforts timeline component:

The visual state if the estimated duration is greater than the effective duration

The visual state if the effective duration exceeds the estimated duration (the overtime is displayed as a yellow bar)

Let's start fleshing out our component by creating a new EffortsTimeline Component class on the lib/efforts/efforts-timeline/efforts-timeline.js path:

…
@Component({
selector: 'ngc-efforts-timeline',
…
})
export class EffortsTimeline {
@Input() estimated;
@Input() effective;
@Input() height;

ngOnChanges(changes) {
   this.done = 0;
   this.overtime = 0;

   if (!this.estimated && this.effective ||
       (this.estimated && this.estimated === this.effective)) {
     // If there's only effective time or if the estimated time
     // is equal to the effective time we are 100% done
     this.done = 100;
   } else if (this.estimated < this.effective) {
     // If we have more effective time than estimated we need to
     // calculate overtime and done in percentage
     this.done = this.estimated / this.effective * 100;
     this.overtime = 100 - this.done;
   } else {
     // The regular case where we have less effective time than
     // estimated
     this.done = this.effective / this.estimated * 100;
   }
}
}

Our component has three input properties:

  • estimated: This is the estimated time duration in milliseconds
  • effective: This is the effective time duration in milliseconds
  • height: This is the desired height of the efforts timeline in pixels

In the OnChanges lifecycle hook, we set two component member fields, which are based on the estimated and effective time:

  • done: This contains the width of the green bar in percentage that displays the effective duration without overtime that exceeds the estimated duration
  • overtime: This contains the width of the yellow bar in percentage that displays any overtime, which is any time duration that exceeds the estimated duration

Let's look at the template of the EffortsTimeline component and see how we can now use the done and overtime member fields to draw our timeline.

We will create a new lib/efforts/efforts-timeline/efforts-timeline.html file:

<svg width="100%" [attr.height]="height">
<rect [attr.height]="height"
       x="0" y="0" width="100%"
       class="efforts-timeline__remaining"></rect>
<rect *ngIf="done" x="0" y="0"
       [attr.width]="done + '%'" [attr.height]="height"
       class="efforts-timeline__done"></rect>
<rect *ngIf="overtime" [attr.x]="done + '%'" y="0"
       [attr.width]="overtime + '%'" [attr.height]="height"
       class="efforts-timeline__overtime"></rect>
</svg>

Our template is SVG-based, and it contains three rectangles for each of the bars that we want to display. The background bar that will be visible if there is remaining effort will always be displayed.

Above the remaining bar, we conditionally display the done and the overtime bar using the calculated widths from our component class.

Now, we can go ahead and include the EffortsTimeline class in our Efforts component. This way our users will have visual feedback when they edit the estimated or effective duration, and it provides them a sense of overview.

Let's look into the template of the Efforts component to see how we integrate the timeline:

…
<ngc-efforts-timeline height="10"
                     [estimated]="estimated"
                     [effective]="effective">
</ngc-efforts-timeline>

As we have the estimated and effective duration times readily available in our Efforts component, we can simply create a binding to the EffortsTimeline component input properties:

The Efforts component displaying our newly-created efforts timeline component (the overtime of six hours is visualized with the yellow bar)

Summary

In this article, we learned about the architecture of the components in Angular. We also learned how to create a tabbed interface component and how to create a visual efforts timeline using SVG.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Mastering Angular 2 Components

Explore Title