feat(vue): add ionic vue beta (#22062)

This commit is contained in:
Liam DeBeasi
2020-09-10 15:20:49 -04:00
committed by GitHub
parent 74af3cb50b
commit 5ffa65f84a
48 changed files with 3949 additions and 26 deletions

View File

@ -0,0 +1,36 @@
import { App } from 'vue';
import {
createRouter as createVueRouter,
createWebHistory as createVueWebHistory,
NavigationGuardNext,
RouteLocationNormalized
} from 'vue-router';
import { createIonRouter } from './router';
import { createViewStacks } from './viewStacks';
import { IonicVueRouterOptions } from './types';
export const createRouter = (opts: IonicVueRouterOptions) => {
const routerOptions = { ...opts };
delete routerOptions.tabsPrefix;
const router = createVueRouter(routerOptions);
const ionRouter = createIonRouter(opts, router);
const viewStacks = createViewStacks();
const oldInstall = router.install.bind(router);
router.install = (app: App) => {
app.provide('navManager', ionRouter);
app.provide('viewStacks', viewStacks);
oldInstall(app);
};
router.beforeEach((to: RouteLocationNormalized, _: RouteLocationNormalized, next: NavigationGuardNext) => {
ionRouter.handleHistoryChange(to);
next();
});
return router;
}
export const createWebHistory = (base?: string) => createVueWebHistory(base);

View File

@ -0,0 +1,160 @@
import { RouteInfo } from './types';
export const createLocationHistory = () => {
const locationHistory: RouteInfo[] = [];
const tabsHistory: { [k: string]: RouteInfo[] } = {};
const add = (routeInfo: RouteInfo) => {
switch (routeInfo.routerAction) {
case "replace":
replaceRoute(routeInfo);
break;
case "pop":
pop(routeInfo);
break;
default:
addRoute(routeInfo);
break;
}
if (routeInfo.routerDirection === 'root') {
clearHistory();
addRoute(routeInfo);
}
}
const update = (routeInfo: RouteInfo) => {
const locationIndex = locationHistory.findIndex(x => x.id === routeInfo.id);
if (locationIndex > -1) {
locationHistory.splice(locationIndex, 1, routeInfo);
}
const tabArray = tabsHistory[routeInfo.tab || ''];
if (tabArray) {
const tabIndex = tabArray.findIndex(x => x.id === routeInfo.id);
if (tabIndex > -1) {
tabArray.splice(tabIndex, 1, routeInfo);
} else {
tabArray.push(routeInfo);
}
} else if (routeInfo.tab) {
tabsHistory[routeInfo.tab] = [routeInfo];
}
}
const replaceRoute = (routeInfo: RouteInfo) => {
const routeInfos = getTabsHistory(routeInfo.tab);
routeInfos && routeInfos.pop();
locationHistory.pop();
addRoute(routeInfo);
}
const pop = (routeInfo: RouteInfo) => {
const tabHistory = getTabsHistory(routeInfo.tab);
let ri;
if (tabHistory) {
// Pop all routes until we are back
ri = tabHistory[tabHistory.length - 1];
while (ri && ri.id !== routeInfo.id) {
tabHistory.pop();
ri = tabHistory[tabHistory.length - 1];
}
// Replace with updated route
tabHistory.pop();
tabHistory.push(routeInfo);
}
ri = locationHistory[locationHistory.length - 1];
while (ri && ri.id !== routeInfo.id) {
locationHistory.pop();
ri = locationHistory[locationHistory.length - 1];
}
// Replace with updated route
locationHistory.pop();
locationHistory.push(routeInfo);
}
const addRoute = (routeInfo: RouteInfo) => {
const tabHistory = getTabsHistory(routeInfo.tab);
if (tabHistory) {
// If the latest routeInfo is the same (going back and forth between tabs), replace it
if (tabHistory[tabHistory.length - 1] && tabHistory[tabHistory.length - 1].id === routeInfo.id) {
tabHistory.pop();
}
tabHistory.push(routeInfo);
}
locationHistory.push(routeInfo);
}
const clearHistory = () => {
locationHistory.length = 0;
Object.keys(tabsHistory).forEach(key => {
tabsHistory[key] = [];
});
}
const getTabsHistory = (tab: string): RouteInfo[] => {
let history;
if (tab) {
history = tabsHistory[tab];
if (!history) {
history = tabsHistory[tab] = [];
}
}
return history;
}
const previous = () => locationHistory[locationHistory.length - 2] || current();
const current = () => locationHistory[locationHistory.length - 1];
const canGoBack = (deep: number = 1) => locationHistory.length > deep;
const getFirstRouteInfoForTab = (tab: string): RouteInfo | undefined => {
const tabHistory = getTabsHistory(tab);
if (tabHistory) {
return tabHistory[0];
}
return undefined;
}
const getCurrentRouteInfoForTab = (tab: string): RouteInfo | undefined => {
const tabHistory = getTabsHistory(tab);
if (tabHistory) {
return tabHistory[tabHistory.length - 1];
}
return undefined;
}
const findLastLocation = (routeInfo: RouteInfo): RouteInfo | undefined => {
const routeInfos = getTabsHistory(routeInfo.tab);
if (routeInfos) {
for (let i = routeInfos.length - 2; i >= 0; i--) {
const ri = routeInfos[i];
if (ri) {
if (ri.pathname === routeInfo.pushedByRoute) {
return ri;
}
}
}
}
for (let i = locationHistory.length - 2; i >= 0; i--) {
const ri = locationHistory[i];
if (ri) {
if (ri.pathname === routeInfo.pushedByRoute) {
return ri;
}
}
}
return undefined;
}
return {
current,
previous,
add,
pop,
canGoBack,
update,
getTabsHistory,
getFirstRouteInfoForTab,
getCurrentRouteInfoForTab,
findLastLocation
}
}

View File

@ -0,0 +1,274 @@
import {
Router,
RouteLocationNormalizedLoaded,
} from 'vue-router';
import { createLocationHistory } from './locationHistory';
import { generateId } from './utils';
import {
ExternalNavigationOptions,
RouteInfo,
RouteParams,
RouteAction,
RouteDirection,
IonicVueRouterOptions
} from './types';
import { AnimationBuilder } from '@ionic/core';
export const createIonRouter = (opts: IonicVueRouterOptions, router: Router) => {
const locationHistory = createLocationHistory();
let currentRouteInfo: RouteInfo;
let incomingRouteParams: RouteParams;
let currentTab: string | undefined;
// TODO types
let historyChangeListeners: any[] = [];
const currentRoute = router.currentRoute.value;
currentRouteInfo = {
id: generateId('routeInfo'),
pathname: currentRoute.path,
search: currentRoute.fullPath.split('?')[1] || '',
params: currentRoute.params
}
locationHistory.add(currentRouteInfo)
if (typeof (document as any) !== 'undefined') {
document.addEventListener('ionBackButton', (ev: Event) => {
(ev as any).detail.register(0, (processNextHandler: () => void) => {
opts.history.go(-1);
processNextHandler();
});
})
}
// NavigationCallback
opts.history.listen((to: any, _: any, info: any) => handleHistoryChange({ path: to }, info.type, info.direction));
const handleNavigateBack = (defaultHref?: string, routerAnimation?: AnimationBuilder) => {
//console.log('--- Begin Navigate Back ---');
// todo grab default back button href from config
const routeInfo = locationHistory.current();
//console.log('Route Info', routeInfo)
if (routeInfo && routeInfo.pushedByRoute) {
const prevInfo = locationHistory.findLastLocation(routeInfo);
if (prevInfo) {
//console.log('Prev Info', prevInfo)
incomingRouteParams = { ...prevInfo, routerAction: 'pop', routerDirection: 'back', routerAnimation: routerAnimation || routeInfo.routerAnimation };
//console.log('Set incoming route params', incomingRouteParams)
if (routeInfo.lastPathname === routeInfo.pushedByRoute) {
router.back();
} else {
router.replace(prevInfo.pathname + (prevInfo.search || ''));
}
} else {
handleNavigate(defaultHref, 'pop', 'back');
}
} else {
handleNavigate(defaultHref, 'pop', 'back');
}
//console.log('--- End Navigate Back ---');
}
const handleNavigate = (path: string, routerAction?: RouteAction, routerDirection?: RouteDirection, routerAnimation?: AnimationBuilder, tab?: string) => {
incomingRouteParams = {
routerAction,
routerDirection,
routerAnimation,
tab
}
if (routerAction === 'push') {
router.push(path);
} else {
router.replace(path);
}
}
// TODO RouteLocationNormalized
const handleHistoryChange = (location: any, action?: RouteAction, direction?: RouteDirection) => {
let leavingLocationInfo: RouteInfo;
if (incomingRouteParams) {
if (incomingRouteParams.routerAction === 'replace') {
leavingLocationInfo = locationHistory.previous();
} else {
leavingLocationInfo = locationHistory.current();
}
} else {
leavingLocationInfo = locationHistory.current();
}
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
if (leavingUrl !== location.fullPath) {
if (!incomingRouteParams) {
if (action === 'replace') {
incomingRouteParams = {
routerAction: 'replace',
routerDirection: 'none',
tab: currentTab
}
} else if (action === 'pop') {
const routeInfo = locationHistory.current();
if (routeInfo && routeInfo.pushedByRoute) {
const prevRouteInfo = locationHistory.findLastLocation(routeInfo);
incomingRouteParams = {
...prevRouteInfo,
routerAction: 'pop',
routerDirection: 'back'
};
} else {
incomingRouteParams = {
routerAction: 'pop',
routerDirection: 'none',
tab: currentTab
}
}
}
if (!incomingRouteParams) {
incomingRouteParams = {
routerAction: 'push',
routerDirection: direction || 'forward',
tab: currentTab
}
//console.log('No route params, setting', incomingRouteParams)
}
}
//console.log('Incoming Route Params', incomingRouteParams)
let routeInfo: RouteInfo;
if (incomingRouteParams?.id) {
routeInfo = {
...incomingRouteParams,
lastPathname: leavingLocationInfo.pathname
}
locationHistory.add(routeInfo);
//console.log('Incoming route params had id, current routeInfo', routeInfo)
} else {
const isPushed = incomingRouteParams.routerAction === 'push' && incomingRouteParams.routerDirection === 'forward';
routeInfo = {
id: generateId('routeInfo'),
...incomingRouteParams,
lastPathname: leavingLocationInfo.pathname,
pathname: location.path,
search: location.fullPath && location.fullPath.split('?')[1] || '',
params: location.params && location.params,
}
//console.log('No id on incoming route params', routeInfo)
if (isPushed) {
routeInfo.tab = leavingLocationInfo.tab;
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
//console.log('Was pushed', routeInfo);
} else if (routeInfo.routerAction === 'pop') {
const route = locationHistory.findLastLocation(routeInfo);
routeInfo.pushedByRoute = route?.pushedByRoute;
//console.log('action pop', routeInfo)
} else if (routeInfo.routerAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
const lastRoute = locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
//console.log('was push and switch tab', routeInfo)
} else if (routeInfo.routerAction === 'replace') {
const currentRouteInfo = locationHistory.current();
routeInfo.lastPathname = currentRouteInfo?.pathname || routeInfo.lastPathname;
routeInfo.pushedByRoute = currentRouteInfo?.pushedByRoute || routeInfo.pushedByRoute;
routeInfo.routerDirection = currentRouteInfo?.routerDirection || routeInfo.routerDirection;
routeInfo.routerAnimation = currentRouteInfo?.routerAnimation || routeInfo.routerAnimation;
//console.log('was repalce',routeInfo)
}
locationHistory.add(routeInfo);
}
currentRouteInfo = routeInfo;
}
incomingRouteParams = undefined;
historyChangeListeners.forEach(cb => cb(currentRouteInfo));
}
const getCurrentRouteInfo = () => currentRouteInfo;
const setInitialRoute = (routeInfo: RouteLocationNormalizedLoaded) => {
const info: RouteInfo = {
id: generateId('routeInfo'),
pathname: routeInfo.fullPath,
search: ''
}
locationHistory.add(info);
}
const canGoBack = (deep: number = 1) => locationHistory.canGoBack(deep);
const setIncomingRouteParams = (params: RouteParams) => {
incomingRouteParams = params;
}
const navigate = (navigationOptions: ExternalNavigationOptions) => {
const { routerAnimation, routerDirection, routerLink } = navigationOptions;
incomingRouteParams = {
routerAnimation,
routerDirection: routerDirection || 'forward',
routerAction: 'push'
}
router.push(routerLink);
}
const getLocationHistory = () => locationHistory;
const resetTab = (tab: string, originalHref: string) => {
const routeInfo = locationHistory.getFirstRouteInfoForTab(tab);
if (routeInfo) {
const newRouteInfo = { ...routeInfo };
newRouteInfo.pathname = originalHref;
incomingRouteParams = { ...newRouteInfo, routerAction: 'pop', routerDirection: 'back' };
router.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
}
}
const changeTab = (tab: string, path: string) => {
const routeInfo = locationHistory.getCurrentRouteInfoForTab(tab);
// TODO search
const [pathname] = path.split('?');
if (routeInfo) {
incomingRouteParams = Object.assign(Object.assign({}, routeInfo), { routerAction: 'push', routerDirection: 'none' });
router.push(routeInfo.pathname + (routeInfo.search || ''));
}
else {
handleNavigate(pathname, 'push', 'none', undefined, tab);
}
}
const handleSetCurrentTab = (tab: string) => {
currentTab = tab;
const ri = { ...locationHistory.current() };
if (ri.tab !== tab) {
ri.tab = tab;
locationHistory.update(ri);
}
}
// TODO types
const registerHistoryChangeListener = (cb: any) => {
historyChangeListeners.push(cb);
}
return {
handleHistoryChange,
handleNavigateBack,
handleSetCurrentTab,
getCurrentRouteInfo,
setInitialRoute,
canGoBack,
navigate,
getLocationHistory,
setIncomingRouteParams,
resetTab,
changeTab,
registerHistoryChangeListener
}
}

View File

@ -0,0 +1,51 @@
import { AnimationBuilder } from '@ionic/core';
import { RouterOptions } from 'vue-router';
export interface IonicVueRouterOptions extends RouterOptions {
tabsPrefix?: string;
}
export interface RouteInfo {
id?: string;
routerAction?: RouteAction;
routerDirection?: RouteDirection;
routerAnimation?: AnimationBuilder;
lastPathname?: string;
pathname?: string;
search?: string;
params?: { [k: string]: any };
pushedByRoute?: string;
tab?: string;
}
export interface RouteParams {
routerAction: RouteAction;
routerDirection: RouteDirection;
routerAnimation?: AnimationBuilder;
tab?: string;
id?: string;
}
export type RouteAction = 'push' | 'pop' | 'replace';
export type RouteDirection = 'forward' | 'back' | 'root' | 'none';
export interface ViewItem {
id: string;
pathname: string;
outletId: number;
matchedRoute: any; // todo
ionPageElement?: HTMLElement;
vueComponent: any; // todo
ionRoute: boolean;
mount: false;
}
export interface ViewStacks {
[k: string]: ViewItem[];
}
export interface ExternalNavigationOptions {
routerLink: string;
routerDirection?: RouteDirection;
routerAnimation?: AnimationBuilder;
}

View File

@ -0,0 +1,7 @@
const ids: { [k: string]: number } = { main: 0 };
export const generateId = (type = 'main') => {
const id = (ids[type] ?? 0) + 1;
ids[type] = id;
return (id).toString();
};

View File

@ -0,0 +1,104 @@
import { generateId } from './utils';
import { RouteInfo,
ViewItem,
ViewStacks,
} from './types';
export const createViewStacks = () => {
let viewStacks: ViewStacks = {};
const getViewStack = (outletId: number) => {
return viewStacks[outletId];
}
const registerIonPage = (viewItem: ViewItem, ionPage: HTMLElement) => {
viewItem.ionPageElement = ionPage;
}
const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => {
return findViewItemByPath(routeInfo.pathname, outletId);
}
const findLeavingViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => {
return findViewItemByPath(routeInfo.lastPathname, outletId);
}
const findViewItemInStack = (path: string, stack: ViewItem[]): ViewItem | undefined => {
return stack.find((viewItem: ViewItem) => {
if (viewItem.pathname === path) {
return viewItem;
}
return undefined;
})
}
const findViewItemByPath = (path: string, outletId?: number): ViewItem | undefined => {
if (outletId) {
const stack = viewStacks[outletId];
if (!stack) return undefined;
return findViewItemInStack(path, stack);
}
for (let outletId in viewStacks) {
const stack = viewStacks[outletId];
const viewItem = findViewItemInStack(path, stack);
if (viewItem) {
return viewItem;
}
}
return undefined;
}
const createViewItem = (outletId: number, vueComponent: any, matchedRoute: any, routeInfo: RouteInfo, ionPage?: HTMLElement): ViewItem => {
return {
id: generateId('viewItem'),
pathname: routeInfo.pathname,
outletId,
matchedRoute,
ionPageElement: ionPage,
vueComponent,
ionRoute: false,
mount: false
};
}
const add = (viewItem: ViewItem): void => {
const { outletId } = viewItem;
if (!viewStacks[outletId]) {
viewStacks[outletId] = [viewItem];
} else {
viewStacks[outletId].push(viewItem);
}
}
const remove = (viewItem: ViewItem, outletId?: number): void => {
if (!outletId) { throw Error('outletId required') }
const viewStack = viewStacks[outletId];
if (viewStack) {
viewStacks[outletId] = viewStack.filter(item => item.id !== viewItem.id);
}
}
const getChildrenToRender = (outletId: number): ViewItem[] => {
const viewStack = viewStacks[outletId];
if (viewStack) {
const components = viewStacks[outletId].filter(v => v.mount);
return components;
}
return [];
}
return {
findViewItemByRouteInfo,
findLeavingViewItemByRouteInfo,
createViewItem,
getChildrenToRender,
add,
remove,
registerIonPage,
getViewStack
}
}