diff --git a/packages/vue-router/src/types.ts b/packages/vue-router/src/types.ts index 0be3640736..9297377452 100644 --- a/packages/vue-router/src/types.ts +++ b/packages/vue-router/src/types.ts @@ -2,6 +2,14 @@ import { AnimationBuilder } from '@ionic/core'; import { RouteLocationMatched, RouterOptions } from 'vue-router'; import { Ref } from 'vue'; +export interface VueComponentData { + /** + * The cached result of the props + * function for a particular view instance. + */ + propsFunctionResult?: any; +} + export interface IonicVueRouterOptions extends RouterOptions { tabsPrefix?: string; } @@ -43,6 +51,7 @@ export interface ViewItem { registerCallback?: () => void; vueComponentRef: Ref; params?: { [k: string]: any }; + vueComponentData: VueComponentData; } export interface ViewStacks { diff --git a/packages/vue-router/src/viewStacks.ts b/packages/vue-router/src/viewStacks.ts index c7b8e54364..1f83ee81b4 100644 --- a/packages/vue-router/src/viewStacks.ts +++ b/packages/vue-router/src/viewStacks.ts @@ -89,7 +89,8 @@ export const createViewStacks = () => { ionRoute: false, mount: false, exact: routeInfo.pathname === matchedRoute.path, - params: routeInfo.params + params: routeInfo.params, + vueComponentData: {} }; } diff --git a/packages/vue/src/components/IonRouterOutlet.ts b/packages/vue/src/components/IonRouterOutlet.ts index 2f3d1e373a..66a207c488 100644 --- a/packages/vue/src/components/IonRouterOutlet.ts +++ b/packages/vue/src/components/IonRouterOutlet.ts @@ -11,13 +11,14 @@ import { onUnmounted } from 'vue'; import { AnimationBuilder, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core'; -import { matchedRouteKey, useRoute } from 'vue-router'; +import { matchedRouteKey, routeLocationKey, useRoute } from 'vue-router'; import { fireLifecycle, generateId, getConfig } from '../utils'; let viewDepthKey: InjectionKey<0> = Symbol(0); export const IonRouterOutlet = defineComponent({ name: 'IonRouterOutlet', setup(_, { attrs }) { + const injectedRoute = inject(routeLocationKey)!; const route = useRoute(); const depth = inject(viewDepthKey, 0); const matchedRouteRef: any = computed(() => { @@ -352,12 +353,13 @@ export const IonRouterOutlet = defineComponent({ return { id, components, + injectedRoute, ionRouterOutlet, registerIonPage } }, render() { - const { components, registerIonPage } = this; + const { components, registerIonPage, injectedRoute } = this; return h( 'ion-router-outlet', @@ -374,22 +376,43 @@ export const IonRouterOutlet = defineComponent({ /** * IonRouterOutlet does not support named outlets. */ - if (c.matchedRoute?.props?.default) { - const matchedRoute = c.matchedRoute; - const routePropsOption = matchedRoute.props.default; - const routeProps = routePropsOption - ? routePropsOption === true - ? c.params - : typeof routePropsOption === 'function' - ? routePropsOption(matchedRoute) - : routePropsOption - : null + const routePropsOption = c.matchedRoute?.props?.default; - props = { - ...props, - ...routeProps + /** + * Since IonRouterOutlet renders multiple components, + * each render will cause all props functions to be + * called again. As a result, we need to cache the function + * result and provide it on each render so that the props + * are not lost when navigating from and back to a page. + * When a component is destroyed and re-created, the + * function is called again. + */ + const getPropsFunctionResult = () => { + const cachedPropsResult = c.vueComponentData?.propsFunctionResult; + if (cachedPropsResult) { + return cachedPropsResult; + } else { + const propsFunctionResult = routePropsOption(injectedRoute); + c.vueComponentData = { + ...c.vueComponentData, + propsFunctionResult + }; + return propsFunctionResult; } } + const routeProps = routePropsOption + ? routePropsOption === true + ? c.params + : typeof routePropsOption === 'function' + ? getPropsFunctionResult() + : routePropsOption + : null + + props = { + ...props, + ...routeProps + } + return h( c.vueComponent, props diff --git a/packages/vue/test-app/tests/unit/routing.spec.ts b/packages/vue/test-app/tests/unit/routing.spec.ts index edfb046a20..6838f954f0 100644 --- a/packages/vue/test-app/tests/unit/routing.spec.ts +++ b/packages/vue/test-app/tests/unit/routing.spec.ts @@ -89,14 +89,19 @@ describe('Routing', () => { } }; + const propsFn = jest.fn((route) => { + return { title: `${route.params.id} Title` } + }); + const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes: [ - { path: '/myPath', component: Page1, props: function(route) { return { title: `${route.path} Title` } } } + { path: '/myPath/:id', component: Page1, props: propsFn }, + { path: '/otherPage', component: Page1 } ] }); - router.push('/myPath'); + router.push('/myPath/123'); await router.isReady(); const wrapper = mount(App, { global: { @@ -105,7 +110,19 @@ describe('Routing', () => { }); const cmp = wrapper.findComponent(Page1); - expect(cmp.props()).toEqual({ title: '/myPath Title' }); + expect(cmp.props()).toEqual({ title: '123 Title' }); + + router.push('/otherPage'); + await waitForRouter(); + + expect(propsFn.mock.calls.length).toBe(1); + + router.back(); + await waitForRouter(); + + expect(propsFn.mock.calls.length).toBe(1); + + expect(cmp.props()).toEqual({ title: '123 Title' }); }); it('should pass route params as props', async () => {