mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 17:42:15 +08:00
feat(react): React Router Enhancements (#21693)
This commit is contained in:
@ -1,15 +1,35 @@
|
||||
import { Action as HistoryAction, Location as HistoryLocation, createHashHistory as createHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { HashRouter, HashRouterProps } from 'react-router-dom';
|
||||
import { BrowserRouterProps, Router } from 'react-router-dom';
|
||||
|
||||
import { RouteManagerWithRouter } from './Router';
|
||||
import { IonRouter } from './IonRouter';
|
||||
|
||||
export class IonReactHashRouter extends React.Component<BrowserRouterProps> {
|
||||
history = createHistory(this.props);
|
||||
historyListenHandler?: ((location: HistoryLocation, action: HistoryAction) => void);
|
||||
|
||||
constructor(props: BrowserRouterProps) {
|
||||
super(props);
|
||||
this.history.listen(this.handleHistoryChange.bind(this));
|
||||
this.registerHistoryListener = this.registerHistoryListener.bind(this);
|
||||
}
|
||||
|
||||
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
|
||||
if (this.historyListenHandler) {
|
||||
this.historyListenHandler(location, action);
|
||||
}
|
||||
}
|
||||
|
||||
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
|
||||
this.historyListenHandler = cb;
|
||||
}
|
||||
|
||||
export class IonReactHashRouter extends React.Component<HashRouterProps> {
|
||||
render() {
|
||||
const { children, ...props } = this.props;
|
||||
return (
|
||||
<HashRouter {...props}>
|
||||
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
|
||||
</HashRouter>
|
||||
<Router history={this.history} {...props}>
|
||||
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,40 @@
|
||||
import { MemoryHistory } from 'history';
|
||||
import { Action as HistoryAction, Location as HistoryLocation, MemoryHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { MemoryRouter, MemoryRouterProps, matchPath } from 'react-router';
|
||||
import { MemoryRouterProps, Router } from 'react-router';
|
||||
|
||||
import { LocationState, RouteManager } from './Router';
|
||||
import { IonRouter, LocationState } from './IonRouter';
|
||||
|
||||
interface IonReactMemoryRouterProps extends MemoryRouterProps {
|
||||
history: MemoryHistory<LocationState>;
|
||||
}
|
||||
|
||||
export class IonReactMemoryRouter extends React.Component<IonReactMemoryRouterProps> {
|
||||
history: MemoryHistory<LocationState>;
|
||||
historyListenHandler?: ((location: HistoryLocation, action: HistoryAction) => void);
|
||||
|
||||
constructor(props: IonReactMemoryRouterProps) {
|
||||
super(props);
|
||||
this.history = props.history;
|
||||
this.history.listen(this.handleHistoryChange.bind(this));
|
||||
this.registerHistoryListener = this.registerHistoryListener.bind(this);
|
||||
}
|
||||
|
||||
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
|
||||
if (this.historyListenHandler) {
|
||||
this.historyListenHandler(location, action);
|
||||
}
|
||||
}
|
||||
|
||||
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
|
||||
this.historyListenHandler = cb;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, history, ...props } = this.props;
|
||||
const match = matchPath(history.location.pathname, this.props);
|
||||
const { children, ...props } = this.props;
|
||||
return (
|
||||
<MemoryRouter {...props}>
|
||||
<RouteManager history={history} location={history.location} match={match!}>{children}</RouteManager>
|
||||
</MemoryRouter>
|
||||
<Router {...props}>
|
||||
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,35 @@
|
||||
import { Action as HistoryAction, Location as HistoryLocation, createBrowserHistory as createHistory } from 'history';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, BrowserRouterProps } from 'react-router-dom';
|
||||
import { BrowserRouterProps, Router } from 'react-router-dom';
|
||||
|
||||
import { RouteManagerWithRouter } from './Router';
|
||||
import { IonRouter } from './IonRouter';
|
||||
|
||||
export class IonReactRouter extends React.Component<BrowserRouterProps> {
|
||||
history = createHistory(this.props);
|
||||
historyListenHandler?: ((location: HistoryLocation, action: HistoryAction) => void);
|
||||
|
||||
constructor(props: BrowserRouterProps) {
|
||||
super(props);
|
||||
this.history.listen(this.handleHistoryChange.bind(this));
|
||||
this.registerHistoryListener = this.registerHistoryListener.bind(this);
|
||||
}
|
||||
|
||||
handleHistoryChange(location: HistoryLocation, action: HistoryAction) {
|
||||
if (this.historyListenHandler) {
|
||||
this.historyListenHandler(location, action);
|
||||
}
|
||||
}
|
||||
|
||||
registerHistoryListener(cb: (location: HistoryLocation, action: HistoryAction) => void) {
|
||||
this.historyListenHandler = cb;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, ...props } = this.props;
|
||||
return (
|
||||
<BrowserRouter {...props}>
|
||||
<RouteManagerWithRouter>{children}</RouteManagerWithRouter>
|
||||
</BrowserRouter>
|
||||
<Router history={this.history} {...props}>
|
||||
<IonRouter registerHistoryListener={this.registerHistoryListener}>{children}</IonRouter>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export type IonRouteAction = 'push' | 'replace' | 'pop';
|
@ -1,6 +0,0 @@
|
||||
import { RouteProps, match } from 'react-router-dom';
|
||||
|
||||
export interface IonRouteData {
|
||||
match: match | null;
|
||||
childProps: RouteProps;
|
||||
}
|
11
packages/react-router/src/ReactRouter/IonRouteInner.tsx
Normal file
11
packages/react-router/src/ReactRouter/IonRouteInner.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { IonRouteProps } from '@ionic/react';
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router';
|
||||
|
||||
export class IonRouteInner extends React.PureComponent<IonRouteProps> {
|
||||
render() {
|
||||
return (
|
||||
<Route path={this.props.path} exact={this.props.exact} render={this.props.render} computedMatch={(this.props as any).computedMatch} />
|
||||
);
|
||||
}
|
||||
}
|
264
packages/react-router/src/ReactRouter/IonRouter.tsx
Normal file
264
packages/react-router/src/ReactRouter/IonRouter.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
import {
|
||||
LocationHistory,
|
||||
NavManager,
|
||||
RouteAction,
|
||||
RouteInfo,
|
||||
RouteManagerContext,
|
||||
RouteManagerContextState,
|
||||
RouterDirection,
|
||||
ViewItem,
|
||||
generateId,
|
||||
getConfig
|
||||
} from '@ionic/react';
|
||||
import { Action as HistoryAction, Location as HistoryLocation } from 'history';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
|
||||
import { IonRouteInner } from './IonRouteInner';
|
||||
import { ReactRouterViewStack } from './ReactRouterViewStack';
|
||||
import StackManager from './StackManager';
|
||||
|
||||
export interface LocationState {
|
||||
direction?: RouterDirection;
|
||||
routerOptions?: { as?: string, unmount?: boolean; };
|
||||
}
|
||||
|
||||
interface IonRouteProps extends RouteComponentProps<{}, {}, LocationState> {
|
||||
registerHistoryListener: (cb: (location: HistoryLocation<any>, action: HistoryAction) => void) => void;
|
||||
}
|
||||
|
||||
interface IonRouteState {
|
||||
routeInfo: RouteInfo;
|
||||
}
|
||||
|
||||
class IonRouterInner extends React.PureComponent<IonRouteProps, IonRouteState> {
|
||||
currentTab?: string;
|
||||
exitViewFromOtherOutletHandlers: ((pathname: string) => ViewItem | undefined)[] = [];
|
||||
incomingRouteParams?: Partial<RouteInfo>;
|
||||
locationHistory = new LocationHistory();
|
||||
viewStack = new ReactRouterViewStack();
|
||||
routeMangerContextState: RouteManagerContextState = {
|
||||
clearOutlet: this.viewStack.clear,
|
||||
getViewItemForTransition: this.viewStack.getViewItemForTransition,
|
||||
getChildrenToRender: this.viewStack.getChildrenToRender,
|
||||
createViewItem: this.viewStack.createViewItem,
|
||||
findViewItemByRouteInfo: this.viewStack.findViewItemByRouteInfo,
|
||||
findLeavingViewItemByRouteInfo: this.viewStack.findLeavingViewItemByRouteInfo,
|
||||
addViewItem: this.viewStack.add,
|
||||
unMountViewItem: this.viewStack.remove
|
||||
};
|
||||
|
||||
constructor(props: IonRouteProps) {
|
||||
super(props);
|
||||
|
||||
const routeInfo = {
|
||||
id: generateId('routeInfo'),
|
||||
pathname: this.props.location.pathname,
|
||||
search: this.props.location.search
|
||||
};
|
||||
|
||||
this.locationHistory.add(routeInfo);
|
||||
this.handleChangeTab = this.handleChangeTab.bind(this);
|
||||
this.handleResetTab = this.handleResetTab.bind(this);
|
||||
this.handleNavigate = this.handleNavigate.bind(this);
|
||||
this.handleNavigateBack = this.handleNavigateBack.bind(this);
|
||||
this.props.registerHistoryListener(this.handleHistoryChange.bind(this));
|
||||
this.handleSetCurrentTab = this.handleSetCurrentTab.bind(this);
|
||||
|
||||
this.state = {
|
||||
routeInfo
|
||||
};
|
||||
}
|
||||
|
||||
handleChangeTab(tab: string, path: string, routeOptions?: any) {
|
||||
const routeInfo = this.locationHistory.getCurrentRouteInfoForTab(tab);
|
||||
const [pathname, search] = path.split('?');
|
||||
if (routeInfo) {
|
||||
this.incomingRouteParams = { ...routeInfo, routeAction: 'push', routeDirection: 'none' };
|
||||
if (routeInfo.pathname === pathname) {
|
||||
this.incomingRouteParams.routeOptions = routeOptions;
|
||||
this.props.history.push(routeInfo.pathname + (routeInfo.search || ''));
|
||||
} else {
|
||||
this.incomingRouteParams.pathname = pathname;
|
||||
this.incomingRouteParams.search = search ? '?' + search : undefined;
|
||||
this.incomingRouteParams.routeOptions = routeOptions;
|
||||
this.props.history.push(pathname + (search ? '?' + search : ''));
|
||||
}
|
||||
} else {
|
||||
this.handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
|
||||
}
|
||||
}
|
||||
|
||||
handleHistoryChange(location: HistoryLocation<LocationState>, action: HistoryAction) {
|
||||
let leavingLocationInfo: RouteInfo;
|
||||
if (this.incomingRouteParams) {
|
||||
if (this.incomingRouteParams.routeAction === 'replace') {
|
||||
leavingLocationInfo = this.locationHistory.previous();
|
||||
} else {
|
||||
leavingLocationInfo = this.locationHistory.current();
|
||||
}
|
||||
} else if (action === 'REPLACE') {
|
||||
leavingLocationInfo = this.locationHistory.previous();
|
||||
} else {
|
||||
leavingLocationInfo = this.locationHistory.current();
|
||||
}
|
||||
|
||||
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
||||
if (leavingUrl !== location.pathname) {
|
||||
if (!this.incomingRouteParams) {
|
||||
if (action === 'REPLACE') {
|
||||
this.incomingRouteParams = {
|
||||
routeAction: 'replace',
|
||||
routeDirection: 'none',
|
||||
tab: this.currentTab
|
||||
};
|
||||
}
|
||||
if (action === 'POP') {
|
||||
const ri = this.locationHistory.current();
|
||||
if (ri && ri.pushedByRoute) {
|
||||
const prevInfo = this.locationHistory.findLastLocation(ri);
|
||||
this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back' };
|
||||
} else {
|
||||
const direction = 'none';
|
||||
this.incomingRouteParams = {
|
||||
routeAction: 'pop',
|
||||
routeDirection: direction,
|
||||
tab: this.currentTab
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!this.incomingRouteParams) {
|
||||
this.incomingRouteParams = {
|
||||
routeAction: 'push',
|
||||
routeDirection: location.state?.direction || 'forward',
|
||||
routeOptions: location.state?.routerOptions,
|
||||
tab: this.currentTab
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let routeInfo: RouteInfo;
|
||||
|
||||
if (this.incomingRouteParams?.id) {
|
||||
routeInfo = {
|
||||
...this.incomingRouteParams as RouteInfo,
|
||||
lastPathname: leavingLocationInfo.pathname
|
||||
};
|
||||
this.locationHistory.add(routeInfo);
|
||||
} else {
|
||||
const isPushed = (this.incomingRouteParams.routeAction === 'push' && this.incomingRouteParams.routeDirection === 'forward');
|
||||
routeInfo = {
|
||||
id: generateId('routeInfo'),
|
||||
...this.incomingRouteParams,
|
||||
lastPathname: leavingLocationInfo.pathname,
|
||||
pathname: location.pathname,
|
||||
search: location.search,
|
||||
params: this.props.match.params
|
||||
};
|
||||
if (isPushed) {
|
||||
routeInfo.tab = leavingLocationInfo.tab;
|
||||
routeInfo.pushedByRoute = leavingLocationInfo.pathname;
|
||||
} else if (routeInfo.routeAction === 'pop') {
|
||||
const r = this.locationHistory.findLastLocation(routeInfo);
|
||||
routeInfo.pushedByRoute = r?.pushedByRoute;
|
||||
} else if (routeInfo.routeAction === 'push' && routeInfo.tab !== leavingLocationInfo.tab) {
|
||||
// If we are switching tabs grab the last route info for the tab and use its pushedByRoute
|
||||
const lastRoute = this.locationHistory.getCurrentRouteInfoForTab(routeInfo.tab);
|
||||
routeInfo.pushedByRoute = lastRoute?.pushedByRoute;
|
||||
}
|
||||
|
||||
this.locationHistory.add(routeInfo);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
routeInfo
|
||||
});
|
||||
}
|
||||
|
||||
this.incomingRouteParams = undefined;
|
||||
}
|
||||
|
||||
handleNavigate(path: string, routeAction: RouteAction, routeDirection?: RouterDirection, routeAnimation?: AnimationBuilder, routeOptions?: any, tab?: string) {
|
||||
this.incomingRouteParams = {
|
||||
routeAction,
|
||||
routeDirection,
|
||||
routeOptions,
|
||||
routeAnimation,
|
||||
tab
|
||||
};
|
||||
|
||||
if (routeAction === 'push') {
|
||||
this.props.history.push(path);
|
||||
} else {
|
||||
this.props.history.replace(path);
|
||||
}
|
||||
}
|
||||
|
||||
handleNavigateBack(defaultHref: string | RouteInfo = '/', routeAnimation?: AnimationBuilder) {
|
||||
const config = getConfig();
|
||||
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref' as any);
|
||||
const routeInfo = this.locationHistory.current();
|
||||
if (routeInfo && routeInfo.pushedByRoute) {
|
||||
const prevInfo = this.locationHistory.findLastLocation(routeInfo);
|
||||
if (prevInfo) {
|
||||
this.incomingRouteParams = { ...prevInfo, routeAction: 'pop', routeDirection: 'back', routeAnimation: routeAnimation || routeInfo.routeAnimation };
|
||||
if (routeInfo.lastPathname === routeInfo.pushedByRoute) {
|
||||
this.props.history.goBack();
|
||||
} else {
|
||||
this.props.history.replace(prevInfo.pathname + (prevInfo.search || ''));
|
||||
}
|
||||
} else {
|
||||
this.handleNavigate(defaultHref as string, 'pop', 'back');
|
||||
}
|
||||
} else {
|
||||
this.handleNavigate(defaultHref as string, 'pop', 'back');
|
||||
}
|
||||
}
|
||||
|
||||
handleResetTab(tab: string, originalHref: string, originalRouteOptions: any) {
|
||||
const routeInfo = this.locationHistory.getFirstRouteInfoForTab(tab);
|
||||
if (routeInfo) {
|
||||
const newRouteInfo = { ...routeInfo };
|
||||
newRouteInfo.pathname = originalHref;
|
||||
newRouteInfo.routeOptions = originalRouteOptions;
|
||||
this.incomingRouteParams = { ...newRouteInfo, routeAction: 'pop', routeDirection: 'back' };
|
||||
this.props.history.push(newRouteInfo.pathname + (newRouteInfo.search || ''));
|
||||
}
|
||||
}
|
||||
|
||||
handleSetCurrentTab(tab: string) {
|
||||
this.currentTab = tab;
|
||||
const ri = { ...this.locationHistory.current() };
|
||||
if (ri.tab !== tab) {
|
||||
ri.tab = tab;
|
||||
this.locationHistory.update(ri);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RouteManagerContext.Provider
|
||||
value={this.routeMangerContextState}
|
||||
>
|
||||
<NavManager
|
||||
ionRoute={IonRouteInner}
|
||||
ionRedirect={{}}
|
||||
stackManager={StackManager}
|
||||
routeInfo={this.state.routeInfo!}
|
||||
onNavigateBack={this.handleNavigateBack}
|
||||
onNavigate={this.handleNavigate}
|
||||
onSetCurrentTab={this.handleSetCurrentTab}
|
||||
onChangeTab={this.handleChangeTab}
|
||||
onResetTab={this.handleResetTab}
|
||||
locationHistory={this.locationHistory}
|
||||
>
|
||||
{this.props.children}
|
||||
</NavManager>
|
||||
</RouteManagerContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const IonRouter = withRouter(IonRouterInner);
|
||||
IonRouter.displayName = 'IonRouter';
|
@ -1,77 +0,0 @@
|
||||
import { RouterDirection } from '@ionic/core';
|
||||
import { NavContext, NavContextState } from '@ionic/react';
|
||||
import { Location as HistoryLocation, UnregisterCallback } from 'history';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { IonRouteAction } from './IonRouteAction';
|
||||
import { StackManager } from './StackManager';
|
||||
|
||||
interface NavManagerProps extends RouteComponentProps {
|
||||
onNavigateBack: (defaultHref?: string) => void;
|
||||
onNavigate: (ionRouteAction: IonRouteAction, path: string, state?: any) => void;
|
||||
}
|
||||
|
||||
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
|
||||
|
||||
listenUnregisterCallback: UnregisterCallback | undefined;
|
||||
|
||||
constructor(props: NavManagerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
goBack: this.goBack.bind(this),
|
||||
hasIonicRouter: () => true,
|
||||
navigate: this.navigate.bind(this),
|
||||
getStackManager: this.getStackManager.bind(this),
|
||||
getPageManager: this.getPageManager.bind(this),
|
||||
currentPath: this.props.location.pathname,
|
||||
registerIonPage: () => { return; } // overridden in View for each IonPage
|
||||
};
|
||||
|
||||
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
|
||||
this.setState({
|
||||
currentPath: location.pathname
|
||||
});
|
||||
});
|
||||
|
||||
if (document) {
|
||||
document.addEventListener('ionBackButton', (e: any) => {
|
||||
e.detail.register(0, (processNextHandler: () => void) => {
|
||||
this.props.history.goBack();
|
||||
processNextHandler();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listenUnregisterCallback) {
|
||||
this.listenUnregisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
goBack(defaultHref?: string) {
|
||||
this.props.onNavigateBack(defaultHref);
|
||||
}
|
||||
|
||||
navigate(path: string, direction?: RouterDirection | 'none', ionRouteAction: IonRouteAction = 'push') {
|
||||
this.props.onNavigate(ionRouteAction, path, direction);
|
||||
}
|
||||
|
||||
getPageManager() {
|
||||
return (children: any) => children;
|
||||
}
|
||||
|
||||
getStackManager() {
|
||||
return StackManager;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NavContext.Provider value={this.state}>
|
||||
{this.props.children}
|
||||
</NavContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
178
packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
Normal file
178
packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { IonRoute, RouteInfo, ViewItem, ViewLifeCycleManager, ViewStacks, generateId } from '@ionic/react';
|
||||
import React from 'react';
|
||||
import { matchPath } from 'react-router';
|
||||
|
||||
export class ReactRouterViewStack extends ViewStacks {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.createViewItem = this.createViewItem.bind(this);
|
||||
this.findViewItemByRouteInfo = this.findViewItemByRouteInfo.bind(this);
|
||||
this.findLeavingViewItemByRouteInfo = this.findLeavingViewItemByRouteInfo.bind(this);
|
||||
this.getChildrenToRender = this.getChildrenToRender.bind(this);
|
||||
this.getViewItemForTransition = this.getViewItemForTransition.bind(this);
|
||||
}
|
||||
|
||||
createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) {
|
||||
const viewItem: ViewItem = {
|
||||
id: generateId('viewItem'),
|
||||
outletId,
|
||||
ionPageElement: page,
|
||||
reactElement,
|
||||
mount: true,
|
||||
ionRoute: false
|
||||
};
|
||||
|
||||
const matchProps = {
|
||||
exact: reactElement.props.exact,
|
||||
path: reactElement.props.path || reactElement.props.from,
|
||||
component: reactElement.props.component
|
||||
};
|
||||
|
||||
const match = matchPath(routeInfo.pathname, matchProps);
|
||||
|
||||
if (reactElement.type === IonRoute) {
|
||||
viewItem.ionRoute = true;
|
||||
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
|
||||
}
|
||||
|
||||
viewItem.routeData = {
|
||||
match,
|
||||
childProps: reactElement.props
|
||||
};
|
||||
|
||||
return viewItem;
|
||||
}
|
||||
|
||||
getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) {
|
||||
const viewItems = this.getViewItemsForOutlet(outletId);
|
||||
|
||||
// Sync latest routes with viewItems
|
||||
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
||||
const viewItem = viewItems.find(v => {
|
||||
return matchComponent(child, v.routeData.childProps.path || v.routeData.childProps.from);
|
||||
});
|
||||
if (viewItem) {
|
||||
viewItem.reactElement = child;
|
||||
}
|
||||
});
|
||||
|
||||
const children = viewItems.map(viewItem => {
|
||||
|
||||
let clonedChild;
|
||||
if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
|
||||
clonedChild = (
|
||||
<ViewLifeCycleManager key={`view-${viewItem.id}`} mount={viewItem.mount} removeView={() => this.remove(viewItem)}>
|
||||
{React.cloneElement(viewItem.reactElement, {
|
||||
computedMatch: viewItem.routeData.match
|
||||
})}
|
||||
</ViewLifeCycleManager>
|
||||
);
|
||||
} else {
|
||||
const match = matchComponent(viewItem.reactElement, routeInfo.pathname);
|
||||
clonedChild = (
|
||||
<ViewLifeCycleManager key={`view-${viewItem.id}`} mount={viewItem.mount} removeView={() => this.remove(viewItem)}>
|
||||
{React.cloneElement(viewItem.reactElement, {
|
||||
computedMatch: viewItem.routeData.match
|
||||
})}
|
||||
</ViewLifeCycleManager>
|
||||
);
|
||||
|
||||
if (!match && viewItem.routeData.match) {
|
||||
viewItem.routeData.match = undefined;
|
||||
viewItem.mount = false;
|
||||
}
|
||||
}
|
||||
|
||||
return clonedChild;
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string) {
|
||||
const { viewItem, match } = this.findViewItemByPath(routeInfo.pathname, outletId);
|
||||
if (viewItem && match) {
|
||||
viewItem.routeData.match = match;
|
||||
}
|
||||
return viewItem;
|
||||
}
|
||||
|
||||
findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string) {
|
||||
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, false, true);
|
||||
return viewItem;
|
||||
}
|
||||
|
||||
getViewItemForTransition(pathname: string) {
|
||||
const { viewItem } = this.findViewItemByPath(pathname, undefined, true, true);
|
||||
return viewItem;
|
||||
}
|
||||
|
||||
private findViewItemByPath(pathname: string, outletId?: string, forceExact?: boolean, mustBeIonRoute?: boolean) {
|
||||
|
||||
let viewItem: ViewItem | undefined;
|
||||
let match: ReturnType<typeof matchPath> | undefined;
|
||||
let viewStack: ViewItem[];
|
||||
|
||||
if (outletId) {
|
||||
viewStack = this.getViewItemsForOutlet(outletId);
|
||||
viewStack.some(matchView);
|
||||
if (!viewItem) {
|
||||
viewStack.some(matchDefaultRoute);
|
||||
}
|
||||
} else {
|
||||
const viewItems = this.getAllViewItems();
|
||||
viewItems.some(matchView);
|
||||
if (!viewItem) {
|
||||
viewItems.some(matchDefaultRoute);
|
||||
}
|
||||
}
|
||||
|
||||
return { viewItem, match };
|
||||
|
||||
function matchView(v: ViewItem) {
|
||||
if (mustBeIonRoute && !v.ionRoute) {
|
||||
return false;
|
||||
}
|
||||
const matchProps = {
|
||||
exact: forceExact ? true : v.routeData.childProps.exact,
|
||||
path: v.routeData.childProps.path || v.routeData.childProps.from,
|
||||
component: v.routeData.childProps.component
|
||||
};
|
||||
const myMatch = matchPath(pathname, matchProps);
|
||||
if (myMatch) {
|
||||
viewItem = v;
|
||||
match = myMatch;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchDefaultRoute(v: ViewItem) {
|
||||
// try to find a route that doesn't have a path or from prop, that will be our default route
|
||||
if (!v.routeData.childProps.path && !v.routeData.childProps.from) {
|
||||
match = {
|
||||
path: pathname,
|
||||
url: pathname,
|
||||
isExact: true,
|
||||
params: {}
|
||||
};
|
||||
viewItem = v;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
|
||||
const matchProps = {
|
||||
exact: forceExact ? true : node.props.exact,
|
||||
path: node.props.path || node.props.from,
|
||||
component: node.props.component
|
||||
};
|
||||
const match = matchPath(pathname, matchProps);
|
||||
|
||||
return match;
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { ViewStacks } from './ViewStacks';
|
||||
|
||||
export interface RouteManagerContextState {
|
||||
syncView: (page: HTMLElement, viewId: string) => void;
|
||||
syncRoute: (route: any) => void;
|
||||
hideView: (viewId: string) => void;
|
||||
viewStacks: ViewStacks;
|
||||
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
|
||||
removeViewStack: (stack: string) => void;
|
||||
getRoute: (id: string) => any;
|
||||
}
|
||||
|
||||
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
|
||||
viewStacks: new ViewStacks(),
|
||||
syncView: () => { navContextNotFoundError(); },
|
||||
syncRoute: () => { navContextNotFoundError(); },
|
||||
hideView: () => { navContextNotFoundError(); },
|
||||
setupIonRouter: () => Promise.reject(navContextNotFoundError()),
|
||||
removeViewStack: () => { navContextNotFoundError(); },
|
||||
getRoute: () => { navContextNotFoundError(); }
|
||||
});
|
||||
|
||||
function navContextNotFoundError() {
|
||||
console.error('IonReactRouter not found, did you add it to the app?');
|
||||
}
|
@ -1,529 +0,0 @@
|
||||
import { NavDirection } from '@ionic/core';
|
||||
import { RouterDirection, getConfig } from '@ionic/react';
|
||||
import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
|
||||
|
||||
import { generateId, isDevMode } from '../utils';
|
||||
import { LocationHistory } from '../utils/LocationHistory';
|
||||
|
||||
import { IonRouteAction } from './IonRouteAction';
|
||||
import { IonRouteData } from './IonRouteData';
|
||||
import { NavManager } from './NavManager';
|
||||
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
||||
import { ViewItem } from './ViewItem';
|
||||
import { ViewStack, ViewStacks } from './ViewStacks';
|
||||
|
||||
export interface LocationState {
|
||||
direction?: RouterDirection;
|
||||
action?: IonRouteAction;
|
||||
}
|
||||
|
||||
interface RouteManagerProps extends RouteComponentProps<{}, {}, LocationState> {
|
||||
location: HistoryLocation<LocationState>;
|
||||
}
|
||||
|
||||
interface RouteManagerState extends RouteManagerContextState {
|
||||
location?: HistoryLocation<LocationState>;
|
||||
action?: IonRouteAction;
|
||||
}
|
||||
|
||||
export class RouteManager extends React.Component<RouteManagerProps, RouteManagerState> {
|
||||
listenUnregisterCallback: UnregisterCallback | undefined;
|
||||
activeIonPageId?: string;
|
||||
currentIonRouteAction?: IonRouteAction;
|
||||
currentRouteDirection?: RouterDirection;
|
||||
locationHistory = new LocationHistory();
|
||||
routes: { [key: string]: React.ReactElement<any>; } = {};
|
||||
ionPageElements: { [key: string]: HTMLElement; } = {};
|
||||
routerOutlets: { [key: string]: HTMLIonRouterOutletElement; } = {};
|
||||
firstRender = true;
|
||||
|
||||
constructor(props: RouteManagerProps) {
|
||||
super(props);
|
||||
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
|
||||
this.handleNavigate = this.handleNavigate.bind(this);
|
||||
this.navigateBack = this.navigateBack.bind(this);
|
||||
this.state = {
|
||||
viewStacks: new ViewStacks(),
|
||||
hideView: this.hideView.bind(this),
|
||||
setupIonRouter: this.setupIonRouter.bind(this),
|
||||
removeViewStack: this.removeViewStack.bind(this),
|
||||
syncView: this.syncView.bind(this),
|
||||
syncRoute: this.syncRoute.bind(this),
|
||||
getRoute: this.getRoute.bind(this)
|
||||
};
|
||||
|
||||
this.locationHistory.add({
|
||||
hash: window.location.hash,
|
||||
key: generateId(),
|
||||
pathname: window.location.pathname,
|
||||
search: window.location.search,
|
||||
state: {}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
|
||||
// Trigger a page change if the location or action is different
|
||||
if (this.state.location && prevState.location !== this.state.location || prevState.action !== this.state.action) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
this.setActiveView(this.state.location!, this.state.action!, viewStacks);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listenUnregisterCallback) {
|
||||
this.listenUnregisterCallback();
|
||||
}
|
||||
}
|
||||
|
||||
getRoute(id: string) {
|
||||
return this.routes[id];
|
||||
}
|
||||
|
||||
hideView(viewId: string) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
if (view) {
|
||||
view.show = false;
|
||||
view.isIonRoute = false;
|
||||
view.prevId = undefined;
|
||||
view.key = generateId();
|
||||
delete this.ionPageElements[view.id];
|
||||
this.setState({
|
||||
viewStacks
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
historyChange(location: HistoryLocation<LocationState>, action: HistoryAction) {
|
||||
const ionRouteAction = this.currentIonRouteAction === 'pop' ? 'pop' : action.toLowerCase() as IonRouteAction;
|
||||
let direction = this.currentRouteDirection;
|
||||
|
||||
if (ionRouteAction === 'push') {
|
||||
this.locationHistory.add(location);
|
||||
} else if (ionRouteAction === 'pop') {
|
||||
this.locationHistory.pop();
|
||||
direction = direction || 'back';
|
||||
} else if (ionRouteAction === 'replace') {
|
||||
this.locationHistory.replace(location);
|
||||
direction = 'none';
|
||||
}
|
||||
|
||||
if (direction === 'root') {
|
||||
this.locationHistory.clear();
|
||||
this.locationHistory.add(location);
|
||||
}
|
||||
|
||||
location.state = location.state || { direction };
|
||||
this.setState({
|
||||
location,
|
||||
action: ionRouteAction as IonRouteAction
|
||||
});
|
||||
this.currentRouteDirection = undefined;
|
||||
this.currentIonRouteAction = undefined;
|
||||
}
|
||||
|
||||
setActiveView(location: HistoryLocation<LocationState>, action: IonRouteAction, viewStacks: ViewStacks) {
|
||||
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
|
||||
let leavingView: ViewItem | undefined;
|
||||
const viewStackKeys = viewStacks.getKeys();
|
||||
let shouldTransitionPage = false;
|
||||
let leavingViewHtml: string | undefined;
|
||||
|
||||
viewStackKeys.forEach(key => {
|
||||
const { view: enteringView, viewStack: enteringViewStack, match } = viewStacks.findViewInfoByLocation(location, key);
|
||||
if (!enteringView || !enteringViewStack) {
|
||||
return;
|
||||
}
|
||||
|
||||
leavingView = viewStacks.findViewInfoById(this.activeIonPageId).view;
|
||||
|
||||
if (enteringView.isIonRoute) {
|
||||
enteringView.show = true;
|
||||
enteringView.mount = true;
|
||||
enteringView.routeData.match = match!;
|
||||
shouldTransitionPage = true;
|
||||
|
||||
this.activeIonPageId = enteringView.id;
|
||||
|
||||
if (leavingView) {
|
||||
if (action === 'push' && direction === 'forward') {
|
||||
/**
|
||||
* If the page is being pushed into the stack by another view,
|
||||
* record the view that originally directed to the new view for back button purposes.
|
||||
*/
|
||||
enteringView.prevId = leavingView.id;
|
||||
} else if (direction !== 'none') {
|
||||
leavingView.mount = false;
|
||||
this.removeOrphanedViews(enteringView, enteringViewStack);
|
||||
}
|
||||
|
||||
leavingViewHtml = enteringView.id === leavingView.id ? this.ionPageElements[leavingView.id].outerHTML : undefined;
|
||||
} else {
|
||||
// If there is not a leavingView, then we shouldn't provide a direction
|
||||
direction = undefined;
|
||||
}
|
||||
|
||||
} else {
|
||||
enteringView.show = true;
|
||||
enteringView.mount = true;
|
||||
enteringView.routeData.match = match!;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (leavingView) {
|
||||
if (!leavingView.isIonRoute) {
|
||||
leavingView.mount = false;
|
||||
leavingView.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
viewStacks
|
||||
}, () => {
|
||||
if (shouldTransitionPage) {
|
||||
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||
if (enteringView && viewStack) {
|
||||
const enteringEl = this.ionPageElements[enteringView.id];
|
||||
const leavingEl = leavingView && this.ionPageElements[leavingView.id];
|
||||
if (enteringEl) {
|
||||
let navDirection: NavDirection | undefined;
|
||||
if (leavingEl && leavingEl.innerHTML === '') {
|
||||
// Don't animate from an empty view
|
||||
navDirection = undefined;
|
||||
} else if (direction === 'none' || direction === 'root') {
|
||||
navDirection = undefined;
|
||||
} else {
|
||||
navDirection = direction;
|
||||
}
|
||||
const shouldGoBack = !!enteringView.prevId;
|
||||
const routerOutlet = this.routerOutlets[viewStack.id];
|
||||
this.commitView(
|
||||
enteringEl!,
|
||||
leavingEl!,
|
||||
routerOutlet,
|
||||
navDirection,
|
||||
shouldGoBack,
|
||||
leavingViewHtml);
|
||||
} else if (leavingEl) {
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if an IonPage was not eventually rendered in Dev Mode
|
||||
if (isDevMode()) {
|
||||
if (enteringView && enteringView.routeData.match!.url !== location.pathname) {
|
||||
setTimeout(() => {
|
||||
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||
if (view!.routeData.match!.url !== location.pathname) {
|
||||
console.warn('No IonPage was found to render. Make sure you wrap your page with an IonPage component.');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeOrphanedViews(view: ViewItem, viewStack: ViewStack) {
|
||||
// Note: This technique is a bit wonky for views that reference each other and get into a circular loop.
|
||||
// It can still remove a view that probably shouldn't be.
|
||||
const viewsToRemove = viewStack.views.filter(v => v.prevId === view.id);
|
||||
viewsToRemove.forEach(v => {
|
||||
// Don't remove if view is currently active
|
||||
if (v.id !== this.activeIonPageId) {
|
||||
this.removeOrphanedViews(v, viewStack);
|
||||
|
||||
// If view is not currently visible, go ahead and remove it from DOM
|
||||
const page = this.ionPageElements[v.id];
|
||||
if (page.classList.contains('ion-page-hidden')) {
|
||||
v.show = false;
|
||||
v.isIonRoute = false;
|
||||
v.prevId = undefined;
|
||||
v.key = generateId();
|
||||
delete this.ionPageElements[v.id];
|
||||
}
|
||||
v.mount = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
|
||||
const views: ViewItem[] = [];
|
||||
let activeId: string | undefined;
|
||||
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
|
||||
let foundMatch = false;
|
||||
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
||||
const routeId = generateId();
|
||||
this.routes[routeId] = child;
|
||||
views.push(createViewItem(child, routeId, this.props.history.location));
|
||||
});
|
||||
|
||||
if (!foundMatch) {
|
||||
const notFoundRoute = views.find(r => {
|
||||
// try to find a route that doesn't have a path or from prop, that will be our not found route
|
||||
return !r.routeData.childProps.path && !r.routeData.childProps.from;
|
||||
});
|
||||
if (notFoundRoute) {
|
||||
notFoundRoute.show = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
|
||||
|
||||
function createViewItem(child: React.ReactElement<any>, routeId: string, location: HistoryLocation) {
|
||||
const viewId = generateId();
|
||||
const key = generateId();
|
||||
|
||||
// const route = child;
|
||||
const matchProps = {
|
||||
exact: child.props.exact,
|
||||
path: child.props.path || child.props.from,
|
||||
component: child.props.component
|
||||
};
|
||||
const match: IonRouteData['match'] = matchPath(location.pathname, matchProps);
|
||||
const view: ViewItem<IonRouteData> = {
|
||||
id: viewId,
|
||||
key,
|
||||
routeData: {
|
||||
match,
|
||||
childProps: child.props
|
||||
},
|
||||
routeId,
|
||||
mount: true,
|
||||
show: !!match,
|
||||
isIonRoute: false
|
||||
};
|
||||
if (match && view.isIonRoute) {
|
||||
activeId = viewId;
|
||||
}
|
||||
if (!foundMatch && match) {
|
||||
foundMatch = true;
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) {
|
||||
this.setState(prevState => {
|
||||
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
|
||||
const newStack: ViewStack = {
|
||||
id: stack,
|
||||
views: stackItems
|
||||
};
|
||||
this.routerOutlets[stack] = routerOutlet;
|
||||
if (activeId) {
|
||||
this.activeIonPageId = activeId;
|
||||
}
|
||||
prevViewStacks.set(stack, newStack);
|
||||
return {
|
||||
viewStacks: prevViewStacks
|
||||
};
|
||||
}, () => {
|
||||
this.setupRouterOutlet(routerOutlet);
|
||||
});
|
||||
}
|
||||
|
||||
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
|
||||
|
||||
const canStart = () => {
|
||||
const config = getConfig();
|
||||
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
|
||||
if (swipeEnabled) {
|
||||
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||
return !!(view && view.prevId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onStart = () => {
|
||||
this.navigateBack();
|
||||
};
|
||||
routerOutlet.swipeHandler = {
|
||||
canStart,
|
||||
onStart,
|
||||
onEnd: _shouldContinue => true
|
||||
};
|
||||
}
|
||||
|
||||
removeViewStack(stack: string) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
viewStacks.delete(stack);
|
||||
this.setState({
|
||||
viewStacks
|
||||
});
|
||||
}
|
||||
|
||||
syncView(page: HTMLElement, viewId: string) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
if (view) {
|
||||
view.isIonRoute = true;
|
||||
this.ionPageElements[view.id] = page;
|
||||
this.setActiveView(this.state.location || this.props.location, this.state.action!, viewStacks);
|
||||
}
|
||||
}
|
||||
|
||||
syncRoute(routerOutlet: any) {
|
||||
const ionRouterOutlet = React.Children.only(routerOutlet) as React.ReactElement;
|
||||
|
||||
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
||||
for (const routeKey in this.routes) {
|
||||
const route = this.routes[routeKey];
|
||||
if (
|
||||
((route.props.path || route.props.from) === (child.props.path || child.props.from)) &&
|
||||
(route.props.exact === child.props.exact) &&
|
||||
(route.props.to === child.props.to)
|
||||
) {
|
||||
this.routes[routeKey] = child;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean, leavingViewHtml?: string) {
|
||||
if (!this.firstRender) {
|
||||
|
||||
if (!('componentOnReady' in ionRouterOutlet)) {
|
||||
await waitUntilRouterOutletReady(ionRouterOutlet);
|
||||
}
|
||||
|
||||
if ((enteringEl === leavingEl) && direction && leavingViewHtml) {
|
||||
// If a page is transitioning to another version of itself
|
||||
// we clone it so we can have an animation to show
|
||||
const newLeavingElement = clonePageElement(leavingViewHtml);
|
||||
ionRouterOutlet.appendChild(newLeavingElement);
|
||||
await ionRouterOutlet.commit(enteringEl, newLeavingElement, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
ionRouterOutlet.removeChild(newLeavingElement);
|
||||
} else {
|
||||
await ionRouterOutlet.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
}
|
||||
|
||||
if (leavingEl && (enteringEl !== leavingEl)) {
|
||||
/** add hidden attributes */
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
} else {
|
||||
enteringEl.classList.remove('ion-page-invisible');
|
||||
enteringEl.style.zIndex = '101';
|
||||
enteringEl.dispatchEvent(new Event('ionViewWillEnter'));
|
||||
enteringEl.dispatchEvent(new Event('ionViewDidEnter'));
|
||||
this.firstRender = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleNavigate(ionRouteAction: IonRouteAction, path: string, direction?: RouterDirection) {
|
||||
this.currentIonRouteAction = ionRouteAction;
|
||||
switch (ionRouteAction) {
|
||||
case 'push':
|
||||
this.currentRouteDirection = direction;
|
||||
this.props.history.push(path);
|
||||
break;
|
||||
case 'pop':
|
||||
this.currentRouteDirection = direction || 'back';
|
||||
this.props.history.replace(path);
|
||||
break;
|
||||
case 'replace':
|
||||
this.currentRouteDirection = 'none';
|
||||
this.props.history.replace(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
navigateBack(defaultHref?: string) {
|
||||
const config = getConfig();
|
||||
defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
|
||||
|
||||
const { view: leavingView } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||
if (leavingView) {
|
||||
if (leavingView.id === leavingView.prevId) {
|
||||
const previousLocation = this.locationHistory.previous();
|
||||
if (previousLocation) {
|
||||
this.handleNavigate('pop', previousLocation.pathname + previousLocation.search);
|
||||
} else {
|
||||
defaultHref && this.handleNavigate('pop', defaultHref);
|
||||
}
|
||||
} else {
|
||||
const { view: enteringView } = this.state.viewStacks.findViewInfoById(leavingView.prevId);
|
||||
if (enteringView) {
|
||||
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
|
||||
if (lastLocation) {
|
||||
this.handleNavigate('pop', lastLocation.pathname + lastLocation.search);
|
||||
} else {
|
||||
this.handleNavigate('pop', enteringView.routeData.match!.url);
|
||||
}
|
||||
} else {
|
||||
const currentLocation = this.locationHistory.previous();
|
||||
if (currentLocation) {
|
||||
this.handleNavigate('pop', currentLocation.pathname + currentLocation.search);
|
||||
} else {
|
||||
if (defaultHref) {
|
||||
this.handleNavigate('pop', defaultHref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (defaultHref) {
|
||||
this.handleNavigate('replace', defaultHref, 'back');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RouteManagerContext.Provider value={this.state}>
|
||||
<NavManager
|
||||
{...this.props}
|
||||
onNavigateBack={this.navigateBack}
|
||||
onNavigate={this.handleNavigate}
|
||||
>
|
||||
{this.props.children}
|
||||
</NavManager>
|
||||
</RouteManagerContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function clonePageElement(leavingViewHtml: string) {
|
||||
const newEl = document.createElement('div');
|
||||
newEl.innerHTML = leavingViewHtml;
|
||||
newEl.classList.add('ion-page-hidden');
|
||||
newEl.style.zIndex = '';
|
||||
// Remove an existing back button so the new element doesn't get two of them
|
||||
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
|
||||
if (ionBackButton[0]) {
|
||||
ionBackButton[0].innerHTML = '';
|
||||
}
|
||||
return newEl.firstChild as HTMLElement;
|
||||
}
|
||||
|
||||
async function waitUntilRouterOutletReady(ionRouterOutlet: HTMLIonRouterElement) {
|
||||
if ('componentOnReady' in ionRouterOutlet) {
|
||||
return;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
waitUntilRouterOutletReady(ionRouterOutlet);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export const RouteManagerWithRouter = withRouter(RouteManager);
|
||||
RouteManagerWithRouter.displayName = 'RouteManager';
|
@ -1,109 +1,239 @@
|
||||
import {
|
||||
RouteInfo,
|
||||
RouteManagerContext,
|
||||
StackContext,
|
||||
StackContextState,
|
||||
ViewItem,
|
||||
generateId
|
||||
} from '@ionic/react';
|
||||
import React from 'react';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
import { generateId, isDevMode } from '../utils';
|
||||
|
||||
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
||||
import { View } from './View';
|
||||
import { ViewItem } from './ViewItem';
|
||||
import { ViewTransitionManager } from './ViewTransitionManager';
|
||||
import { clonePageElement } from './clonePageElement';
|
||||
|
||||
interface StackManagerProps {
|
||||
id?: string;
|
||||
routeManager: RouteManagerContextState;
|
||||
children?: React.ReactNode;
|
||||
routeInfo: RouteInfo;
|
||||
}
|
||||
|
||||
interface StackManagerState { }
|
||||
|
||||
class StackManagerInner extends React.Component<StackManagerProps, StackManagerState> {
|
||||
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
|
||||
export class StackManager extends React.PureComponent<StackManagerProps, StackManagerState> {
|
||||
id: string;
|
||||
context!: React.ContextType<typeof RouteManagerContext>;
|
||||
ionRouterOutlet?: React.ReactElement;
|
||||
routerOutletElement: HTMLIonRouterOutletElement | undefined;
|
||||
|
||||
stackContextValue: StackContextState = {
|
||||
registerIonPage: this.registerIonPage.bind(this),
|
||||
isInOutlet: () => true
|
||||
};
|
||||
|
||||
constructor(props: StackManagerProps) {
|
||||
super(props);
|
||||
this.id = this.props.id || generateId();
|
||||
this.handleViewSync = this.handleViewSync.bind(this);
|
||||
this.handleHideView = this.handleHideView.bind(this);
|
||||
this.state = {};
|
||||
this.registerIonPage = this.registerIonPage.bind(this);
|
||||
this.transitionPage = this.transitionPage.bind(this);
|
||||
this.handlePageTransition = this.handlePageTransition.bind(this);
|
||||
this.id = generateId('routerOutlet');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.routeManager.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
|
||||
if (this.routerOutletElement) {
|
||||
// console.log(`SM Mount - ${this.routerOutletElement.id} (${this.id})`);
|
||||
this.handlePageTransition(this.props.routeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: StackManagerProps, state: StackManagerState) {
|
||||
props.routeManager.syncRoute(props.children);
|
||||
return state;
|
||||
componentDidUpdate(prevProps: StackManagerProps) {
|
||||
if (this.props.routeInfo.pathname !== prevProps.routeInfo.pathname) {
|
||||
this.handlePageTransition(this.props.routeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.routeManager.removeViewStack(this.id);
|
||||
// console.log(`SM UNMount - ${(this.routerOutletElement?.id as any).id} (${this.id})`);
|
||||
this.context.clearOutlet(this.id);
|
||||
}
|
||||
|
||||
handleViewSync(page: HTMLElement, viewId: string) {
|
||||
this.props.routeManager.syncView(page, viewId);
|
||||
async handlePageTransition(routeInfo: RouteInfo) {
|
||||
// let shouldReRender = false;
|
||||
|
||||
// If routerOutlet isn't quite ready, give it another try in a moment
|
||||
if (!this.routerOutletElement || !this.routerOutletElement.commit) {
|
||||
setTimeout(() => this.handlePageTransition(routeInfo), 10);
|
||||
} else {
|
||||
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
||||
const leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
|
||||
|
||||
if (!(routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward')) {
|
||||
const shouldLeavingViewBeRemoved = routeInfo.routeDirection !== 'none' && leavingViewItem && (enteringViewItem !== leavingViewItem);
|
||||
if (shouldLeavingViewBeRemoved) {
|
||||
leavingViewItem!.mount = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (leavingViewItem && routeInfo.routeOptions?.unmount) {
|
||||
leavingViewItem.mount = false;
|
||||
}
|
||||
|
||||
const enteringRoute = matchRoute(this.ionRouterOutlet?.props.children, routeInfo) as React.ReactElement;
|
||||
if (enteringViewItem) {
|
||||
enteringViewItem.reactElement = enteringRoute;
|
||||
}
|
||||
if (!enteringViewItem) {
|
||||
if (enteringRoute) {
|
||||
enteringViewItem = this.context.createViewItem(this.id, enteringRoute, routeInfo);
|
||||
this.context.addViewItem(enteringViewItem);
|
||||
}
|
||||
}
|
||||
if (enteringViewItem && enteringViewItem.ionPageElement) {
|
||||
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
|
||||
} else if (leavingViewItem && !enteringRoute && !enteringViewItem) {
|
||||
// If we have a leavingView but no entering view/route, we are probably leaving to
|
||||
// another outlet, so hide this leavingView. We do it in a timeout to give time for a
|
||||
// transition to finish.
|
||||
setTimeout(() => {
|
||||
if (leavingViewItem.ionPageElement) {
|
||||
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
|
||||
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
handleHideView(viewId: string) {
|
||||
this.props.routeManager.hideView(viewId);
|
||||
registerIonPage(page: HTMLElement, routeInfo: RouteInfo) {
|
||||
const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
|
||||
if (foundView) {
|
||||
foundView.ionPageElement = page;
|
||||
foundView.ionRoute = true;
|
||||
}
|
||||
this.handlePageTransition(routeInfo);
|
||||
}
|
||||
|
||||
renderChild(item: ViewItem, route: any) {
|
||||
const component = React.cloneElement(route, {
|
||||
computedMatch: item.routeData.match
|
||||
});
|
||||
return component;
|
||||
async transitionPage(routeInfo: RouteInfo, enteringViewItem: ViewItem, leavingViewItem?: ViewItem) {
|
||||
|
||||
const routerOutlet = this.routerOutletElement!;
|
||||
|
||||
const direction = (routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root')
|
||||
? undefined
|
||||
: routeInfo.routeDirection;
|
||||
|
||||
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
|
||||
if (leavingViewItem && leavingViewItem.ionPageElement && (enteringViewItem === leavingViewItem)) {
|
||||
// If a page is transitioning to another version of itself
|
||||
// we clone it so we can have an animation to show
|
||||
|
||||
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
|
||||
if (match) {
|
||||
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
|
||||
if (newLeavingElement) {
|
||||
this.routerOutletElement.appendChild(newLeavingElement);
|
||||
await runCommit(enteringViewItem.ionPageElement, newLeavingElement);
|
||||
this.routerOutletElement.removeChild(newLeavingElement);
|
||||
}
|
||||
} else {
|
||||
await runCommit(enteringViewItem.ionPageElement, undefined);
|
||||
}
|
||||
} else {
|
||||
await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
|
||||
if (leavingViewItem && leavingViewItem.ionPageElement) {
|
||||
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
|
||||
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommit(enteringEl: HTMLElement, leavingEl?: HTMLElement) {
|
||||
enteringEl.classList.add('ion-page');
|
||||
enteringEl.classList.add('ion-page-invisible');
|
||||
|
||||
await routerOutlet.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction: direction as any,
|
||||
showGoBack: direction === 'forward',
|
||||
progressAnimation: false,
|
||||
animationBuilder: routeInfo.routeAnimation
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const routeManager = this.props.routeManager;
|
||||
const viewStack = routeManager.viewStacks.get(this.id);
|
||||
const views = (viewStack || { views: [] }).views.filter(x => x.show);
|
||||
const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement;
|
||||
const childElements = views.map(view => {
|
||||
const route = routeManager.getRoute(view.routeId);
|
||||
return (
|
||||
<ViewTransitionManager
|
||||
id={view.id}
|
||||
key={view.key}
|
||||
mount={view.mount}
|
||||
>
|
||||
<View
|
||||
onViewSync={this.handleViewSync}
|
||||
onHideView={this.handleHideView}
|
||||
view={view}
|
||||
route={route}
|
||||
>
|
||||
{this.renderChild(view, route)}
|
||||
</View>
|
||||
</ViewTransitionManager>
|
||||
);
|
||||
});
|
||||
const { children } = this.props;
|
||||
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
|
||||
this.ionRouterOutlet = ionRouterOutlet;
|
||||
|
||||
const elementProps: any = {
|
||||
ref: this.routerOutletEl
|
||||
};
|
||||
const components = this.context.getChildrenToRender(
|
||||
this.id,
|
||||
this.ionRouterOutlet,
|
||||
this.props.routeInfo,
|
||||
() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
return (
|
||||
<StackContext.Provider value={this.stackContextValue}>
|
||||
{React.cloneElement(ionRouterOutlet as any, {
|
||||
ref: (node: HTMLIonRouterOutletElement) => {
|
||||
if (ionRouterOutlet.props.setRef) {
|
||||
ionRouterOutlet.props.setRef(node);
|
||||
}
|
||||
this.routerOutletElement = node;
|
||||
const { ref } = ionRouterOutlet as any;
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
components
|
||||
)}
|
||||
</StackContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
if (ionRouterOutlet.props.forwardedRef) {
|
||||
ionRouterOutlet.props.forwardedRef.current = this.routerOutletEl;
|
||||
}
|
||||
|
||||
if (isDevMode()) {
|
||||
elementProps['data-stack-id'] = this.id;
|
||||
}
|
||||
|
||||
const routerOutletChild = React.cloneElement(ionRouterOutlet, elementProps, childElements);
|
||||
|
||||
return routerOutletChild;
|
||||
static get contextType() {
|
||||
return RouteManagerContext;
|
||||
}
|
||||
}
|
||||
|
||||
const withContext = (Component: any) => {
|
||||
return (props: any) => (
|
||||
<RouteManagerContext.Consumer>
|
||||
{context => <Component {...props} routeManager={context} />}
|
||||
</RouteManagerContext.Consumer>
|
||||
);
|
||||
};
|
||||
export default StackManager;
|
||||
|
||||
export const StackManager = withContext(StackManagerInner);
|
||||
function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
|
||||
let matchedNode: React.ReactNode;
|
||||
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
|
||||
const matchProps = {
|
||||
exact: child.props.exact,
|
||||
path: child.props.path || child.props.from,
|
||||
component: child.props.component
|
||||
};
|
||||
const match = matchPath(routeInfo.pathname, matchProps);
|
||||
if (match) {
|
||||
matchedNode = child;
|
||||
}
|
||||
});
|
||||
|
||||
if (matchedNode) {
|
||||
return matchedNode;
|
||||
}
|
||||
// If we haven't found a node
|
||||
// try to find one that doesn't have a path or from prop, that will be our not found route
|
||||
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
|
||||
if (!(child.props.path || child.props.from)) {
|
||||
matchedNode = child;
|
||||
}
|
||||
});
|
||||
|
||||
return matchedNode;
|
||||
}
|
||||
|
||||
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
|
||||
const matchProps = {
|
||||
exact: forceExact ? true : node.props.exact,
|
||||
path: node.props.path || node.props.from,
|
||||
component: node.props.component
|
||||
};
|
||||
const match = matchPath(pathname, matchProps);
|
||||
|
||||
return match;
|
||||
}
|
||||
|
@ -1,99 +0,0 @@
|
||||
import { IonLifeCycleContext, NavContext } from '@ionic/react';
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router';
|
||||
|
||||
import { isDevMode } from '../utils';
|
||||
|
||||
import { ViewItem } from './ViewItem';
|
||||
|
||||
interface ViewProps extends React.HTMLAttributes<HTMLElement> {
|
||||
onViewSync: (page: HTMLElement, viewId: string) => void;
|
||||
onHideView: (viewId: string) => void;
|
||||
view: ViewItem;
|
||||
route: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The View component helps manage the IonPage's lifecycle and registration
|
||||
*/
|
||||
export class View extends React.Component<ViewProps, {}> {
|
||||
context!: React.ContextType<typeof IonLifeCycleContext>;
|
||||
ionPage?: HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
/**
|
||||
* If we can tell if view is a redirect, hide it so it will work again in future
|
||||
*/
|
||||
const { view, route } = this.props;
|
||||
if (route.type === Redirect) {
|
||||
this.props.onHideView(view.id);
|
||||
} else if (route.props.render && !view.isIonRoute) {
|
||||
// Test the render to see if it returns a redirect
|
||||
if (route.props.render().type === Redirect) {
|
||||
this.props.onHideView(view.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.ionPage) {
|
||||
this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
this.ionPage.removeEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
this.ionPage.removeEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
this.ionPage.removeEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
ionViewWillEnterHandler() {
|
||||
this.context.ionViewWillEnter();
|
||||
}
|
||||
|
||||
ionViewDidEnterHandler() {
|
||||
this.context.ionViewDidEnter();
|
||||
}
|
||||
|
||||
ionViewWillLeaveHandler() {
|
||||
this.context.ionViewWillLeave();
|
||||
}
|
||||
|
||||
ionViewDidLeaveHandler() {
|
||||
this.context.ionViewDidLeave();
|
||||
}
|
||||
|
||||
registerIonPage(page: HTMLElement) {
|
||||
this.ionPage = page;
|
||||
this.ionPage.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
this.ionPage.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
this.ionPage.addEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
this.ionPage.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
this.ionPage.classList.add('ion-page-invisible');
|
||||
if (isDevMode()) {
|
||||
this.ionPage.setAttribute('data-view-id', this.props.view.id);
|
||||
}
|
||||
this.props.onViewSync(page, this.props.view.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NavContext.Consumer>
|
||||
{value => {
|
||||
const newProvider = {
|
||||
...value,
|
||||
registerIonPage: this.registerIonPage.bind(this)
|
||||
};
|
||||
|
||||
return (
|
||||
<NavContext.Provider value={newProvider}>
|
||||
{this.props.children}
|
||||
</NavContext.Provider>
|
||||
);
|
||||
|
||||
}}
|
||||
</NavContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return IonLifeCycleContext;
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
export interface ViewItem<RouteData = any> {
|
||||
/** The generated id of the view */
|
||||
id: string;
|
||||
/** The key used by React. A new key is generated each time the view comes into the DOM so React thinks its a completely new element. */
|
||||
key: string;
|
||||
|
||||
routeId: string;
|
||||
|
||||
/** The routeData for the view. */
|
||||
routeData: RouteData;
|
||||
/** Used to track which page pushed the page into view. Used for back button purposes. */
|
||||
prevId?: string;
|
||||
/**
|
||||
* Mount is used for page transitions. If mount is false, it keeps the view in the DOM long enough to finish the transition.
|
||||
*/
|
||||
mount: boolean;
|
||||
/**
|
||||
* Show determines if the view will be in the DOM or not
|
||||
*/
|
||||
show: boolean;
|
||||
/**
|
||||
* An IonRoute is a Route that contains an IonPage. Only IonPages participate in transition and lifecycle events.
|
||||
*/
|
||||
isIonRoute: boolean;
|
||||
|
||||
/**
|
||||
* location of the view
|
||||
*/
|
||||
location?: string;
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
import { Location as HistoryLocation } from 'history';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
import { IonRouteData } from './IonRouteData';
|
||||
import { ViewItem } from './ViewItem';
|
||||
|
||||
export interface ViewStack {
|
||||
id: string;
|
||||
views: ViewItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The holistic view of all the Routes configured for an application inside of an IonRouterOutlet.
|
||||
*/
|
||||
export class ViewStacks {
|
||||
private viewStacks: { [key: string]: ViewStack | undefined; } = {};
|
||||
|
||||
get(key: string) {
|
||||
return this.viewStacks[key];
|
||||
}
|
||||
|
||||
set(key: string, viewStack: ViewStack) {
|
||||
this.viewStacks[key] = viewStack;
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
return Object.keys(this.viewStacks);
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
delete this.viewStacks[key];
|
||||
}
|
||||
|
||||
findViewInfoByLocation(location: HistoryLocation, viewKey: string) {
|
||||
let view: ViewItem<IonRouteData> | undefined;
|
||||
let match: IonRouteData['match'] | null | undefined;
|
||||
let viewStack: ViewStack | undefined;
|
||||
|
||||
viewStack = this.viewStacks[viewKey];
|
||||
if (viewStack) {
|
||||
viewStack.views.some(matchView);
|
||||
|
||||
if (!view) {
|
||||
viewStack.views.some(r => {
|
||||
// try to find a route that doesn't have a path or from prop, that will be our not found route
|
||||
if (!r.routeData.childProps.path && !r.routeData.childProps.from) {
|
||||
match = {
|
||||
path: location.pathname,
|
||||
url: location.pathname,
|
||||
isExact: true,
|
||||
params: {}
|
||||
};
|
||||
view = r;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { view, viewStack, match };
|
||||
|
||||
function matchView(v: ViewItem) {
|
||||
const matchProps = {
|
||||
exact: v.routeData.childProps.exact,
|
||||
path: v.routeData.childProps.path || v.routeData.childProps.from,
|
||||
component: v.routeData.childProps.component
|
||||
};
|
||||
const myMatch: IonRouteData['match'] | null | undefined = matchPath(location.pathname, matchProps);
|
||||
if (myMatch) {
|
||||
view = v;
|
||||
match = myMatch;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
findViewInfoById(id = '') {
|
||||
let view: ViewItem<IonRouteData> | undefined;
|
||||
let viewStack: ViewStack | undefined;
|
||||
const keys = this.getKeys();
|
||||
keys.some(key => {
|
||||
const vs = this.viewStacks[key];
|
||||
view = vs!.views.find(x => x.id === id);
|
||||
if (view) {
|
||||
viewStack = vs;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return { view, viewStack };
|
||||
}
|
||||
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
import { DefaultIonLifeCycleContext, IonLifeCycleContext } from '@ionic/react';
|
||||
import React from 'react';
|
||||
|
||||
import { RouteManagerContext } from './RouteManagerContext';
|
||||
|
||||
interface ViewTransitionManagerProps {
|
||||
id: string;
|
||||
mount: boolean;
|
||||
}
|
||||
|
||||
interface ViewTransitionManagerState {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the View's DOM lifetime by keeping it around long enough to complete page transitions before removing it.
|
||||
*/
|
||||
export class ViewTransitionManager extends React.Component<ViewTransitionManagerProps, ViewTransitionManagerState> {
|
||||
ionLifeCycleContext = new DefaultIonLifeCycleContext();
|
||||
_isMounted = false;
|
||||
context!: React.ContextType<typeof RouteManagerContext>;
|
||||
|
||||
constructor(props: ViewTransitionManagerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
show: true
|
||||
};
|
||||
|
||||
this.ionLifeCycleContext.onComponentCanBeDestroyed(() => {
|
||||
if (!this.props.mount) {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
show: false
|
||||
}, () => {
|
||||
this.context.hideView(this.props.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { show } = this.state;
|
||||
return (
|
||||
<IonLifeCycleContext.Provider value={this.ionLifeCycleContext}>
|
||||
{show && this.props.children}
|
||||
</IonLifeCycleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return RouteManagerContext;
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { IonApp, IonRouterOutlet, IonPage } from '@ionic/react';
|
||||
import { IonReactRouter } from '../IonReactRouter';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Route } from 'react-router';
|
||||
// import {Router} from '../Router';
|
||||
|
||||
describe('Router', () => {
|
||||
|
||||
|
||||
describe('on first page render', () => {
|
||||
|
||||
let IonTestApp: React.ComponentType<any>;
|
||||
|
||||
beforeEach(() => {
|
||||
IonTestApp = ({ Page }) => {
|
||||
return (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<IonRouterOutlet>
|
||||
<Route path="/" component={Page}></Route>
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
it('should be visible', () => {
|
||||
|
||||
const MyPage = () => {
|
||||
return (
|
||||
<IonPage className="ion-page-invisible">
|
||||
<div>hello</div>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
const { container } = render(<IonTestApp Page={MyPage} />);
|
||||
const page = container.getElementsByClassName('ion-page')[0];
|
||||
expect(page).not.toHaveClass('ion-page-invisible');
|
||||
expect(page).toHaveStyle('z-index: 101')
|
||||
|
||||
});
|
||||
|
||||
it('should fire initial lifecycle events', () => {
|
||||
const ionViewWillEnterListener = jest.fn();
|
||||
const ionViewDidEnterListener = jest.fn();
|
||||
|
||||
const MyPage = () => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current.addEventListener('ionViewWillEnter', ionViewWillEnterListener);
|
||||
ref.current.addEventListener('ionViewDidEnter', ionViewDidEnterListener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IonPage ref={ref}>
|
||||
<div>hello</div>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
render(<IonTestApp Page={MyPage} />);
|
||||
expect(ionViewWillEnterListener).toHaveBeenCalledTimes(1);
|
||||
expect(ionViewDidEnterListener).toHaveBeenCalledTimes(1);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});;
|
20
packages/react-router/src/ReactRouter/clonePageElement.ts
Normal file
20
packages/react-router/src/ReactRouter/clonePageElement.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export function clonePageElement(leavingViewHtml: string | HTMLElement) {
|
||||
let html: string;
|
||||
if (typeof leavingViewHtml === 'string') {
|
||||
html = leavingViewHtml;
|
||||
} else {
|
||||
html = leavingViewHtml.outerHTML;
|
||||
}
|
||||
if (document) {
|
||||
const newEl = document.createElement('div');
|
||||
newEl.innerHTML = html;
|
||||
newEl.style.zIndex = '';
|
||||
// Remove an existing back button so the new element doesn't get two of them
|
||||
const ionBackButton = newEl.getElementsByTagName('ion-back-button');
|
||||
if (ionBackButton[0]) {
|
||||
ionBackButton[0].remove();
|
||||
}
|
||||
return newEl.firstChild as HTMLElement;
|
||||
}
|
||||
return undefined;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export { IonReactRouter } from './IonReactRouter';
|
||||
export { IonReactHashRouter } from './IonReactHashRouter';
|
||||
export { IonReactMemoryRouter } from './IonReactMemoryRouter';
|
Reference in New Issue
Block a user