Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Learning Angular 2: Creating a Tabs Component

DZone's Guide to

Learning Angular 2: Creating a Tabs Component

This is a follow-up article to thoughtram's excellent article on developing a tabs component with Angular 2, where we're going to explore an alternative way of creating a tab component by learning about @ContentChildren and AfterContentInit rather than using the dependency injection approach.

· Web Dev Zone
Free Resource

Add user login and MFA to your next project in minutes. Create a free Okta developer account, drop in one of our SDKs to your application and get back to building.

This is a follow-up article to thoughtram's excellent article on developing a tabs component with Angular 2, where we're going to explore an alternative way of creating a tab component by learning about @ContentChildren and AfterContentInit.

That said, definitely read thoughtram's article on how to develop a tabs component in Angular 2 first. Then come back and continue here .

Ok, to recap, the API of the tabs component looks as follows:

<tabs>
  <tab tabTitle="Tab 1">Tab 1 Content</tab>
  <tab tabTitle="Tab 2">Tab 2 Content</tab>
</tabs>

Whenever a user clicks on the tab header, the <tabs> component takes care of setting that specific tab to be visible and to hide all others. That's why we need to establish a communication between the parent <tabs> and its children <tab>.

For establishing a communication between the two components, the thoughtram article uses Angular 2's powerful dependency injection which allows us to simply ask for an instance of a parent component. In the <tab> child component it simply asked for its parent <tabs> and registered itself on that parent component, using the addTab function:

class Tab {
  constructor(tabs: Tabs) {
    tabs.addTab(this)
  }
}

This is one way of doing it. In fact, as Pascal says:

Angular 2 is so awesome that there is not just one way how to do things!

We can take a totally different approach how to implement our simple tabs ( which isn’t so easily possible in Angular 1 ), leveraging special Angular 2 @ContentChildren property decorator with QueryList type and AfterContentInit life cycle interface. Those are more advanced concepts, which we will cover in future articles.

The Alternative Approach

This article is just about continuing thoughtram's example, but without using the dependency injection approach. So basically, rather than getting a reference to our parent component Tabs in the child component Tab (child => parent), we're doing it the other way round: we'll get a reference to all of the Tab child components from the parent Tabs (parent => child).

If you open the Tabs component, you can see that the child <tab> components are projected into its template via the content projection mechanism using <ng-content>.

...
@Component({
  selector: 'tabs',
  template:`
    <ul class="nav nav-tabs">
      <li *ngFor="#tab of tabs" (click)="selectTab(tab)" [class.active]="tab.active">
        <a href="#"></a>
      </li>
    </ul>
    <ng-content></ng-content>
  `
})
export class Tabs {
    ...
}

What we want is to get a reference to all of the <tab> children that get projected into that section, so that we can act on their corresponding API (i.e. hiding/showing them).

@ContentChildren and QueryList

We can do exactly this using the @ContentChildren decorator. You have to pass the decorator the type you want to get a reference to. In our example, it would look like @ContentChildren(Tab). As a result, we will get a list of instances in the form of a QueryList<Tab>.

Minko Gechev posted an awesome article explaining the difference between @ContentChildren and @ViewChildren on his blog. So, I'm not going to replicate that here, simply check that article!

A QueryList<T> is simply "an unmodifiable list of items that Angular keeps up to date when the state of the application changes" (see docs).

Hence, as a first step, we're going to import the new constructs in our Tabs component.

import { ContentChildren, QueryList } from 'angular2/core';

Then, inside our class we can use it like:

import { Tab } from './tab';
...
export class Tabs {
  @ContentChildren(Tab) tabs: QueryList<Tab>;
}

ngAfterContentInit Lifecycle Hook

To access the list of Tab instances, we need to wait for them to be projected into our Tabs component. There's a dedicated component lifecycle hook for that: ngAfterContentInit. This hook is called after the component content is initialized (more on the official docs).

import { AfterContentInit } from 'angular2/core';

@Component({
  selector: 'tabs',
  ...
})
export class Tabs implements AfterContentInit {

  @ContentChildren(Tab) tabs: QueryList<Tab>;

  // contentChildren are set
  ngAfterContentInit() {
    ...
  }

  ...
}

Also note the AfterContentInit interface we're importing. This is really just for better type checking with TypeScript. It doesn't have any other effect, as TypeScript interfaces do not alter the transpiled JavaScript code, they disappear once you transpile.

Within the ngAfterContentInit function we can now fetch all of our tabs and activate the first one if none is already set to be the active one.

// contentChildren are set
ngAfterContentInit() {
  // get all active tabs
  let activeTabs = this.tabs.filter((tab)=>tab.active);

  // if there is no active tab set, activate the first
  if(activeTabs.length === 0) {
    this.selectTab(this.tabs.first);
  }
}

Similarly, whenever someone clicks on a tab header, we call the selectTab(tab: Tab) function which gets all of the tabs and deactivates all of them to finally set the clicked tab to active and thus visible.

selectTab(tab: Tab){
  // deactivate all tabs
  this.tabs.toArray().forEach(tab => tab.active = false);

  // activate the tab the user has clicked on.
  tab.active = true;
}

The Final Solution

That's it. You did it! Here's the full code to play with.

Launch your application faster with Okta’s user management API. Register today for the free forever developer edition!

Topics:
angular 2.0 ,angular js ,dependecy injection

Published at DZone with permission of Juri Strumpflohner. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}