Components: [Tabs] Nav bar should paginate labels on overflow

Created on 11 Dec 2016  路  59Comments  路  Source: angular/components

Bug, feature request, or proposal:

Feature request

What is the expected behavior?

"md-tab-nav-bar" component to have same functionality / visual fidelity as the "md-tab-group" component.

What is the current behavior?

The 2 components are disjoint and improvements made in "md-tab-group" component won't show up in the "md-tab-nav-bar" component

What are the steps to reproduce?

See the default demo included in this repository

What is the use-case or motivation for changing an existing behavior?

I am building a large application that uses lazy loaded modules to implement specific features. I want to display specific functionality in tabs. For example we have a customer object which can contain up to 11 tabs which represent specific data pertaining to this customer.
The current "md-tab-group" implementation is already looking great (Dynamic height, scrolling, pagination when tabs exceed container width etc..) But with the "md-tab-nav-bar" component none of these features are available.

Which versions of Angular, Material, OS, browsers are affected?

Material 2.0.0-alpha.11

Is there anything else we should know?

P3 feature has pr help wanted

Most helpful comment

@Zehirlolz @mhosman maybe import problem.

I generate router-tab directive module.

https://github.com/zerohouse/router-tab

All 59 comments

Unlike MdTabGroup, MdTabNavBar doesn't own the body, so I'm not sure whether it should support dynamic height. However I would desperately want to have pagination/scrolling and compact mode for narrow screens to be implemented for MdTabNavBar.

Sure, pagination/scrolling would be very nice!

I may be suffering from a related issue. Cant seem to get lists to be independently scroll-able within a tab.

see this plunkr for demo: http://plnkr.co/edit/h67A8ZSeH747E3oYKdv5?p=preview

Interestingly, independently scroll-able lists work fine in md-tab-nav-bar tabs.

Is there a published timeframe for pagination on MdTabNavBar?

Also desperately need this functionality.

Any news?

Also interested in updates as well.

These features is very necessary

any workarounds while we wait? ;)
I need this too.. meanwhile I'll try to write my own workaround

My stupid workaround! :)
i've used md-tab-group... with
style="width:100vw;"
and no content... now I just have to style it and add some click-events.

  <md-tab-group style="width:100vw;">
    <md-tab label="Tab 1"></md-tab>
    <md-tab label="Tab 2"></md-tab>
    <md-tab label="Tab 3"></md-tab>
    <md-tab label="Tab 4"></md-tab>
    <md-tab label="Tab 5"></md-tab>
  </md-tab-group>

almost one year later ... any news on that ? no wonder why most of the dev move to react ...

@anymos This is an open source project. If you need something, you can always create and use a fork or send a PR. Most projects I work with have more incoming feature requests than developers can implement. I expect this should not be a surprise to many developers.

Please try to keep comments constructive.

I have built a workaround for this issue.
I've put the nav bar in a div and I added a overflow-x: hidden and added a display: flex to it.
In the component I've taken control of the div and used it's scrollLeft property(div.nativeElement.scrollLeft)
Afterwards I added 2 buttons which constrolled the left/right scrolling.
It's an easy and quick solution until this issue is resolved fully.

@qwr1000 Hey, could you post a codepen for your quickfix?

@qwr1000 Thanks for the idea!
I created a simple component using styles form tab header. Here's the code: http://plnkr.co/edit/8locpYNLpLEDOKxWneY7

Some additions to the superb solution by @oleges

  • Turn off left/right arrow buttons if the screen is wide enough
  • If screen is wide enough expand the bottom-border line fully
  • Also disable wrapping in Firefox

http://plnkr.co/edit/qJTwkJPzriMhKynhRPBS?p=preview

Also came here looking for this issue. Probably naive but this be something that would be a matter of copying the logic from the one component that already works (mat-tab-group) to this one?

This is a great case of getting community contribution. Unfortunately we only have a limited set of resources with lots of higher priority issues/features to work on.

If anyone wants to work on this, here's a branch on my fork that has some work done to refactor the tab header so it can be used by both the tab group and nav bar: https://github.com/andrewseguin/material2/tree/tabs-pagination-refactor

To be honest, not sure that's the final design or best implementation, just playing with it to see what possible. Still needs some cleanup in implementation and tests.

@andrewseguin do you still need help with that fork?

I like https://angular-material-priority-nav.stackblitz.io/ really a lot - can we have that out of the box? pretty please!

@tommyc38 and @pgrm both of those break the material spec, which I think this repo should be against, (just my opinion). @andrewseguin I'm going to take a whack at styling those tabs. Should I just base it off the spec, or use the design that currently exists? Your branch currently has the styles based on the existing mat-tabs but just from a brief comparison, those also break the material spec.

@tommyc38 could you please provide the link to the source code. Thankyou

Any news on this?

@everyone how about this lol?
for me is working. I am surprised nobody try this? 馃憤

This is the best work around

<nav mat-tab-nav-bar>
    <mat-tab-group>
        <mat-tab *ngFor="let tab of tabs">
             <ng-template mat-tab-label>
                <a  mat-tab-link 
                      [routerLink]="tab.link"
                      [routerLinkActiveOptions]="{exact: true}"
                      routerLinkActive="c-tabs__link--active"
                      [innerHtml]="i18nService.trans(tab.label)">
                </a>
            </ng-template>
       </mat-tab>
    </mat-tab-group>
</nav>

// Overwrite for Angular Material Tabs CSS
.mat-tab-label-container {
    .mat-tab-label  {
        opacity: 1;
        padding: 0;
    }
} 
.mat-tab-group.mat-primary {
     .mat-ink-bar {
          display: none;
      }
}

@everyone

@daiky00 it looks fine. but the keyboard navigation does not work

@daiky00 , the tab labels are fine, but the pagination arrows are missing.

@zerohouse
Hi, I tried to implement your solution but I get the following error:

ERROR in src/app/components/tabs/tabSelector/tabLink.ts(22,11): error TS2345: Argument of type 'Router' is not assignable to parameter of type 'Router'.
Property 'rootComponentType' is missing in type 'Router'.

related to:

  public constructor(private routerc: Router, private host: MatTab, @Host() parent: MatTabGroup,
                     route: ActivatedRoute, renderer: Renderer2, el: ElementRef) {
    super(routerc, route, null, renderer, el);
    this.parent = parent;
  }

I don't really understand... Any Idea why ?
Thanks!

Hey @zerohouse @Zehirlolz I get: Can't bind to 'tabLink' since it isn't a known property of 'mat-tab'.

@Zehirlolz @mhosman maybe import problem.

I generate router-tab directive module.

https://github.com/zerohouse/router-tab

@zerohouse Installed and working like charm

@zerohouse awesome!

@zerohouse thank you. Works great. Can we get this merged into the main repo?

@daiky00 's workaround is longer working as of 6.4.7:
https://stackblitz.com/edit/angular-material2-nav-pagination

@benedict-odonovan check the package of @zerohouse https://github.com/zerohouse/router-tab

It's been almost 2 years and the issue still not fixed...

Was zerohouse's solution ever offered as a pull-request? If so maybe add that here and if not, maybe that would be the normal way to speed up getting the fix into main? I suppose that's the whole point of having all this open-source on GitHub in the first place? ;)

My workaround for this:

// template
<mat-tab-group (selectedTabChange)="navigate($event)">
    <mat-tab *ngFor="let tab of TABS"
             [label]="tab.title"></mat-tab>
</mat-tab-group>

// TS
const TABS: Tab[] = [
    {
        route: 'routeHere',
        index: 0,
        title: 'Title',
        component: SomeComponent
    },
    ...
}

navigate($event: MatTabChangeEvent) {
    const tab = TABS[$event.index];
    this.router.navigateByUrl(tab.route);
}

My workaround for this:

// template
<mat-tab-group (selectedTabChange)="navigate($event)">
    <mat-tab *ngFor="let tab of TABS"
             [label]="tab.title"></mat-tab>
</mat-tab-group>

// TS
const TABS: Tab[] = [
    {
        route: 'routeHere',
        index: 0,
        title: 'Title',
        component: SomeComponent
    },
    ...
}

navigate($event: MatTabChangeEvent) {
    const tab = TABS[$event.index];
    this.router.navigateByUrl(tab.route);
}

Thanks, but this doesn't automatically set active tab on route change.

@Zehirlolz @mhosman maybe import problem.

I generate router-tab directive module.

https://github.com/zerohouse/router-tab

This is working perfectly. Thanks :)

Thanks, but this doesn't automatically set active tab on route change.

It's not supposed to do that. You still can write simple service for your needs, like detection of route change and switching to correspond tab id.
p.s. It's quite simple, idk why the devs can't do that for years ) @jelbourn @DevVersion

Because @angular/material can't have a dependency on @angular/router

Here is a full working workaround with exactly the same behavior than mat-tab-nav-bar (using a component):
router-tab.component.ts:

import { AfterViewInit, Component, ContentChildren, Directive, Input, OnDestroy, QueryList, ViewChild, ViewChildren, ViewEncapsulation } from '@angular/core';
import { MatTab, MatTabGroup } from '@angular/material';
import { NavigationEnd, Router, RouterLink } from '@angular/router';
import { Subscription } from 'rxjs';

/**
 * Directive to retrieve mat-tab options from router-tab.component.html 
 */
@Directive({
    selector: 'mat-tab[routerLink]'
})
export class RouterTab {

    @Input()
    public routerLinkActiveOptions: {
        exact: boolean;
    };

    constructor(public tab: MatTab, public routerLink: RouterLink) {
    }
}

/**
 * Directive to set tabs within app-router-tab
 */
@Directive({ selector: 'app-router-tab-item' })
export class RouterTabItem {

    @Input()
    public routerLink: RouterLink;

    @Input()
    public routerLinkActiveOptions: {
        exact: boolean;
    };

    @Input('disabled')
    public disabled: boolean;

    @Input()
    public label: string;
}

/**
 * RouterTab component with the same behavior than mat-tab-nav-bar
 */
@Component({
    selector: 'app-router-tab',
    templateUrl: './router-tab.component.html',
    styleUrls: ['./router-tab.component.scss'],
    encapsulation: ViewEncapsulation.None
})
export class RouterTabComponent implements AfterViewInit, OnDestroy {

    @ViewChild('matTabGroup')
    public matTabGroup: MatTabGroup;

    @ContentChildren(RouterTabItem)
    public routerTabItems !: QueryList<RouterTabItem>;

    @ViewChildren(RouterTab)
    public routerTabs: QueryList<RouterTab>;

    private subscription = new Subscription();

    constructor(private router: Router) {
    }

    ngAfterViewInit() {
        // Remove tab click event
        this.matTabGroup._handleClick = () => { };
        // Select current tab depending on url
        this.setIndex();
        // Subscription to navigation change
        this.subscription.add(this.router.events.subscribe((e) => {
            if (e instanceof NavigationEnd) {
                this.setIndex();
            }
        }));
    }

    ngOnDestroy(): void {
        this.subscription.unsubscribe();
    }

    /**
     * Set current selected tab depending on navigation
     */
    private setIndex() {
        this.routerTabs.find((tab, i) => {
            if (!this.router.isActive(tab.routerLink.urlTree, tab.routerLinkActiveOptions ? tab.routerLinkActiveOptions.exact : false))
                return false;
            tab.tab.isActive = true;
            this.matTabGroup.selectedIndex = i;
            return true;
        });
    }
}

router-tab.component.html:

<mat-tab-group #matTabGroup class="hide-tab-wrapper">
    <mat-tab *ngFor="let tab of routerTabItems" [routerLink]="tab.routerLink" [disabled]="tab.disabled" [routerLinkActiveOptions]="tab.routerLinkActiveOptions">
        <ng-template mat-tab-label>
            <a *ngIf="!tab.disabled" class="router-tab-link" [routerLink]="tab.routerLink"></a>
            {{ tab.label }}
        </ng-template>
    </mat-tab>
</mat-tab-group>

router-tab.component.scss:

.hide-tab-wrapper .mat-tab-body-wrapper {
    display: none !important;
}

.router-tab-link {
    position: absolute;
    height: 100%;
    width: 100%;
}

router-tab.module.ts: (import this module in your app.module.ts or shared module, depending on your case)

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatTabsModule } from '@angular/material';
import { RouterModule } from '@angular/router';
import { RouterTab, RouterTabComponent, RouterTabItem } from './router-tab.component';

@NgModule({
    imports: [
        CommonModule,
        RouterModule,
        MatTabsModule
    ],
    declarations: [
        RouterTabComponent,
        RouterTabItem,
        RouterTab
    ],
    exports: [
        RouterTabComponent,
        RouterTabItem,
        RouterTab
    ]
})
export class RouterTabModule { }

usage:

<app-router-tab>
    <app-router-tab-item [routerLink]="tab1" label="Tab 1" [routerLinkActiveOptions]="{exact: true}"></app-router-tab-item> 
    <app-router-tab-item [routerLink]="tab2" label="Tab 2"></app-router-tab-item>   
    <app-router-tab-item [routerLink]="tab3" label="Tab 3"></app-router-tab-item>   
    <app-router-tab-item [routerLink]="tab4" label="Tab 4" [disabled]="true"></app-router-tab-item> 
</app-router-tab>

You can use disabled directive to disable the link. You can use routerLinkActiveOptions to configure your link.

With this component, pagination works like a charm.

app-router-tab

This worked beautifully for me... Thank you very much Ben... 馃憤

Thanks @BenDevelopment

@BenDevelopment this solution worked beautifully for our situation. I slightly modified it and made it work for us, but the use of Directives to get the routerLinkActive attribute was genius. Didn't think of it that way.

@BenDevelopment really smart solution, thank you.

@BenDevelopment you're a star

@crisbeto how much work would you gauge this to be? It's been surfaced as an a11y issue in Google since you can lose some of the tabs with you zoom the browser in.

Because @angular/material can't have a dependency on @angular/router

this makes complete sense but is equally as brutal

Try this
<mat-tab-group>
   <mat-tab *ngFor="let x of [1,2,3,4,5,6,7]">
      <ng-template mat-tab-label>
         <a (click)="yourFn(x)">
            <span>{{x}}</span>
         </a>
      </ng-template>
   </mat-tab>
</mat-tab-group>

On style.scss
.mat-tab-body-wrapper {
   display: none;
}

Calling TabHeader's updatePagination() method manually is updating pagination arrows on Tabs. You can call it based on your scenario.
tabGroup._tabHeader.updatePagination()

https://github.com/angular/components/blob/03a9a39c49b75b50f689a38c7dcbf52bc007f657/src/material/tabs/tab-header.ts#L321-L325

A quick hack, works brilliantly:

.mat-tab-header-pagination {
  display: flex;
}

if your entire tabs are in a div with class .xyz then

.xyz {
  width: 100%;
}

(I'm going through the highest voted issues today an commenting on their status)

Definitely one of the issues we really should do, and AFAIK shouldn't be too large of an undertaking. It's one of those things that has consistently been _just_ lower priority than other work. I'll see if we can find someone to contribute something here in the near future since it was recently raised as an a11y issue.

Hey guys, here is a very simple solution to this issue.
And thumbs up @akopchinskiy ; it's very simple, and just want to add on top of what he offers:

template.component.html

Add your router outlet where you want to display your content

<mat-tab-group [selectedIndex]="0" (selectedTabChange)="navigate($event)">
    <mat-tab *ngFor="let tab of navPaths" [label]="tab.label"></mat-tab>
</mat-tab-group>

<router-outlet></router-outlet>

template.component.ts

This is where the big difference is
You may use this.router.navigateByUrl(tab.path) as proposed by @akopchinskiy
But keep in mind that if doing so, the provided URL must be absolute, as per documentation
So, I think using navigate() is much better since it provides you with more flexibility:
you can provide commands with options where you want your route to start, either absolute or relative, based on the activated route, like below

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { MatTabChangeEvent } from '@angular/material/tabs';

@Component({
  selector: 'template',
  templateUrl: './template.component.html',
  styleUrls: ['./template.component.css']
})
export class TemplateComponent implements OnInit {

navPaths = [
   {
        label: 'First Tab'
        path: 'path-one',
        idx: 0
    },
    {
        label: 'Second Tab'
        path: 'path-two',
        idx: 1
    },
    ...
}

constructor(private activatedRoute: ActivatedRoute) {}

ngOnInit() {}

navigate(event: MatTabChangeEvent) {
    const tabData = this.navPaths[event.index];    
   this.router.navigate([tabData.path], { relativeTo: this.activatedRoute });
}

Now, why is this working?
The key here is the (selectedTabChange)="navigate($event)" which captures every change event based on focus or tab selection (click event). Using this emitter, we capture the index of the selected tab (event.index), pass it to the data source and get the data of the selected tab (const tabData = this.navPaths[event.index]), and then grab the path (tabData.path) of the data to be displayed.

To display the current selected tab, we need to do two things:

  • first, set the selectedIndex input property to "0" ([selectedIndex]="0"), on the component
  • second, in the template-routing.module.ts file:
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from "@angular/router";
`

`
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule, Routes } from "@angular/router";

import { TemplateComponent } from "./template.component";

const routes: Routes = [
  {
    path: "",
    component: TemplateComponent,
    children: [
       // You must redirect your route, it's the key here
       { path: '', redirectTo: "path-one", pathMatch: 'full' },
       {
          path: "path-one",
          // This will depend on how you want to load your data: eagerly or lazily; I use lazy loading here
          loadChildren: "../location-of/path-one/path-one.module#PathOneModule"
       },
       ...
    ]
  }
];

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(routes)
  ],
  exports: [RouterModule]
})
export class TemplateRoutingModule { }

That's it! Please let me know how it goes.

By the way, if this is not a feature module, please use RouterModule.forRoot(routes) instead.

Tried your last solution. Works fine for me except updating the current selected tab. I fixed this issue in a slightly different way.

In the template use a numeric variable for the selected index:
<mat-tab-group [selectedIndex]="tabIndex" (selectedTabChange)="navigate($event)"> <mat-tab *ngFor="let tab of navPaths" [label]="tab.label"></mat-tab> </mat-tab-group>

In the controller do the following (snippets):
```tabIndex = 0;

constructor(private activatedRoute: ActivatedRoute, private router: Router) { }

ngOnInit() {
const currentUrl = this.router.url;
const tabData = this.navPaths.find(x => x.path === currentUrl);

if(tabData) {
  this.tabIndex = tabData.idx;
}

}

I saw this issue when it was only some months old. That was more than two years ago. In that time, i moved two times, had three girlfriends, changed my name, started drinking, stopped drinking, begun some personal projects, found work, and graduated twice. It's beautiful to see a life pass around a Github issue.

@elizabeth-dev Thank you for sharing. It also looks like you opened your first GitHub issue and PR during that time (2018) as well! If you are interested in making your first contribution to Angular, I would be happy to help. You may also be interested in joining a community for Angular developers who speak Spanish.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings