Angular Material offers a wide, feature-rich set of UI Components that we can easily introduce in our projects.
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>
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;
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>
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];
}
}
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!!
Top comments (7)
it worked, Thanks!!
Nice solution, used that.
Thanks!
Thank you for haring. There is a missing quote after the selector's value.
I have one question. How to implement "switch preventing" inside the code ?
how did you manage to do it ?
We can use a condition that, if not fulfilled, will skip the apply method call, this should prevent the tab switch.
// 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]);
What if we have two level of tab groups parent tab group and child tab group then how this will be implemented
I have never had this case yet, but I would try to apply the conditional navigation to each tab level independently.