Angular 2: my solution for dynamic tabs

I needed tabs that would not be destroyed by Angular. As powerful as if I would code a normal application for Windows in C#. Here's my solution.

Goal: everything you would imagine about having tabs

My requirement list shall not be ignored!

  1. few predefined types of tabs - content for each tab can have different component (but doesn't have to)
  2. each tab has a name, ID and any user data
  3. lazy tab instantiation
  4. ability to procedurally change set of tabs
  5. ability to procedurally change active tab
  6. tabs are closeable
  7. every tab could have a little different looks (like: different icon or text color)
  8. somewhat copyable solution
  9. available references to components which are tab contents

We'll fulfill those needs one by one. Except the latest one.

TL;DR - The solution in short sentence

It's "just"  ngFor with couple of ngIf:s and [hidden]:s. ngFor maps tab meta data to ngIf:s of ever activated tabs on given condition. Tabs are listed in a service which is available through dependency injection throughout whole application if it's declared in an application module.

The additional trick here is about retaining back a reference to each tab component. The cause of this is that those tabs have to be instantiated through a declarative template. There is other way though - define some TabDirective  that will bind the tab component and tab meta data to together, then catch those directives with @ViewChildren .

Dive into solution

We need:

  1. a service that will contain list of tabs
  2. a use of ngFor  directive over that list which will declaratively show or hide tab content
  3. a custom directive that will help connecting tab meta data with the declared component, if needed

Let's start from the  ngFor . For example, I'll define a template containing some tabs:

Look closely to the code. There are two ngFor:s.

The tab and the content

The first one loops over tabsService.tabs$ (observable) collection to displays only the tabs. There is no condition about displaying them because all tabs should be visible.

The second ngFor loops over same list again but just here are the tab contents. In this example there is only one component defined - the AppMarketplaceComponent (tagged as app-marketplace ). If there were more components (more types of component for tabs) then there would be more <template [ngIf]="tab.everActivated">  declarations.

Lazy instantiation

<template [ngIf]="tab.everActivated"> decides whether the app-marketplace component should be instantiated or not. This line is not concerned about hiding component when tabs are switched! The tab.everActivatedproperty defines whether tab was opened at least once. Here's the lazy part of instantiation.

The hiding part is here:

The [hidden]  property hides the component without destroying or deattaching it.

Note: <template [ngIf]="tab.everActivated"> is not a real template, it's just a way of making conditional instantiation without having a new <div> here. It's the same way how *ngIf  works (notice the asterisk symbol * ).

Tab meta data

All info about one tab is described by this interface:

where dataId is an optional field, you can use it however you want or even decide on not having it.

and type is also somewhat custom. In my app I can have such tab list:

  1. Product List
  2. Product #123
  3. Product #124
  4. Product #125

So you can see I have two types of tabs here:

  1. ProductList
  2. Product

Why would that be useful? For showing special stuff according to type of tab, not a name or id. For instance, a close button (look to the bonus chapter in the end of article).

Service for setting tabs

Though I have multiple different tab lists over the app, lot's of code is repeated. Hence, I've created a base class for all tab panes. I called it DataTabsService , it has methods to add and remove tabs, switch current tab and list all tabs.

First, the example of SettingsTabsService  which inherits from the base DataTabsService :

I can asynchronously download a list of marketplaces and then create tabs each for one marketplace, then call switchToTab()  for the first one. So, while data is still downloading, then no tab is really activated, neither any component is instantiated.

And, for convienence I have created the  switchToMarketplaceTab(marketplaceId) method so users don't have to dig too much into what piece of data is an ID for these tabs.

The base Data Tabs Service

The code below should be self-explainable:

Catching reference to tab components

Now, here's the additional part. Note that this was not used in above example with marketplaces.

Why would I need that? For instance, a component may have a method that we would want to call. It's not declarative approach, yes, it's rather procedural. In past, I was switching tabs in subcomponent this way. Nowadays, I define services, as shown above so I don't need it anymore.

We shall define such field in component which contains all those tabs:

where the directive is:

However, this was my solution until some changes in Angular. It's broken now. Follow this topic if you need:

→ How to access the host component from a directive?

The issue here is that we do not know the type of component. If we would know, then injecting it to the directive would be simple.

Bonus: How to make tabs look more appealing

How? Inside the anchor for each tab you can define various icons.

or define a close button for specific tab type:

which should be placed right after closing the </a> .


Whole this solution was created as a workaround because we don't want to destroy contents of our tabs while switching between them.

It would also be nice to have routing for it. And guess what - it's possible with another couple of ngIf :s. But I didn't cover this part here since router is constantly changing and up to this point it was never good enough.

To follow the general problem there are some issues on Angular's GitHub: