fix(react): IonNav apply properties to page components (#25603)

Resolves #25602
This commit is contained in:
Sean Perkins
2022-07-28 16:32:06 -04:00
committed by GitHub
parent ea830734c5
commit 61e4ffe47f
4 changed files with 127 additions and 74 deletions

View File

@ -4,27 +4,34 @@ import React, { useState } from 'react';
import { ReactDelegate } from '../../framework-delegate'; import { ReactDelegate } from '../../framework-delegate';
import { createReactComponent } from '../react-component-lib'; import { createReactComponent } from '../react-component-lib';
import { createForwardRef } from '../utils';
const IonNavInner = createReactComponent< const IonNavInner = createReactComponent<
JSX.IonNav & { delegate: FrameworkDelegate }, JSX.IonNav & { delegate: FrameworkDelegate },
HTMLIonNavElement HTMLIonNavElement
>('ion-nav', undefined, undefined, defineCustomElement); >('ion-nav', undefined, undefined, defineCustomElement);
export const IonNav: React.FC<JSX.IonNav> = ({ children, ...restOfProps }) => { type IonNavProps = JSX.IonNav & {
const [views, setViews] = useState<React.ReactPortal[]>([]); 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 * Allows us to create React components that are rendered within
* the context of the IonNav component. * the context of the IonNav component.
*/ */
const addView = (view: React.ReactPortal) => setViews([...views, view]); const addView = (view: React.ReactElement) => setViews([...views, view]);
const removeView = (view: React.ReactPortal) => setViews(views.filter((v) => v !== view)); const removeView = (view: React.ReactElement) => setViews(views.filter((v) => v !== view));
const delegate = ReactDelegate(addView, removeView); const delegate = ReactDelegate(addView, removeView);
return ( return (
<IonNavInner delegate={delegate} {...restOfProps}> <IonNavInner delegate={delegate} ref={forwardedRef} {...restOfProps}>
{views} {views}
</IonNavInner> </IonNavInner>
); );
}; };
export const IonNav = createForwardRef<IonNavProps, HTMLIonNavElement>(IonNavInternal, 'IonNav');

View File

@ -1,15 +1,17 @@
import { FrameworkDelegate } from '@ionic/core/components'; import { FrameworkDelegate } from '@ionic/core/components';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
type ReactComponent = (props?: any) => JSX.Element;
export const ReactDelegate = ( export const ReactDelegate = (
addView: (view: React.ReactPortal) => void, addView: (view: React.ReactElement) => void,
removeView: (view: React.ReactPortal) => void removeView: (view: React.ReactElement) => void
): FrameworkDelegate => { ): FrameworkDelegate => {
let Component: React.ReactPortal; const refMap = new WeakMap<ReactComponent, React.ReactElement>();
const attachViewToDom = async ( const attachViewToDom = async (
parentElement: HTMLElement, parentElement: HTMLElement,
component: () => JSX.Element, component: ReactComponent,
propsOrDataObj?: any, propsOrDataObj?: any,
cssClasses?: string[] cssClasses?: string[]
): Promise<any> => { ): Promise<any> => {
@ -17,17 +19,20 @@ export const ReactDelegate = (
cssClasses && div.classList.add(...cssClasses); cssClasses && div.classList.add(...cssClasses);
parentElement.appendChild(div); 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); return Promise.resolve(div);
}; };
const removeViewFromDom = (): Promise<void> => { const removeViewFromDom = (_container: any, component: ReactComponent): Promise<void> => {
Component && removeView(Component); const hostComponent = refMap.get(component);
hostComponent && removeView(hostComponent);
return Promise.resolve(); return Promise.resolve();
}; };

View File

@ -7,6 +7,10 @@ describe('IonNav', () => {
cy.get('ion-nav').contains('Page one content'); 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', () => { it('should push a page', () => {
cy.get('ion-button').contains('Go to Page Two').click(); cy.get('ion-button').contains('Go to Page Two').click();
cy.get('#pageTwoContent').should('be.visible'); cy.get('#pageTwoContent').should('be.visible');
@ -25,4 +29,16 @@ describe('IonNav', () => {
cy.get('ion-nav').contains('Page one content'); 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"}');
});
}); });

View File

@ -11,13 +11,17 @@ import {
IonBackButton, IonBackButton,
IonPage, IonPage,
} from '@ionic/react'; } from '@ionic/react';
import React from 'react'; import React, { useRef } from 'react';
const NavComponent: React.FC = () => { const PageOne = ({
return ( nav,
<IonPage> ...restOfProps
<IonNav }: {
root={() => { someString: string;
someNumber: number;
someBoolean: boolean;
nav: React.MutableRefObject<HTMLIonNavElement>;
}) => {
return ( return (
<> <>
<IonHeader> <IonHeader>
@ -30,9 +34,21 @@ const NavComponent: React.FC = () => {
</IonHeader> </IonHeader>
<IonContent id="pageOneContent"> <IonContent id="pageOneContent">
<IonLabel>Page one content</IonLabel> <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 <IonNavLink
routerDirection="forward" routerDirection="forward"
component={() => { component={PageTwo}
componentProps={{ someValue: 'Hello' }}
>
<IonButton>Go to Page Two</IonButton>
</IonNavLink>
</IonContent>
</>
);
};
const PageTwo = (props?: { someValue: string }) => {
return ( return (
<> <>
<IonHeader> <IonHeader>
@ -45,9 +61,17 @@ const NavComponent: React.FC = () => {
</IonHeader> </IonHeader>
<IonContent id="pageTwoContent"> <IonContent id="pageTwoContent">
<IonLabel>Page two content</IonLabel> <IonLabel>Page two content</IonLabel>
<IonNavLink <div id="pageTwoProps">{JSON.stringify(props)}</div>
routerDirection="forward" <IonNavLink routerDirection="forward" component={PageThree}>
component={() => ( <IonButton>Go to Page Three</IonButton>
</IonNavLink>
</IonContent>
</>
);
};
const PageThree = () => {
return (
<> <>
<IonHeader> <IonHeader>
<IonToolbar> <IonToolbar>
@ -61,22 +85,23 @@ const NavComponent: React.FC = () => {
<IonLabel>Page three content</IonLabel> <IonLabel>Page three content</IonLabel>
</IonContent> </IonContent>
</> </>
)}
>
<IonButton>Go to Page Three</IonButton>
</IonNavLink>
</IonContent>
</>
); );
};
const NavComponent: React.FC = () => {
const ref = useRef<any>();
return (
<IonPage>
<IonNav
ref={ref}
root={PageOne}
rootParams={{
someString: 'Hello',
someNumber: 3,
someBoolean: true,
nav: ref,
}} }}
> />
<IonButton>Go to Page Two</IonButton>
</IonNavLink>
</IonContent>
</>
);
}}
></IonNav>
</IonPage> </IonPage>
); );
}; };