diff --git a/projects/igniteui-angular/src/lib/tabbar/routing-view-components.ts b/projects/igniteui-angular/src/lib/tabbar/routing-view-components.ts new file mode 100644 index 00000000000..48fc11a15b1 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tabbar/routing-view-components.ts @@ -0,0 +1,30 @@ +import { Component, NgModule } from '@angular/core'; + +@Component({ + template: `This is content in component # 1` +}) +export class RoutingView1Component { +} + +@Component({ + template: `This is content in component # 2` +}) +export class RoutingView2Component { +} + +@Component({ + template: `This is content in component # 3` +}) +export class RoutingView3Component { +} + +/** + * @hidden + */ +@NgModule({ + declarations: [RoutingView1Component, RoutingView2Component, RoutingView3Component], + exports: [RoutingView1Component, RoutingView2Component, RoutingView3Component], + // imports: [CommonModule, IgxBadgeModule, IgxIconModule] +}) +export class RoutingViewComponentsModule { +} diff --git a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts index c3292b9bfe4..091564d34b5 100644 --- a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.spec.ts @@ -1,5 +1,8 @@ -import { AfterContentChecked, AfterViewChecked, Component, ContentChildren, QueryList, ViewChild } from '@angular/core'; -import { async, TestBed } from '@angular/core/testing'; +import { AfterContentChecked, AfterViewChecked, Component, ContentChildren, QueryList, ViewChild, NgZone } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; +import { async, inject, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { IgxBottomNavComponent, IgxBottomNavModule, @@ -8,15 +11,27 @@ import { IgxBottomNavComponent, IgxTabTemplateDirective } from './tabbar.component'; import { configureTestSuite } from '../test-utils/configure-suite'; +import { RoutingViewComponentsModule, + RoutingView1Component, + RoutingView2Component, + RoutingView3Component } from './routing-view-components'; + describe('TabBar', () => { configureTestSuite(); beforeEach(async(() => { + + const testRoutes = [ + { path: 'view1', component: RoutingView1Component }, + { path: 'view2', component: RoutingView2Component }, + { path: 'view3', component: RoutingView3Component } + ]; + TestBed.configureTestingModule({ - declarations: [TabBarTestComponent, BottomTabBarTestComponent, TemplatedTabBarTestComponent], - imports: [IgxBottomNavModule] + declarations: [TabBarTestComponent, BottomTabBarTestComponent, TemplatedTabBarTestComponent, TabBarRoutingTestComponent], + imports: [IgxBottomNavModule, RoutingViewComponentsModule, RouterTestingModule.withRoutes(testRoutes)], }) - .compileComponents(); + .compileComponents(); })); it('should initialize igx-bottom-nav, igx-tab-panel and igx-tab', () => { @@ -148,6 +163,74 @@ describe('TabBar', () => { tabbar.tabs.forEach((tab) => expect(tab.relatedPanel.customTabTemplate).toBeDefined()); }); + + it('should navigate to the correct URL when clicking on tab buttons', fakeAsync(() => { + + const router = TestBed.get(Router); + const location = TestBed.get(Location); + const fixture = TestBed.createComponent(TabBarRoutingTestComponent); + const bottomNav = fixture.componentInstance.bottomNavComp; + fixture.detectChanges(); + + fixture.ngZone.run(() => { router.initialNavigation(); }); + + tick(); + expect(location.path()).toBe('/view1'); + + fixture.ngZone.run(() => { bottomNav.tabs.toArray()[2].select(); }); + tick(); + expect(location.path()).toBe('/view3'); + + fixture.ngZone.run(() => { bottomNav.tabs.toArray()[1].select(); }); + tick(); + expect(location.path()).toBe('/view2'); + + fixture.ngZone.run(() => { bottomNav.tabs.toArray()[0].select(); }); + tick(); + expect(location.path()).toBe('/view1'); + })); + + it('should select the correct tab button/panel when navigating an URL', fakeAsync(() => { + + const router = TestBed.get(Router); + const location = TestBed.get(Location); + const fixture = TestBed.createComponent(TabBarRoutingTestComponent); + const bottomNav = fixture.componentInstance.bottomNavComp; + fixture.detectChanges(); + + fixture.ngZone.run(() => { router.initialNavigation(); }); + tick(); + expect(location.path()).toBe('/view1'); + expect(bottomNav.selectedIndex).toBe(0); + expect(bottomNav.panels.toArray()[0].isSelected).toBe(true); + expect(bottomNav.tabs.toArray()[0].isSelected).toBe(true); + + + fixture.ngZone.run(() => { router.navigate(['/view3']); }); + tick(); + expect(location.path()).toBe('/view3'); + fixture.detectChanges(); + expect(bottomNav.selectedIndex).toBe(2); + expect(bottomNav.panels.toArray()[2].isSelected).toBe(true); + expect(bottomNav.tabs.toArray()[2].isSelected).toBe(true); + + fixture.ngZone.run(() => { router.navigate(['/view2']); }); + tick(); + expect(location.path()).toBe('/view2'); + fixture.detectChanges(); + expect(bottomNav.selectedIndex).toBe(1); + expect(bottomNav.panels.toArray()[1].isSelected).toBe(true); + expect(bottomNav.tabs.toArray()[1].isSelected).toBe(true); + + fixture.ngZone.run(() => { router.navigate(['/view1']); }); + tick(); + expect(location.path()).toBe('/view1'); + fixture.detectChanges(); + expect(bottomNav.selectedIndex).toBe(0); + expect(bottomNav.panels.toArray()[0].isSelected).toBe(true); + expect(bottomNav.tabs.toArray()[0].isSelected).toBe(true); + + })); }); @Component({ @@ -255,3 +338,28 @@ class TemplatedTabBarTestComponent { @ViewChild(IgxBottomNavComponent) public tabbar: IgxBottomNavComponent; @ViewChild('wrapperDiv') public wrapperDiv: any; } + +@Component({ + template: ` +
+
+ +
+ + + Content in tab # 1 + + + Content in tab # 2 + + + Content in tab # 3 + + +
+ ` +}) +class TabBarRoutingTestComponent { + @ViewChild(IgxBottomNavComponent) + public bottomNavComp: IgxBottomNavComponent; +} diff --git a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts index 482b8379a42..008287affdb 100644 --- a/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts +++ b/projects/igniteui-angular/src/lib/tabbar/tabbar.component.ts @@ -14,14 +14,17 @@ import { HostListener, Input, NgModule, + OnDestroy, Output, QueryList, TemplateRef, - ViewChild, ViewChildren } from '@angular/core'; import { IgxBadgeModule } from '../badge/badge.component'; import { IgxIconModule } from '../icon/index'; +import { NavigationEnd, Router, RouterLink } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; export interface ISelectTabEventArgs { tab: IgxTabComponent; @@ -38,6 +41,7 @@ export class IgxTabTemplateDirective { constructor(public template: TemplateRef) { } } + /** * **Ignite UI for Angular Tab Bar** - * [Documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/tabbar.html) @@ -62,7 +66,8 @@ export class IgxTabTemplateDirective { } `] }) -export class IgxBottomNavComponent implements AfterViewInit { +export class IgxBottomNavComponent implements AfterViewInit, OnDestroy { + /** * Gets the `IgxTabComponent` elements in the tab bar component. * ```typescript @@ -71,6 +76,7 @@ export class IgxBottomNavComponent implements AfterViewInit { * @memberof IgxBottomNavComponent */ @ViewChildren(forwardRef(() => IgxTabComponent)) public tabs: QueryList; + /** * Gets the `IgxTabPanelComponent` elements in the tab bar component. * ```typescript @@ -94,6 +100,7 @@ export class IgxBottomNavComponent implements AfterViewInit { @HostBinding('attr.id') @Input() public id = `igx-bottom-nav-${NEXT_ID++}`; + /** * Emits an event when a new tab is selected. * Provides references to the `IgxTabComponent` and `IgxTabPanelComponent` as event arguments. @@ -103,6 +110,7 @@ export class IgxBottomNavComponent implements AfterViewInit { * @memberof IgxBottomNavComponent */ @Output() public onTabSelected = new EventEmitter(); + /** * Emits an event when a tab is deselected. * Provides references to the `IgxTabComponent` and `IgxTabPanelComponent` as event arguments. @@ -112,6 +120,7 @@ export class IgxBottomNavComponent implements AfterViewInit { * @memberof IgxBottomNavComponent */ @Output() public onTabDeselected = new EventEmitter(); + /** * Gets the `index` of selected tab/panel in the respective collection. * ```typescript @@ -120,6 +129,7 @@ export class IgxBottomNavComponent implements AfterViewInit { * @memberof IgxBottomNavComponent */ public selectedIndex = -1; + /** * Gets the `itemStyle` of the tab bar. * ```typescript @@ -130,10 +140,12 @@ export class IgxBottomNavComponent implements AfterViewInit { public get itemStyle(): string { return this._itemStyle; } + /** *@hidden */ private _itemStyle = 'igx-bottom-nav'; + /** * Gets the selected tab in the tab bar. * ```typescript @@ -147,24 +159,59 @@ export class IgxBottomNavComponent implements AfterViewInit { } } - constructor(private _element: ElementRef) { + /** + * @hidden + */ + private _navigationEndSubscription: Subscription; + + constructor(private router: Router) { } + /** *@hidden */ public ngAfterViewInit() { // initial selection setTimeout(() => { + this.navigationEndHandler(this); if (this.selectedIndex === -1) { const selectablePanels = this.panels.filter((p) => !p.disabled); const panel = selectablePanels[0]; - if (panel) { panel.select(); } } + this._navigationEndSubscription = this.router.events.pipe( + filter(event => event instanceof NavigationEnd)).subscribe(() => { + this.navigationEndHandler(this); + } + ); }, 0); } + + /** + *@hidden + */ + public navigationEndHandler(bottomNavComponent: IgxBottomNavComponent) { + const panelsArray = bottomNavComponent.panels.toArray(); + for (let i = 0; i < panelsArray.length; i++) { + if (panelsArray[i].routerLinkDirective && + bottomNavComponent.router.url.startsWith(panelsArray[i].routerLinkDirective.urlTree.toString())) { + panelsArray[i]._selectAndEmitEvent(); + break; + } + } + } + + /** + *@hidden + */ + public ngOnDestroy() { + if (this._navigationEndSubscription) { + this._navigationEndSubscription.unsubscribe(); + } + } + /** *@hidden */ @@ -178,6 +225,7 @@ export class IgxBottomNavComponent implements AfterViewInit { } }); } + /** *@hidden */ @@ -204,6 +252,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked *@hidden */ private _itemStyle = 'igx-tab-panel'; + /** * Sets/gets the `label` of the tab panel. * ```html @@ -215,6 +264,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked * @memberof IgxTabPanelComponent */ @Input() public label: string; + /** * Sets/gets the `icon` of the tab panel. * ```html @@ -226,6 +276,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked * @memberof IgxTabPanelComponent */ @Input() public icon: string; + /** * Sets/gets whether the tab panel is disabled. * ```html @@ -237,6 +288,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked * @memberof IgxTabPanelComponent */ @Input() public disabled: boolean; + /** * Gets the role of the tab panel. * ```typescript @@ -245,6 +297,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked * @memberof IgxTabPanelComponent */ @HostBinding('attr.role') public role = 'tabpanel'; + /** * Gets whether a tab panel will have `igx-bottom-nav__panel` class. * ```typescript @@ -256,6 +309,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked get styleClass(): boolean { return (!this.isSelected); } + /** * Sets/gets whether a tab panel is selected. * ```typescript @@ -268,6 +322,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked */ @HostBinding('class.igx-bottom-nav__panel--selected') public isSelected = false; + /** * Gets the `itemStyle` of the tab panel. * ```typescript @@ -278,6 +333,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked public get itemStyle(): string { return this._itemStyle; } + /** * Gets the tab associated with the panel. * ```typescript @@ -290,6 +346,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked return this._tabBar.tabs.toArray()[this.index]; } } + /** * Gets the index of a panel in the panels collection. * ```typescript @@ -302,6 +359,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked return this._tabBar.panels.toArray().indexOf(this); } } + /** * Gets the tab template. * ```typescript @@ -312,6 +370,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked get customTabTemplate(): TemplateRef { return this._tabTemplate; } + /** * Sets the tab template. * ```typescript @@ -322,18 +381,27 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked set customTabTemplate(template: TemplateRef) { this._tabTemplate = template; } + /** *@hidden */ private _tabTemplate: TemplateRef; + /** *@hidden */ @ContentChild(IgxTabTemplateDirective, { read: IgxTabTemplateDirective }) protected tabTemplate: IgxTabTemplateDirective; + /** + *@hidden + */ + @ContentChild(RouterLink) + public routerLinkDirective: RouterLink; + constructor(private _tabBar: IgxBottomNavComponent, private _element: ElementRef) { } + /** *@hidden */ @@ -342,6 +410,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked this._tabTemplate = this.tabTemplate.template; } } + /** *@hidden */ @@ -349,6 +418,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked this._element.nativeElement.setAttribute('aria-labelledby', `igx-tab-${this.index}`); this._element.nativeElement.setAttribute('id', `igx-bottom-nav__panel-${this.index}`); } + /** * Selects the current tab and the tab panel. * ```typescript @@ -360,7 +430,17 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked if (this.disabled || this._tabBar.selectedIndex === this.index) { return; } + if (this.routerLinkDirective) { + this.routerLinkDirective.onClick(); + } else { + this._selectAndEmitEvent(); + } + } + /** + *@hidden + */ + public _selectAndEmitEvent() { this.isSelected = true; this._tabBar.onTabSelected.emit({ tab: this._tabBar.tabs.toArray()[this.index], panel: this }); } @@ -374,6 +454,7 @@ export class IgxTabPanelComponent implements AfterContentInit, AfterViewChecked }) export class IgxTabComponent { + /** * Gets the `role` attribute. * ```typescript @@ -382,6 +463,7 @@ export class IgxTabComponent { * @memberof IgxTabComponent */ @HostBinding('attr.role') public role = 'tab'; + /** * Gets the panel associated with the tab. * ```typescript @@ -390,10 +472,12 @@ export class IgxTabComponent { * @memberof IgxTabComponent */ @Input() public relatedPanel: IgxTabPanelComponent; + /** *@hidden */ private _changesCount = 0; // changes and updates accordingly applied to the tab. + /** * Gets the changes and updates accordingly applied to the tab. * @@ -402,6 +486,7 @@ export class IgxTabComponent { get changesCount(): number { return this._changesCount; } + /** * Gets whether the tab is disabled. * ```typescript @@ -416,6 +501,7 @@ export class IgxTabComponent { return panel.disabled; } } + /** * Gets whether the tab is selected. * ```typescript @@ -430,6 +516,7 @@ export class IgxTabComponent { return panel.isSelected; } } + /** * Gets the `index` of the tab. * ```typescript @@ -443,6 +530,7 @@ export class IgxTabComponent { constructor(private _tabBar: IgxBottomNavComponent, private _element: ElementRef) { } + /** * Selects the current tab and the associated panel. * ```typescript diff --git a/projects/igniteui-angular/src/lib/tabs/routing-view-components.ts b/projects/igniteui-angular/src/lib/tabs/routing-view-components.ts new file mode 100644 index 00000000000..48fc11a15b1 --- /dev/null +++ b/projects/igniteui-angular/src/lib/tabs/routing-view-components.ts @@ -0,0 +1,30 @@ +import { Component, NgModule } from '@angular/core'; + +@Component({ + template: `This is content in component # 1` +}) +export class RoutingView1Component { +} + +@Component({ + template: `This is content in component # 2` +}) +export class RoutingView2Component { +} + +@Component({ + template: `This is content in component # 3` +}) +export class RoutingView3Component { +} + +/** + * @hidden + */ +@NgModule({ + declarations: [RoutingView1Component, RoutingView2Component, RoutingView3Component], + exports: [RoutingView1Component, RoutingView2Component, RoutingView3Component], + // imports: [CommonModule, IgxBadgeModule, IgxIconModule] +}) +export class RoutingViewComponentsModule { +} diff --git a/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts b/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts index 2134dc79e81..23ba3d7b7c2 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs-group.component.ts @@ -13,6 +13,7 @@ import { import { IgxTabItemComponent } from './tab-item.component'; import { IgxTabItemTemplateDirective } from './tabs.directives'; import { IgxTabsBase, IgxTabsGroupBase } from './tabs.common'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'igx-tabs-group', @@ -59,6 +60,12 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit private _tabTemplate: TemplateRef; + /** + * @hidden + */ + @ContentChild(RouterLink) + public routerLinkDirective: RouterLink; + constructor(private _tabs: IgxTabsBase, private _element: ElementRef) { } @@ -166,7 +173,17 @@ export class IgxTabsGroupComponent implements IgxTabsGroupBase, AfterContentInit if (this.disabled || this.isSelected) { return; } + if (this.routerLinkDirective) { + this.routerLinkDirective.onClick(); + } else { + this._selectAndEmitEvent(focusDelay); + } + } + /** + * @hidden + */ + public _selectAndEmitEvent(focusDelay = 200) { this.isSelected = true; this.relatedTab.tabindex = 0; diff --git a/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts b/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts index 1895739e5a6..9551956852d 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs.component.spec.ts @@ -1,5 +1,8 @@ import { Component, QueryList, ViewChild } from '@angular/core'; import { async, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; import { IgxTabItemComponent } from './tab-item.component'; import { IgxTabsGroupComponent } from './tabs-group.component'; import { IgxTabsComponent, IgxTabsModule } from './tabs.component'; @@ -11,16 +14,28 @@ import { IgxToggleModule } from '../directives/toggle/toggle.directive'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { UIInteractions } from '../test-utils/ui-interactions.spec'; +import { RoutingViewComponentsModule, + RoutingView1Component, + RoutingView2Component, + RoutingView3Component } from './routing-view-components'; describe('IgxTabs', () => { configureTestSuite(); beforeEach(async(() => { + + const testRoutes = [ + { path: 'view1', component: RoutingView1Component }, + { path: 'view2', component: RoutingView2Component }, + { path: 'view3', component: RoutingView3Component } + ]; + TestBed.configureTestingModule({ declarations: [TabsTestComponent, TabsTest2Component, TemplatedTabsTestComponent, - TabsTestSelectedTabComponent, TabsTestCustomStylesComponent, TabsTestBug4420Component], - imports: [IgxTabsModule, IgxButtonModule, IgxDropDownModule, IgxToggleModule, BrowserAnimationsModule] + TabsTestSelectedTabComponent, TabsTestCustomStylesComponent, TabsTestBug4420Component, TabsRoutingTestComponent], + imports: [IgxTabsModule, IgxButtonModule, IgxDropDownModule, IgxToggleModule, + RoutingViewComponentsModule, BrowserAnimationsModule, RouterTestingModule.withRoutes(testRoutes)] }) - .compileComponents(); + .compileComponents(); })); it('should initialize igx-tabs, igx-tabs-group and igx-tab-item', fakeAsync(() => { @@ -397,6 +412,69 @@ describe('IgxTabs', () => { const indicator = dom.query(By.css('.igx-tabs__header-menu-item-indicator')); expect(indicator.nativeElement.style.width).toBe('90px'); })); + + it('should navigate to the correct URL when clicking on tab buttons', fakeAsync(() => { + const router = TestBed.get(Router); + const location = TestBed.get(Location); + const fixture = TestBed.createComponent(TabsRoutingTestComponent); + const tabsComp = fixture.componentInstance.tabsComp; + fixture.detectChanges(); + + fixture.ngZone.run(() => { router.initialNavigation(); }); + tick(200); + expect(location.path()).toBe('/view1'); + + fixture.ngZone.run(() => { tabsComp.tabs.toArray()[2].select(); }); + tick(200); + expect(location.path()).toBe('/view3'); + + fixture.ngZone.run(() => { tabsComp.tabs.toArray()[1].select(); }); + tick(200); + expect(location.path()).toBe('/view2'); + + fixture.ngZone.run(() => { tabsComp.tabs.toArray()[0].select(); }); + tick(200); + expect(location.path()).toBe('/view1'); + })); + + it('should select the correct tab button/panel when navigating an URL', fakeAsync(() => { + const router = TestBed.get(Router); + const location = TestBed.get(Location); + const fixture = TestBed.createComponent(TabsRoutingTestComponent); + const tabsComp = fixture.componentInstance.tabsComp; + fixture.detectChanges(); + + fixture.ngZone.run(() => { router.initialNavigation(); }); + tick(300); + expect(location.path()).toBe('/view1'); + expect(tabsComp.selectedIndex).toBe(0); + expect(tabsComp.groups.toArray()[0].isSelected).toBe(true); + expect(tabsComp.tabs.toArray()[0].isSelected).toBe(true); + + fixture.ngZone.run(() => { router.navigate(['/view3']); }); + tick(300); + expect(location.path()).toBe('/view3'); + fixture.detectChanges(); + expect(tabsComp.selectedIndex).toBe(2); + expect(tabsComp.groups.toArray()[2].isSelected).toBe(true); + expect(tabsComp.tabs.toArray()[2].isSelected).toBe(true); + + fixture.ngZone.run(() => { router.navigate(['/view2']); }); + tick(300); + expect(location.path()).toBe('/view2'); + fixture.detectChanges(); + expect(tabsComp.selectedIndex).toBe(1); + expect(tabsComp.groups.toArray()[1].isSelected).toBe(true); + expect(tabsComp.tabs.toArray()[1].isSelected).toBe(true); + + fixture.ngZone.run(() => { router.navigate(['/view1']); }); + tick(300); + expect(location.path()).toBe('/view1'); + fixture.detectChanges(); + expect(tabsComp.selectedIndex).toBe(0); + expect(tabsComp.groups.toArray()[0].isSelected).toBe(true); + expect(tabsComp.tabs.toArray()[0].isSelected).toBe(true); + })); }); @Component({ @@ -584,3 +662,28 @@ class TabsTestCustomStylesComponent { class TabsTestBug4420Component { @ViewChild(IgxTabsComponent) public tabs: IgxTabsComponent; } + +@Component({ + template: ` +
+ + + Content in tab # 1 + + + Content in tab # 2 + + + Content in tab # 3 + + +
+ +
+
+ ` +}) +class TabsRoutingTestComponent { + @ViewChild(IgxTabsComponent) + public tabsComp: IgxTabsComponent; +} diff --git a/projects/igniteui-angular/src/lib/tabs/tabs.component.ts b/projects/igniteui-angular/src/lib/tabs/tabs.component.ts index 29ac44c7ee8..dc207f54324 100644 --- a/projects/igniteui-angular/src/lib/tabs/tabs.component.ts +++ b/projects/igniteui-angular/src/lib/tabs/tabs.component.ts @@ -24,6 +24,8 @@ import { IgxTabItemComponent } from './tab-item.component'; import { IgxTabsGroupComponent } from './tabs-group.component'; import { IgxLeftButtonStyleDirective, IgxRightButtonStyleDirective, IgxTabItemTemplateDirective } from './tabs.directives'; import { IgxTabsBase } from './tabs.common'; +import { NavigationEnd, Router, RouterLink } from '@angular/router'; +import { filter } from 'rxjs/operators'; export enum TabsType { FIXED = 'fixed', @@ -176,6 +178,10 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { */ public offset = 0; + /** + * @hidden + */ + private _navigationEndSubscription: Subscription; private _groupChanges$: Subscription; private _selectedIndex = 0; @@ -266,20 +272,40 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { } } - constructor(private _element: ElementRef) { + constructor(private router: Router) { } /** * @hidden */ public ngAfterViewInit() { + // initially do not navigate to the route set on the tabs-groups to keep the url set by the user + this.navigationEndHandler(this); requestAnimationFrame(() => { this.setSelectedGroup(); }); - this._groupChanges$ = this.groups.changes.subscribe(() => { this.resetSelectionOnCollectionChanged(); }); + this._navigationEndSubscription = this.router.events.pipe( + filter(event => event instanceof NavigationEnd)).subscribe(() => { + this.navigationEndHandler(this); + } + ); + } + + /** + *@hidden + */ + public navigationEndHandler(tabsComponent: IgxTabsComponent) { + const groupsArray = tabsComponent.groups.toArray(); + for (let i = 0; i < groupsArray.length; i++) { + if (groupsArray[i].routerLinkDirective && + tabsComponent.router.url.startsWith(groupsArray[i].routerLinkDirective.urlTree.toString())) { + groupsArray[i]._selectAndEmitEvent(); + break; + } + } } /** @@ -289,6 +315,9 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { if (this._groupChanges$) { this._groupChanges$.unsubscribe(); } + if (this._navigationEndSubscription) { + this._navigationEndSubscription.unsubscribe(); + } } private setSelectedGroup(): void { @@ -316,7 +345,7 @@ export class IgxTabsComponent implements IgxTabsBase, AfterViewInit, OnDestroy { }, 0); } - private selectGroupByIndex(selectedIndex: number): void { + private selectGroupByIndex(selectedIndex: number, navigateToRoute = true): void { const selectableGroups = this.groups.filter((selectableGroup) => !selectableGroup.disabled); const group = selectableGroups[selectedIndex]; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 55e15986928..7881921b4fe 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -53,6 +53,11 @@ export class AppComponent implements OnInit { icon: 'tab', name: 'Bottom Navigation' }, + { + link: '/bottom-navigation-routing', + icon: 'tab', + name: 'Bottom Navigation Routing' + }, { link: '/buttonGroup', icon: 'group_work', @@ -278,6 +283,11 @@ export class AppComponent implements OnInit { icon: 'tab', name: 'Tabs' }, + { + link: '/tabs-routing', + icon: 'tab', + name: 'Tabs Routing' + }, { link: '/timePicker', icon: 'date_range', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b5ed7ec0142..8fe50a96b3a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -89,8 +89,11 @@ import { GridSearchComponent } from './grid-search/grid-search.sample'; import { AutocompleteSampleComponent, AutocompletePipeContains, AutocompleteGroupPipeContains } from './autocomplete/autocomplete.sample'; import { TreeGridLoadOnDemandSampleComponent } from './tree-grid-load-on-demand/tree-grid-load-on-demand.sample'; import { GridFilterTemplateSampleComponent } from './grid-filter-template/grid-filter-template.sample'; - - +import { BottomNavRoutingSampleComponent } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView1Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView2Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView3Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { TabsRoutingSampleComponent } from './tabs-routing/tabs-routing.sample'; const components = [ AppComponent, @@ -131,7 +134,12 @@ const components = [ SliderSampleComponent, SnackbarSampleComponent, BottomNavSampleComponent, + BottomNavRoutingSampleComponent, + RoutingView1Component, + RoutingView2Component, + RoutingView3Component, TabsSampleComponent, + TabsRoutingSampleComponent, TimePickerSampleComponent, ToastSampleComponent, VirtualForSampleComponent, @@ -201,6 +209,11 @@ const components = [ IgxOverlayService, { provide: DisplayDensityToken, useFactory: () => ({ displayDensity: DisplayDensity.comfortable }) } ], - bootstrap: [AppComponent] + bootstrap: [AppComponent], + entryComponents: [ + RoutingView1Component, + RoutingView2Component, + RoutingView3Component, + ] }) export class AppModule { } diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 076a81e330f..33ce886a578 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -25,6 +25,7 @@ import { ColorsSampleComponent } from './styleguide/colors/color.sample'; import { ShadowsSampleComponent } from './styleguide/shadows/shadows.sample'; import { TypographySampleComponent } from './styleguide/typography/typography.sample'; import { BottomNavSampleComponent, CustomContentComponent } from './bottomnav/bottomnav.sample'; +import { BottomNavRoutingSampleComponent } from './bottomnav-routing/bottomnav-routing.sample'; import { TabsSampleComponent } from './tabs/tabs.sample'; import { TimePickerSampleComponent } from './time-picker/time-picker.sample'; import { ToastSampleComponent } from './toast/toast.sample'; @@ -53,6 +54,10 @@ import { CalendarViewsSampleComponent } from './calendar-views/calendar-views.sa import { AutocompleteSampleComponent } from './autocomplete/autocomplete.sample'; import { SelectSampleComponent } from './select/select.sample'; import { TreeGridLoadOnDemandSampleComponent } from './tree-grid-load-on-demand/tree-grid-load-on-demand.sample'; +import { RoutingView1Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView2Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView3Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { TabsRoutingSampleComponent } from './tabs-routing/tabs-routing.sample'; const appRoutes = [ { @@ -185,10 +190,28 @@ const appRoutes = [ path: 'bottom-navigation', component: BottomNavSampleComponent }, + { + path: 'bottom-navigation-routing', + component: BottomNavRoutingSampleComponent, + children: [ + { path: 'view1', component: RoutingView1Component }, + { path: 'view2', component: RoutingView2Component }, + { path: 'view3', component: RoutingView3Component } + ] + }, { path: 'tabs', component: TabsSampleComponent }, + { + path: 'tabs-routing', + component: TabsRoutingSampleComponent, + children: [ + { path: 'view1', component: RoutingView1Component }, + { path: 'view2', component: RoutingView2Component }, + { path: 'view3', component: RoutingView3Component } + ] + }, { path: 'timePicker', component: TimePickerSampleComponent diff --git a/src/app/bottomnav-routing/bottomnav-routing.sample.css b/src/app/bottomnav-routing/bottomnav-routing.sample.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/bottomnav-routing/bottomnav-routing.sample.html b/src/app/bottomnav-routing/bottomnav-routing.sample.html new file mode 100644 index 00000000000..eb04c30ce7d --- /dev/null +++ b/src/app/bottomnav-routing/bottomnav-routing.sample.html @@ -0,0 +1,29 @@ +
+ + Allows the user to navigate between different content displayed in one view. Supports routing. + +
+
+
+ Programmatically change URL:
+
+
+
+
+ +
+ + + Content in tab # 1 + + + Content in tab # 2 + + + Content in tab # 3 + + +
+
+
+
diff --git a/src/app/bottomnav-routing/bottomnav-routing.sample.ts b/src/app/bottomnav-routing/bottomnav-routing.sample.ts new file mode 100644 index 00000000000..2f988630104 --- /dev/null +++ b/src/app/bottomnav-routing/bottomnav-routing.sample.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +@Component({ + selector: 'app-bottomnav-routing-sample', + styleUrls: ['bottomnav-routing.sample.css'], + templateUrl: 'bottomnav-routing.sample.html' +}) +export class BottomNavRoutingSampleComponent { + + constructor(private router: Router) { + } + + public navigateUrl1() { + this.router.navigateByUrl('/bottom-navigation-routing/view1'); + } + + public navigateUrl2() { + this.router.navigateByUrl('/bottom-navigation-routing/view2'); + } + + public navigateUrl3() { + this.router.navigateByUrl('/bottom-navigation-routing/view3'); + } +} + +@Component({ + selector: 'app-bottomnav-routing-view1-sample', + template: `This is content in component # 1` +}) +export class RoutingView1Component { +} + +@Component({ + selector: 'app-bottomnav-routing-view2-sample', + template: `This is content in component # 2` +}) +export class RoutingView2Component { +} + +@Component({ + selector: 'app-bottomnav-routing-view3-sample', + template: `This is content in component # 3` +}) +export class RoutingView3Component { +} diff --git a/src/app/routing.ts b/src/app/routing.ts index fb168bddaa2..b479f8f385a 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -28,6 +28,7 @@ import { ColorsSampleComponent } from './styleguide/colors/color.sample'; import { ShadowsSampleComponent } from './styleguide/shadows/shadows.sample'; import { TypographySampleComponent } from './styleguide/typography/typography.sample'; import { BottomNavSampleComponent, CustomContentComponent } from './bottomnav/bottomnav.sample'; +import { BottomNavRoutingSampleComponent } from './bottomnav-routing/bottomnav-routing.sample'; import { TabsSampleComponent } from './tabs/tabs.sample'; import { TimePickerSampleComponent } from './time-picker/time-picker.sample'; import { ToastSampleComponent } from './toast/toast.sample'; @@ -67,6 +68,10 @@ import { GridSearchComponent } from './grid-search/grid-search.sample'; import { AutocompleteSampleComponent } from './autocomplete/autocomplete.sample'; import { TreeGridLoadOnDemandSampleComponent } from './tree-grid-load-on-demand/tree-grid-load-on-demand.sample'; import { GridFilterTemplateSampleComponent } from './grid-filter-template/grid-filter-template.sample'; +import { RoutingView1Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView2Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { RoutingView3Component } from './bottomnav-routing/bottomnav-routing.sample'; +import { TabsRoutingSampleComponent } from './tabs-routing/tabs-routing.sample'; const appRoutes = [ { @@ -231,10 +236,28 @@ const appRoutes = [ path: 'bottom-navigation', component: BottomNavSampleComponent }, + { + path: 'bottom-navigation-routing', + component: BottomNavRoutingSampleComponent, + children: [ + { path: 'view1', component: RoutingView1Component }, + { path: 'view2', component: RoutingView2Component }, + { path: 'view3', component: RoutingView3Component } + ] + }, { path: 'tabs', component: TabsSampleComponent }, + { + path: 'tabs-routing', + component: TabsRoutingSampleComponent, + children: [ + { path: 'view1', component: RoutingView1Component }, + { path: 'view2', component: RoutingView2Component }, + { path: 'view3', component: RoutingView3Component } + ] + }, { path: 'timePicker', component: TimePickerSampleComponent diff --git a/src/app/tabs-routing/tabs-routing.sample.css b/src/app/tabs-routing/tabs-routing.sample.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/tabs-routing/tabs-routing.sample.html b/src/app/tabs-routing/tabs-routing.sample.html new file mode 100644 index 00000000000..e1e398da707 --- /dev/null +++ b/src/app/tabs-routing/tabs-routing.sample.html @@ -0,0 +1,32 @@ +
+ + Allows the user to navigate between different content displayed in one view. The tabs are placed at the top and + allows scrolling. Supports routing. + + +
+
+ + + + Content in tab # 1 + + + + Content in tab # 2 + + + + Content in tab # 3 + + +
+ +
+ Programmatically change URL:
+
+
+ +
+
+
diff --git a/src/app/tabs-routing/tabs-routing.sample.ts b/src/app/tabs-routing/tabs-routing.sample.ts new file mode 100644 index 00000000000..b2784502218 --- /dev/null +++ b/src/app/tabs-routing/tabs-routing.sample.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +@Component({ + selector: 'app-tabs-routing-sample', + styleUrls: ['tabs-routing.sample.css'], + templateUrl: 'tabs-routing.sample.html' +}) +export class TabsRoutingSampleComponent { + + constructor(private router: Router) { + } + + public navigateUrl1() { + this.router.navigateByUrl('/tabs-routing/view1'); + } + + public navigateUrl2() { + this.router.navigateByUrl('/tabs-routing/view2'); + } + + public navigateUrl3() { + this.router.navigateByUrl('/tabs-routing/view3'); + } + +}