0

I am using Angular and the MaterialTabs module.

I have a list of tabs in my component that I display using mat-tabs and ngFor, plus one more disabled tab with a plus button. When clicking the plus button, I would like to add a new tab and immediately focus on that tab. (Similarly to how browser tabs work.)

This is how it looks like:

Example

I do this by two-way property-binding to the selectedIndex property of the mat-tab-group and setting it from my component inside the button click event handler.

You can see that the tabgroup's property and the property bound to it is equal, when it works.

However, I ran into a problem, where if I freshly load the page and click on the button first, the property binding somehow breaks:

Issue

Clicking once on any of the Tabs seems to fix the issue forever. The issue comes back when you reload the page.

From my understanding the following property binding would make sure that the values will always be equal and if one of them changes the other will follow:

[(selectedIndex)]="selectedIndexBinding"

So how can selectedIndex be 1, while selectedIndexBinding is 0?

How can I fix this issue?

Live example:

https://stackblitz.com/edit/angular-82vgrj

Code:

src/app/app.component.html

<mat-tab-group [(selectedIndex)]="selectedIndexBinding" #tabGroup>
  <mat-tab *ngFor="let tab of tabs">
    <ng-template mat-tab-label>
      {{ tab }}
    </ng-template>
  </mat-tab>
  <mat-tab disabled>
      <ng-template mat-tab-label>
        <button mat-icon-button (click)="addTab()">
          +
        </button>
      </ng-template>
  </mat-tab>
</mat-tab-group>

selectedIndex: {{ tabGroup.selectedIndex }}<br />
selectedIndexBinding: {{ selectedIndexBinding }}

src/app/app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  public selectedIndexBinding = 0;
  public tabs = [];

  public addTab() {
    this.tabs.push("Tab");
    this.selectedIndexBinding = this.tabs.length - 1;
  }
}
Edric
  • 24,639
  • 13
  • 81
  • 91
nemkin
  • 95
  • 2
  • 9

1 Answers1

3

Your issue comes from the way you're setting the new index, since the button counts for a tab in any way.

You need to retrieve the original mat-tab-group with:

@ViewChild('tabGroup', {static: false}) tab: MatTabGroup;

and set the index you want directly:

this.tab.selectedIndex = this.tabs.length - 1;

You may notice I put it all in a setTimeout. It's for Angular lifecycle hooks, setTimeout triggers a new Angular cycle.

Here's your working StackBlitz.

Code:

src/app/app.component.html

<mat-tab-group [(selectedIndex)]="selectedIndexBinding" #tabGroup>
  <mat-tab *ngFor="let tab of tabs">
    <ng-template mat-tab-label>
      {{ tab }}
    </ng-template>
  </mat-tab>
  <mat-tab disabled>
      <ng-template mat-tab-label>
        <button mat-icon-button (click)="addTab($event)">
          +
        </button>
      </ng-template>
  </mat-tab>
</mat-tab-group>

selectedIndex: {{ tabGroup.selectedIndex }}<br />
selectedIndexBinding: {{ selectedIndexBinding }}

src/app/app.component.ts

import { Component, ViewChild } from '@angular/core';
import { MatTabGroup } from '@angular/material';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  @ViewChild('tabGroup', {static: false}) tab: MatTabGroup;
  public selectedIndexBinding = 0;
  public tabs = [];

  public addTab(e: Event) {
    this.tabs.push("Tab");
    e.preventDefault();

    setTimeout(() => {
      console.log(this.tabs, this.tab.selectedIndex);
      this.tab.selectedIndex = this.tabs.length - 1;
    });
  }
}
nemkin
  • 95
  • 2
  • 9
Maxime Lafarie
  • 2,172
  • 1
  • 22
  • 41
  • What does e.preventDefault(); in the addTab handler do in this situation? I thought the binding method would work, because even though the button is a tab as well, its index is always tabs.length, and I never set it to that, so it shouldn't focus on that tab. It's a bit weird that it doesn't. – nemkin Jan 31 '20 at 16:17
  • @nemkin you're right, it does nothing special. I forgot to remove it. ;) – Maxime Lafarie Feb 03 '20 at 09:35
  • 3
    It's unbelievable that there isn't a better solution to this than setting a timeout. Loading this tabs dynamically has to be a common paradigm. – James Parker Jun 12 '20 at 18:04
  • Agreed James. I spent 3 hours on this very common use case, only to have to use setTimeout. – Casey Plummer May 02 '23 at 13:06