mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 02:31:34 +08:00
fix(tabs): preserve route navigation extras when changing tabs (#18493)
fixes #18717
This commit is contained in:

committed by
Liam DeBeasi

parent
b3b3312711
commit
4c8f32fae9
@ -242,6 +242,22 @@ export class IonRouterOutlet implements OnDestroy, OnInit {
|
|||||||
return active ? active.url : undefined;
|
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.
|
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
|
||||||
*/
|
*/
|
||||||
@ -315,7 +331,7 @@ class OutletInjector implements Injector {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private childContexts: ChildrenOutletContexts,
|
private childContexts: ChildrenOutletContexts,
|
||||||
private parent: Injector
|
private parent: Injector
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
get(token: any, notFoundValue?: any): any {
|
get(token: any, notFoundValue?: any): any {
|
||||||
if (token === ActivatedRoute) {
|
if (token === ActivatedRoute) {
|
||||||
|
@ -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'])
|
@HostListener('ionTabButtonClick', ['$event.detail.tab'])
|
||||||
select(tab: string) {
|
select(tab: string) {
|
||||||
const alreadySelected = this.outlet.getActiveStackId() === tab;
|
const alreadySelected = this.outlet.getActiveStackId() === tab;
|
||||||
const href = `${this.outlet.tabsPrefix}/${tab}`;
|
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
|
||||||
const url = alreadySelected
|
if (alreadySelected) {
|
||||||
? href
|
const rootView = this.outlet.getRootView(tab);
|
||||||
: this.outlet.getLastUrl(tab) || href;
|
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, {
|
return this.navCtrl.navigateRoot(url, {
|
||||||
animated: true,
|
...(navigationExtras),
|
||||||
animationDirection: 'back'
|
animated: true,
|
||||||
});
|
animationDirection: 'back',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelected(): string | undefined {
|
getSelected(): string | undefined {
|
||||||
|
@ -70,7 +70,7 @@ export class StackController {
|
|||||||
if (router.getCurrentNavigation) {
|
if (router.getCurrentNavigation) {
|
||||||
currentNavigation = router.getCurrentNavigation();
|
currentNavigation = router.getCurrentNavigation();
|
||||||
|
|
||||||
// Angular < 7.2.0
|
// Angular < 7.2.0
|
||||||
} else if (
|
} else if (
|
||||||
router.navigations &&
|
router.navigations &&
|
||||||
router.navigations.value
|
router.navigations.value
|
||||||
@ -191,6 +191,14 @@ export class StackController {
|
|||||||
return views.length > 0 ? views[views.length - 1] : undefined;
|
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 {
|
getActiveStackId(): string | undefined {
|
||||||
return this.activeView ? this.activeView.stackId : undefined;
|
return this.activeView ? this.activeView.stackId : undefined;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { browser, element, by, ElementFinder } from 'protractor';
|
import { browser, by, element, ElementFinder, ExpectedConditions } from 'protractor';
|
||||||
import { waitTime, testStack, handleErrorMessages } from './utils';
|
import { handleErrorMessages, testStack, waitTime } from './utils';
|
||||||
|
|
||||||
describe('tabs', () => {
|
describe('tabs', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -131,6 +131,94 @@ describe('tabs', () => {
|
|||||||
await testTabTitle('Tab 3 - Page 1');
|
await testTabTitle('Tab 3 - Page 1');
|
||||||
await testStack('ion-tabs ion-router-outlet', ['app-tabs-tab1', 'app-tabs-tab3']);
|
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', () => {
|
describe('entry url - /tabs/account/nested/1', () => {
|
||||||
@ -159,7 +247,7 @@ describe('tabs', () => {
|
|||||||
await tab.$('#goto-next').click();
|
await tab.$('#goto-next').click();
|
||||||
tab = await testTabTitle('Tab 1 - Page 2 (3)');
|
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',
|
'app-tabs-tab1-nested',
|
||||||
'app-tabs-tab1-nested'
|
'app-tabs-tab1-nested'
|
||||||
@ -226,6 +314,18 @@ async function testTabTitle(title: string) {
|
|||||||
return tab;
|
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> {
|
async function getSelectedTab(): Promise<ElementFinder> {
|
||||||
const tabs = element.all(by.css('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)'));
|
const tabs = element.all(by.css('ion-tabs ion-router-outlet > *:not(.ion-page-hidden)'));
|
||||||
expect(await tabs.count()).toEqual(1);
|
expect(await tabs.count()).toEqual(1);
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<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" 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="/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>
|
<ion-button routerLink="/nested-outlet/page" id="goto-nested-page1">Go to nested</ion-button>
|
||||||
</p>
|
</p>
|
||||||
|
Reference in New Issue
Block a user