mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
feat(react): React Router Enhancements (#21693)
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
import PageManager from '../routing/PageManager';
|
||||
|
||||
import { IonicReactProps } from './IonicReactProps';
|
||||
import { createForwardRef } from './utils';
|
||||
@ -14,28 +15,29 @@ interface IonPageInternalProps extends IonPageProps {
|
||||
|
||||
class IonPageInternal extends React.Component<IonPageInternalProps> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
ref: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: IonPageInternalProps) {
|
||||
super(props);
|
||||
this.ref = this.props.forwardedRef || React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.context && this.ref && this.ref.current) {
|
||||
if (this.context.hasIonicRouter()) {
|
||||
this.context.registerIonPage(this.ref.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, forwardedRef, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={this.ref} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
this.context.hasIonicRouter() ? (
|
||||
<PageManager
|
||||
className={className ? `${className}` : ''}
|
||||
routeInfo={this.context.routeInfo}
|
||||
forwardedRef={forwardedRef}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</PageManager>
|
||||
) : (
|
||||
<div className={className ? `ion-page ${className}` : 'ion-page'} ref={forwardedRef} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
38
packages/react/src/components/IonRedirect.tsx
Normal file
38
packages/react/src/components/IonRedirect.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
|
||||
export interface IonRedirectProps {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
to: string;
|
||||
routerOptions?: unknown;
|
||||
}
|
||||
|
||||
interface IonRedirectState {
|
||||
|
||||
}
|
||||
|
||||
export class IonRedirect extends React.PureComponent<IonRedirectProps, IonRedirectState> {
|
||||
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
render() {
|
||||
|
||||
const IonRedirectInner = this.context.getIonRedirect();
|
||||
|
||||
if (!this.context.hasIonicRouter() || !IonRedirect) {
|
||||
console.error('You either do not have an Ionic Router package, or your router does not support using <IonRedirect>');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonRedirectInner {...this.props} />
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
|
||||
}
|
39
packages/react/src/components/IonRoute.tsx
Normal file
39
packages/react/src/components/IonRoute.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
|
||||
export interface IonRouteProps {
|
||||
path?: string;
|
||||
exact?: boolean;
|
||||
show?: boolean;
|
||||
render: (props?: any) => JSX.Element;
|
||||
disableIonPageManagement?: boolean;
|
||||
}
|
||||
|
||||
interface IonRouteState {
|
||||
|
||||
}
|
||||
|
||||
export class IonRoute extends React.PureComponent<IonRouteProps, IonRouteState> {
|
||||
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
render() {
|
||||
|
||||
const IonRouteInner = this.context.getIonRoute();
|
||||
|
||||
if (!this.context.hasIonicRouter() || !IonRoute) {
|
||||
console.error('You either do not have an Ionic Router package, or your router does not support using <IonRoute>');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IonRouteInner {...this.props} />
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
|
||||
}
|
24
packages/react/src/components/IonRouterContext.tsx
Normal file
24
packages/react/src/components/IonRouterContext.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { RouteAction, RouterDirection, RouterOptions } from '../models';
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
|
||||
export interface IonRouterContextState {
|
||||
routeInfo: RouteInfo;
|
||||
push: (pathname: string, routerDirection?: RouterDirection, routeAction?: RouteAction, routerOptions?: RouterOptions, animationBuilder?: AnimationBuilder) => void;
|
||||
back: (animationBuilder?: AnimationBuilder) => void;
|
||||
canGoBack: () => boolean;
|
||||
}
|
||||
|
||||
export const IonRouterContext = React.createContext<IonRouterContextState>({
|
||||
routeInfo: undefined as any,
|
||||
push: () => { throw new Error('An Ionic Router is required for IonRouterContext'); },
|
||||
back: () => { throw new Error('An Ionic Router is required for IonRouterContext'); },
|
||||
canGoBack: () => { throw new Error('An Ionic Router is required for IonRouterContext'); }
|
||||
});
|
||||
|
||||
export function useIonRouter() {
|
||||
const context = useContext(IonRouterContext);
|
||||
return context;
|
||||
}
|
@ -3,35 +3,56 @@ import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
import OutletPageManager from '../routing/OutletPageManager';
|
||||
|
||||
import { IonicReactProps } from './IonicReactProps';
|
||||
import { IonRouterOutletInner } from './inner-proxies';
|
||||
import { createForwardRef } from './utils';
|
||||
|
||||
type Props = LocalJSX.IonRouterOutlet & {
|
||||
basePath?: string;
|
||||
ref?: React.RefObject<any>;
|
||||
ionPage?: boolean;
|
||||
};
|
||||
|
||||
type InternalProps = Props & {
|
||||
interface InternalProps extends Props {
|
||||
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
|
||||
};
|
||||
}
|
||||
|
||||
const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Component<InternalProps> {
|
||||
interface InternalState {
|
||||
}
|
||||
|
||||
class IonRouterOutletContainer extends React.Component<InternalProps, InternalState> {
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
constructor(props: InternalProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const StackManager = this.context.getStackManager();
|
||||
const { children, forwardedRef, ...props } = this.props;
|
||||
|
||||
return (
|
||||
this.context.hasIonicRouter() ? (
|
||||
<StackManager>
|
||||
<IonRouterOutletInner ref={this.props.forwardedRef} {...this.props}>
|
||||
{this.props.children}
|
||||
</IonRouterOutletInner>
|
||||
</StackManager>
|
||||
props.ionPage ? (
|
||||
<OutletPageManager
|
||||
StackManager={StackManager}
|
||||
routeInfo={this.context.routeInfo}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</OutletPageManager>
|
||||
) : (
|
||||
<StackManager routeInfo={this.context.routeInfo}>
|
||||
<IonRouterOutletInner {...props}>
|
||||
{children}
|
||||
</IonRouterOutletInner>
|
||||
</StackManager>
|
||||
)
|
||||
) : (
|
||||
<IonRouterOutletInner ref={this.props.forwardedRef} {...this.props}>
|
||||
<IonRouterOutletInner ref={forwardedRef} {...this.props}>
|
||||
{this.props.children}
|
||||
</IonRouterOutletInner>
|
||||
)
|
||||
@ -41,6 +62,6 @@ const IonRouterOutletContainer = /*@__PURE__*/(() => class extends React.Compone
|
||||
static get contextType() {
|
||||
return NavContext;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export const IonRouterOutlet = createForwardRef<Props & IonicReactProps, HTMLIonRouterOutletElement>(IonRouterOutletContainer, 'IonRouterOutlet');
|
||||
|
@ -10,7 +10,7 @@ describe('IonTabs', () => {
|
||||
const { container } = render(
|
||||
<IonTabs>
|
||||
<IonRouterOutlet></IonRouterOutlet>
|
||||
<IonTabBar slot="bottom" currentPath={'/'}>
|
||||
<IonTabBar slot="bottom">
|
||||
<IonTabButton tab="schedule">
|
||||
<IonLabel>Schedule</IonLabel>
|
||||
<IonIcon name="schedule"></IonIcon>
|
||||
@ -44,7 +44,7 @@ describe('IonTabs', () => {
|
||||
const { container } = render(
|
||||
<IonTabs>
|
||||
<IonRouterOutlet></IonRouterOutlet>
|
||||
<IonTabBar slot="bottom" currentPath={'/'}>
|
||||
<IonTabBar slot="bottom">
|
||||
{false &&
|
||||
<IonTabButton tab="schedule">
|
||||
<IonLabel>Schedule</IonLabel>
|
||||
|
@ -3,11 +3,11 @@ import '@testing-library/jest-dom/extend-expect';
|
||||
|
||||
describe('isCoveredByReact', () => {
|
||||
it('should identify standard events as covered by React', () => {
|
||||
expect(utils.isCoveredByReact('click', document)).toEqual(true);
|
||||
expect(utils.isCoveredByReact('click')).toEqual(true);
|
||||
});
|
||||
it('should identify custom events as not covered by React', () => {
|
||||
expect(utils.isCoveredByReact('change', document)).toEqual(true);
|
||||
expect(utils.isCoveredByReact('ionchange', document)).toEqual(false);
|
||||
expect(utils.isCoveredByReact('change')).toEqual(true);
|
||||
expect(utils.isCoveredByReact('ionchange')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
import React from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
|
||||
import { NavContext } from '../contexts/NavContext';
|
||||
import { RouterOptions } from '../models';
|
||||
import { RouterDirection } from '../models/RouterDirection';
|
||||
|
||||
import { RouterDirection } from './hrefprops';
|
||||
import { attachProps, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
|
||||
import { attachProps, camelToDashCase, createForwardRef, dashToPascalCase, isCoveredByReact } from './utils';
|
||||
|
||||
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
|
||||
forwardedRef?: React.Ref<ElementType>;
|
||||
@ -12,6 +14,8 @@ interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<Elem
|
||||
routerLink?: string;
|
||||
ref?: React.Ref<any>;
|
||||
routerDirection?: RouterDirection;
|
||||
routerOptions?: RouterOptions;
|
||||
routerAnimation?: AnimationBuilder;
|
||||
}
|
||||
|
||||
export const createReactComponent = <PropType, ElementType>(
|
||||
@ -36,10 +40,10 @@ export const createReactComponent = <PropType, ElementType>(
|
||||
}
|
||||
|
||||
private handleClick = (e: React.MouseEvent<PropType>) => {
|
||||
const { routerLink, routerDirection } = this.props;
|
||||
const { routerLink, routerDirection, routerOptions, routerAnimation } = this.props;
|
||||
if (routerLink !== undefined) {
|
||||
e.preventDefault();
|
||||
this.context.navigate(routerLink, routerDirection);
|
||||
this.context.navigate(routerLink, routerDirection, undefined, routerAnimation, routerOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +56,8 @@ export const createReactComponent = <PropType, ElementType>(
|
||||
if (isCoveredByReact(eventName)) {
|
||||
(acc as any)[name] = (cProps as any)[name];
|
||||
}
|
||||
} else if (typeof (cProps as any)[name] === 'string') {
|
||||
(acc as any)[camelToDashCase(name)] = (cProps as any)[name];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
@ -33,11 +33,13 @@ export const createOverlayComponent = <OverlayComponent extends object, OverlayT
|
||||
|
||||
class Overlay extends React.Component<Props> {
|
||||
overlay?: OverlayType;
|
||||
el: HTMLDivElement;
|
||||
el!: HTMLDivElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.el = document.createElement('div');
|
||||
if (typeof document !== 'undefined') {
|
||||
this.el = document.createElement('div');
|
||||
}
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
export declare type RouterDirection = 'forward' | 'back' | 'root' | 'none';
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
|
||||
import { RouterOptions } from '../models';
|
||||
import { RouterDirection } from '../models/RouterDirection';
|
||||
|
||||
export type HrefProps<T> = Omit<T, 'routerDirection'> & {
|
||||
routerLink?: string;
|
||||
routerDirection?: RouterDirection;
|
||||
routerOptions?: RouterOptions;
|
||||
routerAnimation?: AnimationBuilder;
|
||||
};
|
||||
|
@ -21,13 +21,17 @@ export { IonPage } from './IonPage';
|
||||
export { IonTabsContext, IonTabsContextState } from './navigation/IonTabsContext';
|
||||
export { IonTabs } from './navigation/IonTabs';
|
||||
export { IonTabBar } from './navigation/IonTabBar';
|
||||
export { IonTabButton } from './navigation/IonTabButton';
|
||||
export { IonBackButton } from './navigation/IonBackButton';
|
||||
export { IonRouterOutlet } from './IonRouterOutlet';
|
||||
export { IonIcon } from './IonIcon';
|
||||
export * from './IonRoute';
|
||||
export * from './IonRedirect';
|
||||
export * from './IonRouterContext';
|
||||
|
||||
// Utils
|
||||
export { isPlatform, getPlatforms, getConfig } from './utils';
|
||||
export { RouterDirection } from './hrefprops';
|
||||
export { isPlatform, getPlatforms, getConfig, ionRenderToString } from './utils';
|
||||
export * from './hrefprops';
|
||||
|
||||
// Ionic Animations
|
||||
export { CreateAnimation } from './CreateAnimation';
|
||||
@ -51,4 +55,6 @@ addIcons({
|
||||
|
||||
// TODO: defineCustomElements() is asyncronous
|
||||
// We need to use the promise
|
||||
defineCustomElements(window);
|
||||
if (typeof window !== 'undefined') {
|
||||
defineCustomElements(window);
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import { JSX as IoniconsJSX } from 'ionicons';
|
||||
|
||||
import { /*@__PURE__*/ createReactComponent } from './createComponent';
|
||||
|
||||
export const IonTabButtonInner = /*@__PURE__*/createReactComponent<JSX.IonTabButton & { onIonTabButtonClick?: (e: CustomEvent) => void; }, HTMLIonTabButtonElement>('ion-tab-button');
|
||||
export const IonTabBarInner = /*@__PURE__*/createReactComponent<JSX.IonTabBar, HTMLIonTabBarElement>('ion-tab-bar');
|
||||
export const IonBackButtonInner = /*@__PURE__*/createReactComponent<Omit<JSX.IonBackButton, 'icon'>, HTMLIonBackButtonElement>('ion-back-button');
|
||||
export const IonRouterOutletInner = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet, HTMLIonRouterOutletElement>('ion-router-outlet');
|
||||
export const IonRouterOutletInner = /*@__PURE__*/createReactComponent<JSX.IonRouterOutlet & { setRef?: (val: HTMLIonRouterOutletElement) => void; }, HTMLIonRouterOutletElement>('ion-router-outlet');
|
||||
|
||||
// ionicons
|
||||
export const IonIconInner = /*@__PURE__*/createReactComponent<IoniconsJSX.IonIcon, HTMLIonIconElement>('ion-icon');
|
||||
|
@ -17,10 +17,10 @@ export const IonBackButton = /*@__PURE__*/(() => class extends React.Component<P
|
||||
context!: React.ContextType<typeof NavContext>;
|
||||
|
||||
clickButton = (e: React.MouseEvent) => {
|
||||
const defaultHref = this.props.defaultHref;
|
||||
const { defaultHref, routerAnimation } = this.props;
|
||||
if (this.context.hasIonicRouter()) {
|
||||
e.stopPropagation();
|
||||
this.context.goBack(defaultHref);
|
||||
this.context.goBack(defaultHref, routerAnimation);
|
||||
} else if (defaultHref !== undefined) {
|
||||
window.location.href = defaultHref;
|
||||
}
|
||||
|
@ -2,25 +2,31 @@ import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { NavContext } from '../../contexts/NavContext';
|
||||
import { RouteInfo } from '../../models';
|
||||
import { IonicReactProps } from '../IonicReactProps';
|
||||
import { IonTabBarInner } from '../inner-proxies';
|
||||
import { IonTabButton } from '../proxies';
|
||||
import { createForwardRef } from '../utils';
|
||||
|
||||
import { IonTabButton } from './IonTabButton';
|
||||
|
||||
type IonTabBarProps = LocalJSX.IonTabBar & IonicReactProps & {
|
||||
onIonTabsDidChange?: (event: CustomEvent<{ tab: string; }>) => void;
|
||||
onIonTabsWillChange?: (event: CustomEvent<{ tab: string; }>) => void;
|
||||
currentPath?: string;
|
||||
slot?: 'bottom' | 'top';
|
||||
style?: { [key: string]: string; };
|
||||
};
|
||||
|
||||
interface InternalProps extends IonTabBarProps {
|
||||
forwardedRef?: React.RefObject<HTMLIonIconElement>;
|
||||
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||
routeInfo: RouteInfo;
|
||||
}
|
||||
|
||||
interface TabUrls {
|
||||
originalHref: string;
|
||||
currentHref: string;
|
||||
originalRouteOptions?: unknown;
|
||||
currentRouteOptions?: unknown;
|
||||
}
|
||||
|
||||
interface IonTabBarState {
|
||||
@ -34,12 +40,13 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
constructor(props: InternalProps) {
|
||||
super(props);
|
||||
const tabs: { [key: string]: TabUrls; } = {};
|
||||
|
||||
React.Children.forEach((props as any).children, (child: any) => {
|
||||
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
|
||||
tabs[child.props.tab] = {
|
||||
originalHref: child.props.href,
|
||||
currentHref: child.props.href
|
||||
currentHref: child.props.href,
|
||||
originalRouteOptions: child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
|
||||
currentRouteOptions: child.props.href === props.routeInfo?.pathname ? props.routeInfo?.routeOptions : undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -48,7 +55,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
const activeTab = tabKeys
|
||||
.find(key => {
|
||||
const href = tabs[key].originalHref;
|
||||
return props.currentPath!.startsWith(href);
|
||||
return props.routeInfo!.pathname.startsWith(href);
|
||||
}) || tabKeys[0];
|
||||
|
||||
this.state = {
|
||||
@ -59,71 +66,74 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
this.onTabButtonClick = this.onTabButtonClick.bind(this);
|
||||
this.renderTabButton = this.renderTabButton.bind(this);
|
||||
this.setActiveTabOnContext = this.setActiveTabOnContext.bind(this);
|
||||
this.selectTab = this.selectTab.bind(this);
|
||||
}
|
||||
|
||||
setActiveTabOnContext = (_tab: string) => { };
|
||||
|
||||
selectTab(tab: string) {
|
||||
const tabUrl = this.state.tabs[tab];
|
||||
if (tabUrl) {
|
||||
this.onTabButtonClick(new CustomEvent('ionTabButtonClick', {
|
||||
detail: {
|
||||
href: tabUrl.currentHref,
|
||||
tab,
|
||||
selected: tab === this.state.activeTab
|
||||
}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: IonTabBarProps, state: IonTabBarState) {
|
||||
|
||||
static getDerivedStateFromProps(props: InternalProps, state: IonTabBarState) {
|
||||
const tabs = { ...state.tabs };
|
||||
const activeTab = Object.keys(state.tabs)
|
||||
const tabKeys = Object.keys(state.tabs);
|
||||
const activeTab = tabKeys
|
||||
.find(key => {
|
||||
const href = state.tabs[key].originalHref;
|
||||
return props.currentPath!.startsWith(href);
|
||||
return props.routeInfo!.pathname.startsWith(href);
|
||||
});
|
||||
|
||||
// Check to see if the tab button href has changed, and if so, update it in the tabs state
|
||||
React.Children.forEach((props as any).children, (child: any) => {
|
||||
if (child != null && typeof child === 'object' && child.props && child.type === IonTabButton) {
|
||||
const tab = tabs[child.props.tab];
|
||||
if (tab.originalHref !== child.props.href) {
|
||||
if (!tab || (tab.originalHref !== child.props.href)) {
|
||||
tabs[child.props.tab] = {
|
||||
originalHref: child.props.href,
|
||||
currentHref: child.props.href
|
||||
currentHref: child.props.href,
|
||||
originalRouteOptions: child.props.routeOptions,
|
||||
currentRouteOptions: child.props.routeOptions
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(activeTab === undefined || (activeTab === state.activeTab && state.tabs[activeTab].currentHref === props.currentPath))) {
|
||||
tabs[activeTab] = {
|
||||
originalHref: tabs[activeTab].originalHref,
|
||||
currentHref: props.currentPath!
|
||||
};
|
||||
const { activeTab: prevActiveTab } = state;
|
||||
if (activeTab && prevActiveTab) {
|
||||
const prevHref = state.tabs[prevActiveTab].currentHref;
|
||||
const prevRouteOptions = state.tabs[prevActiveTab].currentRouteOptions;
|
||||
if (activeTab !== prevActiveTab || (prevHref !== props.routeInfo?.pathname || prevRouteOptions !== props.routeInfo?.routeOptions)) {
|
||||
tabs[activeTab] = {
|
||||
originalHref: tabs[activeTab].originalHref,
|
||||
currentHref: props.routeInfo!.pathname + (props.routeInfo!.search || ''),
|
||||
originalRouteOptions: tabs[activeTab].originalRouteOptions,
|
||||
currentRouteOptions: props.routeInfo?.routeOptions
|
||||
};
|
||||
if (props.routeInfo.routeAction === 'pop') {
|
||||
// If navigating back and the tabs change, set the prev tab back to its original href
|
||||
tabs[prevActiveTab] = {
|
||||
originalHref: tabs[prevActiveTab].originalHref,
|
||||
currentHref: tabs[prevActiveTab].originalHref,
|
||||
originalRouteOptions: tabs[prevActiveTab].originalRouteOptions,
|
||||
currentRouteOptions: tabs[prevActiveTab].currentRouteOptions
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activeTab && props.onSetCurrentTab(activeTab, props.routeInfo);
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
tabs
|
||||
};
|
||||
}
|
||||
|
||||
private onTabButtonClick(e: CustomEvent<{ href: string, selected: boolean, tab: string; }>) {
|
||||
const originalHref = this.state.tabs[e.detail.tab].originalHref;
|
||||
private onTabButtonClick(e: CustomEvent<{ href: string, selected: boolean, tab: string; routeOptions: unknown; }>) {
|
||||
const tappedTab = this.state.tabs[e.detail.tab];
|
||||
const originalHref = tappedTab.originalHref;
|
||||
const currentHref = e.detail.href;
|
||||
const { activeTab: prevActiveTab } = this.state;
|
||||
// this.props.onSetCurrentTab(e.detail.tab, this.props.routeInfo);
|
||||
// Clicking the current tab will bring you back to the original href
|
||||
if (prevActiveTab === e.detail.tab) {
|
||||
if (originalHref === currentHref) {
|
||||
this.context.navigate(originalHref, 'none');
|
||||
} else {
|
||||
this.context.navigate(originalHref, 'back', 'pop');
|
||||
if (originalHref !== currentHref) {
|
||||
this.context.resetTab(e.detail.tab, originalHref, tappedTab.originalRouteOptions);
|
||||
}
|
||||
} else {
|
||||
if (this.props.onIonTabsWillChange) {
|
||||
@ -132,19 +142,22 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
|
||||
if (this.props.onIonTabsDidChange) {
|
||||
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
|
||||
}
|
||||
this.setActiveTabOnContext(e.detail.tab);
|
||||
this.context.navigate(currentHref, 'none');
|
||||
|
||||
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private renderTabButton(activeTab: string | null | undefined) {
|
||||
return (child: (React.ReactElement<LocalJSX.IonTabButton & { onIonTabButtonClick: (e: CustomEvent) => void; }>) | null | undefined) => {
|
||||
|
||||
return (child: (React.ReactElement<LocalJSX.IonTabButton & { onClick: (e: any) => void; routeOptions?: unknown; }>) | null | undefined) => {
|
||||
if (child != null && child.props && child.type === IonTabButton) {
|
||||
const href = (child.props.tab === activeTab) ? this.props.currentPath : (this.state.tabs[child.props.tab!].currentHref);
|
||||
const href = (child.props.tab === activeTab) ? this.props.routeInfo?.pathname : (this.state.tabs[child.props.tab!].currentHref);
|
||||
const routeOptions = (child.props.tab === activeTab) ? this.props.routeInfo?.routeOptions : (this.state.tabs[child.props.tab!].currentRouteOptions);
|
||||
|
||||
return React.cloneElement(child, {
|
||||
href,
|
||||
onIonTabButtonClick: this.onTabButtonClick
|
||||
routeOptions,
|
||||
onClick: this.onTabButtonClick
|
||||
});
|
||||
}
|
||||
return null;
|
||||
@ -171,7 +184,8 @@ const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({
|
||||
<IonTabBarUnwrapped
|
||||
ref={forwardedRef}
|
||||
{...props as any}
|
||||
currentPath={props.currentPath || context.currentPath}
|
||||
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
|
||||
onSetCurrentTab={context.setCurrentTab}
|
||||
>
|
||||
{props.children}
|
||||
</IonTabBarUnwrapped>
|
||||
|
39
packages/react/src/components/navigation/IonTabButton.tsx
Normal file
39
packages/react/src/components/navigation/IonTabButton.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { RouterOptions } from '../../models';
|
||||
import { IonicReactProps } from '../IonicReactProps';
|
||||
import { IonTabButtonInner } from '../inner-proxies';
|
||||
|
||||
type Props = LocalJSX.IonTabButton & IonicReactProps & {
|
||||
routerOptions?: RouterOptions;
|
||||
ref?: React.RefObject<HTMLIonTabButtonElement>;
|
||||
onClick?: (e: any) => void;
|
||||
};
|
||||
|
||||
export class IonTabButton extends React.Component<Props> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.handleIonTabButtonClick = this.handleIonTabButtonClick.bind(this);
|
||||
}
|
||||
|
||||
handleIonTabButtonClick() {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(new CustomEvent('ionTabButtonClick', {
|
||||
detail: { tab: this.props.tab, href: this.props.href, routeOptions: this.props.routerOptions }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClick, ...rest } = this.props;
|
||||
return (
|
||||
<IonTabButtonInner onIonTabButtonClick={this.handleIonTabButtonClick} {...rest}></IonTabButtonInner>
|
||||
);
|
||||
}
|
||||
|
||||
static get displayName() {
|
||||
return 'IonTabButton';
|
||||
}
|
||||
}
|
@ -2,14 +2,34 @@ import { JSX as LocalJSX } from '@ionic/core';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { NavContext } from '../../contexts/NavContext';
|
||||
import PageManager from '../../routing/PageManager';
|
||||
import { IonRouterOutlet } from '../IonRouterOutlet';
|
||||
|
||||
import { IonTabBar } from './IonTabBar';
|
||||
import { IonTabsContext, IonTabsContextState } from './IonTabsContext';
|
||||
|
||||
class IonTabsElement extends HTMLDivElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
if (window && window.customElements) {
|
||||
customElements.define('ion-tabs', IonTabsElement, { extends: 'div' });
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'ion-tabs': any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ChildFunction = (ionTabContext: IonTabsContextState) => React.ReactNode;
|
||||
|
||||
interface Props extends LocalJSX.IonTabs {
|
||||
className?: string;
|
||||
children: ChildFunction | React.ReactNode;
|
||||
}
|
||||
|
||||
@ -71,7 +91,7 @@ export class IonTabs extends React.Component<Props> {
|
||||
return;
|
||||
}
|
||||
if (child.type === IonRouterOutlet) {
|
||||
outlet = child;
|
||||
outlet = React.cloneElement(child, { tabs: true });
|
||||
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
|
||||
outlet = child.props.children[0];
|
||||
}
|
||||
@ -96,21 +116,34 @@ export class IonTabs extends React.Component<Props> {
|
||||
throw new Error('IonTabs must contain an IonRouterOutlet');
|
||||
}
|
||||
if (!tabBar) {
|
||||
// TODO, this is not required
|
||||
throw new Error('IonTabs needs a IonTabBar');
|
||||
}
|
||||
|
||||
const { className, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<IonTabsContext.Provider
|
||||
value={this.ionTabContextState}
|
||||
>
|
||||
<div style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</div>
|
||||
{this.context.hasIonicRouter() ? (
|
||||
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
|
||||
<ion-tabs className="ion-tabs" style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</ion-tabs>
|
||||
</PageManager>
|
||||
) : (
|
||||
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>
|
||||
{tabBar.props.slot === 'top' ? tabBar : null}
|
||||
<div style={tabsInner} className="tabs-inner">
|
||||
{outlet}
|
||||
</div>
|
||||
{tabBar.props.slot === 'bottom' ? tabBar : null}
|
||||
</div>
|
||||
)}
|
||||
</IonTabsContext.Provider >
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import { HrefProps } from './hrefprops';
|
||||
// ionic/core
|
||||
export const IonApp = /*@__PURE__*/createReactComponent<JSX.IonApp, HTMLIonAppElement>('ion-app');
|
||||
export const IonTab = /*@__PURE__*/createReactComponent<JSX.IonTab, HTMLIonTabElement>('ion-tab');
|
||||
export const IonTabButton = /*@__PURE__*/createReactComponent<JSX.IonTabButton, HTMLIonTabButtonElement>('ion-tab-button');
|
||||
export const IonRouterLink = /*@__PURE__*/createReactComponent<HrefProps<JSX.IonRouterLink>, HTMLIonRouterLinkElement>('ion-router-link', true);
|
||||
export const IonAvatar = /*@__PURE__*/createReactComponent<JSX.IonAvatar, HTMLIonAvatarElement>('ion-avatar');
|
||||
export const IonBackdrop = /*@__PURE__*/createReactComponent<JSX.IonBackdrop, HTMLIonBackdropElement>('ion-backdrop');
|
||||
|
@ -21,7 +21,6 @@ export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}
|
||||
syncEvent(node, eventNameLc, newProps[name]);
|
||||
}
|
||||
} else {
|
||||
(node as any)[name] = newProps[name];
|
||||
const propType = typeof newProps[name];
|
||||
if (propType === 'string') {
|
||||
node.setAttribute(camelToDashCase(name), newProps[name]);
|
||||
@ -61,20 +60,24 @@ export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: a
|
||||
* Checks if an event is supported in the current execution environment.
|
||||
* @license Modernizr 3.0.0pre (Custom Build) | MIT
|
||||
*/
|
||||
export const isCoveredByReact = (eventNameSuffix: string, doc: Document = document) => {
|
||||
const eventName = 'on' + eventNameSuffix;
|
||||
let isSupported = eventName in doc;
|
||||
export const isCoveredByReact = (eventNameSuffix: string) => {
|
||||
if (typeof document === 'undefined') {
|
||||
return true;
|
||||
} else {
|
||||
const eventName = 'on' + eventNameSuffix;
|
||||
let isSupported = eventName in document;
|
||||
|
||||
if (!isSupported) {
|
||||
const element = doc.createElement('div');
|
||||
element.setAttribute(eventName, 'return;');
|
||||
isSupported = typeof (element as any)[eventName] === 'function';
|
||||
if (!isSupported) {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute(eventName, 'return;');
|
||||
isSupported = typeof (element as any)[eventName] === 'function';
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
}
|
||||
|
||||
return isSupported;
|
||||
};
|
||||
|
||||
export const syncEvent = (node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined } }, eventName: string, newEventHandler?: (e: Event) => any) => {
|
||||
export const syncEvent = (node: Element & { __events?: { [key: string]: ((e: Event) => any) | undefined; }; }, eventName: string, newEventHandler?: (e: Event) => any) => {
|
||||
const eventStore = node.__events || (node.__events = {});
|
||||
const oldEventHandler = eventStore[eventName];
|
||||
|
||||
|
@ -16,6 +16,7 @@ export const createForwardRef = <PropType, ElementType>(ReactComponent: any, dis
|
||||
|
||||
export * from './attachProps';
|
||||
export * from './case';
|
||||
export * from './ionRenderToString';
|
||||
|
||||
export const isPlatform = (platform: Platforms) => {
|
||||
return isPlatformCore(window, platform);
|
||||
|
37
packages/react/src/components/utils/ionRenderToString.ts
Normal file
37
packages/react/src/components/utils/ionRenderToString.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { SerializeDocumentOptions, renderToString } from '@ionic/core/hydrate';
|
||||
|
||||
export async function ionRenderToString(html: string, userAgent: string, options: SerializeDocumentOptions = {}) {
|
||||
|
||||
const renderToStringOptions = Object.assign({}, {
|
||||
clientHydrateAnnotations: false,
|
||||
excludeComponents: [
|
||||
// overlays
|
||||
'ion-action-sheet',
|
||||
'ion-alert',
|
||||
'ion-loading',
|
||||
'ion-modal',
|
||||
'ion-picker',
|
||||
'ion-popover',
|
||||
'ion-toast',
|
||||
|
||||
// navigation
|
||||
'ion-router',
|
||||
'ion-route',
|
||||
'ion-route-redirect',
|
||||
'ion-router-link',
|
||||
'ion-router-outlet',
|
||||
|
||||
// tabs
|
||||
'ion-tabs',
|
||||
'ion-tab',
|
||||
|
||||
// auxiliary
|
||||
'ion-picker-column',
|
||||
'ion-virtual-scroll'
|
||||
],
|
||||
userAgent
|
||||
}, options);
|
||||
|
||||
const ionHtml = await renderToString(html, renderToStringOptions);
|
||||
return ionHtml.html;
|
||||
}
|
@ -1,28 +1,52 @@
|
||||
import { RouterDirection } from '@ionic/core';
|
||||
import { AnimationBuilder, RouterDirection } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { RouteInfo } from '../models';
|
||||
|
||||
export interface NavContextState {
|
||||
getIonRoute: () => any;
|
||||
getIonRedirect: () => any;
|
||||
getPageManager: () => any;
|
||||
getStackManager: () => any;
|
||||
goBack: (defaultHref?: string) => void;
|
||||
navigate: (path: string, direction?: RouterDirection | 'none', ionRouteAction?: 'push' | 'replace' | 'pop') => void;
|
||||
goBack: (route?: string | RouteInfo, animationBuilder?: AnimationBuilder) => void;
|
||||
navigate: (path: string, direction?: RouterDirection | 'none', ionRouteAction?: 'push' | 'replace' | 'pop', animationBuilder?: AnimationBuilder, options?: any, tab?: string) => void;
|
||||
hasIonicRouter: () => boolean;
|
||||
registerIonPage: (page: HTMLElement) => void;
|
||||
currentPath: string | undefined;
|
||||
routeInfo?: RouteInfo;
|
||||
setCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||
changeTab: (tab: string, path: string, routeOptions?: any) => void;
|
||||
resetTab: (tab: string, originalHref: string, originalRouteOptions?: any) => void;
|
||||
}
|
||||
|
||||
export const NavContext = /*@__PURE__*/React.createContext<NavContextState>({
|
||||
getIonRedirect: () => undefined,
|
||||
getIonRoute: () => undefined,
|
||||
getPageManager: () => undefined,
|
||||
getStackManager: () => undefined,
|
||||
goBack: (defaultHref?: string) => {
|
||||
if (defaultHref !== undefined) {
|
||||
window.location.pathname = defaultHref;
|
||||
} else {
|
||||
window.history.back();
|
||||
goBack: (route?: string | RouteInfo) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof (route) === 'string') {
|
||||
window.location.pathname = route;
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
},
|
||||
navigate: (path: string) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.pathname = path;
|
||||
}
|
||||
},
|
||||
navigate: (path: string) => { window.location.pathname = path; },
|
||||
hasIonicRouter: () => false,
|
||||
registerIonPage: () => undefined,
|
||||
currentPath: undefined
|
||||
routeInfo: undefined,
|
||||
setCurrentTab: () => undefined,
|
||||
changeTab: (_tab: string, path: string) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.pathname = path;
|
||||
}
|
||||
},
|
||||
resetTab: (_tab: string, path: string) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.pathname = path;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -2,3 +2,6 @@ export * from './lifecycle';
|
||||
export * from './contexts/NavContext';
|
||||
export * from './contexts/IonLifeCycleContext';
|
||||
export * from './components';
|
||||
export * from './routing';
|
||||
export * from './models';
|
||||
export * from './utils/generateId';
|
||||
|
1
packages/react/src/models/RouteAction.ts
Normal file
1
packages/react/src/models/RouteAction.ts
Normal file
@ -0,0 +1 @@
|
||||
export type RouteAction = 'push' | 'replace' | 'pop';
|
19
packages/react/src/models/RouteInfo.ts
Normal file
19
packages/react/src/models/RouteInfo.ts
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
|
||||
import { RouteAction } from './RouteAction';
|
||||
import { RouterDirection } from './RouterDirection';
|
||||
|
||||
export interface RouteInfo<TOptions = any> {
|
||||
id: string;
|
||||
lastPathname?: string;
|
||||
routeAction?: RouteAction;
|
||||
routeDirection?: RouterDirection;
|
||||
routeAnimation?: AnimationBuilder;
|
||||
routeOptions?: TOptions;
|
||||
params?: {[key: string]: string | string[]};
|
||||
pushedByRoute?: string;
|
||||
pathname: string;
|
||||
search: string;
|
||||
tab?: string;
|
||||
}
|
1
packages/react/src/models/RouterDirection.ts
Normal file
1
packages/react/src/models/RouterDirection.ts
Normal file
@ -0,0 +1 @@
|
||||
export type RouterDirection = 'forward' | 'back' | 'root' | 'none';
|
4
packages/react/src/models/RouterOptions.ts
Normal file
4
packages/react/src/models/RouterOptions.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface RouterOptions {
|
||||
as?: string;
|
||||
unmount?: boolean;
|
||||
}
|
4
packages/react/src/models/index.ts
Normal file
4
packages/react/src/models/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './RouteAction';
|
||||
export * from './RouteInfo';
|
||||
export * from './RouterDirection';
|
||||
export * from './RouterOptions';
|
166
packages/react/src/routing/LocationHistory.ts
Normal file
166
packages/react/src/routing/LocationHistory.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
|
||||
// const RESTRICT_SIZE = 100;
|
||||
|
||||
export class LocationHistory {
|
||||
private locationHistory: RouteInfo[] = [];
|
||||
private tabHistory: {
|
||||
[key: string]: RouteInfo[];
|
||||
} = {};
|
||||
|
||||
add(routeInfo: RouteInfo) {
|
||||
|
||||
if (routeInfo.routeAction === 'push' || routeInfo.routeAction == null) {
|
||||
this._add(routeInfo);
|
||||
} else if (routeInfo.routeAction === 'pop') {
|
||||
this._pop(routeInfo);
|
||||
} else if (routeInfo.routeAction === 'replace') {
|
||||
this._replace(routeInfo);
|
||||
}
|
||||
|
||||
if (routeInfo.routeDirection === 'root') {
|
||||
this._clear();
|
||||
this._add(routeInfo);
|
||||
}
|
||||
}
|
||||
|
||||
clearTabStack(tab: string) {
|
||||
const routeInfos = this._getRouteInfosByKey(tab);
|
||||
if (routeInfos) {
|
||||
routeInfos.forEach(ri => {
|
||||
this.locationHistory = this.locationHistory.filter(x => x.id !== ri.id);
|
||||
});
|
||||
this.tabHistory[tab] = [];
|
||||
}
|
||||
}
|
||||
|
||||
update(routeInfo: RouteInfo) {
|
||||
const locationIndex = this.locationHistory.findIndex(x => x.id === routeInfo.id);
|
||||
if (locationIndex > -1) {
|
||||
this.locationHistory.splice(locationIndex, 1, routeInfo);
|
||||
}
|
||||
const tabArray = this.tabHistory[routeInfo.tab || ''];
|
||||
if (tabArray) {
|
||||
const tabIndex = tabArray.findIndex(x => x.id === routeInfo.id);
|
||||
if (tabIndex > -1) {
|
||||
tabArray.splice(tabIndex, 1, routeInfo);
|
||||
} else {
|
||||
tabArray.push(routeInfo);
|
||||
}
|
||||
} else if (routeInfo.tab) {
|
||||
this.tabHistory[routeInfo.tab] = [routeInfo];
|
||||
}
|
||||
}
|
||||
|
||||
private _add(routeInfo: RouteInfo) {
|
||||
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
|
||||
if (routeInfos) {
|
||||
// If the latest routeInfo is the same (going back and forth between tabs), replace it
|
||||
if (routeInfos[routeInfos.length - 1]?.id === routeInfo.id) {
|
||||
routeInfos.pop();
|
||||
}
|
||||
routeInfos.push(routeInfo);
|
||||
}
|
||||
this.locationHistory.push(routeInfo);
|
||||
}
|
||||
|
||||
private _pop(routeInfo: RouteInfo) {
|
||||
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
|
||||
let ri: RouteInfo;
|
||||
if (routeInfos) {
|
||||
// Pop all routes until we are back
|
||||
ri = routeInfos[routeInfos.length - 1];
|
||||
while (ri && ri.id !== routeInfo.id) {
|
||||
routeInfos.pop();
|
||||
ri = routeInfos[routeInfos.length - 1];
|
||||
}
|
||||
// Replace with updated route
|
||||
routeInfos.pop();
|
||||
routeInfos.push(routeInfo);
|
||||
}
|
||||
|
||||
ri = this.locationHistory[this.locationHistory.length - 1];
|
||||
while (ri && ri.id !== routeInfo.id) {
|
||||
this.locationHistory.pop();
|
||||
ri = this.locationHistory[this.locationHistory.length - 1];
|
||||
}
|
||||
// Replace with updated route
|
||||
this.locationHistory.pop();
|
||||
this.locationHistory.push(routeInfo);
|
||||
}
|
||||
|
||||
private _replace(routeInfo: RouteInfo) {
|
||||
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
|
||||
routeInfos && routeInfos.pop();
|
||||
this.locationHistory.pop();
|
||||
this._add(routeInfo);
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
const keys = Object.keys(this.tabHistory);
|
||||
keys.forEach(k => this.tabHistory[k] = []);
|
||||
this.locationHistory = [];
|
||||
}
|
||||
|
||||
private _getRouteInfosByKey(key?: string) {
|
||||
let routeInfos: RouteInfo[] | undefined;
|
||||
if (key) {
|
||||
routeInfos = this.tabHistory[key];
|
||||
if (!routeInfos) {
|
||||
routeInfos = this.tabHistory[key] = [];
|
||||
}
|
||||
}
|
||||
return routeInfos;
|
||||
}
|
||||
|
||||
getFirstRouteInfoForTab(tab: string) {
|
||||
const routeInfos = this._getRouteInfosByKey(tab);
|
||||
if (routeInfos) {
|
||||
return routeInfos[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getCurrentRouteInfoForTab(tab?: string) {
|
||||
const routeInfos = this._getRouteInfosByKey(tab);
|
||||
if (routeInfos) {
|
||||
return routeInfos[routeInfos.length - 1];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
findLastLocation(routeInfo: RouteInfo) {
|
||||
const routeInfos = this._getRouteInfosByKey(routeInfo.tab);
|
||||
if (routeInfos) {
|
||||
for (let i = routeInfos.length - 2; i >= 0; i--) {
|
||||
const ri = routeInfos[i];
|
||||
if (ri) {
|
||||
if (ri.pathname === routeInfo.pushedByRoute) {
|
||||
return ri;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = this.locationHistory.length - 2; i >= 0; i--) {
|
||||
const ri = this.locationHistory[i];
|
||||
if (ri) {
|
||||
if (ri.pathname === routeInfo.pushedByRoute) {
|
||||
return ri;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
previous() {
|
||||
return this.locationHistory[this.locationHistory.length - 2] || this.locationHistory[this.locationHistory.length - 1];
|
||||
}
|
||||
|
||||
current() {
|
||||
return this.locationHistory[this.locationHistory.length - 1];
|
||||
}
|
||||
|
||||
canGoBack() {
|
||||
return this.locationHistory.length > 1;
|
||||
}
|
||||
}
|
101
packages/react/src/routing/NavManager.tsx
Normal file
101
packages/react/src/routing/NavManager.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
import React from 'react';
|
||||
|
||||
import { IonRouterContext, IonRouterContextState } from '../components/IonRouterContext';
|
||||
import { NavContext, NavContextState } from '../contexts/NavContext';
|
||||
import { RouteAction } from '../models/RouteAction';
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
import { RouterDirection } from '../models/RouterDirection';
|
||||
import { RouterOptions } from '../models/RouterOptions';
|
||||
|
||||
import { LocationHistory } from './LocationHistory';
|
||||
import PageManager from './PageManager';
|
||||
|
||||
interface NavManagerProps {
|
||||
routeInfo: RouteInfo;
|
||||
onNavigateBack: (route?: string | RouteInfo, animationBuilder?: AnimationBuilder) => void;
|
||||
onNavigate: (path: string, action: RouteAction, direction?: RouterDirection, animationBuilder?: AnimationBuilder, options?: any, tab?: string) => void;
|
||||
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
|
||||
onChangeTab: (tab: string, path: string, routeOptions?: any) => void;
|
||||
onResetTab: (tab: string, path: string, routeOptions?: any) => void;
|
||||
ionRedirect: any;
|
||||
ionRoute: any;
|
||||
stackManager: any;
|
||||
locationHistory: LocationHistory;
|
||||
}
|
||||
|
||||
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
|
||||
|
||||
ionRouterContextValue: IonRouterContextState = {
|
||||
push: (pathname: string, routerDirection?: RouterDirection, routeAction?: RouteAction, routerOptions?: RouterOptions, animationBuilder?: AnimationBuilder) => {
|
||||
this.navigate(pathname, routerDirection, routeAction, animationBuilder, routerOptions);
|
||||
},
|
||||
back: (animationBuilder?: AnimationBuilder) => {
|
||||
this.goBack(undefined, animationBuilder);
|
||||
},
|
||||
canGoBack: () => this.props.locationHistory.canGoBack(),
|
||||
routeInfo: this.props.routeInfo
|
||||
};
|
||||
|
||||
constructor(props: NavManagerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
goBack: this.goBack.bind(this),
|
||||
hasIonicRouter: () => true,
|
||||
navigate: this.navigate.bind(this),
|
||||
getIonRedirect: this.getIonRedirect.bind(this),
|
||||
getIonRoute: this.getIonRoute.bind(this),
|
||||
getStackManager: this.getStackManager.bind(this),
|
||||
getPageManager: this.getPageManager.bind(this),
|
||||
routeInfo: this.props.routeInfo,
|
||||
setCurrentTab: this.props.onSetCurrentTab,
|
||||
changeTab: this.props.onChangeTab,
|
||||
resetTab: this.props.onResetTab,
|
||||
};
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.addEventListener('ionBackButton', (e: any) => {
|
||||
e.detail.register(0, (processNextHandler: () => void) => {
|
||||
this.goBack();
|
||||
processNextHandler();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
goBack(route?: string | RouteInfo, animationBuilder?: AnimationBuilder) {
|
||||
this.props.onNavigateBack(route, animationBuilder);
|
||||
}
|
||||
|
||||
navigate(path: string, direction: RouterDirection = 'forward', action: RouteAction = 'push', animationBuilder?: AnimationBuilder, options?: any, tab?: string) {
|
||||
this.props.onNavigate(path, action, direction, animationBuilder, options, tab);
|
||||
}
|
||||
|
||||
getPageManager() {
|
||||
return PageManager;
|
||||
}
|
||||
|
||||
getIonRedirect() {
|
||||
return this.props.ionRedirect;
|
||||
}
|
||||
|
||||
getIonRoute() {
|
||||
return this.props.ionRoute;
|
||||
}
|
||||
|
||||
getStackManager() {
|
||||
return this.props.stackManager;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<NavContext.Provider value={{ ...this.state, routeInfo: this.props.routeInfo }}>
|
||||
<IonRouterContext.Provider value={{ ...this.ionRouterContextValue, routeInfo: this.props.routeInfo }}>
|
||||
{this.props.children}
|
||||
</IonRouterContext.Provider>
|
||||
</NavContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
86
packages/react/src/routing/OutletPageManager.tsx
Normal file
86
packages/react/src/routing/OutletPageManager.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IonRouterOutletInner } from '../components/inner-proxies';
|
||||
import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext';
|
||||
import { RouteInfo } from '../models';
|
||||
|
||||
import { StackContext } from './StackContext';
|
||||
|
||||
interface OutletPageManagerProps {
|
||||
className?: string;
|
||||
forwardedRef?: React.RefObject<HTMLDivElement>;
|
||||
routeInfo?: RouteInfo;
|
||||
StackManager: any;
|
||||
}
|
||||
|
||||
export class OutletPageManager extends React.Component<OutletPageManagerProps> {
|
||||
ionLifeCycleContext!: React.ContextType<typeof IonLifeCycleContext>;
|
||||
context!: React.ContextType<typeof StackContext>;
|
||||
ionRouterOutlet: HTMLIonRouterOutletElement | undefined;
|
||||
|
||||
constructor(props: OutletPageManagerProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.ionRouterOutlet) {
|
||||
setTimeout(() => {
|
||||
this.context.registerIonPage(this.ionRouterOutlet!, this.props.routeInfo!);
|
||||
}, 25);
|
||||
|
||||
this.ionRouterOutlet.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
this.ionRouterOutlet.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
this.ionRouterOutlet.addEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
this.ionRouterOutlet.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.ionRouterOutlet) {
|
||||
this.ionRouterOutlet.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
this.ionRouterOutlet.removeEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
this.ionRouterOutlet.removeEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
this.ionRouterOutlet.removeEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
ionViewWillEnterHandler() {
|
||||
this.ionLifeCycleContext.ionViewWillEnter();
|
||||
}
|
||||
|
||||
ionViewDidEnterHandler() {
|
||||
this.ionLifeCycleContext.ionViewDidEnter();
|
||||
}
|
||||
|
||||
ionViewWillLeaveHandler() {
|
||||
this.ionLifeCycleContext.ionViewWillLeave();
|
||||
}
|
||||
|
||||
ionViewDidLeaveHandler() {
|
||||
this.ionLifeCycleContext.ionViewDidLeave();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { StackManager, children, routeInfo, ...props } = this.props;
|
||||
return (
|
||||
<IonLifeCycleContext.Consumer>
|
||||
{context => {
|
||||
this.ionLifeCycleContext = context;
|
||||
return (
|
||||
<StackManager routeInfo={routeInfo}>
|
||||
<IonRouterOutletInner setRef={(val: HTMLIonRouterOutletElement) => this.ionRouterOutlet = val} {...props}>
|
||||
{children}
|
||||
</IonRouterOutletInner>
|
||||
</StackManager>
|
||||
);
|
||||
}}
|
||||
</IonLifeCycleContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return StackContext;
|
||||
}
|
||||
}
|
||||
export default OutletPageManager;
|
86
packages/react/src/routing/PageManager.tsx
Normal file
86
packages/react/src/routing/PageManager.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext';
|
||||
import { RouteInfo } from '../models';
|
||||
|
||||
import { StackContext } from './StackContext';
|
||||
|
||||
interface PageManagerProps {
|
||||
className?: string;
|
||||
forwardedRef?: React.RefObject<HTMLDivElement>;
|
||||
routeInfo?: RouteInfo;
|
||||
}
|
||||
|
||||
export class PageManager extends React.PureComponent<PageManagerProps> {
|
||||
ionLifeCycleContext!: React.ContextType<typeof IonLifeCycleContext>;
|
||||
context!: React.ContextType<typeof StackContext>;
|
||||
ionPageElementRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: PageManagerProps) {
|
||||
super(props);
|
||||
this.ionPageElementRef = this.props.forwardedRef || React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.ionPageElementRef.current) {
|
||||
this.context.registerIonPage(this.ionPageElementRef.current, this.props.routeInfo!);
|
||||
this.ionPageElementRef.current.addEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
this.ionPageElementRef.current.addEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
this.ionPageElementRef.current.addEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
this.ionPageElementRef.current.addEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.ionPageElementRef.current) {
|
||||
this.ionPageElementRef.current.removeEventListener('ionViewWillEnter', this.ionViewWillEnterHandler.bind(this));
|
||||
this.ionPageElementRef.current.removeEventListener('ionViewDidEnter', this.ionViewDidEnterHandler.bind(this));
|
||||
this.ionPageElementRef.current.removeEventListener('ionViewWillLeave', this.ionViewWillLeaveHandler.bind(this));
|
||||
this.ionPageElementRef.current.removeEventListener('ionViewDidLeave', this.ionViewDidLeaveHandler.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
ionViewWillEnterHandler() {
|
||||
this.ionLifeCycleContext.ionViewWillEnter();
|
||||
}
|
||||
|
||||
ionViewDidEnterHandler() {
|
||||
this.ionLifeCycleContext.ionViewDidEnter();
|
||||
}
|
||||
|
||||
ionViewWillLeaveHandler() {
|
||||
this.ionLifeCycleContext.ionViewWillLeave();
|
||||
}
|
||||
|
||||
ionViewDidLeaveHandler() {
|
||||
this.ionLifeCycleContext.ionViewDidLeave();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, children, routeInfo, forwardedRef, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<IonLifeCycleContext.Consumer>
|
||||
{context => {
|
||||
this.ionLifeCycleContext = context;
|
||||
const hidePageClass = this.context.isInOutlet() ? 'ion-page-invisible' : '';
|
||||
return (
|
||||
<div
|
||||
className={className ? `${className} ion-page ${hidePageClass}` : `ion-page ${hidePageClass}`}
|
||||
ref={this.ionPageElementRef}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</IonLifeCycleContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
static get contextType() {
|
||||
return StackContext;
|
||||
}
|
||||
}
|
||||
export default PageManager;
|
29
packages/react/src/routing/RouteManagerContext.ts
Normal file
29
packages/react/src/routing/RouteManagerContext.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
|
||||
import { ViewItem } from './ViewItem';
|
||||
|
||||
export interface RouteManagerContextState {
|
||||
addViewItem: (viewItem: ViewItem) => void;
|
||||
clearOutlet: (outletId: string) => void;
|
||||
createViewItem: (outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement) => ViewItem;
|
||||
findLeavingViewItemByRouteInfo: (routeInfo: RouteInfo, outletId?: string) => ViewItem | undefined;
|
||||
// findViewItemByPathname: (pathname: string, outletId?: string) => ViewItem | undefined;
|
||||
findViewItemByRouteInfo: (routeInfo: RouteInfo, outletId?: string) => ViewItem | undefined;
|
||||
getChildrenToRender: (outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo, reRender: () => void) => React.ReactNode[];
|
||||
getViewItemForTransition: (pathname: string) => ViewItem | undefined;
|
||||
unMountViewItem: (viewItem: ViewItem) => void;
|
||||
}
|
||||
|
||||
export const RouteManagerContext = /*@__PURE__*/React.createContext<RouteManagerContextState>({
|
||||
addViewItem: () => undefined,
|
||||
clearOutlet: () => undefined,
|
||||
createViewItem: () => undefined as any,
|
||||
findLeavingViewItemByRouteInfo: () => undefined,
|
||||
// findViewItemByPathname: () => undefined,
|
||||
findViewItemByRouteInfo: () => undefined,
|
||||
getChildrenToRender: () => undefined as any,
|
||||
getViewItemForTransition: () => undefined,
|
||||
unMountViewItem: () => undefined,
|
||||
});
|
13
packages/react/src/routing/StackContext.tsx
Normal file
13
packages/react/src/routing/StackContext.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
|
||||
export interface StackContextState {
|
||||
registerIonPage: (page: HTMLElement, routeInfo: RouteInfo) => void;
|
||||
isInOutlet: () => boolean;
|
||||
}
|
||||
|
||||
export const StackContext = React.createContext<StackContextState>({
|
||||
registerIonPage: () => undefined,
|
||||
isInOutlet: () => false
|
||||
});
|
13
packages/react/src/routing/ViewItem.ts
Normal file
13
packages/react/src/routing/ViewItem.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export interface ViewItem<T = any> {
|
||||
id: string;
|
||||
reactElement: ReactElement;
|
||||
ionPageElement?: HTMLElement | undefined;
|
||||
ionRoute?: boolean;
|
||||
mount: boolean;
|
||||
routeData?: T;
|
||||
transitionHtml?: string;
|
||||
outletId: string;
|
||||
disableIonPageManagement?: boolean;
|
||||
}
|
53
packages/react/src/routing/ViewLifeCycleManager.tsx
Normal file
53
packages/react/src/routing/ViewLifeCycleManager.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { DefaultIonLifeCycleContext, IonLifeCycleContext } from '../contexts/IonLifeCycleContext';
|
||||
|
||||
interface ViewTransitionManagerProps {
|
||||
removeView: () => void;
|
||||
mount: boolean;
|
||||
}
|
||||
|
||||
interface ViewTransitionManagerState {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export class ViewLifeCycleManager extends React.Component<ViewTransitionManagerProps, ViewTransitionManagerState> {
|
||||
ionLifeCycleContext = new DefaultIonLifeCycleContext();
|
||||
private _isMounted = false;
|
||||
|
||||
constructor(props: ViewTransitionManagerProps) {
|
||||
super(props);
|
||||
|
||||
this.ionLifeCycleContext.onComponentCanBeDestroyed(() => {
|
||||
if (!this.props.mount) {
|
||||
if (this._isMounted) {
|
||||
this.setState({
|
||||
show: false
|
||||
}, () => this.props.removeView());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.state = {
|
||||
show: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { show } = this.state;
|
||||
return (
|
||||
<IonLifeCycleContext.Provider value={this.ionLifeCycleContext}>
|
||||
{show && this.props.children}
|
||||
</IonLifeCycleContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
67
packages/react/src/routing/ViewStacks.ts
Normal file
67
packages/react/src/routing/ViewStacks.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { RouteInfo } from '../models/RouteInfo';
|
||||
|
||||
import { ViewItem } from './ViewItem';
|
||||
|
||||
export abstract class ViewStacks {
|
||||
private viewStacks: { [key: string]: ViewItem[]; } = {};
|
||||
|
||||
constructor() {
|
||||
this.add = this.add.bind(this);
|
||||
this.clear = this.clear.bind(this);
|
||||
this.getViewItemsForOutlet = this.getViewItemsForOutlet.bind(this);
|
||||
this.remove = this.remove.bind(this);
|
||||
}
|
||||
|
||||
add(viewItem: ViewItem) {
|
||||
const { outletId } = viewItem;
|
||||
if (!this.viewStacks[outletId]) {
|
||||
this.viewStacks[outletId] = [viewItem];
|
||||
} else {
|
||||
this.viewStacks[outletId].push(viewItem);
|
||||
}
|
||||
}
|
||||
|
||||
clear(outletId: string) {
|
||||
// Give some time for the leaving views to transition before removing
|
||||
setTimeout(() => {
|
||||
// console.log('Removing viewstack for outletID ' + outletId);
|
||||
delete this.viewStacks[outletId];
|
||||
}, 500);
|
||||
}
|
||||
|
||||
getViewItemsForOutlet(outletId: string) {
|
||||
return (this.viewStacks[outletId] || []);
|
||||
}
|
||||
|
||||
remove(viewItem: ViewItem) {
|
||||
const { outletId } = viewItem;
|
||||
const viewStack = this.viewStacks[outletId];
|
||||
if (viewStack) {
|
||||
const viewItemToRemove = viewStack.find(x => x.id === viewItem.id);
|
||||
if (viewItemToRemove) {
|
||||
viewItemToRemove.mount = false;
|
||||
this.viewStacks[outletId] = viewStack.filter(x => x.id !== viewItemToRemove.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getStackIds() {
|
||||
return Object.keys(this.viewStacks);
|
||||
}
|
||||
|
||||
protected getAllViewItems() {
|
||||
const keys = this.getStackIds();
|
||||
const viewItems: ViewItem[] = [];
|
||||
keys.forEach(k => {
|
||||
viewItems.push(...this.viewStacks[k]);
|
||||
});
|
||||
return viewItems;
|
||||
}
|
||||
|
||||
abstract createViewItem(outletId: string, reactElement: React.ReactElement, routeInfo: RouteInfo, page?: HTMLElement): ViewItem;
|
||||
// abstract findViewItemByPathname(pathname: string, outletId?: string): ViewItem | undefined;
|
||||
abstract findViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string): ViewItem | undefined;
|
||||
abstract findLeavingViewItemByRouteInfo(routeInfo: RouteInfo, outletId?: string): ViewItem | undefined;
|
||||
abstract getChildrenToRender(outletId: string, ionRouterOutlet: React.ReactElement, routeInfo: RouteInfo, reRender: () => void, setInTransition: () => void): React.ReactNode[];
|
||||
abstract getViewItemForTransition(pathname: string): ViewItem | undefined;
|
||||
}
|
7
packages/react/src/routing/index.ts
Normal file
7
packages/react/src/routing/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * from './RouteManagerContext';
|
||||
export * from './ViewLifeCycleManager';
|
||||
export * from './LocationHistory';
|
||||
export * from './NavManager';
|
||||
export * from './ViewItem';
|
||||
export * from './StackContext';
|
||||
export * from './ViewStacks';
|
8
packages/react/src/routing/package.json
Normal file
8
packages/react/src/routing/package.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@ionic/react/routing",
|
||||
"version": "5.0.1",
|
||||
"module": "index.mjs",
|
||||
"main": "index.js",
|
||||
"typings": "../dist/types/routing/index.d.ts",
|
||||
"private": true
|
||||
}
|
8
packages/react/src/utils/generateId.ts
Normal file
8
packages/react/src/utils/generateId.ts
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
const ids: { [key: string]: number; } = { main: 0 };
|
||||
|
||||
export const generateId = (type = 'main') => {
|
||||
const id = (ids[type] ?? 0) + 1;
|
||||
ids[type] = id;
|
||||
return (id).toString();
|
||||
};
|
Reference in New Issue
Block a user