mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 09:34:19 +08:00
fix(react): support navigating to same page and route updates in IonRouterOutlet, fixes #19891, #19892, #19986
This commit is contained in:
@ -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';
|
||||
@ -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);
|
||||
@ -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({
|
||||
@ -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) {
|
||||
@ -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);
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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) {
|
||||
@ -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,
|
||||
@ -234,7 +257,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
match,
|
||||
childProps: child.props
|
||||
},
|
||||
route,
|
||||
routeId,
|
||||
mount: true,
|
||||
show: !!match,
|
||||
isIonRoute: false
|
||||
@ -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 */
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
Reference in New Issue
Block a user