fix(vue): mount correct views when navigating (#24056)

resolves #23914
This commit is contained in:
Liam DeBeasi
2021-10-14 11:28:36 -04:00
committed by GitHub
parent a09d7d4ab6
commit 24659a527a
6 changed files with 125 additions and 39 deletions

View File

@ -110,7 +110,7 @@ describe('View Stacks', () => {
const itemC = createRegisteredViewItem(viewStacks, 1, '/home/3', true); const itemC = createRegisteredViewItem(viewStacks, 1, '/home/3', true);
const itemD = createRegisteredViewItem(viewStacks, 1, '/home/4', true); const itemD = createRegisteredViewItem(viewStacks, 1, '/home/4', true);
viewStacks.unmountLeavingViews(1, itemA, itemD); viewStacks.unmountLeavingViews(1, itemA, -3);
expect(itemB.mount).toEqual(false); expect(itemB.mount).toEqual(false);
expect(itemB.ionPageElement).toEqual(undefined); expect(itemB.ionPageElement).toEqual(undefined);
@ -127,7 +127,7 @@ describe('View Stacks', () => {
const itemC = createRegisteredViewItem(viewStacks); const itemC = createRegisteredViewItem(viewStacks);
const itemD = createRegisteredViewItem(viewStacks); const itemD = createRegisteredViewItem(viewStacks);
viewStacks.mountIntermediaryViews(1, itemD, itemA); viewStacks.mountIntermediaryViews(1, itemA, 3);
expect(itemB.mount).toEqual(true); expect(itemB.mount).toEqual(true);
expect(itemC.mount).toEqual(true); expect(itemC.mount).toEqual(true);

View File

@ -262,6 +262,7 @@ export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) =>
} }
routeInfo.position = currentHistoryPosition; routeInfo.position = currentHistoryPosition;
routeInfo.delta = delta;
const historySize = locationHistory.size(); const historySize = locationHistory.size();
const historyDiff = currentHistoryPosition - initialHistoryPosition; const historyDiff = currentHistoryPosition - initialHistoryPosition;

View File

@ -27,6 +27,7 @@ export interface RouteInfo {
pushedByRoute?: string; pushedByRoute?: string;
tab?: string; tab?: string;
position?: number; position?: number;
delta?: number;
} }
export interface RouteParams { export interface RouteParams {

View File

@ -164,34 +164,6 @@ export const createViewStacks = (router: Router) => {
return []; return [];
} }
/**
* Given a view stack and entering/leaving views,
* determine the position of each item in the stack.
* This is useful for removing/adding views in between
* the view items when navigating using router.go.
* Use this method instead of doing an `Array.findIndex`
* for both view items.
*/
const findViewIndex = (viewStack: ViewItem[], enteringViewItem: ViewItem, leavingViewItem: ViewItem) => {
let enteringIndex = -1;
let leavingIndex = -1;
for (let i = 0; i <= viewStack.length - 1; i++) {
const viewItem = viewStack[i];
if (viewItem === enteringViewItem) {
enteringIndex = i;
} else if (viewItem === leavingViewItem) {
leavingIndex = i;
}
if (enteringIndex > -1 && leavingIndex > -1) {
break;
}
}
return { enteringIndex, leavingIndex };
}
/** /**
* When navigating backwards, we need to clean up and * When navigating backwards, we need to clean up and
* leaving pages so that they are re-created if * leaving pages so that they are re-created if
@ -199,13 +171,13 @@ export const createViewStacks = (router: Router) => {
* important when using router.go and stepping back * important when using router.go and stepping back
* multiple pages at a time. * multiple pages at a time.
*/ */
const unmountLeavingViews = (outletId: number, enteringViewItem: ViewItem, leavingViewItem: ViewItem) => { const unmountLeavingViews = (outletId: number, viewItem: ViewItem, delta: number = 1) => {
const viewStack = viewStacks[outletId]; const viewStack = viewStacks[outletId];
if (!viewStack) return; if (!viewStack) return;
const { enteringIndex: startIndex, leavingIndex: endIndex } = findViewIndex(viewStack, enteringViewItem, leavingViewItem); const startIndex = viewStack.findIndex(v => v === viewItem);
for (let i = startIndex + 1; i < endIndex; i++) { for (let i = startIndex + 1; i < startIndex - delta; i++) {
const viewItem = viewStack[i]; const viewItem = viewStack[i];
viewItem.mount = false; viewItem.mount = false;
viewItem.ionPageElement = undefined; viewItem.ionPageElement = undefined;
@ -219,14 +191,26 @@ export const createViewStacks = (router: Router) => {
* developers to step forward over multiple views. * developers to step forward over multiple views.
* The intermediary views need to be remounted so that * The intermediary views need to be remounted so that
* swipe to go back works properly. * swipe to go back works properly.
* We need to account for the delta value here too because
* we do not want to remount an unrelated view.
* Example:
* /home --> /page2 --> router.back() --> /page3
* Going to /page3 would remount /page2 since we do
* not prune /page2 from the stack. However, /page2
* needs to remain in the stack.
* Example:
* /home --> /page2 --> /page3 --> router.go(-2) --> router.go(2)
* We would end up on /page3, but users need to be able to swipe
* to go back to /page2 and /home, so we need both pages mounted
* in the DOM.
*/ */
const mountIntermediaryViews = (outletId: number, enteringViewItem: ViewItem, leavingViewItem: ViewItem) => { const mountIntermediaryViews = (outletId: number, viewItem: ViewItem, delta: number = 1) => {
const viewStack = viewStacks[outletId]; const viewStack = viewStacks[outletId];
if (!viewStack) return; if (!viewStack) return;
const { enteringIndex: endIndex, leavingIndex: startIndex } = findViewIndex(viewStack, enteringViewItem, leavingViewItem); const startIndex = viewStack.findIndex(v => v === viewItem);
for (let i = startIndex + 1; i < endIndex; i++) { for (let i = startIndex + 1; i < startIndex + delta; i++) {
viewStack[i].mount = true; viewStack[i].mount = true;
} }
} }

View File

@ -214,7 +214,7 @@ export const IonRouterOutlet = defineComponent({
const handlePageTransition = async () => { const handlePageTransition = async () => {
const routeInfo = ionRouter.getCurrentRouteInfo(); const routeInfo = ionRouter.getCurrentRouteInfo();
const { routerDirection, routerAction, routerAnimation, prevRouteLastPathname } = routeInfo; const { routerDirection, routerAction, routerAnimation, prevRouteLastPathname, delta } = routeInfo;
const enteringViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id, usingDeprecatedRouteSetup); const enteringViewItem = viewStacks.findViewItemByRouteInfo(routeInfo, id, usingDeprecatedRouteSetup);
let leavingViewItem = viewStacks.findLeavingViewItemByRouteInfo(routeInfo, id, true, usingDeprecatedRouteSetup); let leavingViewItem = viewStacks.findLeavingViewItemByRouteInfo(routeInfo, id, true, usingDeprecatedRouteSetup);
@ -286,10 +286,10 @@ See https://ionicframework.com/docs/vue/navigation#ionpage for more information.
leavingViewItem.mount = false; leavingViewItem.mount = false;
leavingViewItem.ionPageElement = undefined; leavingViewItem.ionPageElement = undefined;
leavingViewItem.ionRoute = false; leavingViewItem.ionRoute = false;
viewStacks.unmountLeavingViews(id, enteringViewItem, leavingViewItem); viewStacks.unmountLeavingViews(id, enteringViewItem, delta);
} }
} else { } else {
viewStacks.mountIntermediaryViews(id, enteringViewItem, leavingViewItem); viewStacks.mountIntermediaryViews(id, leavingViewItem, delta);
} }
fireLifecycle(leavingViewItem.vueComponent, leavingViewItem.vueComponentRef, LIFECYCLE_DID_LEAVE); fireLifecycle(leavingViewItem.vueComponent, leavingViewItem.vueComponentRef, LIFECYCLE_DID_LEAVE);

View File

@ -441,4 +441,104 @@ describe('Routing', () => {
expect(beforeRouteEnterSpy).toHaveBeenCalledTimes(2); expect(beforeRouteEnterSpy).toHaveBeenCalledTimes(2);
}); });
it('should not mount intermediary components when delta is 1', async () => {
const Page = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const Page2 = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const Page3 = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{ path: '/page', component: Page },
{ path: '/page2', component: Page2 },
{ path: '/page3', component: Page3 },
{ path: '/', redirect: '/page' }
]
});
router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});
expect(wrapper.findComponent(Page).exists()).toBe(true);
router.push('/page2');
await waitForRouter();
expect(wrapper.findComponent(Page2).exists()).toBe(true);
router.back();
await waitForRouter();
expect(wrapper.findComponent(Page2).exists()).toBe(false);
router.push('/page3');
await waitForRouter();
expect(wrapper.findComponent(Page2).exists()).toBe(false);
expect(wrapper.findComponent(Page3).exists()).toBe(true);
});
it('should unmount intermediary components when using router.go', async () => {
const Page = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const Page2 = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const Page3 = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{ path: '/page', component: Page },
{ path: '/page2', component: Page2 },
{ path: '/page3', component: Page3 },
{ path: '/', redirect: '/page' }
]
});
router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});
router.push('/page2');
await waitForRouter();
router.push('/page3');
await waitForRouter();
expect(wrapper.findComponent(Page2).exists()).toBe(true);
expect(wrapper.findComponent(Page3).exists()).toBe(true);
router.go(-2);
await waitForRouter();
expect(wrapper.findComponent(Page).exists()).toBe(true);
expect(wrapper.findComponent(Page2).exists()).toBe(false);
expect(wrapper.findComponent(Page3).exists()).toBe(false);
});
}); });