Search code examples
angularangular-materialmat-tab

Mat-tab-group's [(selectedIndex)]=... binding is flaky


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;
  }
}

Solution

  • 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;
        });
      }
    }