DEV Community

Cover image for Add logic to run *before* current Material Tab changes
Francesco Leardini
Francesco Leardini

Posted on

Add logic to run *before* current Material Tab changes

Angular Material offers a wide, feature-rich set of UI Components that we can easily introduce in our projects.

Angular Material Gallery

Although the proposed functionalities already cover many common scenarios, it can be that we need to implement a special requirement in our current project.

In my last project I needed to store in a NgRx UI State the vertical scroll position of the content of Angular Material Tabs. The client wanted to restore the last scroll position of each tab, while navigating among them.

There are multiple approaches to fulfill this requirement, but I wanted to avoid costly event listeners that get triggered multiple times while scrolling through the page.

All I wanted, was the possibility to target the scrolling element in the template and register its position just before navigating to the next tab, at the end of the interaction with the current tab.



This functionality can also be useful in other situations. Let's imagine the case where we want to prevent switching to a different tab if a form is not in a valid state, for instance.

The MatTabGroup object provides a public event that gets triggered when a new Tab is selected:

@Output()
selectedTabChange: EventEmitter<MatTabChangeEvent>
Enter fullscreen mode Exit fullscreen mode

The problem is that it gets triggered too late for our needs. We want to be able to detect the content's state of the current tab before navigating to a different one.

Luckily we can use a private event handler _handleClick from the _MatTabGroupBase abstract class to intercept a Tab click and then apply our logic accordingly:

/** Handle click events, setting new selected index if appropriate. */
_handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number): void;
Enter fullscreen mode Exit fullscreen mode

To describe the solution, we start from the template, where we simply define the material tab component:

<mat-tab-group mat-align-tabs="start">
  <mat-tab label="First">Content first tab</mat-tab>
  <mat-tab label="Second">Content second tab</mat-tab>
  <mat-tab label="Third">Content third tab</mat-tab>
</mat-tab-group>
Enter fullscreen mode Exit fullscreen mode

All the logic happens on the component side:

import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ViewChild,
} from '@angular/core';
import { MatTabChangeEvent, MatTabGroup } from '@angular/material/tabs';
@Component({
  selector: 'app-component,
  templateUrl: './app-component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements AfterViewInit {

// Get a reference of the MatTabGroup form the template
@ViewChild(MatTabGroup)
tabs?: MatTabGroup;

private currentTabIndex = 0;

ngAfterViewInit() {
  this.registerTabClick();
}

registerTabClick(): void {
  // Get the handler reference
  const handleTabClick = this.tabs._handleClick;

  this.tabs._handleClick = (tab, header, index) => {  
    // Get tab content reference for the current Tab -> currentTabIndex
    // since index here is already the "new" tab index
    const tabContent = this.getTabContentElement(this.currentTabIndex);

    const scrollPosition = Math.round(tabContent.scrollTop) || 0;

    this.store.dispatch(
       setTabScrollPosition({
         scrollPosition: scrollPosition,
         // ... other props
       })
    );

   // If you want to prevent the user to navigate to the new tab,
   // you can avoid invoking the 'apply' method below 
   handleTabClick.apply(this.tabs, [tab, header, index]);

   // We update the currentIndex, as we need it again when
   // another tab is clicked
   this.currentTabIndex = index;
  };
}

 // Returns the <mat-tab-body-content> with the tab content scroll 
 position given the target tab index
 private getTabContentElement(tabIndex: number) {
   return document.getElementsByClassName('mat-tab-body-content')[tabIndex];
 }
}
Enter fullscreen mode Exit fullscreen mode

The code above is pretty straightforward. When the user clicks on a new tab, the code inside this.tabs._handleClick is invoked and this gives us the possibility to handle the current state according to our needs.

Even if probably selectedTabChange will cover all your needs, it is still useful to know that we have further possibilities to cover also edge cases.

Do you know even more customizations or other special cases you needed to face? If so, feel free to add your experience in the comments below!!

Discussion (0)