Skip to content

Commit

Permalink
fix(react): IonNav apply properties to page components (#25603)
Browse files Browse the repository at this point in the history
Resolves #25602
  • Loading branch information
sean-perkins authored Jul 28, 2022
1 parent ea83073 commit 61e4ffe
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 74 deletions.
17 changes: 12 additions & 5 deletions packages/react/src/components/navigation/IonNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,34 @@ import React, { useState } from 'react';

import { ReactDelegate } from '../../framework-delegate';
import { createReactComponent } from '../react-component-lib';
import { createForwardRef } from '../utils';

const IonNavInner = createReactComponent<
JSX.IonNav & { delegate: FrameworkDelegate },
HTMLIonNavElement
>('ion-nav', undefined, undefined, defineCustomElement);

export const IonNav: React.FC<JSX.IonNav> = ({ children, ...restOfProps }) => {
const [views, setViews] = useState<React.ReactPortal[]>([]);
type IonNavProps = JSX.IonNav & {
forwardedRef?: React.ForwardedRef<HTMLIonNavElement>;
};

const IonNavInternal: React.FC<IonNavProps> = ({ children, forwardedRef, ...restOfProps }) => {
const [views, setViews] = useState<React.ReactElement[]>([]);

/**
* Allows us to create React components that are rendered within
* the context of the IonNav component.
*/
const addView = (view: React.ReactPortal) => setViews([...views, view]);
const removeView = (view: React.ReactPortal) => setViews(views.filter((v) => v !== view));
const addView = (view: React.ReactElement) => setViews([...views, view]);
const removeView = (view: React.ReactElement) => setViews(views.filter((v) => v !== view));

const delegate = ReactDelegate(addView, removeView);

return (
<IonNavInner delegate={delegate} {...restOfProps}>
<IonNavInner delegate={delegate} ref={forwardedRef} {...restOfProps}>
{views}
</IonNavInner>
);
};

export const IonNav = createForwardRef<IonNavProps, HTMLIonNavElement>(IonNavInternal, 'IonNav');
23 changes: 14 additions & 9 deletions packages/react/src/framework-delegate.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
import { FrameworkDelegate } from '@ionic/core/components';
import { createPortal } from 'react-dom';

type ReactComponent = (props?: any) => JSX.Element;

export const ReactDelegate = (
addView: (view: React.ReactPortal) => void,
removeView: (view: React.ReactPortal) => void
addView: (view: React.ReactElement) => void,
removeView: (view: React.ReactElement) => void
): FrameworkDelegate => {
let Component: React.ReactPortal;
const refMap = new WeakMap<ReactComponent, React.ReactElement>();

const attachViewToDom = async (
parentElement: HTMLElement,
component: () => JSX.Element,
component: ReactComponent,
propsOrDataObj?: any,
cssClasses?: string[]
): Promise<any> => {
const div = document.createElement('div');
cssClasses && div.classList.add(...cssClasses);
parentElement.appendChild(div);

Component = createPortal(component(), div);
const componentWithProps = component(propsOrDataObj);
const hostComponent = createPortal(componentWithProps, div);

Component.props = propsOrDataObj;
refMap.set(component, hostComponent);

addView(Component);
addView(hostComponent);

return Promise.resolve(div);
};

const removeViewFromDom = (): Promise<void> => {
Component && removeView(Component);
const removeViewFromDom = (_container: any, component: ReactComponent): Promise<void> => {
const hostComponent = refMap.get(component);
hostComponent && removeView(hostComponent);

return Promise.resolve();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ describe('IonNav', () => {
cy.get('ion-nav').contains('Page one content');
});

it('should have a ref defined', () => {
cy.get('#navRef').should('have.text', 'Nav ref is defined: true');
});

it('should push a page', () => {
cy.get('ion-button').contains('Go to Page Two').click();
cy.get('#pageTwoContent').should('be.visible');
Expand All @@ -25,4 +29,16 @@ describe('IonNav', () => {
cy.get('ion-nav').contains('Page one content');
});

it('should pass params to the page', () => {
cy.get('#pageOneProps').should('have.text', '{"someString":"Hello","someNumber":3,"someBoolean":true}');
});

it('should pass componentProps to sub pages', () => {
cy.get('ion-button').contains('Go to Page Two').click();

cy.get('#pageTwoContent').should('be.visible');

cy.get('#pageTwoProps').should('have.text', '{"someValue":"Hello"}');
});

});
145 changes: 85 additions & 60 deletions packages/react/test-app/src/pages/navigation/NavComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,72 +11,97 @@ import {
IonBackButton,
IonPage,
} from '@ionic/react';
import React from 'react';
import React, { useRef } from 'react';

const PageOne = ({
nav,
...restOfProps
}: {
someString: string;
someNumber: number;
someBoolean: boolean;
nav: React.MutableRefObject<HTMLIonNavElement>;
}) => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Page One</IonTitle>
<IonButtons>
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent id="pageOneContent">
<IonLabel>Page one content</IonLabel>
<div id="pageOneProps">{JSON.stringify(restOfProps)}</div>
<div id="navRef">Nav ref is defined: {nav.current !== null ? 'true' : 'false'}</div>
<IonNavLink
routerDirection="forward"
component={PageTwo}
componentProps={{ someValue: 'Hello' }}
>
<IonButton>Go to Page Two</IonButton>
</IonNavLink>
</IonContent>
</>
);
};

const PageTwo = (props?: { someValue: string }) => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Page Two</IonTitle>
<IonButtons>
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent id="pageTwoContent">
<IonLabel>Page two content</IonLabel>
<div id="pageTwoProps">{JSON.stringify(props)}</div>
<IonNavLink routerDirection="forward" component={PageThree}>
<IonButton>Go to Page Three</IonButton>
</IonNavLink>
</IonContent>
</>
);
};

const PageThree = () => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Page Three</IonTitle>
<IonButtons>
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonLabel>Page three content</IonLabel>
</IonContent>
</>
);
};

const NavComponent: React.FC = () => {
const ref = useRef<any>();
return (
<IonPage>
<IonNav
root={() => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Page One</IonTitle>
<IonButtons>
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent id="pageOneContent">
<IonLabel>Page one content</IonLabel>
<IonNavLink
routerDirection="forward"
component={() => {
return (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Page Two</IonTitle>
<IonButtons>
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent id="pageTwoContent">
<IonLabel>Page two content</IonLabel>
<IonNavLink
routerDirection="forward"
component={() => (
<>
<IonHeader>
<IonToolbar>
<IonTitle>Page Three</IonTitle>
<IonButtons>
<IonBackButton />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonLabel>Page three content</IonLabel>
</IonContent>
</>
)}
>
<IonButton>Go to Page Three</IonButton>
</IonNavLink>
</IonContent>
</>
);
}}
>
<IonButton>Go to Page Two</IonButton>
</IonNavLink>
</IonContent>
</>
);
ref={ref}
root={PageOne}
rootParams={{
someString: 'Hello',
someNumber: 3,
someBoolean: true,
nav: ref,
}}
></IonNav>
/>
</IonPage>
);
};
Expand Down

0 comments on commit 61e4ffe

Please sign in to comment.