Skip to content

Commit

Permalink
fix(react): support navigating to same page and route updates in IonR…
Browse files Browse the repository at this point in the history
…outerOutlet, fixes #19891, #19892, #19986
  • Loading branch information
elylucas authored Dec 3, 2019
1 parent eef55bb commit f9bf8db
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 90 deletions.
10 changes: 2 additions & 8 deletions packages/react-router/src/ReactRouter/NavManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { NavContext, NavContextState } from '@ionic/react';
import { Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';

import { StackManager } from './StackManager';

interface NavManagerProps extends RouteComponentProps {
Expand All @@ -25,7 +24,6 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
getPageManager: this.getPageManager.bind(this),
currentPath: this.props.location.pathname,
registerIonPage: () => { return; }, // overridden in View for each IonPage
tabNavigate: this.tabNavigate.bind(this)
};

this.listenUnregisterCallback = this.props.history.listen((location: HistoryLocation) => {
Expand Down Expand Up @@ -53,12 +51,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', type: 'push' | 'replace' = 'push') {
this.props.onNavigate(type, path, direction);
}

getPageManager() {
Expand Down
6 changes: 5 additions & 1 deletion packages/react-router/src/ReactRouter/RouteManagerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
167 changes: 118 additions & 49 deletions packages/react-router/src/ReactRouter/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { RouteComponentProps, withRouter, matchPath } from 'react-router-dom';

import { generateId, isDevMode } from '../utils';
import { LocationHistory } from '../utils/LocationHistory';
Expand All @@ -23,6 +23,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
activeIonPageId?: string;
currentDirection?: RouterDirection;
locationHistory = new LocationHistory();
routes: { [key: string]: any } = {};

constructor(props: RouteComponentProps) {
super(props);
Expand All @@ -34,7 +35,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({
Expand All @@ -46,6 +49,10 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
});
}

// componentDidMount() {
// this.setActiveView(this.props.location, this.state.action!);
// }

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) {
Expand All @@ -59,6 +66,10 @@ 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);
Expand Down Expand Up @@ -95,18 +106,22 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
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;

Expand All @@ -129,10 +144,12 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
leavingView.mount = false;
this.removeOrphanedViews(enteringView, enteringViewStack);
}
leavingViewHtml = enteringView.id === leavingView.id ? leavingView.ionPageElement!.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;
Expand All @@ -151,28 +168,31 @@ 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 = 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,
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) {
Expand Down Expand Up @@ -212,15 +232,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,
Expand All @@ -234,7 +257,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
match,
childProps: child.props
},
route,
routeId,
mount: true,
show: !!match,
isIonRoute: false
Expand Down Expand Up @@ -326,19 +349,43 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
});
}

private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) {
syncRoute(_id: string, routerOutlet: any) {
const ionRouterOutlet = React.Children.only(routerOutlet) as React.ReactElement;

if (enteringEl === leavingEl) {
return;
}

await ionRouterOuter.commit(enteringEl, leavingEl, {
deepWait: true,
duration: direction === undefined ? 0 : undefined,
direction,
showGoBack,
progressAnimation: false
React.Children.forEach(ionRouterOutlet.props.children, (child: React.ReactElement) => {
for (let 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 ((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 */
Expand All @@ -357,23 +404,32 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}

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('replace', previousLocation.pathname + previousLocation.search, 'back');
} else {
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
}
defaultHref && this.handleNavigate('replace', defaultHref, 'back');
}
} 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('replace', lastLocation.pathname + lastLocation.search, 'back');
} else {
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
}
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
const currentLocation = this.locationHistory.previous();
if (currentLocation) {
this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back');
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
}
}
}
}
Expand All @@ -399,5 +455,18 @@ 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 = null;
// 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;
}

export const RouteManagerWithRouter = withRouter(RouteManager);
RouteManagerWithRouter.displayName = 'RouteManager';
Loading

0 comments on commit f9bf8db

Please sign in to comment.