UI-Router with ngx-bootstrap tabs

Posted on August 1, 2020
Tags: angular

This is a very quick run through of one approach to using a UI-Router with Angular 10 to manage the state of the ngx-bootstrap tabs.

The basic approach has two parts:

  1. The router config that encodes the state in a known way, and
  2. A directive that integrates with the tab directive.

The UI-router state is setup to take encode the tab identifier as a parameter in the URL. It is also configured to pass the new router value into the component, as opposed to replacing the component which is the default behaviour. This is done using the dynamic property:

UI-Router Setup

{
    name: 'home',
    url: '/:tab',
    dynamic: true,
    component: HomeComponent
  }

To bootstrap things, we also redirect to a default tab:

{
    name: 'landing',
    url: '/',
    redirectTo: { state: 'home', params: { tab: 'tab1' } },
  }

Directive

The approach is to create a directive that interfaces with the ngx-bootstrap tab directive. As this directive is on the tab itself, we can use it as a way of capturing the identifier that we want to use for that tab in the URL. We’ll call this routerTab.

<div class="container">
    <tabset>
        <tab heading="Tab 1" routerTab="tab1">Tab 1 content</tab>
        <tab heading="Tab 2" routerTab="tab2">Tab 2 content</tab>
        <tab heading="Tab 3" routerTab="tab3">Tab 3 content </tab>
    </tabset>
</div>

The directive needs to do two things:

  1. Selecting a tab based on the UI-router state, and
  2. Updating the UI-router state based on a tab change.

The tab directive has an @Output() that emits when the tab is selected. So, we utilise this to tell us when the tab has changed. And similarly, it has a property that can be set to select the tab.

@Directive({
  selector: '[routerTab]',
})
export class RouterTabDirective {
  @Input()
  routerTab: string;

  private subscription: Subscription;

  constructor(private tab: TabDirective, private $state: StateService) {}

  ngOnInit() {
    this.subscription = this.tab.selectTab
      .pipe(
        filter((t) => t === this.tab)
      )
      .subscribe((t) => {
        this.$state.go(
          '.',
          {
            ...this.$state.$current.params,
            tab: this.routerTab,
          },
          { reload: false }
        );
      });

    if (this.$state.params.tab === this.routerTab) {
      this.tab.active = true;
    }
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

The approach assumes that when a new tab is selected we want to use the same state with a different parameter.

Improvements

This is quite a specific solution, as it doesn’t allow flexibility in which URL parameter is used store the tab identifier. A more general solution would require some additional directives, or other configuration to allow some customisation.

Also, by listening to each tab independently seems like an inefficient way of handling the integration. This isn’t really an issue at the scale which would be found real applications, but in terms of clean architecture a better approach would have the tabset emitting events, and allowing selection to be controlled. This would require an update an the ngx-bootstrap end, which may or may not be in line with the scope of that project.

A working example is available here.