fix(vue): lifecycle events are correctly fired in component context (#22348)

resolves #22338
This commit is contained in:
Liam DeBeasi
2020-10-21 11:22:52 -04:00
committed by GitHub
parent 1a2e5322fb
commit bcef804dea
10 changed files with 148 additions and 20 deletions

View File

@@ -15,5 +15,5 @@ export default {
plugins: [terser()]
}
],
external: ['vue-router']
external: ['vue-router', 'vue']
};

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)

View File

@@ -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<any>, lifecycle: string) => {
if (vueComponentRef && vueComponentRef.value && vueComponentRef.value[lifecycle]) {
vueComponentRef.value[lifecycle]();
}
}

View File

@@ -7,6 +7,10 @@ const routes: Array<RouteRecordRaw> = [
path: '/',
component: Home
},
{
path: '/lifecycle',
component: () => import('@/views/Lifecycle.vue')
},
{
path: '/overlays',
name: 'Overlays',

View File

@@ -41,6 +41,9 @@
<ion-item router-link="/tabs-secondary" id="tab-secondary">
<ion-label>Tabs Secondary</ion-label>
</ion-item>
<ion-item router-link="/lifecycle" id="lifecycle">
<ion-label>Lifecycle</ion-label>
</ion-item>
</ion-list>
</ion-content>

View File

@@ -0,0 +1,77 @@
<template>
<ion-page data-pageid="lifecycle">
<ion-header :translucent="true">
<ion-toolbar>
<ion-buttons>
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Lifecycle</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">Lifecycle</ion-title>
</ion-toolbar>
</ion-header>
<div class="ion-padding">
ionViewWillEnter: <div id="willEnter">{{ willEnter }}</div><br />
ionViewDidEnter: <div id="didEnter">{{ didEnter }}</div><br />
ionViewWillLeave: <div id="willLeave">{{ willLeave }}</div><br />
ionViewDidLeave: <div id="didLeave">{{ didLeave }}</div><br />
<ion-button router-link="/navigation" id="lifecycle-navigation">Go to another page</ion-button>
</div>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import {
IonButton,
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar
} from '@ionic/vue';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
IonButton,
IonBackButton,
IonButtons,
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar
},
methods: {
ionViewWillEnter() {
this.willEnter++;
},
ionViewDidEnter() {
this.didEnter++;
},
ionViewWillLeave() {
this.willLeave++;
},
ionViewDidLeave() {
this.didLeave++;
}
},
data() {
return {
willEnter: 0,
didEnter: 0,
willLeave: 0,
didLeave: 0
}
}
});
</script>

View File

@@ -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);
}

View File

@@ -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)
})
})