Override default click on Angular Material Tab
Welcome to the first Angular Tips post of this blog. With this series, we will discover how we can enhance our knowledge of Angular (and also Angular Material) to make things of our own.
This first post will discuss of something very simple, so let's get into it.
Angular Material Tab
If you are here it's because you are searching a way of overriding the default behavior of the tab component when a user click on one of our tab child.
By default, Angular Material will update the indexed of the selected tab, and thus, changing the component we are currently displaying. We can see it on their Github.
/** Handle click events, setting new selected index if appropriate. */
_handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number) {
if (!tab.disabled) {
this.selectedIndex = tabHeader.focusIndex = index;
}
}
But, even if Material is not easy to customize, we have some ways to override default behaviors.
The ViewChild solution
One of the key element to solve this problem is to get a reference of the MatTabGroup element inside our html.
<mat-tab-group>...</mat-tab-group>
To do so, we will use the @ViewChild
decorator to access the element.
@ViewChild(MatTabGroup)
set tabGroup(el: MatTabGroup) {
...
}
We use it with the decorator, and we then can access it via a variable. But if we do so, we will have some problems related to the fact that our variable is undefined
and not yet available.
To counter this, we use a setter
function with the keyword set
to call this function every time the ViewChild emit something new. The first values emitted will be undefined
but after the DOM is rendered, the function will have a defined value of the current MatTabGroup
displayed.
Overriding functions
Now that we have access to the MatTabGroup
variable, we can do some work inside of it to override the default behavior.
Usually, when I want to check if I can override Material behavior, I access the variable associated to the Material element, and check inside the class if I can use some properties, functions, methods.. to do as I wish.
For this case, if you open the definition of the tab-group.d.ts
by clicking on Cmd+
the MatTabGroup
property, you'll have access to this:
export declare class MatTabGroup extends _MatTabGroupBase {
_allTabs: QueryList<MatTab>;
_tabBodyWrapper: ElementRef;
_tabHeader: MatTabGroupBaseHeader;
...
}
Inside of it, nothing important for us, but we can see that the MatTabGroup
class extends another class, the "base" one. By going to the definition of the base class, we now have access to a lot of methods, properties to deal with.
export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements AfterContentInit, AfterContentChecked, OnDestroy, CanColor, CanDisableRipple {
...
get dynamicHeight(): boolean;
set dynamicHeight(value: BooleanInput);
private _dynamicHeight;
/** The index of the active tab. */
get selectedIndex(): number | null;
set selectedIndex(value: NumberInput);
private _selectedIndex;
/** Position of the tab header. */
headerPosition: MatTabHeaderPosition;
/** Duration for the tab animation. Will be normalized to milliseconds if no units are set. */
get animationDuration(): string;
set animationDuration(value: NumberInput);
private _animationDuration;
/**
* `tabindex` to be set on the inner element that wraps the tab content. Can be used for improved
* accessibility when the tab does not have focusable elements or if it has scrollable content.
* The `tabindex` will be removed automatically for inactive tabs.
* Read more at https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html
*/
get contentTabIndex(): number | null;
set contentTabIndex(value: NumberInput);
private _contentTabIndex;
...
constructor(elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, defaultConfig?: MatTabsConfig, _animationMode?: string | undefined);
/**
* After the content is checked, this component knows what tabs have been defined
* and what the selected index should be. This is where we can know exactly what position
* each tab should be in according to the new selected index, and additionally we know how
* a new selected tab should transition in (from the left or right).
*/
ngAfterContentChecked(): void;
ngAfterContentInit(): void;
/** Listens to changes in all of the tabs. */
private _subscribeToAllTabChanges;
ngOnDestroy(): void;
/** Re-aligns the ink bar to the selected tab element. */
realignInkBar(): void;
...
/** Removes the height of the tab body wrapper. */
_removeTabBodyWrapperHeight(): void;
/** Handle click events, setting new selected index if appropriate. */
_handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number): void;
/** Retrieves the tabindex for the tab. */
_getTabIndex(tab: MatTab, index: number): number | null;
/** Callback for when the focused state of a tab has changed. */
_tabFocusChanged(focusOrigin: FocusOrigin, index: number): void;
}
I deliberately omitted some properties not important for our use case, but you can check all the class inside their Github.
However, we can see on line 49, the _handleClick
method, which is not private and thus, accessible from outside the class. We already saw that this method is responsible of dealing with the click event of the user on the tab group.
So, let's put the final piece.
Wrapping everything up
To achieve what we want: overriding the default click of the mat tab group, and set something else to determine what is the currently active tab; we can now mix all of the solution with this:
@ViewChild(MatTabGroup)
set tabGroup(el: MatTabGroup) {
if (el) {
/**
* Override default click handler on Tab click
* With this, instead of changing the tab by clicking on it and letting the
* MatTabComponent do it's work we change the URL and based on this info,
* we change the index of the selected tab
*/
el._handleClick = (_tab, _tabHeader, index) => {
const newTab = 'tab-number' + index;
// For my purpose, I wanted to update the url when a user click on a tab child
// And get the currently active tab
this.navigate({ urlQuery: newTab });
};
}
// We set the MatTabGroup inside a variable if we want to access it from elsewhere
this.matTabGroup = el;
}
//
//
//
// Observable<number>
this.activeTab$ = this.route.queryParamMap.pipe(
map(paramMap => {
const currentTab = paramMap.get('urlQuery') ?? defaultTab;
return ['tab-number-1', 'tab-number-2', 'tab-number-3']
.findIndex(val => val === currentTab);
})
);
So now, when a user click on a mat tab element, we will navigate to another url by changing a query param. When the query param change (by using the ActivatedRoute
package), we have created an observable that hold the index of the current tab.
All we have to do now is to react to this observable and change the active tab.
<mat-tab-group [selectedIndex]="activeTab$ | async">...</mat-tab-group>
Et voilà.
Thank you for reaching the end of my first post, see you for another tips in Angular.