Material Tabs have a default min-width of 160px
so the ink bar looks wide for any tab names that are short. The request was to have the ink bar extend approximitely 4px
from the left and right sides of the tab text. In order to acheive this in a way that would be dynamic to account for changes in tab font size or text, the below solution was developed.
The nav
component was given a template name of #shopNav
<nav mat-tab-nav-bar #shopNav class="shop-nav-tabs" [disableRipple]="true" [backgroundColor]="'white'">
Next, the individual tabs where given a (click)
event that calls a function, passing in a numerical value of the index for the tab.
<a mat-tab-link
[routerLink]="shopLinks[0].path"
routerLinkActive #rlaa="routerLinkActive"
(click)="doSomething(0)"
[active]="rlaa.isActive">
{{shopLinks[0].label}}
</a>
<div class="pad-lg-l"></div>
<a mat-tab-link
[routerLink]="shopLinks[1].path"
routerLinkActive #rlab="routerLinkActive"
(click)="doSomething(1)"
[active]="rlab.isActive">
{{shopLinks[1].label}}
</a>
<div class="pad-xl-l"></div>
<a mat-tab-link
[routerLink]="shopLinks[2].path"
routerLinkActive #rlac="routerLinkActive"
(click)="doSomething(2)"
[active]="rlac.isActive">
{{shopLinks[2].label}}
</a>
</nav>
This takes care of the template for the Material Tab component.
Lets walk from the top down through the component class, explaining each piece of the solution.
imports...
@Component({
selector: 'smp-shop',
templateUrl: './shop.component.html',
styleUrls: ['./shop.component.scss']
})
export class ShopComponent implements OnInit, AfterViewInit, OnDestroy {
We declare the nav template name as a ViewChild
within the component. This provides us access to a plethora of values under the nativeElement
.
@ViewChild('shopNav') shopNav: any;
The variable inkBar
is declared and typed as ElementRef
. Once the view has been initialized, inkBar
will be assigned the value of this.shopNav._inkbar._elementRef
. This allows us to type this.inkBar.nativeElement
instead of having to remember to type this.shopNav._inkBar._elementRef.nativeElement
.
inkBar: ElementRef;
shopLinks = [
{ path: 'plans', label: 'Plans' },
{ path: 'phones', label: 'Phones' },
{ path: 'accessories', label: 'Accessories' }
];
activeInkBarClass: string;
The variable routerEventsUnsubscribe
is declared and typed as a new Subject
. You'll see within ngOnInit()
, we are using this variable inside takeUntil
(provided via rxjs) so that we unsubscribe to our router.events
. I subscription to router.events
was necessary because the function to dynamically size the inkBar needs to be called whether a user clicks on a tab, navigates forward or backward, or navigates to the page via URL.
routerEventsUnsubscribe = new Subject<void>();
constructor(public router: Router, public renderer: Renderer2) {}
ngOnInit() {
this.router.events.takeUntil(this.routerEventsUnsubscribe).subscribe((e: Event) => {
if (e instanceof NavigationEnd) {
switch (e.url) {
case '/shop/plans':
this.doSomething(0);
return;
case '/shop/phones':
this.doSomething(1);
return;
case '/shop/accessories':
this.doSomething(2);
return;
}
}
});
this.router.navigateByUrl('shop/plans');
this.shopNav._inkBar._elementRef.nativeElement.classList.add('plans');
}
As previously described, the variable inkBar
is assigned after the view has initialized. The function (poorly & temporarily named..) doSomething
is also called with the default landing path index of 0.
ngAfterViewInit() {
this.inkBar = this.shopNav._inkBar._elementRef;
this.doSomething(0);
}
Upon component destruction, we set this.routerEventsUnsubscribe.complete()
so the subscription to router.events
is terminated.
ngOnDestroy() {
this.routerEventsUnsubscribe.next();
this.routerEventsUnsubscribe.complete();
}
In order to base the inkBar width off the tab text width, the min-width
for .mat-tab-link
had to be changed from 160px
to fit-content
. doSomething
receives a number for the index coorsponding to the active tab. First we grab the offsetWidth
and offsetLeft
of the tabLink
's nativeElement.
Since the default padding on the tabs was 24px
and the request was to have the inkBar extend 4px
for left and right sides of the tab text, we create a variable widthMinusPadding
and assign it a value of width - 40
. The left
value of the inkBar also needed to be adjusted, hence the leftMinus
const that was created. Utilizing the renderer
functionallity from Angular, we can then remove the existing width
and left
style values from the inkBar and assign it the newly created values. These functions are wrapped in a setTimeout
call to allow for the inkbar element to become available.
doSomething(index: number) {
const width = this.shopNav._tabLinks._results[index]._elementRef.nativeElement.offsetWidth;
const left = this.shopNav._tabLinks._results[index]._elementRef.nativeElement.offsetLeft;
const widthMinusPadding = width - 40 + 'px';
const leftMinus = left - 20 + 'px';
this.inkBar.nativeElement.classList.remove(this.activeInkBarClass);
setTimeout(() => {
this.renderer.removeStyle(this.inkBar.nativeElement, 'width');
this.renderer.setStyle(this.inkBar.nativeElement, 'width', widthMinusPadding);
this.renderer.removeStyle(this.inkBar.nativeElement, 'left');
this.renderer.setStyle(this.inkBar.nativeElement, 'left', leftMinus);
}, 100);
this.activeInkBarClass = this.shopLinks[index].path;
}
}