From dd1c8dbf3b20fbd423f70c96846d9c366d90e7c5 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Mon, 1 Mar 2021 10:35:25 -0500 Subject: [PATCH] feat(vue): add composition API ionic lifecycle hooks (#22970) resolves #22769 --- packages/vue/src/hooks.ts | 44 ++++++++++++++++++- packages/vue/src/index.ts | 12 ++++- packages/vue/src/utils.ts | 25 +++++++++++ packages/vue/test-app/src/views/Lifecycle.vue | 31 ++++++++++++- .../vue/test-app/tests/e2e/specs/lifecycle.js | 29 ++++++++++-- packages/vue/tsconfig.json | 2 +- 6 files changed, 134 insertions(+), 9 deletions(-) diff --git a/packages/vue/src/hooks.ts b/packages/vue/src/hooks.ts index b297e53f15..68d3e023c6 100644 --- a/packages/vue/src/hooks.ts +++ b/packages/vue/src/hooks.ts @@ -1,5 +1,12 @@ import { BackButtonEvent } from '@ionic/core'; -import { inject, ref, Ref } from 'vue'; +import { + inject, + ref, + Ref, + ComponentInternalInstance, + getCurrentInstance +} from 'vue'; +import { LifecycleHooks } from './utils'; type Handler = (processNextHandler: () => void) => Promise | void | null; @@ -62,3 +69,38 @@ export const useKeyboard = (): IonKeyboardRef => { unregister } } + +/** + * Creates an returns a function that + * can be used to provide a lifecycle hook. + */ +const injectHook = (lifecycleType: LifecycleHooks, hook: Function, component: ComponentInternalInstance | null): Function | undefined => { + if (component) { + + // Add to public instance so it is accessible to IonRouterOutlet + const target = component as any; + const hooks = target.proxy[lifecycleType] || (target.proxy[lifecycleType] = []); + const wrappedHook = (...args: unknown[]) => { + if (target.isUnmounted) { + return; + } + + return args ? hook(...args) : hook(); + }; + + hooks.push(wrappedHook); + + return wrappedHook; + } else { + console.warn('[@ionic/vue]: Ionic Lifecycle Hooks can only be used during execution of setup().'); + } +} + +const createHook = any>(lifecycle: LifecycleHooks) => { + return (hook: T, target: ComponentInternalInstance | null = getCurrentInstance()) => injectHook(lifecycle, hook, target); +} + +export const onIonViewWillEnter = createHook(LifecycleHooks.WillEnter); +export const onIonViewDidEnter = createHook(LifecycleHooks.DidEnter); +export const onIonViewWillLeave = createHook(LifecycleHooks.WillLeave); +export const onIonViewDidLeave = createHook(LifecycleHooks.DidLeave); diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 14543ebf39..f121e581ce 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -16,7 +16,17 @@ export { IonApp } from './components/IonApp'; export * from './components/Overlays'; -export { IonKeyboardRef, IonRouter, useBackButton, useIonRouter, useKeyboard } from './hooks'; +export { + IonKeyboardRef, + IonRouter, + useBackButton, + useIonRouter, + useKeyboard, + onIonViewWillEnter, + onIonViewDidEnter, + onIonViewWillLeave, + onIonViewDidLeave +} from './hooks'; export { modalController, diff --git a/packages/vue/src/utils.ts b/packages/vue/src/utils.ts index b01505407d..9cb505d110 100644 --- a/packages/vue/src/utils.ts +++ b/packages/vue/src/utils.ts @@ -3,6 +3,19 @@ import { Config as CoreConfig, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYC type LIFECYCLE_EVENTS = typeof LIFECYCLE_WILL_ENTER | typeof LIFECYCLE_DID_ENTER | typeof LIFECYCLE_WILL_LEAVE | typeof LIFECYCLE_DID_LEAVE; +export enum LifecycleHooks { + WillEnter = 'onIonViewWillEnter', + DidEnter = 'onIonViewDidEnter', + WillLeave = 'onIonViewWillLeave', + DidLeave = 'onIonViewDidLeave' +} +const hookNames = { + [LIFECYCLE_WILL_ENTER]: LifecycleHooks.WillEnter, + [LIFECYCLE_DID_ENTER]: LifecycleHooks.DidEnter, + [LIFECYCLE_WILL_LEAVE]: LifecycleHooks.WillLeave, + [LIFECYCLE_DID_LEAVE]: LifecycleHooks.DidLeave +} + const ids: { [k: string]: number } = { main: 0 }; export const generateId = (type = 'main') => { @@ -21,6 +34,18 @@ export const fireLifecycle = (vueComponent: any, vueInstance: Ref hook()); + } + } } export const getConfig = (): CoreConfig | null => { diff --git a/packages/vue/test-app/src/views/Lifecycle.vue b/packages/vue/test-app/src/views/Lifecycle.vue index 5acdf7dc9b..c7779c72b3 100644 --- a/packages/vue/test-app/src/views/Lifecycle.vue +++ b/packages/vue/test-app/src/views/Lifecycle.vue @@ -22,6 +22,11 @@ ionViewWillLeave:
{{ willLeave }}

ionViewDidLeave:
{{ didLeave }}

+ onIonViewWillEnter:
{{ onWillEnter }}

+ onIonViewDidEnter:
{{ onDidEnter }}

+ onIonViewWillLeave:
{{ onWillLeave }}

+ onIonViewDidLeave:
{{ onDidLeave }}

+ Go to another page @@ -37,9 +42,13 @@ import { IonHeader, IonPage, IonTitle, - IonToolbar + IonToolbar, + onIonViewWillEnter, + onIonViewDidEnter, + onIonViewWillLeave, + onIonViewDidLeave } from '@ionic/vue'; -import { defineComponent } from 'vue'; +import { defineComponent, ref } from 'vue'; export default defineComponent({ components: { IonButton, @@ -65,6 +74,24 @@ export default defineComponent({ this.didLeave++; } }, + setup() { + const onWillEnter = ref(0); + const onDidEnter = ref(0); + const onWillLeave = ref(0); + const onDidLeave = ref(0); + + onIonViewWillEnter(() => onWillEnter.value += 1); + onIonViewDidEnter(() => onDidEnter.value += 1); + onIonViewWillLeave(() => onWillLeave.value += 1); + onIonViewDidLeave(() => onDidLeave.value += 1); + + return { + onWillEnter, + onDidEnter, + onWillLeave, + onDidLeave + } + }, data() { return { willEnter: 0, diff --git a/packages/vue/test-app/tests/e2e/specs/lifecycle.js b/packages/vue/test-app/tests/e2e/specs/lifecycle.js index 30a05862e6..478e3f119b 100644 --- a/packages/vue/test-app/tests/e2e/specs/lifecycle.js +++ b/packages/vue/test-app/tests/e2e/specs/lifecycle.js @@ -7,7 +7,11 @@ describe('Lifecycle', () => { ionViewWillEnter: 1, ionViewDidEnter: 1, ionViewWillLeave: 0, - ionViewDidLeave: 0 + ionViewDidLeave: 0, + onIonViewWillEnter: 1, + onIonViewDidEnter: 1, + onIonViewWillLeave: 0, + onIonViewDidLeave: 0 }); cy.get('#lifecycle-navigation').click(); @@ -16,7 +20,11 @@ describe('Lifecycle', () => { ionViewWillEnter: 1, ionViewDidEnter: 1, ionViewWillLeave: 1, - ionViewDidLeave: 1 + ionViewDidLeave: 1, + onIonViewWillEnter: 1, + onIonViewDidEnter: 1, + onIonViewWillLeave: 1, + onIonViewDidLeave: 1 }); cy.ionBackClick('navigation'); @@ -25,7 +33,11 @@ describe('Lifecycle', () => { ionViewWillEnter: 2, ionViewDidEnter: 2, ionViewWillLeave: 1, - ionViewDidLeave: 1 + ionViewDidLeave: 1, + onIonViewWillEnter: 2, + onIonViewDidEnter: 2, + onIonViewWillLeave: 1, + onIonViewDidLeave: 1 }); }); @@ -36,7 +48,11 @@ describe('Lifecycle', () => { ionViewWillEnter: 1, ionViewDidEnter: 1, ionViewWillLeave: 0, - ionViewDidLeave: 0 + ionViewDidLeave: 0, + onIonViewWillEnter: 1, + onIonViewDidEnter: 1, + onIonViewWillLeave: 0, + onIonViewDidLeave: 0 }); }); }) @@ -46,4 +62,9 @@ const testLifecycle = (selector, expected = {}) => { 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); + + cy.get(`[data-pageid=${selector}] #onWillEnter`).should('have.text', expected.onIonViewWillEnter); + cy.get(`[data-pageid=${selector}] #onDidEnter`).should('have.text', expected.onIonViewDidEnter); + cy.get(`[data-pageid=${selector}] #onWillLeave`).should('have.text', expected.onIonViewWillLeave); + cy.get(`[data-pageid=${selector}] #onDidLeave`).should('have.text', expected.onIonViewDidLeave); } diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index 0344dc7639..b5f4dabe80 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -10,7 +10,7 @@ "module": "es2015", "moduleResolution": "node", "noImplicitAny": true, - "noImplicitReturns": true, + "noImplicitReturns": false, "noUnusedLocals": true, "noUnusedParameters": true, "outDir": "dist-transpiled",