diff --git a/vue/rollup.config.js b/vue/rollup.config.js index 392cac5ff49..2aca83ffe50 100644 --- a/vue/rollup.config.js +++ b/vue/rollup.config.js @@ -41,7 +41,14 @@ function baseConfig() { '@ionic/core/dist/ionic/svg', 'ionicons/dist/collection/icon/icon.css', ], - plugins: [vue(), typescript({ useTsconfigDeclarationDir: true })], + plugins: [ + vue(), + typescript({ + useTsconfigDeclarationDir: true, + objectHashIgnoreUnknownHack: true, + clean: true + }) + ], } } diff --git a/vue/src/components/ion-vue-router-transitionless.vue b/vue/src/components/ion-vue-router-transitionless.vue deleted file mode 100644 index 1695a63801a..00000000000 --- a/vue/src/components/ion-vue-router-transitionless.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/vue/src/components/ion-vue-router.ts b/vue/src/components/ion-vue-router.ts new file mode 100644 index 00000000000..41f4f6881ce --- /dev/null +++ b/vue/src/components/ion-vue-router.ts @@ -0,0 +1,149 @@ +import Vue, { CreateElement, RenderContext, VNodeData } from 'vue'; + +type TransitionDone = () => void; +interface Props { + name: string; + animated: boolean; +} + +// Component entering the view +let enteringEl: HTMLElement; + +export default { + name: 'IonVueRouter', + functional: true, + + props: { + // Router view name + name: { default: 'default', type: String }, + // Disable transitions + animated: { default: true, type: Boolean }, + }, + + render(h: CreateElement, { parent, props, data, children }: RenderContext) { + if (!parent.$router) { + throw new Error('IonTabs requires an instance of either VueRouter or IonicVueRouter'); + } + + const ionRouterOutletData: VNodeData = { + ...data, + ref: 'ionRouterOutlet', + on: { click: (event: Event) => catchIonicGoBack(parent, event) }, + }; + const routerViewData: VNodeData = { props: { name: props.name } }; + const transitionData: VNodeData = { + props: { css: false, mode: 'in-out' }, + on: { + leave: (el: HTMLElement, done: TransitionDone) => { + leave(parent, props as Props, el, done); + }, + beforeEnter, + enter, + afterEnter, + beforeLeave, + afterLeave, + enterCancelled, + leaveCancelled, + } + }; + + return h('ion-router-outlet', ionRouterOutletData, [ + h('transition', transitionData, [ + h('router-view', routerViewData, children) + ]) + ]); + } +}; + +function catchIonicGoBack(parent: Vue, event: Event): void { + if (!event.target) return; + + // We only care for the event coming from Ionic's back button + const backButton = (event.target as HTMLElement).closest('ion-back-button') as HTMLIonBackButtonElement; + if (!backButton) return; + + const $router = parent.$router; + let defaultHref: string; + + // Explicitly override router direction to always trigger a back transition + $router.directionOverride = -1; + + // If we can go back - do so + if ($router.canGoBack()) { + event.preventDefault(); + $router.back(); + return; + } + + // If there's a default fallback - use it + defaultHref = backButton.defaultHref as string; + if (undefined !== defaultHref) { + event.preventDefault(); + $router.push(defaultHref); + } +} + +// Transition when we leave the route +function leave(parent: Vue, props: Props, el: HTMLElement, done: TransitionDone) { + const promise = transition(parent, props, el); + + // Skip any transition if we don't get back a Promise + if (!promise) { + done(); + return; + } + + // Perform navigation once the transition was finished + parent.$router.transition = new Promise(resolve => { + promise.then(() => { + resolve(); + done(); + }).catch(console.error); + }); +} + +// Trigger the ionic/core transitions +function transition(parent: Vue, props: Props, leavingEl: HTMLElement) { + const ionRouterOutlet = parent.$refs.ionRouterOutlet as HTMLIonRouterOutletElement; + + // The Ionic framework didn't load - skip animations + if (typeof ionRouterOutlet.componentOnReady === 'undefined') { + return; + } + + // Skip animations if there's no component to navigate to + // or the current and the "to-be-rendered" components are the same + if (!enteringEl || enteringEl === leavingEl) { + return; + } + + // Add the proper Ionic classes, important for smooth transitions + enteringEl.classList.add('ion-page', 'ion-page-invisible'); + + // Commit to the transition as soon as the Ionic Router Outlet is ready + return ionRouterOutlet.componentOnReady().then((el: HTMLIonRouterOutletElement) => { + return el.commit(enteringEl, leavingEl, { + deepWait: true, + duration: !props.animated ? 0 : undefined, + direction: parent.$router.direction === 1 ? 'forward' : 'back', + showGoBack: parent.$router.canGoBack(), + }); + }).catch(console.error); +} + +// Set the component to be rendered before we render the new route +function beforeEnter(el: HTMLElement) { + enteringEl = el; +} + +// Enter the new route +function enter(_el: HTMLElement, done: TransitionDone) { + done(); +} + +// Vue transition stub functions +function afterEnter(_el: HTMLElement) { /* */ } +function afterLeave(_el: HTMLElement) { /* */ } +function beforeLeave(_el: HTMLElement) { /* */ } +function enterCancelled(_el: HTMLElement) { /* */ } +function leaveCancelled(_el: HTMLElement) { /* */ } diff --git a/vue/src/components/ion-vue-router.vue b/vue/src/components/ion-vue-router.vue deleted file mode 100644 index 97028607bef..00000000000 --- a/vue/src/components/ion-vue-router.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - diff --git a/vue/src/interfaces.ts b/vue/src/interfaces.ts index 5c2b0e35619..35ea96b3971 100644 --- a/vue/src/interfaces.ts +++ b/vue/src/interfaces.ts @@ -6,6 +6,7 @@ declare module 'vue-router/types/router' { interface VueRouter { direction: number; directionOverride: number | null; + transition: Promise; canGoBack(): boolean; } } @@ -60,8 +61,8 @@ export interface ApiCache { } export interface RouterArgs extends RouterOptions { - direction: number; - viewCount: number; + direction?: number; + viewCount?: number; } export interface ProxyControllerInterface { diff --git a/vue/src/ionic.ts b/vue/src/ionic.ts index 51ec3383b28..a5fa1d3e718 100644 --- a/vue/src/ionic.ts +++ b/vue/src/ionic.ts @@ -11,7 +11,7 @@ import { import { IonicConfig } from '@ionic/core'; import { appInitialize } from './app-initialize'; import { VueDelegate } from './controllers/vue-delegate'; -import IonTabs from './components/navigation/IonTabs'; +import IonTabs from './components/navigation/ion-tabs'; export interface Controllers { actionSheetController: ActionSheetController; diff --git a/vue/src/mixins/catch-ionic-go-back.ts b/vue/src/mixins/catch-ionic-go-back.ts deleted file mode 100644 index f7957a33861..00000000000 --- a/vue/src/mixins/catch-ionic-go-back.ts +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; -import Router from '../router'; -import Component from 'vue-class-component'; - -@Component -export default class CatchIonicGoBack extends Vue { - // Catch the bubbled-up event from the Ionic's back button - catchIonicGoBack(event: Event): void { - if (!event.target) return; - - // We only care for the event coming from Ionic's back button - const backButton = (event.target as HTMLElement).closest('ion-back-button') as HTMLIonBackButtonElement; - if (!backButton) return; - - const $router = this.$router as Router; - let defaultHref: string; - - // Explicitly override router direction to always trigger a back transition - $router.directionOverride = -1; - - // If we can go back - do so - if ($router.canGoBack()) { - event.preventDefault(); - $router.back(); - return; - } - - // If there's a default fallback - use it - defaultHref = backButton.defaultHref as string; - if (undefined !== defaultHref) { - event.preventDefault(); - $router.push(defaultHref); - } - } -} diff --git a/vue/src/router.ts b/vue/src/router.ts index e6a7c3436f5..1f4ea038ffc 100644 --- a/vue/src/router.ts +++ b/vue/src/router.ts @@ -1,18 +1,11 @@ import VueRouter, { Route } from 'vue-router'; import { PluginFunction } from 'vue'; -import { RouterArgs, VueWindow } from './interfaces'; -import IonVueRouter from './components/ion-vue-router.vue'; -import IonVueRouterTransitionless from './components/ion-vue-router-transitionless.vue'; +import { RouterArgs } from './interfaces'; +import IonVueRouter from './components/ion-vue-router'; import { BackButtonEvent } from '@ionic/core'; -const vueWindow = window as VueWindow; -const inBrowser: boolean = typeof window !== 'undefined'; - -// Detect environment (browser, module, etc.) -const _VueRouter: typeof VueRouter = inBrowser && vueWindow.VueRouter ? vueWindow.VueRouter : VueRouter; - // Extend the official VueRouter -export default class Router extends _VueRouter { +export default class Router extends VueRouter { direction: number; directionOverride: number | null; viewCount: number; @@ -39,15 +32,26 @@ export default class Router extends _VueRouter { // Extend the existing history object this.extendHistory(); + // Wait for transition to finish before confirming navigation + this.extendTransitionConfirmation(); + // Listen to Ionic's back button event document.addEventListener('ionBackButton', (e: Event) => { - (e as BackButtonEvent).detail.register(0, () => { - this.back(); - }); + (e as BackButtonEvent).detail.register(0, () => this.back()); }); } - extendHistory(): void { + extendTransitionConfirmation() { + this.history._confirmTransition = this.history.confirmTransition; + this.history.confirmTransition = async (...opts: any) => { + if (undefined !== this.transition) { + await this.transition; + } + this.history._confirmTransition(...opts); + }; + } + + extendHistory() { // Save a reference to the original method this.history._updateRoute = this.history.updateRoute; @@ -101,7 +105,7 @@ export default class Router extends _VueRouter { } } -Router.install = (Vue, { disableIonicTransitions = false }: { disableIonicTransitions?: boolean } = {}): void => { +Router.install = (Vue) => { // If already installed - skip if (Router.installed) { return; @@ -110,14 +114,8 @@ Router.install = (Vue, { disableIonicTransitions = false }: { disableIonicTransi Router.installed = true; // Install the official VueRouter - _VueRouter.install(Vue); + VueRouter.install(Vue); // Register the IonVueRouter component globally - // either with default Ionic transitions turned on or off - Vue.component('IonVueRouter', disableIonicTransitions ? IonVueRouterTransitionless : IonVueRouter); + Vue.component('IonVueRouter', IonVueRouter); }; - -// Auto-install when Vue is found (i.e. in browser via