This commit is contained in:
Sean Perkins
2023-08-30 18:32:41 -04:00
parent 1ab433be07
commit f49ce7a835
6 changed files with 247 additions and 209 deletions

View File

@@ -15,43 +15,30 @@ export class ReactRouterViewStack extends ViewStacks {
this.findViewItemByPathname = this.findViewItemByPathname.bind(this);
}
createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) {
const viewItem: ViewItem = {
createViewItem(
outletId: string,
reactElement: React.ReactElement,
routeInfo: RouteInfo,
ionPage?: HTMLElement
): ViewItem {
const ionRoute = reactElement.type === IonRoute;
return {
id: generateId('viewItem'),
outletId,
ionPageElement: page,
ionPageElement: ionPage,
reactElement,
mount: true,
ionRoute: false,
ionRoute,
disableIonPageManagement: ionRoute && reactElement.props.disableIonPageManagement,
mount: false,
routeData: {
match: matchPath({
pathname: routeInfo.pathname,
componentProps: reactElement.props,
}),
childProps: reactElement.props,
},
};
// const matchProps = {
// exact: reactElement.props.exact,
// path: reactElement.props.path || reactElement.props.from,
// component: reactElement.props.component,
// };
// const match = matchPath({
// pathname: routeInfo.pathname,
// componentProps: reactElement.props,
// });
// const match = matchPath(routeInfo.pathname, matchProps);
if (reactElement.type === IonRoute) {
viewItem.ionRoute = true;
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
}
viewItem.routeData = {
match: matchPath({
pathname: routeInfo.pathname,
componentProps: reactElement.props,
}),
childProps: reactElement.props,
};
return viewItem;
}
getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo) {
@@ -68,7 +55,7 @@ export class ReactRouterViewStack extends ViewStacks {
});
const children = viewItems.map((viewItem) => {
let clonedChild;
let clonedChild: React.ReactNode;
if (viewItem.ionRoute && !viewItem.disableIonPageManagement) {
clonedChild = (
<ViewLifeCycleManager
@@ -106,15 +93,16 @@ export class ReactRouterViewStack extends ViewStacks {
return children;
}
registerIonPage(routeInfo: RouteInfo, ionPage: HTMLElement) {
const viewItem = this.findViewItemByRouteInfo(routeInfo);
/**
* Registers the `<IonPage>` element reference to
* the view item with the matching route info.
*/
registerIonPage(viewItem: ViewItem, ionPage: HTMLElement) {
if (viewItem) {
// TODO view doesn't check if it exists
viewItem.ionPageElement = ionPage;
viewItem.ionRoute = true;
}
// TODO Vue has additional vue specific logic for viewItem.matchedRoute
}
findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, updateMatch?: boolean) {
@@ -139,19 +127,18 @@ export class ReactRouterViewStack extends ViewStacks {
private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute = false) {
let viewItem: ViewItem | undefined;
let match: ReturnType<typeof matchPath> | undefined;
let viewStack: ViewItem[];
if (outletId) {
viewStack = this.getViewItemsForOutlet(outletId);
const viewStack = this.getViewItemsForOutlet(outletId);
viewStack.some(matchView);
if (!viewItem) {
viewStack.some(matchDefaultRoute);
}
} else {
const viewItems = this.getAllViewItems();
viewItems.some(matchView);
const viewStack = this.getAllViewItems();
viewStack.some(matchView);
if (!viewItem) {
viewItems.some(matchDefaultRoute);
viewStack.some(matchDefaultRoute);
}
}

View File

@@ -1,24 +1,23 @@
import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react';
import type { AnimationBuilder, RouteInfo, RouterDirection, StackContextState } from '@ionic/react';
import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import type { ReactNode } from 'react';
import React from 'react';
import { matchPath } from 'react-router-dom';
import { clonePageElement } from './clonePageElement';
// TODO(FW-2959): types
interface StackManagerProps {
routeInfo: RouteInfo;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface StackManagerState {}
interface StackManagerState {
components: ReactNode[];
}
const isViewVisible = (el: HTMLElement) =>
!el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
const isViewVisible = (enteringEl: HTMLElement) => {
return !enteringEl.classList.contains('ion-page-invisible') && !enteringEl.classList.contains('ion-page-hidden');
};
export class StackManager extends React.PureComponent<StackManagerProps, StackManagerState> {
id: string; // TODO pretty sure this should be a number
id: string;
context!: React.ContextType<typeof RouteManagerContext>;
ionRouterOutlet?: React.ReactElement;
routerOutletElement: HTMLIonRouterOutletElement | undefined;
@@ -31,16 +30,18 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
};
private clearOutletTimeout: any;
// private pendingPageTransition = false;
constructor(props: StackManagerProps) {
super(props);
this.registerIonPage = this.registerIonPage.bind(this);
this.transitionPage = this.transitionPage.bind(this);
this.transition = this.transition.bind(this);
this.handlePageTransition = this.handlePageTransition.bind(this);
this.id = generateId('routerOutlet');
this.id = generateId('ion-router-outlet');
this.prevProps = undefined;
this.skipTransition = false;
this.state = {
components: [],
};
}
componentDidMount() {
@@ -57,10 +58,8 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
clearTimeout(this.clearOutletTimeout);
}
if (this.routerOutletElement) {
console.log('calling page transition because of mount');
this.setupRouterOutlet(this.routerOutletElement);
this.setupViewItem();
// this.handlePageTransition(this.props.routeInfo);
}
}
@@ -70,6 +69,7 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
if (pathname !== prevPathname) {
this.prevProps = prevProps;
console.log('pathname changed... setup view item?!');
this.setupViewItem();
}
@@ -88,6 +88,11 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
}
componentWillUnmount() {
/**
* Remove stack data for this outlet
* when outlet is destroyed otherwise
* we will see cached view data.
*/
this.clearOutletTimeout = this.context.clearOutlet(this.id);
}
@@ -96,6 +101,7 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
const { id, props } = this;
const { routeInfo } = props;
/**
* This function is responsible for creating the entering view item
* and adding it to the view stack.
@@ -105,11 +111,25 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
let enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, id);
if (!enteringViewItem) {
/**
* If the entering view item does not exist, this is
* the first time we are rendering this route.
*
* We need to create a view item instance and add it to the view stack.
* Later, we will mount the view item and transition the page.
*/
enteringViewItem = this.context.createViewItem(id, currentRoute, routeInfo);
this.context.addViewItem(enteringViewItem);
}
if (!enteringViewItem.mount) {
/**
* If the entering view item is not mounted,
* that means it has not been rendered yet.
* We need to mount it and then finish the transition
* after the React component has mounted.
*/
enteringViewItem.mount = true;
enteringViewItem.registerCallback = () => {
this.handlePageTransition();
@@ -118,17 +138,25 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
}
};
} else {
/**
* If the entering view item has already mounted,
* that means the page reference should already
* exist and we can transition immediately.
*/
this.handlePageTransition();
}
this.setState({
components: this.context.getChildrenToRender(id, this.ionRouterOutlet!, routeInfo),
});
}
async handlePageTransition() {
const { id, props } = this;
const { routeInfo } = props;
const { prevRouteLastPathname } = routeInfo;
const { prevRouteLastPathname, routeDirection, pushedByRoute, routeAnimation } = routeInfo;
const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, id)!;
console.log('find view item by route info', routeInfo, enteringViewItem);
let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, id);
const enteringEl = enteringViewItem?.ionPageElement;
@@ -138,7 +166,6 @@ export class StackManager extends React.PureComponent<StackManagerProps, StackMa
* methods to work properly.
*/
if (enteringEl === undefined) {
console.log('** THIS ISNT SUPPOSED TO HAPPEN....', enteringViewItem);
console.warn(`[@ionic/react Warning]: The view you are trying to render for path ${routeInfo.pathname} does not have the required <IonPage> component. Transitions and lifecycle methods may not work as expected.
See https://ionicframework.com/docs/react/navigation#ionpage for more information.`);
@@ -194,18 +221,25 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
* route or on an initial page load (i.e. refreshing). In cases when loading
* /tabs/tab-1, we need to transition the /tabs page element into the view.
*/
this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
this.transition(
enteringViewItem.ionPageElement!,
leavingViewItem.ionPageElement!,
routeDirection!,
!!pushedByRoute,
false,
routeAnimation
);
} else {
/**
* If there is no leaving element, just show
* the entering element. Wrap it in an raf
* in case ion-content's fullscreen callback
* in case IonContent's fullscreen callback
* is running. Otherwise we'd have a flicker.
*/
requestAnimationFrame(() => enteringEl?.classList.remove('ion-page-invisible'));
}
this.forceUpdate();
// this.forceUpdate();
}
// async handlePageTransition(routeInfo: RouteInfo) {
@@ -324,17 +358,9 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
registerIonPage(routeInfo: RouteInfo, ionPageEl: HTMLElement) {
const { id } = this;
const viewItem = this.context.findViewItemByRouteInfo(routeInfo, id)!;
if (!viewItem) {
console.log('could not find view item for route info', routeInfo, id);
}
const oldIonPageEl = viewItem.ionPageElement;
// this.context.registerIonPage(viewItem, ionPageEl);
// this.context.registerIonPage(viewItem, ionPageEl);
// viewStacks.registerIonPage(viewItem, ionpageEl);
this.context.registerIonPage(viewItem, ionPageEl);
/**
* If there is a registerCallback,
@@ -362,33 +388,6 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
}
}
// registerIonPage(page: HTMLElement, routeInfo: RouteInfo) {
// // was findViewItemByRouteInfo
// const foundView = this.context.findViewItemByPathname(routeInfo.pathname, this.id, false);
// // const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id, true);
// console.log('calling page transition from registering ion page...', {
// routeInfo,
// page,
// foundView,
// });
// // const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
// if (foundView) {
// const oldPageElement = foundView.ionPageElement;
// foundView.ionPageElement = page;
// foundView.ionRoute = true;
// /**
// * React 18 will unmount and remount IonPage
// * elements in development mode when using createRoot.
// * This can cause duplicate page transitions to occur.
// */
// if (oldPageElement === page) {
// return;
// }
// }
// this.handlePageTransition(routeInfo);
// }
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const canStart = () => {
const config = getConfig();
@@ -427,24 +426,52 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
};
const onStart = async () => {
const { id } = this;
const { routeInfo } = this.props;
let { routeAnimation: animationBuilder } = routeInfo;
const propsToUse =
this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
? this.prevProps.routeInfo
: ({ pathname: routeInfo.pushedByRoute || '' } as any);
const enteringViewItem = this.context.findViewItemByRouteInfo(propsToUse, this.id, false);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
// const propsToUse =
// this.prevProps && this.prevProps.routeInfo.pathname === routeInfo.pushedByRoute
// ? this.prevProps.routeInfo
// : ({ pathname: routeInfo.pushedByRoute || '' } as any);
/**
* When the gesture starts, kick off
* a transition that is controlled
* via a swipe gesture.
*/
if (enteringViewItem && leavingViewItem) {
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
const enteringViewItem = this.context.findViewItemByRouteInfo(
{ pathname: routeInfo.pushedByRoute || '' } as RouteInfo,
id,
false
);
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, id, false);
if (leavingViewItem) {
const enteringEl = enteringViewItem?.ionPageElement;
const leavingEl = leavingViewItem?.ionPageElement;
/**
* If we are going back from a page that
* was presented using a custom animation
* we should default to using that
* unless the developer explicitly
* provided another animation.
*/
const customAnimation = enteringViewItem!.routerAnimation;
if (animationBuilder === undefined && customAnimation !== undefined) {
animationBuilder = customAnimation;
}
leavingViewItem.routerAnimation = animationBuilder;
await this.transition(enteringEl!, leavingEl!, 'back', this.context.canGoBack(), true, animationBuilder);
}
// /**
// * When the gesture starts, kick off
// * a transition that is controlled
// * via a swipe gesture.
// */
// if (enteringViewItem && leavingViewItem) {
// await this.transition(enteringViewItem.ionPageElement!, leavingViewItem.ionPageElement!, 'back', true, true);
// }
return Promise.resolve();
};
const onEnd = (shouldContinue: boolean) => {
@@ -491,88 +518,103 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
};
}
async transitionPage(
routeInfo: RouteInfo,
enteringViewItem: ViewItem,
leavingViewItem?: ViewItem,
direction?: 'forward' | 'back',
progressAnimation = false
async transition(
enteringEl: HTMLElement,
leavingEl: HTMLElement,
direction: RouterDirection,
showGoBack: boolean,
progressAnimation: boolean,
animationBuilder?: AnimationBuilder
) {
console.log('transitionPage', {
enteringViewItem,
leavingViewItem,
console.log('transition', {
enteringEl,
leavingEl,
direction,
routeInfo,
});
const runCommit = async (enteringEl: HTMLElement, leavingEl?: HTMLElement) => {
const skipTransition = this.skipTransition;
const { skipTransition, routerOutletElement } = this;
/**
* If the transition was handled
* via the swipe to go back gesture,
* then we do not want to perform
* another transition.
*
* We skip adding ion-page or ion-page-invisible
* because the entering view already exists in the DOM.
* If we added the classes, there would be a flicker where
* the view would be briefly hidden.
*/
if (skipTransition) {
/**
* If the transition was handled
* via the swipe to go back gesture,
* then we do not want to perform
* another transition.
*
* We skip adding ion-page or ion-page-invisible
* because the entering view already exists in the DOM.
* If we added the classes, there would be a flicker where
* the view would be briefly hidden.
* We need to reset skipTransition before
* we call routerOutlet.commit otherwise
* the transition triggered by the swipe
* to go back gesture would reset it. In
* that case you would see a duplicate
* transition triggered by handlePageTransition
* in componentDidUpdate.
*/
if (skipTransition) {
/**
* We need to reset skipTransition before
* we call routerOutlet.commit otherwise
* the transition triggered by the swipe
* to go back gesture would reset it. In
* that case you would see a duplicate
* transition triggered by handlePageTransition
* in componentDidUpdate.
*/
this.skipTransition = false;
} else {
enteringEl.classList.add('ion-page');
enteringEl.classList.add('ion-page-invisible');
}
this.skipTransition = false;
await routerOutlet.commit(enteringEl, leavingEl, {
duration: skipTransition || directionToUse === undefined ? 0 : undefined,
direction: directionToUse,
showGoBack: !!routeInfo.pushedByRoute,
progressAnimation,
animationBuilder: routeInfo.routeAnimation,
});
};
const routerOutlet = this.routerOutletElement!;
const routeInfoFallbackDirection =
routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
const directionToUse = direction ?? routeInfoFallbackDirection;
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
// If a page is transitioning to another version of itself
// we clone it so we can have an animation to show
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
if (match) {
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
if (newLeavingElement) {
this.routerOutletElement.appendChild(newLeavingElement);
await runCommit(enteringViewItem.ionPageElement, newLeavingElement);
this.routerOutletElement.removeChild(newLeavingElement);
}
} else {
await runCommit(enteringViewItem.ionPageElement, undefined);
}
} else {
await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
}
}
return Promise.resolve(false);
}
if (enteringEl === leavingEl) {
return Promise.resolve(false);
}
enteringEl.classList.add('ion-page-invisible');
const hasRootDirection = direction === undefined || direction === 'root' || direction === 'none';
const result = await routerOutletElement!.commit(enteringEl, leavingEl, {
/**
* replace operations result in a direction of none.
* These typically do not have need animations, so we set
* the duration to 0. However, if a developer explicitly
* passes an animationBuilder, we should assume that
* they want an animation to be played even
* though it is a replace operation.
*/
duration: hasRootDirection && animationBuilder === undefined ? 0 : undefined,
direction: direction as any, // TODO none isn't a valid direction, investigate
showGoBack,
progressAnimation,
animationBuilder,
});
return result;
// };
// const routerOutlet = this.routerOutletElement!;
// const routeInfoFallbackDirection =
// routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
// const directionToUse = direction ?? routeInfoFallbackDirection;
// if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
// if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
// // If a page is transitioning to another version of itself
// // we clone it so we can have an animation to show
// const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, true);
// if (match) {
// const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
// if (newLeavingElement) {
// this.routerOutletElement.appendChild(newLeavingElement);
// await runCommit(enteringViewItem.ionPageElement, newLeavingElement);
// this.routerOutletElement.removeChild(newLeavingElement);
// }
// } else {
// await runCommit(enteringViewItem.ionPageElement, undefined);
// }
// } else {
// await runCommit(enteringViewItem.ionPageElement, leavingViewItem?.ionPageElement);
// if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
// leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
// leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
// }
// }
// }
}
render() {
@@ -580,10 +622,6 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
this.ionRouterOutlet = ionRouterOutlet;
const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
this.forceUpdate();
});
return (
<StackContext.Provider value={this.stackContextValue}>
{React.cloneElement(
@@ -596,14 +634,17 @@ See https://ionicframework.com/docs/react/navigation#ionpage for more informatio
if (ionRouterOutlet.props.forwardedRef) {
ionRouterOutlet.props.forwardedRef.current = node;
}
this.routerOutletElement = node;
if (node) {
this.routerOutletElement = node;
console.log('assigned router outlet element node...', node);
}
const { ref } = ionRouterOutlet as any;
if (typeof ref === 'function') {
ref(node);
}
},
},
components
this.state.components
)}
</StackContext.Provider>
);
@@ -644,13 +685,13 @@ function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
return matchedNode;
}
function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
const matchProps = {
exact: forceExact ? true : node.props.exact,
path: node.props.path || node.props.from,
component: node.props.component,
};
const match = matchPath(pathname, matchProps);
// function matchComponent(node: React.ReactElement, pathname: string, forceExact?: boolean) {
// const matchProps = {
// exact: forceExact ? true : node.props.exact,
// path: node.props.path || node.props.from,
// component: node.props.component,
// };
// const match = matchPath(pathname, matchProps);
return match;
}
// return match;
// }

View File

@@ -41,6 +41,17 @@ export const matchPath = ({ pathname, componentProps }: MatchPathOptions): false
}
const hasParameter = match.path.includes(':');
if (hasParameter) {
console.log('the match path has a parameter!!', {
pathname,
url: match.url,
match
})
}
if (hasParameter && match.url.includes(':')) {
return false;
}
if (hasParameter && pathname !== match.url) {
console.log('discarding the match because it has a path parameter', {
pathname,

View File

@@ -20,12 +20,11 @@ export interface RouteManagerContextState {
getChildrenToRender: (
outletId: string,
ionRouterOutlet: React.ReactElement,
routeInfo: RouteInfo,
reRender: () => void
routeInfo: RouteInfo
) => React.ReactNode[];
goBack: () => void;
unMountViewItem: (viewItem: ViewItem) => void;
registerIonPage: (routeInfo: RouteInfo, ionPage: HTMLElement) => void;
registerIonPage: (viewItem: ViewItem, ionPage: HTMLElement) => void;
}
// TODO(FW-2959): types
@@ -37,7 +36,7 @@ export const RouteManagerContext = /*@__PURE__*/ React.createContext<RouteManage
findViewItemByPathname: () => undefined,
findLeavingViewItemByRouteInfo: () => undefined,
findViewItemByRouteInfo: () => undefined,
getChildrenToRender: () => undefined as any,
getChildrenToRender: () => [],
goBack: () => undefined,
unMountViewItem: () => undefined,
registerIonPage: () => undefined

View File

@@ -1,3 +1,4 @@
import type { AnimationBuilder } from '@ionic/core';
import type { ReactElement } from 'react';
export interface ViewItem<T = any> {
@@ -10,6 +11,7 @@ export interface ViewItem<T = any> {
transitionHtml?: string;
outletId: string;
disableIonPageManagement?: boolean;
routerAnimation?: AnimationBuilder;
/**
* Callback function when the view item is registered.
*/

View File

@@ -74,7 +74,5 @@ export abstract class ViewStacks {
outletId: string,
ionRouterOutlet: React.ReactElement,
routeInfo: RouteInfo,
reRender: () => void,
setInTransition: () => void
): React.ReactNode[];
}