mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 18:17:31 +08:00
feature(react): rc2 release
* fix(): add a page with class ion-page back to ionrouteroutlet - fixes #19146 * wip * fix(react): attributes show up in dom * chore(): adding ion-page to core wip * wip * fix destroy method * wrap dom writes in raf * Add comments * fix(react): IonPage work * chore(): ionpage rc3 changelog text * fix(): syncing ion-page in a new way to get rid of timeout loop * chore(): ViewStacks refactor out of router * fix(): remove unused method in router * wip - before setActiveView rework * fix(): react router ion page work * chore(): cleanup and dev release * fix(): remove need to name tabs * chore(): adding dev mode helpers * fix(): adding className prop to back button fixes #19251 * fix(): routerDirection changes * chore(): rc2 release * fix(): fix react version in package * chores(): build kickoff
This commit is contained in:
6
packages/react-router/src/ReactRouter/IonRouteData.ts
Normal file
6
packages/react-router/src/ReactRouter/IonRouteData.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { match, RouteProps } from 'react-router-dom';
|
||||
|
||||
export interface IonRouteData {
|
||||
match: match<{ tab: string }> | null;
|
||||
childProps: RouteProps;
|
||||
}
|
@ -3,15 +3,16 @@ import { NavContext, NavContextState } from '@ionic/react';
|
||||
import { Location as HistoryLocation, UnregisterCallback } from 'history';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { ViewManager } from './ViewManager';
|
||||
import { StackManager } from './StackManager';
|
||||
import { generateUniqueId } from '../utils';
|
||||
import { LocationHistory } from '../utils/LocationHistory'
|
||||
import { ViewItem } from './ViewItem';
|
||||
import { ViewStack } from './RouteManagerContext';
|
||||
import { ViewStack } from './ViewStacks';
|
||||
|
||||
interface NavManagerProps extends RouteComponentProps {
|
||||
findViewInfoByLocation: (location: HistoryLocation) => {view?: ViewItem, viewStack?: ViewStack };
|
||||
findViewInfoById: (id: string) => {view?: ViewItem, viewStack?: ViewStack };
|
||||
getActiveIonPage: () => {view?: ViewItem, viewStack?: ViewStack };
|
||||
};
|
||||
interface NavManagerState extends NavContextState {};
|
||||
|
||||
@ -28,8 +29,10 @@ export class NavManager extends React.Component<NavManagerProps, NavManagerState
|
||||
getHistory: this.getHistory.bind(this),
|
||||
getLocation: this.getLocation.bind(this),
|
||||
navigate: this.navigate.bind(this),
|
||||
getViewManager: this.getViewManager.bind(this),
|
||||
currentPath: this.props.location.pathname
|
||||
getStackManager: this.getStackManager.bind(this),
|
||||
getPageManager: this.getPageManager.bind(this),
|
||||
currentPath: this.props.location.pathname,
|
||||
registerIonPage: () => {} //overridden in View for each IonPage
|
||||
}
|
||||
|
||||
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
|
||||
@ -55,9 +58,9 @@ export class NavManager extends React.Component<NavManagerProps, NavManagerState
|
||||
}
|
||||
|
||||
goBack(defaultHref?: string) {
|
||||
const { view: leavingView } = this.props.findViewInfoByLocation(this.props.location);
|
||||
if (leavingView) {
|
||||
const { view: enteringView } = this.props.findViewInfoById(leavingView.prevId!);
|
||||
const { view: activeIonPage } = this.props.getActiveIonPage();
|
||||
if (activeIonPage) {
|
||||
const { view: enteringView } = this.props.findViewInfoById(activeIonPage.prevId!);
|
||||
if (enteringView) {
|
||||
const lastLocation = this.locationHistory.findLastLocation(enteringView.routeData.match.url);
|
||||
if (lastLocation) {
|
||||
@ -81,12 +84,16 @@ export class NavManager extends React.Component<NavManagerProps, NavManagerState
|
||||
return this.props.location as any;
|
||||
}
|
||||
|
||||
navigate(path: string, direction?: RouterDirection) {
|
||||
navigate(path: string, direction?: RouterDirection | 'none') {
|
||||
this.props.history.push(path, { direction });
|
||||
}
|
||||
|
||||
getViewManager() {
|
||||
return ViewManager;
|
||||
getPageManager() {
|
||||
return (children: any) => children;
|
||||
}
|
||||
|
||||
getStackManager() {
|
||||
return StackManager;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -1,32 +1,22 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { NavDirection } from '@ionic/core';
|
||||
import { ViewItem } from './ViewItem';
|
||||
|
||||
export interface ViewStack {
|
||||
routerOutlet: HTMLIonRouterOutletElement;
|
||||
activeId?: string,
|
||||
views: ViewItem[]
|
||||
}
|
||||
|
||||
export interface ViewStacks {
|
||||
[key: string]: ViewStack;
|
||||
}
|
||||
import { ViewStacks } from './ViewStacks';
|
||||
|
||||
export interface RouteManagerContextState {
|
||||
syncView: (page: HTMLElement, viewId: string) => void;
|
||||
hideView: (viewId: string) => void;
|
||||
viewStacks: ViewStacks;
|
||||
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
|
||||
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => Promise<void>;
|
||||
removeViewStack: (stack: string) => void;
|
||||
renderChild: (item: ViewItem) => void;
|
||||
transitionView: (enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) => void;
|
||||
}
|
||||
|
||||
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
|
||||
viewStacks: {},
|
||||
viewStacks: new ViewStacks(),
|
||||
syncView: () => { navContextNotFoundError(); },
|
||||
hideView: () => { navContextNotFoundError(); },
|
||||
setupIonRouter: () => { navContextNotFoundError() },
|
||||
setupIonRouter: () => { return Promise.reject(navContextNotFoundError()) },
|
||||
removeViewStack: () => { navContextNotFoundError(); },
|
||||
renderChild: () => { navContextNotFoundError(); },
|
||||
transitionView: () => { navContextNotFoundError(); }
|
||||
});
|
||||
|
||||
|
@ -1,44 +1,54 @@
|
||||
import { NavDirection } from '@ionic/core';
|
||||
import { RouterDirection } from '@ionic/react';
|
||||
import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history';
|
||||
import React from 'react';
|
||||
import { BrowserRouter, BrowserRouterProps, match, matchPath, Redirect, Route, RouteComponentProps, RouteProps, withRouter } from 'react-router-dom';
|
||||
import { BrowserRouter, BrowserRouterProps, matchPath, RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { generateUniqueId } from '../utils';
|
||||
import { IonRouteData } from './IonRouteData';
|
||||
import { NavManager } from './NavManager';
|
||||
import { RouteManagerContext, RouteManagerContextState, ViewStack, ViewStacks } from './RouteManagerContext';
|
||||
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
||||
import { ViewItem } from './ViewItem';
|
||||
import { ViewStacks, ViewStack } from './ViewStacks';
|
||||
|
||||
interface RouterManagerProps extends RouteComponentProps { }
|
||||
|
||||
interface RouteManagerState extends RouteManagerContextState { }
|
||||
interface RouteManagerProps extends RouteComponentProps { }
|
||||
|
||||
interface IonRouteData {
|
||||
match: match<{ tab: string }> | null;
|
||||
childProps: RouteProps;
|
||||
interface RouteManagerState extends RouteManagerContextState {
|
||||
location?: HistoryLocation,
|
||||
action?: HistoryAction
|
||||
}
|
||||
|
||||
class RouteManager extends React.Component<RouterManagerProps, RouteManagerState> {
|
||||
class RouteManager extends React.Component<RouteManagerProps, RouteManagerState> {
|
||||
listenUnregisterCallback: UnregisterCallback | undefined;
|
||||
activeViewId?: string;
|
||||
prevViewId?: string;
|
||||
activeIonPageId?: string;
|
||||
|
||||
constructor(props: RouterManagerProps) {
|
||||
constructor(props: RouteManagerProps) {
|
||||
super(props);
|
||||
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
|
||||
this.state = {
|
||||
viewStacks: {},
|
||||
viewStacks: new ViewStacks(),
|
||||
hideView: this.hideView.bind(this),
|
||||
setupIonRouter: this.setupIonRouter.bind(this),
|
||||
removeViewStack: this.removeViewStack.bind(this),
|
||||
renderChild: this.renderChild.bind(this),
|
||||
transitionView: this.transitionView.bind(this)
|
||||
syncView: this.syncView.bind(this),
|
||||
transitionView: this.transitionView.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: RouteManagerProps, 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) {
|
||||
this.setActiveView(this.state.location!, this.state.action!);
|
||||
}
|
||||
}
|
||||
|
||||
hideView(viewId: string) {
|
||||
const viewStacks = Object.assign({}, this.state.viewStacks);
|
||||
const { view } = this.findViewInfoById(viewId, viewStacks);
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
if (view) {
|
||||
view.show = false;
|
||||
view.ionPageElement = undefined;
|
||||
view.isIonRoute = false;
|
||||
view.key = generateUniqueId();
|
||||
this.setState({
|
||||
viewStacks
|
||||
@ -47,146 +57,111 @@ class RouteManager extends React.Component<RouterManagerProps, RouteManagerState
|
||||
}
|
||||
|
||||
historyChange(location: HistoryLocation, action: HistoryAction) {
|
||||
this.setActiveView(location, action);
|
||||
}
|
||||
|
||||
findViewInfoByLocation(location: HistoryLocation, viewStacks: ViewStacks) {
|
||||
let view: ViewItem<IonRouteData> | undefined;
|
||||
let match: IonRouteData["match"] | null | undefined;
|
||||
let viewStack: ViewStack | undefined;
|
||||
const keys = Object.keys(viewStacks);
|
||||
keys.some(key => {
|
||||
const vs = viewStacks[key];
|
||||
return vs.views.some(x => {
|
||||
const matchProps = {
|
||||
exact: x.routeData.childProps.exact,
|
||||
path: x.routeData.childProps.path || x.routeData.childProps.from,
|
||||
component: x.routeData.childProps.component
|
||||
};
|
||||
match = matchPath(location.pathname, matchProps)
|
||||
if (match) {
|
||||
view = x;
|
||||
viewStack = vs;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this.setState({
|
||||
location,
|
||||
action
|
||||
})
|
||||
|
||||
const result = { view, viewStack, match };
|
||||
return result;
|
||||
}
|
||||
|
||||
findViewInfoById(id: string, viewStacks: ViewStacks) {
|
||||
let view: ViewItem<IonRouteData> | undefined;
|
||||
let viewStack: ViewStack | undefined;
|
||||
const keys = Object.keys(viewStacks);
|
||||
keys.some(key => {
|
||||
const vs = viewStacks[key];
|
||||
view = vs.views.find(x => x.id === id);
|
||||
if (view) {
|
||||
viewStack = vs;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return { view, viewStack };
|
||||
}
|
||||
|
||||
setActiveView(location: HistoryLocation, action: HistoryAction) {
|
||||
const viewStacks = Object.assign({}, this.state.viewStacks);
|
||||
const { view: enteringView, viewStack: enteringViewStack, match } = this.findViewInfoByLocation(location, viewStacks);
|
||||
let direction: NavDirection = location.state && location.state.direction;
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
let direction: RouterDirection = location.state && location.state.direction || 'forward';
|
||||
let leavingView: ViewItem | undefined;
|
||||
const viewStackKeys = viewStacks.getKeys();
|
||||
|
||||
if (!enteringViewStack) {
|
||||
return;
|
||||
}
|
||||
viewStackKeys.forEach(key => {
|
||||
const { view: enteringView, viewStack: enteringViewStack, match } = viewStacks.findViewInfoByLocation(location, key);
|
||||
if (!enteringView || !enteringViewStack) {
|
||||
return;
|
||||
}
|
||||
leavingView = viewStacks.findViewInfoById(this.activeIonPageId).view;
|
||||
|
||||
const { view: leavingView } = this.findViewInfoById(this.activeViewId!, viewStacks);
|
||||
|
||||
if (leavingView && leavingView.routeData.match!.url === location.pathname) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enteringView) {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
if (!enteringView.show && action === 'PUSH') {
|
||||
enteringView.prevId = leavingView && leavingView.id;
|
||||
if (leavingView && leavingView.routeData.match!.url === location.pathname) {
|
||||
return;
|
||||
}
|
||||
|
||||
enteringView.show = true;
|
||||
enteringView.mount = true;
|
||||
enteringView.routeData.match = match!;
|
||||
enteringViewStack.activeId = enteringView.id;
|
||||
this.activeViewId = enteringView.id;
|
||||
if (enteringView) {
|
||||
|
||||
if (leavingView) {
|
||||
this.prevViewId = leavingView.id
|
||||
if (leavingView.routeData.match!.params.tab === enteringView.routeData.match.params.tab) {
|
||||
if (action === 'PUSH') {
|
||||
direction = direction || 'forward';
|
||||
} else {
|
||||
direction = direction || 'back';
|
||||
leavingView.mount = false;
|
||||
if (enteringView.isIonRoute) {
|
||||
enteringView.show = true;
|
||||
enteringView.mount = true;
|
||||
enteringView.routeData.match = match!;
|
||||
|
||||
this.activeIonPageId = enteringView.id;
|
||||
|
||||
if (leavingView) {
|
||||
if (direction === 'forward') {
|
||||
if (action === 'PUSH') {
|
||||
/**
|
||||
* 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 {
|
||||
direction = direction || 'back';
|
||||
leavingView.mount = false;
|
||||
}
|
||||
} else if (action === 'REPLACE') {
|
||||
leavingView.mount = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If the leaving view is a Redirect, take it out of the rendering phase.
|
||||
*/
|
||||
if(leavingView.element.type === Redirect) {
|
||||
leavingView.mount = false;
|
||||
leavingView.show = false;
|
||||
}
|
||||
|
||||
|
||||
if (leavingView.element.type === Route && leavingView.element.props.render) {
|
||||
if (leavingView.element.props.render().type === Redirect) {
|
||||
leavingView.mount = false;
|
||||
leavingView.show = false;
|
||||
}
|
||||
} else if (leavingView.element.type === Redirect) {
|
||||
leavingView.mount = false;
|
||||
leavingView.show = false;
|
||||
} else {
|
||||
enteringView.show = true;
|
||||
enteringView.mount = true;
|
||||
enteringView.routeData.match = match!;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
viewStacks
|
||||
}, () => {
|
||||
const enteringEl = enteringView.ref && enteringView.ref.current ? enteringView.ref.current : undefined;
|
||||
const leavingEl = leavingView && leavingView.ref && leavingView.ref.current ? leavingView.ref.current : undefined;
|
||||
this.transitionView(
|
||||
enteringEl!,
|
||||
leavingEl!,
|
||||
enteringViewStack.routerOutlet,
|
||||
leavingEl && leavingEl.innerHTML !== '' ? direction : undefined!) // Don't animate from an empty view
|
||||
});
|
||||
if (leavingView) {
|
||||
if (!leavingView.isIonRoute) {
|
||||
leavingView.mount = false;
|
||||
leavingView.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
viewStacks
|
||||
}, () => {
|
||||
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId)
|
||||
if (enteringView && viewStack) {
|
||||
const enteringEl = enteringView.ionPageElement ? enteringView.ionPageElement : undefined;
|
||||
const leavingEl = leavingView && leavingView.ionPageElement ? leavingView.ionPageElement : undefined;
|
||||
|
||||
if (enteringEl) {
|
||||
// Don't animate from an empty view
|
||||
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
|
||||
this.transitionView(
|
||||
enteringEl!,
|
||||
leavingEl!,
|
||||
viewStack.routerOutlet,
|
||||
navDirection)
|
||||
} else if (leavingEl) {
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.listenUnregisterCallback && this.listenUnregisterCallback();
|
||||
}
|
||||
|
||||
setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
|
||||
async setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
|
||||
const views: ViewItem[] = [];
|
||||
let activeId: string | undefined;
|
||||
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
|
||||
|
||||
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
|
||||
views.push(createViewItem(child, this.props.history.location));
|
||||
});
|
||||
|
||||
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
|
||||
await this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
|
||||
|
||||
function createViewItem(child: React.ReactElement<any>, location: HistoryLocation) {
|
||||
const viewId = generateUniqueId();
|
||||
const key = generateUniqueId();
|
||||
const element = child;
|
||||
const route = child;
|
||||
const matchProps = {
|
||||
exact: child.props.exact,
|
||||
path: child.props.path || child.props.from,
|
||||
@ -200,89 +175,87 @@ class RouteManager extends React.Component<RouterManagerProps, RouteManagerState
|
||||
match,
|
||||
childProps: child.props
|
||||
},
|
||||
element,
|
||||
route: route,
|
||||
mount: true,
|
||||
show: !!match,
|
||||
ref: React.createRef()
|
||||
isIonRoute: false
|
||||
};
|
||||
if (!!match) {
|
||||
if (!!match && view.isIonRoute) {
|
||||
activeId = viewId;
|
||||
};
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, location: HistoryLocation) {
|
||||
this.setState((prevState) => {
|
||||
const prevViewStacks = Object.assign({}, prevState.viewStacks);
|
||||
prevViewStacks[stack] = {
|
||||
activeId: activeId,
|
||||
views: stackItems,
|
||||
routerOutlet
|
||||
};
|
||||
return {
|
||||
viewStacks: prevViewStacks
|
||||
};
|
||||
}, () => {
|
||||
const { view: activeView } = this.findViewInfoById(activeId!, this.state.viewStacks);
|
||||
async registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) {
|
||||
|
||||
if (activeView) {
|
||||
this.prevViewId = this.activeViewId;
|
||||
this.activeViewId = activeView.id;
|
||||
const direction = location.state && location.state.direction;
|
||||
const { view: prevView } = this.findViewInfoById(this.prevViewId!, this.state.viewStacks);
|
||||
this.transitionView(
|
||||
activeView.ref!.current!,
|
||||
prevView && prevView.ref!.current || undefined!,
|
||||
routerOutlet,
|
||||
direction);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.setState((prevState) => {
|
||||
const prevViewStacks = Object.assign(new ViewStacks, prevState.viewStacks);
|
||||
const newStack: ViewStack = {
|
||||
id: stack,
|
||||
views: stackItems,
|
||||
routerOutlet
|
||||
};
|
||||
if (activeId) {
|
||||
this.activeIonPageId = activeId;
|
||||
}
|
||||
prevViewStacks.set(stack, newStack);
|
||||
return {
|
||||
viewStacks: prevViewStacks
|
||||
};
|
||||
}, () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
removeViewStack(stack: string) {
|
||||
const viewStacks = Object.assign({}, this.state.viewStacks);
|
||||
delete viewStacks[stack];
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
viewStacks.delete(stack);
|
||||
this.setState({
|
||||
viewStacks
|
||||
});
|
||||
}
|
||||
|
||||
renderChild(item: ViewItem<IonRouteData>) {
|
||||
const component = React.cloneElement(item.element, {
|
||||
computedMatch: item.routeData.match
|
||||
});
|
||||
return component;
|
||||
}
|
||||
syncView(page: HTMLElement, viewId: string) {
|
||||
this.setState((state) => {
|
||||
|
||||
findActiveView(views: ViewItem[]) {
|
||||
let view: ViewItem<IonRouteData> | undefined;
|
||||
views.some(x => {
|
||||
const match = matchPath(this.props.location.pathname, x.routeData.childProps)
|
||||
if (match) {
|
||||
view = x;
|
||||
return true;
|
||||
const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
|
||||
view!.ionPageElement = page;
|
||||
view!.isIonRoute = true;
|
||||
|
||||
return {
|
||||
viewStacks
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return view;
|
||||
|
||||
}, () => {
|
||||
this.setActiveView(this.state.location || this.props.location, this.state.action!);
|
||||
})
|
||||
}
|
||||
|
||||
transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) {
|
||||
transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection) {
|
||||
/**
|
||||
* Super hacky workaround to make sure ionRouterOutlet is available
|
||||
* since transitionView might be called before IonRouterOutlet is fully mounted
|
||||
*/
|
||||
if (ionRouterOuter && ionRouterOuter.componentOnReady) {
|
||||
this.commitView(enteringEl, leavingEl, ionRouterOuter, direction);
|
||||
if (ionRouterOutlet && ionRouterOutlet.componentOnReady) {
|
||||
this.commitView(enteringEl, leavingEl, ionRouterOutlet, direction);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.transitionView(enteringEl, leavingEl, ionRouterOuter, direction);
|
||||
this.transitionView(enteringEl, leavingEl, ionRouterOutlet, direction);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction: NavDirection) {
|
||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection) {
|
||||
|
||||
if (enteringEl === leavingEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ionRouterOuter.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
@ -292,9 +265,7 @@ class RouteManager extends React.Component<RouterManagerProps, RouteManagerState
|
||||
});
|
||||
|
||||
if (leavingEl && (enteringEl !== leavingEl)) {
|
||||
/**
|
||||
* add hidden attributes
|
||||
*/
|
||||
/** add hidden attributes */
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
@ -304,8 +275,9 @@ class RouteManager extends React.Component<RouterManagerProps, RouteManagerState
|
||||
return (
|
||||
<RouteManagerContext.Provider value={this.state}>
|
||||
<NavManager {...this.props}
|
||||
findViewInfoById={(id: string) => this.findViewInfoById(id, this.state.viewStacks)}
|
||||
findViewInfoByLocation={(location: HistoryLocation) => this.findViewInfoByLocation(location, this.state.viewStacks)}
|
||||
findViewInfoById={(id: string) => this.state.viewStacks.findViewInfoById(id)}
|
||||
findViewInfoByLocation={(location: HistoryLocation) => this.state.viewStacks.findViewInfoByLocation(location)}
|
||||
getActiveIonPage={() => this.state.viewStacks.findViewInfoById(this.activeIonPageId)}
|
||||
>
|
||||
{this.props.children}
|
||||
</NavManager>
|
||||
|
90
packages/react-router/src/ReactRouter/StackManager.tsx
Normal file
90
packages/react-router/src/ReactRouter/StackManager.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { generateUniqueId, isDevMode } from '../utils';
|
||||
import { View } from './View';
|
||||
import { ViewTransitionManager } from './ViewTransitionManager';
|
||||
import { RouteManagerContext } from './RouteManagerContext';
|
||||
import { ViewItem } from './ViewItem';
|
||||
|
||||
type StackManagerProps = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
type StackManagerState = {}
|
||||
|
||||
export class StackManager extends React.Component<StackManagerProps, StackManagerState> {
|
||||
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
|
||||
context!: React.ContextType<typeof RouteManagerContext>;
|
||||
id: string;
|
||||
|
||||
constructor(props: StackManagerProps) {
|
||||
super(props);
|
||||
this.id = this.props.id || generateUniqueId();
|
||||
this.handleViewSync = this.handleViewSync.bind(this);
|
||||
this.handleHideView = this.handleHideView.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.context.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.context.removeViewStack(this.id);
|
||||
}
|
||||
|
||||
handleViewSync(page: HTMLElement, viewId: string) {
|
||||
this.context.syncView(page, viewId);
|
||||
}
|
||||
|
||||
handleHideView(viewId: string) {
|
||||
this.context.hideView(viewId);
|
||||
}
|
||||
|
||||
renderChild(item: ViewItem) {
|
||||
const component = React.cloneElement(item.route, {
|
||||
computedMatch: item.routeData.match
|
||||
});
|
||||
return component;
|
||||
}
|
||||
|
||||
render() {
|
||||
const context = this.context;
|
||||
const viewStack = context.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) => {
|
||||
return (
|
||||
<ViewTransitionManager
|
||||
id={view.id}
|
||||
key={view.key}
|
||||
mount={view.mount}
|
||||
>
|
||||
<View
|
||||
onViewSync={this.handleViewSync}
|
||||
onHideView={this.handleHideView}
|
||||
view={view}
|
||||
>
|
||||
{this.renderChild(view)}
|
||||
</View>
|
||||
</ViewTransitionManager>
|
||||
);
|
||||
});
|
||||
|
||||
const elementProps: any = {
|
||||
ref: this.routerOutletEl
|
||||
}
|
||||
|
||||
if(isDevMode()) {
|
||||
elementProps['data-stack-id'] = this.id
|
||||
}
|
||||
|
||||
const routerOutletChild = React.cloneElement(ionRouterOutlet, elementProps, childElements);
|
||||
|
||||
|
||||
return routerOutletChild;
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return RouteManagerContext;
|
||||
}
|
||||
}
|
@ -1,48 +1,44 @@
|
||||
import React from 'react';
|
||||
import { IonLifeCycleContext } from '@ionic/react';
|
||||
import { IonLifeCycleContext, NavContext } from '@ionic/react';
|
||||
import { ViewItem } from './ViewItem';
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
import { isDevMode } from '../utils';
|
||||
|
||||
type Props = React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
|
||||
|
||||
interface InternalProps extends React.HTMLAttributes<HTMLElement> {
|
||||
forwardedRef?: React.RefObject<HTMLElement>,
|
||||
interface ViewProps extends React.HTMLAttributes<HTMLElement> {
|
||||
onViewSync: (page: HTMLElement, viewId: string) => void;
|
||||
onHideView: (viewId: string) => void;
|
||||
view: ViewItem;
|
||||
};
|
||||
|
||||
type ExternalProps = Props & {
|
||||
ref?: React.RefObject<HTMLElement>
|
||||
};
|
||||
interface StackViewState { }
|
||||
|
||||
interface StackViewState {
|
||||
ref: any;
|
||||
}
|
||||
|
||||
class ViewInternal extends React.Component<InternalProps, StackViewState> {
|
||||
/**
|
||||
* The View component helps manage the IonPage's lifecycle and registration
|
||||
*/
|
||||
export class View extends React.Component<ViewProps, StackViewState> {
|
||||
context!: React.ContextType<typeof IonLifeCycleContext>;
|
||||
|
||||
constructor(props: InternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ref: null
|
||||
}
|
||||
}
|
||||
ionPage?: HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
const { forwardedRef } = this.props;
|
||||
this.setState({ ref: forwardedRef });
|
||||
if (forwardedRef && forwardedRef.current) {
|
||||
forwardedRef.current.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
forwardedRef.current.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
forwardedRef.current.addEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
forwardedRef.current.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
/**
|
||||
* If we can tell if view is a redirect, hide it so it will work again in future
|
||||
*/
|
||||
const { view } = this.props;
|
||||
if (view.route.type === Redirect) {
|
||||
this.props.onHideView(view.id);
|
||||
} else if (view.route.type === Route && view.route.props.render) {
|
||||
if (view.route.props.render().type === Redirect) {
|
||||
this.props.onHideView(view.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { forwardedRef } = this.props;
|
||||
if (forwardedRef && forwardedRef.current) {
|
||||
forwardedRef.current.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
forwardedRef.current.removeEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
forwardedRef.current.removeEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
forwardedRef.current.removeEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,28 +58,39 @@ class ViewInternal extends React.Component<InternalProps, StackViewState> {
|
||||
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() {
|
||||
const { className, children, forwardedRef, ...rest } = this.props;
|
||||
const { ref } = this.state;
|
||||
return (
|
||||
<div
|
||||
className={className ? `ion-page ${className}` : 'ion-page'}
|
||||
ref={forwardedRef as any}
|
||||
{...rest}
|
||||
>
|
||||
{ref && children}
|
||||
</div>
|
||||
)
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
||||
function forwardRef(props: InternalProps, ref: React.RefObject<HTMLElement>) {
|
||||
return <ViewInternal forwardedRef={ref} {...props} />;
|
||||
}
|
||||
forwardRef.displayName = 'View';
|
||||
|
||||
export const View = /*@__PURE__*/React.forwardRef<HTMLElement, ExternalProps>(forwardRef as any);
|
||||
|
@ -1,10 +1,26 @@
|
||||
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;
|
||||
element: React.ReactElement<any>;
|
||||
ref?: React.RefObject<HTMLElement>;
|
||||
/** The <Route /> or <Redirect /> component associated with the view */
|
||||
route: React.ReactElement<any>;
|
||||
/** The reference to the <IonPage /> element. */
|
||||
ionPageElement?: HTMLElement;
|
||||
/** 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;
|
||||
}
|
||||
|
@ -1,75 +1,17 @@
|
||||
import React from 'react';
|
||||
import { generateUniqueId } from '../utils';
|
||||
import { View } from './View';
|
||||
import { ViewItemManager } from './ViewItemManager';
|
||||
import { RouteManagerContext } from './RouteManagerContext';
|
||||
import * as React from 'react';
|
||||
import { deprecationWarning } from '../utils';
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'ion-router-outlet': any;
|
||||
}
|
||||
}
|
||||
}
|
||||
interface ViewManagerProps { }
|
||||
|
||||
type ViewManagerProps = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
type ViewManagerState = {}
|
||||
interface ViewManagerState { }
|
||||
|
||||
export class ViewManager extends React.Component<ViewManagerProps, ViewManagerState> {
|
||||
containerEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
|
||||
context!: React.ContextType<typeof RouteManagerContext>;
|
||||
id: string;
|
||||
|
||||
constructor(props: ViewManagerProps) {
|
||||
super(props);
|
||||
this.id = this.props.id || generateUniqueId();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.context.setupIonRouter(this.id, this.props.children, this.containerEl.current!);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.context.removeViewStack(this.id);
|
||||
deprecationWarning('As of @ionic/react RC2, ViewManager is no longer needed and can be removed. This component is now deprecated will be removed from @ionic/react final.')
|
||||
}
|
||||
|
||||
render() {
|
||||
const context = this.context;
|
||||
const viewStack = context.viewStacks[this.id];
|
||||
const activeId = viewStack ? viewStack.activeId : '';
|
||||
const views = (viewStack || { views: [] }).views.filter(x => x.show);
|
||||
return (
|
||||
<ion-router-outlet data-id={this.id} ref={this.containerEl}>
|
||||
{views.map((item) => {
|
||||
let props: any = {};
|
||||
if (item.id === activeId) {
|
||||
props = {
|
||||
'className': ' ion-page-invisible'
|
||||
};
|
||||
}
|
||||
return (
|
||||
<ViewItemManager
|
||||
id={item.id}
|
||||
key={item.key}
|
||||
mount={item.mount}
|
||||
>
|
||||
<View
|
||||
ref={item.ref}
|
||||
{...props}
|
||||
>
|
||||
{this.context.renderChild(item)}
|
||||
</View>
|
||||
</ViewItemManager>
|
||||
);
|
||||
})}
|
||||
</ion-router-outlet>
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return RouteManagerContext;
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
100
packages/react-router/src/ReactRouter/ViewStacks.ts
Normal file
100
packages/react-router/src/ReactRouter/ViewStacks.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Location as HistoryLocation } from 'history';
|
||||
import { ViewItem } from './ViewItem';
|
||||
import { IonRouteData } from './IonRouteData';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
export interface ViewStack {
|
||||
id: string;
|
||||
routerOutlet: HTMLIonRouterOutletElement;
|
||||
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 } = {};
|
||||
|
||||
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, key?: string) {
|
||||
let view: ViewItem<IonRouteData> | undefined;
|
||||
let match: IonRouteData["match"] | null | undefined;
|
||||
let viewStack: ViewStack | undefined;
|
||||
if (key) {
|
||||
viewStack = this.viewStacks[key];
|
||||
if (viewStack) {
|
||||
viewStack.views.some(matchView);
|
||||
}
|
||||
} else {
|
||||
const keys = this.getKeys();
|
||||
keys.some(key => {
|
||||
viewStack = this.viewStacks[key];
|
||||
return viewStack.views.some(matchView);
|
||||
});
|
||||
}
|
||||
|
||||
const result = { view, viewStack, match };
|
||||
return result;
|
||||
|
||||
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
|
||||
};
|
||||
match = matchPath(location.pathname, matchProps)
|
||||
if (match) {
|
||||
view = v;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
findViewInfoById(id: string = '') {
|
||||
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 };
|
||||
}
|
||||
|
||||
setHiddenViews() {
|
||||
const keys = this.getKeys();
|
||||
keys.forEach(key => {
|
||||
const viewStack = this.viewStacks[key];
|
||||
viewStack.views.forEach(view => {
|
||||
if(!view.routeData.match && !view.isIonRoute) {
|
||||
view.show = false;
|
||||
view.mount = false;
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,24 @@ import React from 'react';
|
||||
import { IonLifeCycleContext, DefaultIonLifeCycleContext } from '@ionic/react';
|
||||
import { RouteManagerContext } from './RouteManagerContext';
|
||||
|
||||
interface StackItemManagerProps {
|
||||
interface ViewTransitionManagerProps {
|
||||
id: string;
|
||||
mount: boolean;
|
||||
}
|
||||
|
||||
interface StackItemManagerState {
|
||||
interface ViewTransitionManagerState {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export class ViewItemManager extends React.Component<StackItemManagerProps, StackItemManagerState> {
|
||||
/**
|
||||
* 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: StackItemManagerProps) {
|
||||
constructor(props: ViewTransitionManagerProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
show: true
|
@ -1,3 +1,3 @@
|
||||
export * from './Router';
|
||||
export * from './ViewManager';
|
||||
|
||||
export { IonReactRouter } from './Router';
|
||||
export { ViewManager } from './ViewManager';
|
||||
|
Reference in New Issue
Block a user