mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(vue): support ion-vue-router (#17821)
This commit is contained in:
committed by
Mike Hartington
parent
ee7167512f
commit
71e5994c94
@@ -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
|
||||
})
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<div @click="catchIonicGoBack">
|
||||
<router-view :name="name"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Prop } from 'vue-property-decorator';
|
||||
import Component, { mixins } from 'vue-class-component';
|
||||
import CatchIonicGoBack from '../mixins/catch-ionic-go-back';
|
||||
|
||||
@Component
|
||||
export default class IonVueRouter extends mixins(CatchIonicGoBack) {
|
||||
@Prop({ default: 'default'}) name!: string;
|
||||
}
|
||||
</script>
|
||||
149
vue/src/components/ion-vue-router.ts
Normal file
149
vue/src/components/ion-vue-router.ts
Normal file
@@ -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) { /* */ }
|
||||
@@ -1,134 +0,0 @@
|
||||
<template>
|
||||
<ion-router-outlet
|
||||
ref="ionRouterOutlet"
|
||||
@click="catchIonicGoBack">
|
||||
<router-view
|
||||
v-if="customTransition"
|
||||
:name="name"/>
|
||||
<transition
|
||||
v-else
|
||||
:css="bindCSS"
|
||||
mode="in-out"
|
||||
@before-enter="beforeEnter"
|
||||
@enter="enter"
|
||||
@after-enter="afterEnter"
|
||||
@enter-cancelled="enterCancelled"
|
||||
@before-leave="beforeLeave"
|
||||
@leave="leave"
|
||||
@after-leave="afterLeave"
|
||||
@leave-cancelled="leaveCancelled"
|
||||
>
|
||||
<router-view :name="name"/>
|
||||
</transition>
|
||||
</ion-router-outlet>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Prop } from 'vue-property-decorator';
|
||||
import Component, { mixins } from 'vue-class-component';
|
||||
import CatchIonicGoBack from '../mixins/catch-ionic-go-back';
|
||||
import { IonRouterOutlet } from '../interfaces';
|
||||
|
||||
@Component
|
||||
export default class IonVueRouter extends mixins(CatchIonicGoBack) {
|
||||
@Prop({ default: 'default'}) name!: string;
|
||||
@Prop({ default: false }) bindCSS!: boolean;
|
||||
@Prop({ default: true }) animated!: boolean;
|
||||
|
||||
// Currently visible component
|
||||
leavingEl: HTMLElement;
|
||||
// Component to be rendered
|
||||
enteringEl: HTMLElement;
|
||||
// Flag to see if we're still in a transition
|
||||
inTransition = false;
|
||||
customTransition = false;
|
||||
|
||||
created() {
|
||||
// Cancel navigation if there's a running transition
|
||||
this.$router.beforeEach((to, _from, next) => {
|
||||
this.customTransition = to.meta.customTransition || false
|
||||
return this.$nextTick(() => {
|
||||
return next(!this.inTransition as false);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
transition(enteringEl: HTMLElement, leavingEl: HTMLElement) {
|
||||
// Get the reference to the Ionic component handling the transitions
|
||||
const ionRouterOutlet = this.$refs.ionRouterOutlet as IonRouterOutlet;
|
||||
|
||||
// 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 => {
|
||||
return el.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: this.getDuration(),
|
||||
direction: this.getDirection(),
|
||||
showGoBack: this.$router.canGoBack(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Instant transition if we don't want to animate
|
||||
getDuration() {
|
||||
return !this.animated ? 0 : undefined;
|
||||
}
|
||||
|
||||
// Get the navigation direction from the router
|
||||
getDirection() {
|
||||
return this.$router.direction === 1 ? 'forward' : 'back';
|
||||
}
|
||||
|
||||
// Set the component to be rendered before we render the new route
|
||||
beforeEnter(el: HTMLElement) {
|
||||
this.enteringEl = el;
|
||||
}
|
||||
|
||||
// Remember the current component before we leave the route
|
||||
beforeLeave(el: HTMLElement) {
|
||||
this.leavingEl = el;
|
||||
}
|
||||
|
||||
// Transition when we leave the route
|
||||
leave(el: HTMLElement, done: (opts?: boolean) => void): void {
|
||||
const promise = this.transition(this.enteringEl, el);
|
||||
|
||||
this.inTransition = true;
|
||||
|
||||
// Skip any transition if we don't get back a Promise
|
||||
if (!promise) {
|
||||
this.inTransition = false;
|
||||
return done();
|
||||
}
|
||||
|
||||
// Perform navigation once the transition was finished
|
||||
promise.then(() => {
|
||||
this.inTransition = false;
|
||||
return done(true);
|
||||
});
|
||||
}
|
||||
|
||||
// Enter the new route
|
||||
enter(_el: HTMLElement, done: () => void): void {
|
||||
done();
|
||||
}
|
||||
|
||||
afterEnter(/* el */) {}
|
||||
enterCancelled(/* el */) {}
|
||||
afterLeave(/* el */) {}
|
||||
leaveCancelled(/* el */) {}
|
||||
}
|
||||
</script>
|
||||
@@ -6,6 +6,7 @@ declare module 'vue-router/types/router' {
|
||||
interface VueRouter {
|
||||
direction: number;
|
||||
directionOverride: number | null;
|
||||
transition: Promise<void>;
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <script> tag)
|
||||
if (inBrowser && vueWindow.Vue) {
|
||||
vueWindow.Vue.use(Router, { disableIonicTransitions: vueWindow.disableIonicTransitions });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user