Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 34 additions & 31 deletions packages/react-router/src/ReactRouter/ReactRouterViewStack.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { RouteInfo, ViewItem } from '@ionic/react';
import { IonRoute, ViewLifeCycleManager, ViewStacks, generateId } from '@ionic/react';
import React from 'react';
import { matchPath } from 'react-router';

import { matchPath } from './utils/matchPath';

export class ReactRouterViewStack extends ViewStacks {
constructor() {
Expand All @@ -23,21 +24,16 @@ export class ReactRouterViewStack extends ViewStacks {
ionRoute: false,
};

const matchProps = {
exact: reactElement.props.exact,
path: reactElement.props.path || reactElement.props.from,
component: reactElement.props.component,
};

const match = matchPath(routeInfo.pathname, matchProps);

if (reactElement.type === IonRoute) {
viewItem.ionRoute = true;
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
}

viewItem.routeData = {
match,
match: matchPath({
pathname: routeInfo.pathname,
componentProps: reactElement.props,
}),
childProps: reactElement.props,
};

Expand Down Expand Up @@ -106,7 +102,7 @@ export class ReactRouterViewStack extends ViewStacks {
}

findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string, mustBeIonRoute = true) {
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, false, mustBeIonRoute);
const { viewItem } = this.findViewItemByPath(routeInfo.lastPathname!, outletId, mustBeIonRoute);
return viewItem;
}

Expand All @@ -115,7 +111,10 @@ export class ReactRouterViewStack extends ViewStacks {
return viewItem;
}

private findViewItemByPath(pathname: string, outletId?: string, forceExact?: boolean, mustBeIonRoute?: boolean) {
/**
* Returns the matching view item and the match result for a given pathname.
*/
private findViewItemByPath(pathname: string, outletId?: string, mustBeIonRoute?: boolean) {
let viewItem: ViewItem | undefined;
let match: ReturnType<typeof matchPath> | undefined;
let viewStack: ViewItem[];
Expand All @@ -140,16 +139,24 @@ export class ReactRouterViewStack extends ViewStacks {
if (mustBeIonRoute && !v.ionRoute) {
return false;
}
const matchProps = {
exact: forceExact ? true : v.routeData.childProps.exact,
path: v.routeData.childProps.path || v.routeData.childProps.from,
component: v.routeData.childProps.component,
};
const myMatch = matchPath(pathname, matchProps);
if (myMatch) {
viewItem = v;
match = myMatch;
return true;

match = matchPath({
pathname,
componentProps: v.routeData.childProps,
});

if (match) {
/**
* Even though we have a match from react-router, we do not know if the match
* is for this specific view item.
*
* To validate this, we need to check if the path and url match the view item's route data.
*/
const hasParameter = match.path.includes(':');
if (!hasParameter || (hasParameter && match.url === v.routeData?.match?.url)) {
viewItem = v;
return true;
}
}
return false;
}
Expand All @@ -171,13 +178,9 @@ export class ReactRouterViewStack extends ViewStacks {
}
}

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;
function matchComponent(node: React.ReactElement, pathname: string) {
return matchPath({
pathname,
componentProps: node.props,
});
}
27 changes: 12 additions & 15 deletions packages/react-router/src/ReactRouter/StackManager.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { RouteInfo, StackContextState, ViewItem } from '@ionic/react';
import { RouteManagerContext, StackContext, generateId, getConfig } from '@ionic/react';
import React from 'react';
import { matchPath } from 'react-router-dom';

import { clonePageElement } from './clonePageElement';
import { matchPath } from './utils/matchPath';

// TODO(FW-2959): types

Expand Down Expand Up @@ -433,12 +433,10 @@ export default StackManager;
function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
let matchedNode: React.ReactNode;
React.Children.forEach(node as React.ReactElement, (child: React.ReactElement) => {
const matchProps = {
exact: child.props.exact,
path: child.props.path || child.props.from,
component: child.props.component,
};
const match = matchPath(routeInfo.pathname, matchProps);
const match = matchPath({
pathname: routeInfo.pathname,
componentProps: child.props,
});
if (match) {
matchedNode = child;
}
Expand All @@ -459,12 +457,11 @@ function matchRoute(node: React.ReactNode, routeInfo: RouteInfo) {
}

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 matchPath({
pathname,
componentProps: {
...node.props,
exact: forceExact,
},
});
}
47 changes: 47 additions & 0 deletions packages/react-router/src/ReactRouter/utils/matchPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { matchPath as reactRouterMatchPath } from 'react-router';

interface MatchPathOptions {
/**
* The pathname to match against.
*/
pathname: string;
/**
* The props to match against, they are identical to the matching props `Route` accepts.
*/
componentProps: {
path?: string;
from?: string;
component?: any;
exact?: boolean;
};
}

/**
* @see https://v5.reactrouter.com/web/api/matchPath
*/
export const matchPath = ({
pathname,
componentProps,
}: MatchPathOptions): false | ReturnType<typeof reactRouterMatchPath> => {
const { exact, component } = componentProps;

const path = componentProps.path || componentProps.from;
/***
* The props to match against, they are identical
* to the matching props `Route` accepts. It could also be a string
* or an array of strings as shortcut for `{ path }`.
*/
const matchProps = {
exact,
path,
component,
};

const match = reactRouterMatchPath(pathname, matchProps);

if (!match) {
return false;
}

return match;
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ const Details: React.FC<DetailsProps> = () => {
return () => console.log('Home Details unmount');
}, []);

// useIonViewWillEnter(() => {
// console.log('IVWE Details')
// })

const nextId = parseInt(id, 10) + 1;

return (
Expand Down Expand Up @@ -58,6 +54,9 @@ const Details: React.FC<DetailsProps> = () => {
<IonButton routerLink={`/routing/tabs/settings/details/1`}>
<IonLabel>Go to Settings Details 1</IonLabel>
</IonButton>
<br />
<br />
<input data-testid="details-input" />
</IonContent>
</IonPage>
);
Expand Down
35 changes: 35 additions & 0 deletions packages/react-router/test/base/tests/e2e/specs/routing.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,41 @@ describe('Routing Tests', () => {
cy.ionPageDoesNotExist('home-details-page-1');
cy.ionPageVisible('home-page');
});

it('should mount new view item instances of parameterized routes', () => {
cy.visit(`http://localhost:${port}/routing/tabs/home/details/1`);

cy.get('div.ion-page[data-pageid=home-details-page-1]')
.get('[data-testid="details-input"]')
.should('have.value', '');

cy.get('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]').type('1');

cy.ionNav('ion-button', 'Go to Details 2');
cy.ionPageVisible('home-details-page-2');

cy.get('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]').should('have.value', '');

cy.get('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]').type('2');

cy.ionNav('ion-button', 'Go to Details 3');
cy.ionPageVisible('home-details-page-3');

cy.get('div.ion-page[data-pageid=home-details-page-3] [data-testid="details-input"]').should('have.value', '');

cy.get('div.ion-page[data-pageid=home-details-page-3] [data-testid="details-input"]').type('3');

cy.ionBackClick('home-details-page-3');
cy.ionPageVisible('home-details-page-2');

cy.get('div.ion-page[data-pageid=home-details-page-2] [data-testid="details-input"]').should('have.value', '2');

cy.ionBackClick('home-details-page-2');
cy.ionPageVisible('home-details-page-1');

cy.get('div.ion-page[data-pageid=home-details-page-1] [data-testid="details-input"]').should('have.value', '1');
});

/*
Tests to add:
Test that lifecycle events fire
Expand Down