merge release-4.11.6

This commit is contained in:
Ely Lucas
2019-12-11 10:07:10 -07:00
committed by GitHub
22 changed files with 391 additions and 218 deletions

View File

@ -1,3 +1,19 @@
## [4.11.6](https://github.com/ionic-team/ionic/compare/v4.11.5...v4.11.6) (2019-12-11)
### Bug Fixes
* **react:** don't show back button when not appropriate ([684293d](https://github.com/ionic-team/ionic/commit/684293ddbf1ad4edce590d56f7ff66fcd6c817a5))
* **react:** first render performance improvements ([1c7d1e5](https://github.com/ionic-team/ionic/commit/1c7d1e5cf1ad7e53ebbee2566e8fa89f567f7fb5))
* **react:** fix refs for controllers, overlays, ionpage, and ionrouteroutlet, fixes [#19924](https://github.com/ionic-team/ionic/issues/19924) ([#20012](https://github.com/ionic-team/ionic/issues/20012)) ([eef55bb](https://github.com/ionic-team/ionic/commit/eef55bb0072a9e54b1fd7d1c8c69e7fd43b2a5c5))
* **react:** support for 'root' router direction, fixes [#19982](https://github.com/ionic-team/ionic/issues/19982) ([#20052](https://github.com/ionic-team/ionic/issues/20052)) ([e116712](https://github.com/ionic-team/ionic/commit/e1167122758b23221935e897bcd65839b75c59aa))
* **react:** support navigating to same page and route updates in IonRouterOutlet, fixes [#19891](https://github.com/ionic-team/ionic/issues/19891), [#19892](https://github.com/ionic-team/ionic/issues/19892), [#19986](https://github.com/ionic-team/ionic/issues/19986) ([f9bf8db](https://github.com/ionic-team/ionic/commit/f9bf8dbe6f952ee53b6b213a4c0d043d25f49b93))
### Upgrade Note
If you run into a "Property 'translate' is missing in type" error building after updating to 4.11.6, update your React Typings library to the latest:
npm i @types/react@latest @types/react-dom@latest
# [5.0.0-beta.1](https://github.com/ionic-team/ionic/compare/v5.0.0-beta.0...v5.0.0-beta.1) (2019-11-20)

View File

@ -50,7 +50,7 @@
"@ionic/core": "5.0.0-beta.1",
"@ionic/react": "5.0.0-beta.1",
"@types/jest": "^23.3.9",
"@types/node": "12.6.9",
"@types/node": "^12.12.14",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-router": "^5.0.3",
@ -70,7 +70,7 @@
"tslint": "^5.20.0",
"tslint-ionic-rules": "0.0.21",
"tslint-react": "^4.1.0",
"typescript": "3.5.3"
"typescript": "^3.7.2"
},
"jest": {
"preset": "ts-jest",

View File

@ -0,0 +1 @@
export type IonRouteAction = 'push' | 'replace' | 'pop';

View File

@ -4,11 +4,12 @@ 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: (type: 'push' | 'replace', path: string, state?: any) => void;
onNavigate: (ionRouteAction: IonRouteAction, path: string, state?: any) => void;
}
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
@ -24,8 +25,7 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
getStackManager: this.getStackManager.bind(this),
getPageManager: this.getPageManager.bind(this),
currentPath: this.props.location.pathname,
registerIonPage: () => { return; }, // overridden in View for each IonPage
tabNavigate: this.tabNavigate.bind(this)
registerIonPage: () => { return; } // overridden in View for each IonPage
};
this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
@ -53,12 +53,8 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
this.props.onNavigateBack(defaultHref);
}
navigate(path: string, direction?: RouterDirection | 'none') {
this.props.onNavigate('push', path, direction);
}
tabNavigate(path: string) {
this.props.onNavigate('replace', path, 'back');
navigate(path: string, direction?: RouterDirection | 'none', ionRouteAction: IonRouteAction = 'push') {
this.props.onNavigate(ionRouteAction, path, direction);
}
getPageManager() {

View File

@ -4,18 +4,22 @@ import { ViewStacks } from './ViewStacks';
export interface RouteManagerContextState {
syncView: (page: HTMLElement, viewId: string) => void;
syncRoute: (id: string, 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(); }
removeViewStack: () => { navContextNotFoundError(); },
getRoute: () => { navContextNotFoundError(); }
});
function navContextNotFoundError() {

View File

@ -7,6 +7,7 @@ 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';
@ -15,14 +16,19 @@ import { ViewStack, ViewStacks } from './ViewStacks';
interface RouteManagerState extends RouteManagerContextState {
location?: HistoryLocation;
action?: HistoryAction;
action?: IonRouteAction;
}
class RouteManager extends React.Component<RouteComponentProps, RouteManagerState> {
listenUnregisterCallback: UnregisterCallback | undefined;
activeIonPageId?: string;
currentDirection?: RouterDirection;
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: RouteComponentProps) {
super(props);
@ -34,7 +40,9 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
hideView: this.hideView.bind(this),
setupIonRouter: this.setupIonRouter.bind(this),
removeViewStack: this.removeViewStack.bind(this),
syncView: this.syncView.bind(this)
syncView: this.syncView.bind(this),
syncRoute: this.syncRoute.bind(this),
getRoute: this.getRoute.bind(this)
};
this.locationHistory.add({
@ -49,7 +57,8 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
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) {
this.setActiveView(this.state.location!, this.state.action!);
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
this.setActiveView(this.state.location!, this.state.action!, viewStacks);
}
}
@ -59,15 +68,19 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
}
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.ionPageElement = undefined;
view.isIonRoute = false;
view.prevId = undefined;
view.key = generateId();
delete this.ionPageElements[view.id];
this.setState({
viewStacks
});
@ -75,64 +88,74 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
historyChange(location: HistoryLocation, action: HistoryAction) {
location.state = location.state || { direction: this.currentDirection };
this.currentDirection = undefined;
if (action === 'PUSH') {
const ionRouteAction = this.currentIonRouteAction === 'pop' ? 'pop' : action.toLowerCase() as IonRouteAction;
let direction = this.currentRouteDirection;
if (ionRouteAction === 'push') {
this.locationHistory.add(location);
} else if ((action === 'REPLACE' && location.state.direction === 'back') || action === 'POP') {
} else if (ionRouteAction === 'pop') {
this.locationHistory.pop();
} else {
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
action: ionRouteAction as IonRouteAction
});
this.currentRouteDirection = undefined;
this.currentIonRouteAction = undefined;
}
setActiveView(location: HistoryLocation, action: HistoryAction) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
setActiveView(location: HistoryLocation, 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 (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 if (action === 'POP') {
direction = leavingView.prevId === enteringView.id ? 'back' : 'none';
} else {
direction = direction || 'back';
leavingView.mount = false;
}
}
if (direction === 'back' || action === 'REPLACE') {
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 (action === 'pop' || action === 'replace') {
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;
@ -151,28 +174,39 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
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;
const shouldGoBack = !!enteringView.prevId || !!this.locationHistory.previous();
this.commitView(
enteringEl!,
leavingEl!,
viewStack.routerOutlet,
navDirection,
shouldGoBack);
} else if (leavingEl) {
leavingEl.classList.add('ion-page-hidden');
leavingEl.setAttribute('aria-hidden', 'true');
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.routeData.match!.url !== location.pathname) {
if (enteringView && enteringView.routeData.match!.url !== location.pathname) {
setTimeout(() => {
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (view!.routeData.match!.url !== location.pathname) {
@ -195,12 +229,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
this.removeOrphanedViews(v, viewStack);
// If view is not currently visible, go ahead and remove it from DOM
if (v.ionPageElement!.classList.contains('ion-page-hidden')) {
const page = this.ionPageElements[v.id];
if (page.classList.contains('ion-page-hidden')) {
v.show = false;
v.ionPageElement = undefined;
v.isIonRoute = false;
v.prevId = undefined;
v.key = generateId();
delete this.ionPageElements[v.id];
}
v.mount = false;
}
@ -212,15 +247,18 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
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));
const routeId = generateId();
this.routes[routeId] = child;
views.push(createViewItem(child, routeId, this.props.history.location));
});
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
function createViewItem(child: React.ReactElement<any>, location: HistoryLocation) {
function createViewItem(child: React.ReactElement<any>, routeId: string, location: HistoryLocation) {
const viewId = generateId();
const key = generateId();
const route = child;
// const route = child;
const matchProps = {
exact: child.props.exact,
path: child.props.path || child.props.from,
@ -234,7 +272,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
match,
childProps: child.props
},
route,
routeId,
mount: true,
show: !!match,
isIonRoute: false
@ -251,9 +289,9 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
const newStack: ViewStack = {
id: stack,
views: stackItems,
routerOutlet
views: stackItems
};
this.routerOutlets[stack] = routerOutlet;
if (activeId) {
this.activeIonPageId = activeId;
}
@ -267,18 +305,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const waitUntilReady = async () => {
if (routerOutlet.componentOnReady) {
routerOutlet.dispatchEvent(new Event('routerOutletReady'));
return;
} else {
setTimeout(() => {
waitUntilReady();
}, 0);
}
};
await waitUntilReady();
const canStart = () => {
const config = getConfig();
@ -310,70 +336,115 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
syncView(page: HTMLElement, viewId: string) {
this.setState(state => {
const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
const { view } = viewStacks.findViewInfoById(viewId);
view!.ionPageElement = page;
view!.isIonRoute = true;
return {
viewStacks
};
}, () => {
this.setActiveView(this.state.location || this.props.location, this.state.action!);
});
}
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) {
if (enteringEl === leavingEl) {
return;
}
await ionRouterOuter.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');
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);
}
}
handleNavigate(type: 'push' | 'replace', path: string, direction?: RouterDirection) {
this.currentDirection = direction;
if (type === 'push') {
this.props.history.push(path);
syncRoute(_id: string, 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 === child.props.path) {
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 {
this.props.history.replace(path);
enteringEl.classList.remove('ion-page-invisible');
enteringEl.style.zIndex = '101';
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 { view: activeIonPage } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (activeIonPage) {
const { view: enteringView } = this.state.viewStacks.findViewInfoById(activeIonPage.prevId);
if (enteringView) {
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
if (lastLocation) {
this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
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 {
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
defaultHref && this.handleNavigate('pop', defaultHref);
}
} else {
const currentLocation = this.locationHistory.previous();
if (currentLocation) {
this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back');
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 {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
const currentLocation = this.locationHistory.previous();
if (currentLocation) {
this.handleNavigate('pop', currentLocation.pathname + currentLocation.search);
} else {
if (defaultHref) {
this.handleNavigate('pop', defaultHref);
}
}
}
}
@ -399,5 +470,28 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
}
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';

View File

@ -2,22 +2,21 @@ import React from 'react';
import { generateId, isDevMode } from '../utils';
import { RouteManagerContext } from './RouteManagerContext';
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
import { View } from './View';
import { ViewItem } from './ViewItem';
import { ViewTransitionManager } from './ViewTransitionManager';
interface StackManagerProps {
id?: string;
routeManager: RouteManagerContextState;
children?: React.ReactNode;
}
interface StackManagerState {
routerOutletReady: boolean;
}
interface StackManagerState { }
export class StackManager extends React.Component<StackManagerProps, StackManagerState> {
class StackManagerInner extends React.Component<StackManagerProps, StackManagerState> {
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
context!: React.ContextType<typeof RouteManagerContext>;
id: string;
constructor(props: StackManagerProps) {
@ -25,47 +24,44 @@ export class StackManager extends React.Component<StackManagerProps, StackManage
this.id = this.props.id || generateId();
this.handleViewSync = this.handleViewSync.bind(this);
this.handleHideView = this.handleHideView.bind(this);
this.state = {
routerOutletReady: false
};
this.state = {};
}
componentDidMount() {
this.context.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
this.routerOutletEl.current!.addEventListener('routerOutletReady', () => {
this.setState({
routerOutletReady: true
});
});
this.props.routeManager.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
}
static getDerivedStateFromProps(props: StackManagerProps, state: StackManagerState) {
props.routeManager.syncRoute('', props.children);
return state;
}
componentWillUnmount() {
this.context.removeViewStack(this.id);
this.props.routeManager.removeViewStack(this.id);
}
handleViewSync(page: HTMLElement, viewId: string) {
this.context.syncView(page, viewId);
this.props.routeManager.syncView(page, viewId);
}
handleHideView(viewId: string) {
this.context.hideView(viewId);
this.props.routeManager.hideView(viewId);
}
renderChild(item: ViewItem) {
const component = React.cloneElement(item.route, {
renderChild(item: ViewItem, route: any) {
const component = React.cloneElement(route, {
computedMatch: item.routeData.match
});
return component;
}
render() {
const context = this.context;
const viewStack = context.viewStacks.get(this.id);
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 { routerOutletReady } = this.state;
const childElements = routerOutletReady ? views.map(view => {
const childElements = views.map(view => {
const route = routeManager.getRoute(view.routeId);
return (
<ViewTransitionManager
id={view.id}
@ -76,17 +72,22 @@ export class StackManager extends React.Component<StackManagerProps, StackManage
onViewSync={this.handleViewSync}
onHideView={this.handleHideView}
view={view}
route={route}
>
{this.renderChild(view)}
{this.renderChild(view, route)}
</View>
</ViewTransitionManager>
);
}) : <div></div>;
});
const elementProps: any = {
ref: this.routerOutletEl
};
if (ionRouterOutlet.props.forwardedRef) {
ionRouterOutlet.props.forwardedRef.current = this.routerOutletEl;
}
if (isDevMode()) {
elementProps['data-stack-id'] = this.id;
}
@ -95,8 +96,14 @@ export class StackManager extends React.Component<StackManagerProps, StackManage
return routerOutletChild;
}
static get contextType() {
return RouteManagerContext;
}
}
const withContext = (Component: any) => {
return (props: any) => (
<RouteManagerContext.Consumer>
{context => <Component {...props} routeManager={context} />}
</RouteManagerContext.Consumer>
);
};
export const StackManager = withContext(StackManagerInner);

View File

@ -1,6 +1,5 @@
import { IonLifeCycleContext, NavContext } from '@ionic/react';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import { isDevMode } from '../utils';
@ -10,6 +9,7 @@ interface ViewProps extends React.HTMLAttributes<HTMLElement> {
onViewSync: (page: HTMLElement, viewId: string) => void;
onHideView: (viewId: string) => void;
view: ViewItem;
route: any;
}
/**
@ -19,20 +19,6 @@ 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 } = 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() {
if (this.ionPage) {
this.ionPage.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));

View File

@ -3,10 +3,9 @@ export interface ViewItem<RouteData = any> {
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;
/** The <Route /> or <Redirect /> component associated with the view */
route: React.ReactElement<any>;
/** The reference to the <IonPage /> element. */
ionPageElement?: HTMLElement;
routeId: string;
/** The routeData for the view. */
routeData: RouteData;
/** Used to track which page pushed the page into view. Used for back button purposes. */
@ -23,4 +22,9 @@ export interface ViewItem<RouteData = any> {
* 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;
}

View File

@ -6,7 +6,6 @@ import { ViewItem } from './ViewItem';
export interface ViewStack {
id: string;
routerOutlet: HTMLIonRouterOutletElement;
views: ViewItem[];
}
@ -58,10 +57,11 @@ export class ViewStacks {
path: v.routeData.childProps.path || v.routeData.childProps.from,
component: v.routeData.childProps.component
};
match = matchPath(location.pathname, matchProps);
if (match) {
const myMatch: IonRouteData['match'] | null | undefined = matchPath(location.pathname, matchProps);
if (myMatch) {
view = v;
return true;
match = myMatch;
return view.location === location.pathname;
}
return false;
}

View File

@ -3,7 +3,7 @@ import { Location as HistoryLocation } from 'history';
const RESTRICT_SIZE = 25;
export class LocationHistory {
locationHistory: HistoryLocation[] = [];
private locationHistory: HistoryLocation[] = [];
add(location: HistoryLocation) {
this.locationHistory.push(location);
@ -21,6 +21,10 @@ export class LocationHistory {
this.locationHistory.push(location);
}
clear() {
this.locationHistory = [];
}
findLastLocationByUrl(url: string) {
for (let i = this.locationHistory.length - 1; i >= 0; i--) {
const location = this.locationHistory[i];

View File

@ -27,6 +27,11 @@
"jsx-no-bind": false,
"jsx-no-lambda": false,
"jsx-no-multiline-js": false,
"jsx-wrap-multiline": false
"jsx-wrap-multiline": false,
"forin": false,
"strict-type-predicates": false,
"no-unused-expression": false,
"no-constant-condition": false,
"no-empty-interface": false
}
}

View File

@ -49,7 +49,7 @@
},
"devDependencies": {
"@types/jest": "^23.3.9",
"@types/node": "10.12.9",
"@types/node": "^12.12.14",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"fs-extra": "^8.1.0",
@ -67,7 +67,7 @@
"tslint": "^5.18.0",
"tslint-ionic-rules": "0.0.21",
"tslint-react": "^4.0.0",
"typescript": "3.5.3"
"typescript": "^3.7.2"
},
"jest": {
"preset": "ts-jest",

View File

@ -3,13 +3,26 @@ import React from 'react';
import { NavContext } from '../contexts/NavContext';
import { IonicReactProps } from './IonicReactProps';
import { createForwardRef } from './utils';
export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.Component<React.HTMLAttributes<HTMLElement> & IonicReactProps> {
interface IonPageProps extends IonicReactProps {
}
interface IonPageInternalProps extends IonPageProps {
forwardedRef?: React.RefObject<HTMLDivElement>;
}
class IonPageInternal extends React.Component<IonPageInternalProps> {
context!: React.ContextType<typeof NavContext>;
ref = React.createRef<HTMLDivElement>();
ref: React.RefObject<HTMLDivElement>;
constructor(props: IonPageInternalProps) {
super(props);
this.ref = this.props.forwardedRef || React.createRef();
}
componentDidMount() {
if (this.context && this.ref.current) {
if (this.context && this.ref && this.ref.current) {
if (this.context.hasIonicRouter()) {
this.context.registerIonPage(this.ref.current);
}
@ -17,7 +30,7 @@ export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.C
}
render() {
const { className, children, ...props } = this.props;
const { className, children, forwardedRef, ...props } = this.props;
return (
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={this.ref} {...props}>
@ -33,4 +46,6 @@ export const IonPage = /*@__PURE__*/(() => class IonPageInternal extends React.C
static get contextType() {
return NavContext;
}
})();
}
export const IonPage = createForwardRef(IonPageInternal, 'IonPage');

View File

@ -13,7 +13,7 @@ type Props = LocalJSX.IonRouterOutlet & {
};
type InternalProps = Props & {
forwardedRef: any;
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
};
const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component<InternalProps> {

View File

@ -1,5 +1,6 @@
export interface IonicReactProps {
class?: string;
className?: string;
style?: {[key: string]: any };
}

View File

@ -15,18 +15,21 @@ export interface ReactControllerProps {
export const createControllerComponent = <OptionsType extends object, OverlayType extends OverlayBase>(
displayName: string,
controller: { create: (options: OptionsType) => Promise<OverlayType> }
controller: { create: (options: OptionsType) => Promise<OverlayType>; }
) => {
const dismissEventName = `on${displayName}DidDismiss`;
type Props = OptionsType & ReactControllerProps;
type Props = OptionsType & ReactControllerProps & {
forwardedRef?: React.RefObject<OverlayType>;
};
return class extends React.Component<Props> {
class Overlay extends React.Component<Props> {
overlay?: OverlayType;
isUnmounted = false;
constructor(props: Props) {
super(props);
this.handleDismiss = this.handleDismiss.bind(this);
}
static get displayName() {
@ -54,17 +57,29 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
}
}
handleDismiss(event: CustomEvent<OverlayEventDetail<any>>) {
if (this.props.onDidDismiss) {
this.props.onDidDismiss(event);
}
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = undefined;
}
}
async present(prevProps?: Props) {
const { isOpen, onDidDismiss, ...cProps } = this.props;
this.overlay = await controller.create({
...cProps as any
});
attachProps(this.overlay, {
[dismissEventName]: onDidDismiss
[dismissEventName]: this.handleDismiss
}, prevProps);
// Check isOpen again since the value could have changed during the async call to controller.create
// It's also possible for the component to have become unmounted.
if (this.props.isOpen === true && this.isUnmounted === false) {
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = this.overlay;
}
await this.overlay.present();
}
}
@ -72,5 +87,9 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
render(): null {
return null;
}
};
}
return React.forwardRef<OverlayType, Props>((props, ref) => {
return <Overlay {...props} forwardedRef={ref} />;
});
};

View File

@ -15,21 +15,24 @@ export interface ReactOverlayProps {
onDidDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
}
export const createOverlayComponent = <T extends object, OverlayType extends OverlayElement>(
export const createOverlayComponent = <OverlayComponent extends object, OverlayType extends OverlayElement>(
displayName: string,
controller: { create: (options: any) => Promise<OverlayType> }
controller: { create: (options: any) => Promise<OverlayType>; }
) => {
const dismissEventName = `on${displayName}DidDismiss`;
type Props = T & ReactOverlayProps;
type Props = OverlayComponent & ReactOverlayProps & {
forwardedRef?: React.RefObject<OverlayType>;
};
return class extends React.Component<Props> {
class Overlay extends React.Component<Props> {
overlay?: OverlayType;
el: HTMLDivElement;
constructor(props: Props) {
super(props);
this.el = document.createElement('div');
this.handleDismiss = this.handleDismiss.bind(this);
}
static get displayName() {
@ -46,6 +49,15 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
if (this.overlay) { this.overlay.dismiss(); }
}
handleDismiss(event: CustomEvent<OverlayEventDetail<any>>) {
if (this.props.onDidDismiss) {
this.props.onDidDismiss(event);
}
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = undefined;
}
}
async componentDidUpdate(prevProps: Props) {
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) {
this.present(prevProps);
@ -56,10 +68,11 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
}
async present(prevProps?: Props) {
const { children, isOpen, onDidDismiss = () => { return; }, ...cProps } = this.props;
const { children, isOpen, onDidDismiss, ...cProps } = this.props;
const elementProps = {
...cProps,
[dismissEventName]: onDidDismiss
ref: this.props.forwardedRef,
[dismissEventName]: this.handleDismiss
};
const overlay = this.overlay = await controller.create({
@ -68,6 +81,10 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
componentProps: {}
});
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = overlay;
}
attachProps(overlay, elementProps, prevProps);
await overlay.present();
@ -76,8 +93,12 @@ export const createOverlayComponent = <T extends object, OverlayType extends Ove
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
this.el
);
}
};
}
return React.forwardRef<OverlayType, Props>((props, ref) => {
return <Overlay {...props} forwardedRef={ref} />;
});
};

View File

@ -1,4 +1,4 @@
export declare type RouterDirection = 'forward' | 'back' | 'none';
export declare type RouterDirection = 'forward' | 'back' | 'root' | 'none';
export type HrefProps<T> = Omit<T, 'routerDirection'> & {
routerLink?: string;

View File

@ -69,12 +69,13 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Pro
}
private onTabButtonClick = (e: CustomEvent<{ href: string, selected: boolean, tab: string }>) => {
const originalHref = this.state.tabs[e.detail.tab].originalHref;
const currentHref = this.state.tabs[e.detail.tab].currentHref;
if (this.state.activeTab === e.detail.tab) {
const originalHref = this.state.tabs[e.detail.tab].originalHref;
if (this.context.hasIonicRouter()) {
this.context.tabNavigate(originalHref);
if (originalHref === currentHref) {
this.context.navigate(originalHref, 'none');
} else {
this.context.navigate(originalHref, 'back');
this.context.navigate(originalHref, 'back', 'pop');
}
} else {
if (this.props.onIonTabsWillChange) {
@ -83,7 +84,7 @@ const IonTabBarUnwrapped = /*@__PURE__*/(() => class extends React.Component<Pro
if (this.props.onIonTabsDidChange) {
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
}
this.context.navigate(this.state.tabs[e.detail.tab].currentHref, 'none');
this.context.navigate(currentHref, 'none');
}
}

View File

@ -5,10 +5,9 @@ export interface NavContextState {
getPageManager: () => any;
getStackManager: () => any;
goBack: (defaultHref?: string) => void;
navigate: (path: string, direction?: RouterDirection | 'none') => void;
navigate: (path: string, direction?: RouterDirection | 'none', ionRouteAction?: 'push' | 'replace' | 'pop') => void;
hasIonicRouter: () => boolean;
registerIonPage: (page: HTMLElement) => void;
tabNavigate: (url: string) => void;
currentPath: string | undefined;
}
@ -23,7 +22,6 @@ export const NavContext = /*@__PURE__*/React.createContext<NavContextState>({
}
},
navigate: (path: string) => { window.location.pathname = path; },
tabNavigate: () => undefined,
hasIonicRouter: () => false,
registerIonPage: () => undefined,
currentPath: undefined

View File

@ -14,7 +14,6 @@
"trailing-comma": false,
"no-null-keyword": false,
"no-console": false,
"no-unbound-method": true,
"no-floating-promises": false,
"no-invalid-template-strings": true,
"ban-export-const-enum": true,
@ -27,6 +26,8 @@
"jsx-no-bind": false,
"jsx-no-lambda": false,
"jsx-no-multiline-js": false,
"jsx-wrap-multiline": false
"jsx-wrap-multiline": false,
"no-empty-interface": false,
"no-unbound-method": false
}
}