From baf73dd2bd38bcdc17d94d2c411019530819cd3d Mon Sep 17 00:00:00 2001 From: Batyr Kanzitdinov Date: Thu, 2 Feb 2023 02:31:06 +0100 Subject: [PATCH] better tabs logic now it can generate multiple tabs and used as Root --- src/index.tsx | 23 +---- src/navio-navigation.tsx | 27 ++++-- src/navio.tsx | 203 ++++++++++++++++++++++++++------------- src/types.tsx | 31 +++--- 4 files changed, 174 insertions(+), 110 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 2f951ba..d9adfa8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,25 +4,4 @@ import {NavioScreen} from './types'; export {Navio}; export type {NavioScreen}; -// const navio = Navio.build({ -// screens: { -// Main: { -// component: () => { -// return <>Main; -// }, -// options: { -// title: 'Main', -// }, -// }, -// }, -// stacks: { -// MainStack: { -// screens: ['Main'], -// }, -// }, -// drawers: { -// MainDrawer: { -// content: [{name: '', stack: ''}], -// }, -// }, -// }); +// TODO There is a problem when there are more than 2 drawers or 2 tabs, then .jumpTo() won't help with autocompletion diff --git a/src/navio-navigation.tsx b/src/navio-navigation.tsx index 8eea125..6fad319 100644 --- a/src/navio-navigation.tsx +++ b/src/navio-navigation.tsx @@ -7,23 +7,36 @@ import { DrawerActions, } from '@react-navigation/native'; import React from 'react'; -import {Keys, TDrawerData, TModalData, TRootName, TScreenData, TStackData, TTabData} from './types'; +import { + ContentKeys, + TDrawerData, + TModalData, + TRootName, + TScreenData, + TStackData, + TTabsData, +} from './types'; export class NavioNavigation< ScreenName extends string, StackName extends string, - TabName extends string, + TabsName extends string, ModalName extends string, DrawerName extends string, - DrawerContentName extends Keys, // ScreenData extends TScreenData, StackData extends TStackData, - TabData extends TTabData, + TabsData extends TTabsData, ModalData extends TModalData, DrawerData extends TDrawerData, // - RootName extends TRootName = TRootName, + TabsContentName extends ContentKeys = ContentKeys, + DrawerContentName extends ContentKeys = ContentKeys, + RootName extends TRootName = TRootName< + StackName, + TabsName, + DrawerName + >, > { protected navRef: NavigationContainerRefWithCurrent; protected navIsReadyRef: React.MutableRefObject; @@ -50,7 +63,7 @@ export class NavioNavigation< // | Methods | // =========== protected navigate = < - T extends ScreenName | StackName | TabName | ModalName, + T extends ScreenName | StackName | TabsName | ModalName, Params extends object | undefined, >( name: T, @@ -189,7 +202,7 @@ export class NavioNavigation< * * @param name TabName */ - jumpTo(name: T) { + jumpTo(name: T) { if (self.navIsReady) { self.navRef.current?.dispatch(TabActions.jumpTo(name as string)); } diff --git a/src/navio.tsx b/src/navio.tsx index 42cf1d7..71503aa 100644 --- a/src/navio.tsx +++ b/src/navio.tsx @@ -10,70 +10,82 @@ import {NavioNavigation} from './navio-navigation'; import { TScreenData, TStackData, - TTabData, + TTabsData, TModalData, TRootName, Layout, - TStack, + TStackDefinition, NavioScreen, BaseOptions, - ExtractProps, RootProps, TStackDataObj, TDrawerData, - TDrawer, + TDrawerDefinition, Keys, + TTabsDefinition, + ContentKeys, } from './types'; export class Navio< ScreenName extends string, StackName extends string, - TabName extends string, + TabsName extends string, ModalName extends string, DrawerName extends string, - DrawerContentName extends Keys, // ScreenData extends TScreenData, StackData extends TStackData, - TabData extends TTabData, + TabsData extends TTabsData, ModalData extends TModalData, DrawerData extends TDrawerData, // - RootName extends TRootName = TRootName, + TabsContentName extends ContentKeys = ContentKeys, + DrawerContentName extends ContentKeys = ContentKeys, + RootName extends TRootName = TRootName< + StackName, + TabsName, + DrawerName + >, > extends NavioNavigation< ScreenName, StackName, - TabName, + TabsName, ModalName, DrawerName, - DrawerContentName, ScreenData, StackData, - TabData, + TabsData, ModalData, DrawerData, + TabsContentName, + DrawerContentName, RootName > { static build< ScreenName extends string, StackName extends string, - TabName extends string, + TabsName extends string, ModalName extends string, DrawerName extends string, - DrawerContentName extends Keys, // ScreenData extends TScreenData, StackData extends TStackData, - TabData extends TTabData, + TabsData extends TTabsData, ModalData extends TModalData, DrawerData extends TDrawerData, // - RootName extends TRootName = TRootName, + TabsContentName extends ContentKeys = ContentKeys, + DrawerContentName extends ContentKeys = ContentKeys, + RootName extends TRootName = TRootName< + StackName, + TabsName, + DrawerName + >, >( data: Layout< Record, Record, - Record, + Record, Record, Record, RootName @@ -82,15 +94,17 @@ export class Navio< const _navio = new Navio< ScreenName, StackName, - TabName, + TabsName, ModalName, DrawerName, - DrawerContentName, ScreenData, StackData, - TabData, + TabsData, ModalData, - DrawerData + DrawerData, + TabsContentName, + DrawerContentName, + RootName >(data); return _navio; } @@ -101,7 +115,7 @@ export class Navio< private layout: Layout< Record, Record, - Record, + Record, Record, Record, RootName @@ -114,7 +128,7 @@ export class Navio< data: Layout< Record, Record, - Record, + Record, Record, Record, RootName @@ -129,51 +143,83 @@ export class Navio< // | Layouts | // =========== private Stack: React.FC<{ - stackDef: TStack | undefined; + stackDef: TStackDefinition | undefined; }> = ({stackDef}) => { if (!stackDef) return null; const {screens, stacks, hooks} = this.layout; + if (!screens) { + this.log('No screens registered'); + return <>; + } + if (!stacks) { + this.log('No stacks registered'); + return <>; + } + + // -- getting navigator props + const navigatorProps = Array.isArray(stackDef) + ? // if stackDef is ScreenName[] + {} + : // if stackDev is TStackDataObj + typeof stackDef === 'object' + ? (stackDef as TStackDataObj).navigatorProps ?? {} + : // if stackDev is StackName -> look into stacks[...] + typeof stackDef === 'string' + ? // if stacks[name] is ScreenName[] + Array.isArray(stacks[stackDef]) + ? {} + : // if stacks[name] is TStackDataObj + typeof stacks[stackDef] === 'object' + ? (stacks[stackDef] as TStackDataObj).navigatorProps ?? {} + : {} + : {}; + // -- running hooks if (hooks) for (const h of hooks) if (h) h(); // -- building navigator const Stack = createNativeStackNavigator(); const StackScreensMemo = useMemo(() => { - if (!screens || !stacks) return null; - const screensKeys: ScreenName[] = Array.isArray(stackDef) - ? stackDef - : typeof stackDef === 'string' - ? Array.isArray(stacks[stackDef]) + ? // if stackDef is ScreenName[] + stackDef + : // if stackDev is TStackDataObj + typeof stackDef === 'object' + ? (stackDef as TStackDataObj).screens ?? [] + : // if stackDev is StackName -> look into stacks[...] + typeof stackDef === 'string' + ? // if stacks[name] is ScreenName[] + Array.isArray(stacks[stackDef]) ? (stacks[stackDef] as ScreenName[]) - : typeof stacks[stackDef] === 'object' - ? (stacks[stackDef] as TStackDataObj).screens + : // if stacks[name] is TStackDataObj + typeof stacks[stackDef] === 'object' + ? (stacks[stackDef] as TStackDataObj).screens ?? [] : [] : []; return screensKeys.map(sk => { const key = String(sk) as string; - const s = screens[key as ScreenName]; + const screen = screens[key as ScreenName]; // component // -- handling when screen is a component or object{component,options} let sComponent: NavioScreen; let sOptions: BaseOptions; - if (typeof s === 'object') { - if (s.component) { + if (typeof screen === 'object') { + if (screen.component) { // {component,options} - sComponent = s.component; - sOptions = s.options ?? {}; + sComponent = screen.component; + sOptions = screen.options ?? {}; } else { // component // this might happen if a screen is provided as wrapped component, for ex. const Main: React.FC = observer(() => {}); (observer from mobx) - sComponent = s as any; + sComponent = screen as any; sOptions = {}; } } else { // component - sComponent = s; + sComponent = screen; sOptions = {}; } const C = sComponent; @@ -181,9 +227,9 @@ export class Navio< // options const defaultOptions = this.layout.defaultOptions?.stack ?? {}; const Opts: BaseOptions = props => ({ - // navio.defaultOptions + // navio.defaultOptions.stack ...(typeof defaultOptions === 'function' ? defaultOptions(props) : defaultOptions), - // navio.screens.[].options + // navio.screens.Name.options ...(typeof sOptions === 'function' ? sOptions(props) : sOptions), // component-based options ...(typeof C.options === 'function' ? C.options(props) : C.options), @@ -194,11 +240,11 @@ export class Navio< }); }, [stackDef, screens, stacks]); - return {StackScreensMemo}; + return {StackScreensMemo}; }; private Drawer: React.FC<{ - drawerDef: TDrawer | undefined; + drawerDef: TDrawerDefinition | undefined; }> = ({drawerDef}) => { const {drawers, hooks} = this.layout; if (!drawers) { @@ -206,9 +252,9 @@ export class Navio< return <>; } - const drawer: TDrawerData | undefined = + const currentDrawer: TDrawerData | undefined = typeof drawerDef === 'string' ? drawers[drawerDef] : undefined; - if (!drawer) { + if (!currentDrawer) { this.log('No drawer found'); return <>; } @@ -219,8 +265,10 @@ export class Navio< // -- building navigator const Drawer = createDrawerNavigator(); const DrawerScreensMemo = useMemo(() => { - const dContent = drawer.content; - const dContentKeys: DrawerContentName[] = Object.keys(drawer.content) as DrawerContentName[]; + const dContent = currentDrawer.content; + const dContentKeys: DrawerContentName[] = Object.keys( + currentDrawer.content, + ) as DrawerContentName[]; return dContentKeys.map(dck => { const key = String(dck) as string; // drawer content key const dcsDef = dContent[key]; // drawer content stack definition @@ -231,10 +279,12 @@ export class Navio< // options const defaultOptions = this.layout.defaultOptions?.drawer ?? {}; const Opts: BaseOptions = props => ({ - // navio.defaultOptions + // navio.defaultOptions.drawer ...(typeof defaultOptions === 'function' ? defaultOptions(props) : defaultOptions), - // navio.drawers.[].options - ...(typeof drawer.options === 'function' ? drawer.options(props) : drawer.options), + // navio.drawers.Name.options + ...(typeof currentDrawer.options === 'function' + ? currentDrawer.options(props) + : currentDrawer.options), }); // must be function. merge options from buildNavio. also providing default options // screen @@ -242,44 +292,59 @@ export class Navio< }); }, []); - return {DrawerScreensMemo}; + return ( + {DrawerScreensMemo} + ); }; - private Tabs: React.FC = () => { - const {tabs, hooks} = this.layout; + private Tabs: React.FC<{ + tabsDef: TTabsDefinition | undefined; + }> = ({tabsDef}) => { + const {tabs, hooks, defaultOptions} = this.layout; if (!tabs) { this.log('No tabs registered'); return <>; } + const currentTabs: TTabsData | undefined = + typeof tabsDef === 'string' ? tabs[tabsDef] : undefined; + if (!currentTabs) { + this.log('No tabs found'); + return <>; + } + // -- running hooks if (hooks) for (const h of hooks) if (h) h(); // -- building navigator const Tabs = createBottomTabNavigator(); const TabsScreensMemo = useMemo(() => { - const tabsKeys = Object.keys(tabs); - return tabsKeys.map(tk => { - const key = String(tk) as string; - const t = tabs[key as TabName]; + const tContent = currentTabs.content; + const tContentKeys: TabsContentName[] = Object.keys(currentTabs.content) as TabsContentName[]; + return tContentKeys.map(tck => { + const key = String(tck) as string; // tabs content key + const tcsDef = tContent[key as TabsName]; // tabs content stack definition + // component - const C = () => this.Stack({stackDef: t.stack}); + const C = () => this.Stack({stackDef: tcsDef}); // options - const defaultOptions = this.layout.defaultOptions?.tab ?? {}; + const defaultOpts = defaultOptions?.tab ?? {}; const Opts: BaseOptions = props => ({ - // navio.defaultOptions - ...(typeof defaultOptions === 'function' ? defaultOptions(props) : defaultOptions), - // navio.tabs.[].options - ...(typeof t.options === 'function' ? t.options(props) : t.options), + // navio.defaultOptions.tab + ...(typeof defaultOpts === 'function' ? defaultOpts(props) : defaultOpts), + // navio.tabs.Name.options + ...(typeof currentTabs.options === 'function' + ? currentTabs.options(props) + : currentTabs.options), }); // must be function. merge options from buildNavio. also providing default options // screen return ; }); - }, [tabs]); + }, [tabs, currentTabs]); - return {TabsScreensMemo}; + return {TabsScreensMemo}; }; /** @@ -293,7 +358,7 @@ export class Navio< // Effects useEffect(() => { - // if `initialRouteName` is changed, we need to set new root + // if `initialRouteName` is changed, we set new root if (initialRouteName) { this.setRoot(initialRouteName); } @@ -329,8 +394,12 @@ export class Navio< const TabsMemo = useMemo(() => { if (!tabs) return null; - const C = () => this.Tabs({}); - return ; + const tabsKeys = Object.keys(tabs); + return tabsKeys.map(dk => { + const key = String(dk) as string; + const C = () => this.Tabs({tabsDef: dk as TabsName}); + return ; + }); }, [tabs]); // -- generating modals @@ -387,7 +456,7 @@ export class Navio< screenOptions={{headerShown: false}} > {/* Tabs, Stacks, Drawers */} - {[AppScreensMemo]} + {AppScreensMemo} {/* Modals */} {ModalsMemo} diff --git a/src/types.tsx b/src/types.tsx index e6e7619..3a33352 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -3,12 +3,20 @@ import {BottomTabNavigationOptions} from '@react-navigation/bottom-tabs'; import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; import {NavigationContainer, ParamListBase, RouteProp} from '@react-navigation/native'; import {DrawerNavigationOptions} from '@react-navigation/drawer'; +import {NativeStackNavigatorProps} from '@react-navigation/native-stack/lib/typescript/src/types'; export type Keys = keyof T; +export type ContentKeys = Keys; export type BaseOptions = | Return | ((props?: {route?: RouteProp; navigation?: any}) => Return); +export type TStackDefinition = + | StackName + | ScreenName[] + | TStackDataObj; +export type TDrawerDefinition = DrawerName; // maybe smth else will be added +export type TTabsDefinition = TabsName; // maybe smth else will be added export type TScreenData = | NavioScreen | { @@ -17,26 +25,21 @@ export type TScreenData = }; export type TStackDataObj = { screens: ScreenName[]; - screenProps?: any; // TODO - navigatorOptions?: any; // TODO + navigatorProps?: NativeStackNavigatorProps; }; export type TStackData = ScreenName[] | TStackDataObj; -export type TStack = StackName | TStackData; -export type TDrawer = DrawerName; // maybe smth else will be added -export type TTabData = { - stack: TStack; +export type TTabsData = { + content: Record>; options?: BaseOptions; - screenProps?: any; // TODO - navigatorProps?: any; // TODO + navigatorProps?: any; // TODO BottomTabNavigatorProps doesn't exist :( }; -export type TModalData = TStack; +export type TModalData = TStackDefinition; export type TDrawerData = { - content: Record>; + content: Record>; options?: BaseOptions; - screenProps?: any; // TODO - navigatorProps?: any; // TODO + navigatorProps?: any; // TODO DrawerNavigatorProps doesn't exist :( }; -export type TRootName = 'Tabs' | StackName | DrawerName; +export type TRootName = TabsName | StackName | DrawerName; export type ExtractProps = Type extends React.FC ? X : never; export type Layout< @@ -91,7 +94,7 @@ export type Layout< /** * `(optional)` - * Default options to be applied per each stack's screens, tab or drawer generated within the app layout. + * Default options to be applied per each stack's, tab's or drawer's screens generated within the app. */ defaultOptions?: DefaultOptions; };