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> .

Summary

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:

  • Charles Duncan

    Hi Kamil,
    You have really put in a lot of work on this and it appears to be an absolutely perfect implementation of tabs. I would like very much to download it and make it work in my application. I am a little confused as at times you talk as though there are things that don’t work, but when I follow the links at the end, it appears these problems have all been resolved. Two questions then: what (if anything) do you see as outstanding (that doesn’t work) AND is it possible to get a full download of the source at this point or should I cut-n-paste from the article.
    Thanks very much for all your work,
    “Chuck” (Charles)

    • ” it appears these problems have all been resolved” – if you see an issue noted as “Closed” then this status may lie. What’s more, some people are convinced they have a solution and often they don’t because they never tried it nor understand it at all or sometimes even completely miss the point of issue. Please note – Angular Team is built of 10+ people so things may drastically quickly change due to reported bugs or other lacking features. Thins happened multiple times during over my year of adventure (since beta 0) with this tech.

      You ask what I see as outsanding issue? Well, it all depends on perspective. Back then when I wanted to use Router I couldn’t imagine that lazy loaded and persisting component could be a problem. But still is. Some report that there is a solution and no one showed a real example with a hierarchy of components through a hierarchy of routes, where component instance is persisted (just hidden instead of recreated). The word “component Reuse” is misleading a lot. I have explained shortly this in this comment https://github.com/angular/angular/issues/5275#issuecomment-292000845 and this one https://github.com/angular/angular/issues/6634#issuecomment-283332338

      Another “outstanding” issue to me is this one – https://github.com/angular/angular/issues/8277 it was solved with a hack previously, but in some next version (I think 2.4) it broke. Workarounds may still appear but there is no official respond about API. Hacky workarounds last only for a short time so those are not solutions. Or, don’t ever upgrade angular if something hacky works for you and has to :)

      About downloading source code. This article was based on my internal work at company I’m no longer working with. Thus, I don’t have full working example online. I guess you have to follow and create your own, you may share it here as well. I don’t intend to work with Angular 2 on my personal projects, I’m thinking about writing an article about

      Not sure if I tackled everything you asked for or if I didn’t misunderstood your questions. I hope so.