Skip to content

Commit

Permalink
fix(tabs): preserve route navigation extras when changing tabs (#18493)
Browse files Browse the repository at this point in the history
fixes #18717
  • Loading branch information
jmannau authored and liamdebeasi committed Dec 11, 2019
1 parent b3b3312 commit 4c8f32f
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 13 deletions.
18 changes: 17 additions & 1 deletion angular/src/directives/navigation/ion-router-outlet.ts
Expand Up @@ -242,6 +242,22 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
return active ? active.url : undefined;
}

/**
* Returns the RouteView of the active page of each stack.
* @internal
*/
getLastRouteView(stackId?: string): RouteView | undefined {
return this.stackCtrl.getLastUrl(stackId);
}

/**
* Returns the root view in the tab stack.
* @internal
*/
getRootView(stackId?: string): RouteView | undefined {
return this.stackCtrl.getRootUrl(stackId);
}

/**
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
*/
Expand Down Expand Up @@ -315,7 +331,7 @@ class OutletInjector implements Injector {
private route: ActivatedRoute,
private childContexts: ChildrenOutletContexts,
private parent: Injector
) {}
) { }

get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
Expand Down
51 changes: 43 additions & 8 deletions angular/src/directives/navigation/ion-tabs.ts
Expand Up @@ -66,18 +66,53 @@ export class IonTabs {
}
}

/**
* When a tab button is clicked, there are several scenarios:
* 1. If the selected tab is currently active (the tab button has been clicked
* again), then it should go to the root view for that tab.
*
* a. Get the saved root view from the router outlet. If the saved root view
* matches the tabRootUrl, set the route view to this view including the
* navigation extras.
* b. If the saved root view from the router outlet does
* not match, navigate to the tabRootUrl. No navigation extras are
* included.
*
* 2. If the current tab tab is not currently selected, get the last route
* view from the router outlet.
*
* a. If the last route view exists, navigate to that view including any
* navigation extras
* b. If the last route view doesn't exist, then navigate
* to the default tabRootUrl
*/
@HostListener('ionTabButtonClick', ['$event.detail.tab'])
select(tab: string) {
const alreadySelected = this.outlet.getActiveStackId() === tab;
const href = `${this.outlet.tabsPrefix}/${tab}`;
const url = alreadySelected
? href
: this.outlet.getLastUrl(tab) || href;
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
if (alreadySelected) {
const rootView = this.outlet.getRootView(tab);
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
return this.navCtrl.navigateRoot(tabRootUrl, {
...(navigationExtras),
animated: true,
animationDirection: 'back',
});
} else {
const lastRoute = this.outlet.getLastRouteView(tab);
/**
* If there is a lastRoute, goto that, otherwise goto the fallback url of the
* selected tab
*/
const url = lastRoute && lastRoute.url || tabRootUrl;
const navigationExtras = lastRoute && lastRoute.savedExtras;

return this.navCtrl.navigateRoot(url, {
animated: true,
animationDirection: 'back'
});
return this.navCtrl.navigateRoot(url, {
...(navigationExtras),
animated: true,
animationDirection: 'back',
});
}
}

getSelected(): string | undefined {
Expand Down
10 changes: 9 additions & 1 deletion angular/src/directives/navigation/stack-controller.ts
Expand Up @@ -70,7 +70,7 @@ export class StackController {
if (router.getCurrentNavigation) {
currentNavigation = router.getCurrentNavigation();

// Angular < 7.2.0
// Angular < 7.2.0
} else if (
router.navigations &&
router.navigations.value
Expand Down Expand Up @@ -191,6 +191,14 @@ export class StackController {
return views.length > 0 ? views[views.length - 1] : undefined;
}

/**
* @internal
*/
getRootUrl(stackId?: string) {
const views = this.getStack(stackId);
return views.length > 0 ? views[0] : undefined;
}

getActiveStackId(): string | undefined {
return this.activeView ? this.activeView.stackId : undefined;
}
Expand Down
106 changes: 103 additions & 3 deletions angular/test/test-app/e2e/src/tabs.e2e-spec.ts
@@ -1,5 +1,5 @@
import { browser, element, by, ElementFinder } from 'protractor';
import { waitTime, testStack, handleErrorMessages } from './utils';
import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor';
import { handleErrorMessages, testStack, waitTime } from './utils';

describe('tabs', () => {
afterEach(() => {
Expand Down Expand Up @@ -131,6 +131,94 @@ describe('tabs', () => {
await testTabTitle('Tab 3 - Page 1');
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3']);
});

it('should preserve navigation extras when switching tabs', async () => {
const expectUrlToContain = 'search=hello#fragment';
let tab = await getSelectedTab() as ElementFinder;
await tab.$('#goto-nested-page1-with-query-params').click();
await testTabTitle('Tab 1 - Page 2 (1)');
await testUrlContains(expectUrlToContain);

await element(by.css('#tab-button-contact')).click();
await testTabTitle('Tab 2 - Page 1');

await element(by.css('#tab-button-account')).click();
tab = await testTabTitle('Tab 1 - Page 2 (1)');
await testUrlContains(expectUrlToContain);
});

it('should set root when clicking on an active tab to navigate to the root', async () => {
const expectNestedTabUrlToContain = 'search=hello#fragment';
let tab = await getSelectedTab() as ElementFinder;
const initialUrl = await browser.getCurrentUrl();
await tab.$('#goto-nested-page1-with-query-params').click();
await testTabTitle('Tab 1 - Page 2 (1)');
await testUrlContains(expectNestedTabUrlToContain);

await element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 1');

await testUrlEquals(initialUrl);
});

});

describe('entry tab contains navigation extras', () => {
const expectNestedTabUrlToContain = 'search=hello#fragment';
const rootUrlParams = 'test=123#rootFragment';
const rootUrl = `/tabs/account?${rootUrlParams}`;

beforeEach(async () => {
await browser.get(rootUrl);
await waitTime(30);
});

it('should preserve root url navigation extras when clicking on an active tab to navigate to the root', async () => {
await browser.get(rootUrl);

let tab = await getSelectedTab() as ElementFinder;
await tab.$('#goto-nested-page1-with-query-params').click();
await testTabTitle('Tab 1 - Page 2 (1)');
await testUrlContains(expectNestedTabUrlToContain);

await element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 1');

await testUrlContains(rootUrl);
});

it('should preserve root url navigation extras when changing tabs', async () => {
await browser.get(rootUrl);

let tab = await getSelectedTab() as ElementFinder;
await element(by.css('#tab-button-contact')).click();
tab = await testTabTitle('Tab 2 - Page 1');

await element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 1');

await testUrlContains(rootUrl);
});

it('should navigate deep then go home and preserve navigation extras', async () => {
let tab = await getSelectedTab();
await tab.$('#goto-tab1-page2').click();
tab = await testTabTitle('Tab 1 - Page 2 (1)');

await tab.$('#goto-next').click();
tab = await testTabTitle('Tab 1 - Page 2 (2)');

await element(by.css('#tab-button-contact')).click();
tab = await testTabTitle('Tab 2 - Page 1');

await element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 2 (2)');

await element(by.css('#tab-button-account')).click();
await testTabTitle('Tab 1 - Page 1');

await testUrlContains(rootUrl);
});
});

describe('entry url - /tabs/account/nested/1', () => {
Expand Down Expand Up @@ -159,7 +247,7 @@ describe('tabs', () => {
await tab.$('#goto-next').click();
tab = await testTabTitle('Tab 1 - Page 2 (3)');

await testStack('ion-tabs ion-router-outlet',[
await testStack('ion-tabs ion-router-outlet', [
'app-tabs-tab1-nested',
'app-tabs-tab1-nested',
'app-tabs-tab1-nested'
Expand Down Expand Up @@ -226,6 +314,18 @@ async function testTabTitle(title: string) {
return tab;
}

async function testUrlContains(urlFragment: string) {
await browser.wait(ExpectedConditions.urlContains(urlFragment),
5000,
`expected ${browser.getCurrentUrl()} to contain ${urlFragment}`);
}

async function testUrlEquals(url: string) {
await browser.wait(ExpectedConditions.urlIs(url),
5000,
`expected ${browser.getCurrentUrl()} to equal ${url}`);
}

async function getSelectedTab(): Promise<ElementFinder> {
const tabs = element.all(by.css('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)'));
expect(await tabs.count()).toEqual(1);
Expand Down
Expand Up @@ -15,6 +15,8 @@ <h1>Welcome to Tab1</h1>
</p>
<p>
<ion-button routerLink="/tabs/account/nested/1" id="goto-tab1-page2">Go to Page 2</ion-button>
<ion-button routerLink="/tabs/account/nested/1" [queryParams]="{search:'hello'}" fragment="fragment"
id="goto-nested-page1-with-query-params">Go to Page 2 with Query Params</ion-button>
<ion-button routerLink="/tabs/lazy/nested" id="goto-tab3-page2">Go to Tab 3 - Page 2</ion-button>
<ion-button routerLink="/nested-outlet/page" id="goto-nested-page1">Go to nested</ion-button>
</p>
Expand Down

0 comments on commit 4c8f32f

Please sign in to comment.