diff --git a/packages/vue-router/rollup.config.js b/packages/vue-router/rollup.config.js index bf5fe86d53..bae455541d 100644 --- a/packages/vue-router/rollup.config.js +++ b/packages/vue-router/rollup.config.js @@ -15,5 +15,5 @@ export default { plugins: [terser()] } ], - external: ['vue-router'] + external: ['vue-router', 'vue'] }; diff --git a/packages/vue-router/src/types.ts b/packages/vue-router/src/types.ts index 839a70980d..0154182bb0 100644 --- a/packages/vue-router/src/types.ts +++ b/packages/vue-router/src/types.ts @@ -1,5 +1,6 @@ import { AnimationBuilder } from '@ionic/core'; import { RouteLocationMatched, RouterOptions } from 'vue-router'; +import { Ref } from 'vue'; export interface IonicVueRouterOptions extends RouterOptions { tabsPrefix?: string; @@ -40,6 +41,7 @@ export interface ViewItem { mount: boolean; exact: boolean; registerCallback?: () => void; + vueComponentRef: Ref; } export interface ViewStacks { diff --git a/packages/vue-router/src/viewStacks.ts b/packages/vue-router/src/viewStacks.ts index 02ce0f9b1f..f6d710ccb6 100644 --- a/packages/vue-router/src/viewStacks.ts +++ b/packages/vue-router/src/viewStacks.ts @@ -5,6 +5,7 @@ import { RouteInfo, ViewStacks, } from './types'; import { RouteLocationMatched } from 'vue-router'; +import { shallowRef } from 'vue'; export const createViewStacks = () => { let viewStacks: ViewStacks = {}; @@ -95,6 +96,7 @@ export const createViewStacks = () => { matchedRoute, ionPageElement: ionPage, vueComponent, + vueComponentRef: shallowRef(), ionRoute: false, mount: false, exact: routeInfo.pathname === matchedRoute.path diff --git a/packages/vue/src/components/IonRouterOutlet.ts b/packages/vue/src/components/IonRouterOutlet.ts index 841fee129c..ad7bd51cb8 100644 --- a/packages/vue/src/components/IonRouterOutlet.ts +++ b/packages/vue/src/components/IonRouterOutlet.ts @@ -182,13 +182,13 @@ export const IonRouterOutlet = defineComponent({ if (enteringViewItem === leavingViewItem) return; - fireLifecycle(enteringViewItem.vueComponent, LIFECYCLE_WILL_ENTER); + fireLifecycle(enteringViewItem.vueComponentRef, LIFECYCLE_WILL_ENTER); if (leavingViewItem) { let animationBuilder = routerAnimation; const leavingEl = leavingViewItem.ionPageElement; - fireLifecycle(leavingViewItem.vueComponent, LIFECYCLE_WILL_LEAVE); + fireLifecycle(leavingViewItem.vueComponentRef, LIFECYCLE_WILL_LEAVE); /** * If we are going back from a page that @@ -230,7 +230,7 @@ export const IonRouterOutlet = defineComponent({ } } - fireLifecycle(leavingViewItem.vueComponent, LIFECYCLE_DID_LEAVE); + fireLifecycle(leavingViewItem.vueComponentRef, LIFECYCLE_DID_LEAVE); } else { /** * If there is no leaving element, just show @@ -241,7 +241,7 @@ export const IonRouterOutlet = defineComponent({ requestAnimationFrame(() => enteringEl.classList.remove('ion-page-invisible')); } - fireLifecycle(enteringViewItem.vueComponent, LIFECYCLE_DID_ENTER); + fireLifecycle(enteringViewItem.vueComponentRef, LIFECYCLE_DID_ENTER); components.value = viewStacks.getChildrenToRender(id); } @@ -359,6 +359,7 @@ export const IonRouterOutlet = defineComponent({ return h( c.vueComponent, { + ref: c.vueComponentRef, key: c.pathname, isInOutlet: true, registerIonPage: (ionPageEl: HTMLElement) => registerIonPage(c, ionPageEl) diff --git a/packages/vue/src/utils.ts b/packages/vue/src/utils.ts index be19bf417f..316befc7fb 100644 --- a/packages/vue/src/utils.ts +++ b/packages/vue/src/utils.ts @@ -1,3 +1,5 @@ +import { Ref } from 'vue'; + export const LIFECYCLE_WILL_ENTER = 'ionViewWillEnter'; export const LIFECYCLE_DID_ENTER = 'ionViewDidEnter'; export const LIFECYCLE_WILL_LEAVE = 'ionViewWillLeave'; @@ -12,8 +14,8 @@ export const generateId = (type = 'main') => { }; // TODO types -export const fireLifecycle = (vueComponent: any, lifecycle: string) => { - if (vueComponent && vueComponent.methods && vueComponent.methods[lifecycle]) { - vueComponent.methods[lifecycle](); +export const fireLifecycle = (vueComponentRef: Ref, lifecycle: string) => { + if (vueComponentRef && vueComponentRef.value && vueComponentRef.value[lifecycle]) { + vueComponentRef.value[lifecycle](); } } diff --git a/packages/vue/test-app/src/router/index.ts b/packages/vue/test-app/src/router/index.ts index 19d4ac6893..00818b4e0a 100644 --- a/packages/vue/test-app/src/router/index.ts +++ b/packages/vue/test-app/src/router/index.ts @@ -7,6 +7,10 @@ const routes: Array = [ path: '/', component: Home }, + { + path: '/lifecycle', + component: () => import('@/views/Lifecycle.vue') + }, { path: '/overlays', name: 'Overlays', diff --git a/packages/vue/test-app/src/views/Home.vue b/packages/vue/test-app/src/views/Home.vue index ee3077b92d..a84860b6fa 100644 --- a/packages/vue/test-app/src/views/Home.vue +++ b/packages/vue/test-app/src/views/Home.vue @@ -41,6 +41,9 @@ Tabs Secondary + + Lifecycle + diff --git a/packages/vue/test-app/src/views/Lifecycle.vue b/packages/vue/test-app/src/views/Lifecycle.vue new file mode 100644 index 0000000000..5acdf7dc9b --- /dev/null +++ b/packages/vue/test-app/src/views/Lifecycle.vue @@ -0,0 +1,77 @@ + + + diff --git a/packages/vue/test-app/tests/e2e/specs/lifecycle.js b/packages/vue/test-app/tests/e2e/specs/lifecycle.js new file mode 100644 index 0000000000..30a05862e6 --- /dev/null +++ b/packages/vue/test-app/tests/e2e/specs/lifecycle.js @@ -0,0 +1,49 @@ +describe('Lifecycle', () => { + it('should fire lifecycle events when navigating to and from a page', () => { + cy.visit('http://localhost:8080'); + cy.get('#lifecycle').click(); + + testLifecycle('lifecycle', { + ionViewWillEnter: 1, + ionViewDidEnter: 1, + ionViewWillLeave: 0, + ionViewDidLeave: 0 + }); + + cy.get('#lifecycle-navigation').click(); + + testLifecycle('lifecycle', { + ionViewWillEnter: 1, + ionViewDidEnter: 1, + ionViewWillLeave: 1, + ionViewDidLeave: 1 + }); + + cy.ionBackClick('navigation'); + + testLifecycle('lifecycle', { + ionViewWillEnter: 2, + ionViewDidEnter: 2, + ionViewWillLeave: 1, + ionViewDidLeave: 1 + }); + }); + + it('should fire lifecycle events when landed on directly', () => { + cy.visit('http://localhost:8080/lifecycle'); + + testLifecycle('lifecycle', { + ionViewWillEnter: 1, + ionViewDidEnter: 1, + ionViewWillLeave: 0, + ionViewDidLeave: 0 + }); + }); +}) + +const testLifecycle = (selector, expected = {}) => { + cy.get(`[data-pageid=${selector}] #willEnter`).should('have.text', expected.ionViewWillEnter); + cy.get(`[data-pageid=${selector}] #didEnter`).should('have.text', expected.ionViewDidEnter); + cy.get(`[data-pageid=${selector}] #willLeave`).should('have.text', expected.ionViewWillLeave); + cy.get(`[data-pageid=${selector}] #didLeave`).should('have.text', expected.ionViewDidLeave); +} diff --git a/packages/vue/test-app/tests/unit/example.spec.ts b/packages/vue/test-app/tests/unit/example.spec.ts deleted file mode 100644 index 4b21ca7d9d..0000000000 --- a/packages/vue/test-app/tests/unit/example.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { shallowMount } from '@vue/test-utils' -import HelloWorld from '@/components/HelloWorld.vue' - -describe('HelloWorld.vue', () => { - it('renders props.msg when passed', () => { - const msg = 'new message' - const wrapper = shallowMount(HelloWorld, { - props: { msg } - }) - expect(wrapper.text()).toMatch(msg) - }) -})