├── .gitignore ├── .gitattributes ├── .prettierrc ├── .release-it.json ├── tsconfig.json ├── src ├── help.tsx ├── index.tsx ├── tunnel.tsx ├── types.tsx └── navio.tsx ├── LICENSE.md ├── package.json ├── docs └── layout-examples.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .env 4 | node_modules/ 5 | dist/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "bracketSpacing": false, 6 | "arrowParens": "avoid" 7 | } -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "non-interactive": true, 3 | "git": { 4 | "requireCleanWorkingDir": true 5 | }, 6 | "npm": { 7 | "publish": false 8 | }, 9 | "github": { 10 | "release": true, 11 | "releaseName": "v${version}" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "noEmit": false, 6 | "rootDir": "src", 7 | "outDir": "dist", 8 | "declaration": true, 9 | "skipLibCheck": true, 10 | "module": "ESNext", 11 | "types": ["node"] 12 | }, 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | -------------------------------------------------------------------------------- /src/help.tsx: -------------------------------------------------------------------------------- 1 | import {BaseOptions, BaseOptionsProps} from './types'; 2 | 3 | export const safeOpts = 4 | (opts: BaseOptions | BaseOptions[]) => (props: BaseOptionsProps) => { 5 | if (Array.isArray(opts)) { 6 | let all_opts = {}; 7 | for (const opt of opts) { 8 | all_opts = { 9 | ...all_opts, 10 | ...safeOpts(opt)(props), 11 | }; 12 | } 13 | return all_opts; 14 | } 15 | 16 | return typeof opts === 'function' ? opts(props) : opts; 17 | }; 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import {Navio} from './navio'; 2 | import {NavioScreen} from './types'; 3 | 4 | export {Navio}; 5 | export type {NavioScreen}; 6 | 7 | // minor issues 8 | // TODO [ISSUE][TS] When there are more than 2 drawers or 2 tabs, then `.jumpTo()` won't help with autocompletion. 9 | // TODO [ISSUE][TS] `BottomTabNavigatorProps` and `DrawerNavigatorProps` are missing from react-navigation exports. Type `any` is currently used. 10 | // TODO [ISSUE][TS] `navio.setParams` - think about how to take all names and not getting plain string when some name is not defined. It's related to `RootName` issue. 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Batyr K. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tunnel.tsx: -------------------------------------------------------------------------------- 1 | import {TunnelEvent, TunnelEvents, TunnelListener, TunnelParams} from './types'; 2 | 3 | export class NavioTunnel { 4 | private events: TunnelEvents = {}; 5 | 6 | on(event: TunnelEvent, listener: TunnelListener) { 7 | if (!(event in this.events)) { 8 | this.events[event] = []; 9 | } 10 | this.events[event]?.push(listener); 11 | return () => this.removeListener(event, listener); 12 | } 13 | 14 | removeListener(event: TunnelEvent, listener: TunnelListener) { 15 | if (!(event in this.events)) { 16 | return; 17 | } 18 | const idx = this.events[event]?.indexOf(listener); 19 | if (idx && idx > -1) { 20 | this.events[event]?.splice(idx, 1); 21 | } 22 | if (this.events[event]?.length === 0) { 23 | delete this.events[event]; 24 | } 25 | } 26 | 27 | echo(event: TunnelEvent, params?: TunnelParams) { 28 | if (!(event in this.events)) { 29 | return; 30 | } 31 | this.events[event]?.forEach((listener: TunnelListener) => listener(params)); 32 | } 33 | 34 | once(event: TunnelEvent, listener: TunnelListener) { 35 | const remove = this.on(event, (params?: TunnelParams) => { 36 | remove(); 37 | listener(params); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-navio", 3 | "version": "0.1.0", 4 | "description": "🧭 Navigation library for React Native. Build once, navigate from anywhere to everywhere!", 5 | "author": "Batyr ", 6 | "homepage": "https://github.com/kanzitelli/rn-navio", 7 | "bugs": "https://github.com/kanzitelli/rn-navio/issues", 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "clean": "rimraf dist", 15 | "format": "prettier --write \"{src,}/**/*.{ts,tsx}\"", 16 | "build": "run-s clean format build:*", 17 | "build:main": "tsc -p tsconfig.json", 18 | "release": "run-s release:github release:npm", 19 | "release:github": "dotenv release-it", 20 | "release:npm": "npm publish --access public", 21 | "release:npm:next": "run-s build && npm publish --tag next" 22 | }, 23 | "peerDependencies": { 24 | "@react-navigation/bottom-tabs": "^6.4.0", 25 | "@react-navigation/drawer": "^6.5.8", 26 | "@react-navigation/native": "^6.0.13", 27 | "@react-navigation/native-stack": "^6.9.0", 28 | "react": "*", 29 | "react-native": "*" 30 | }, 31 | "devDependencies": { 32 | "@react-native-community/eslint-config": "^3.1.0", 33 | "@react-navigation/bottom-tabs": "^6.4.0", 34 | "@react-navigation/drawer": "^6.5.8", 35 | "@react-navigation/native": "^6.0.13", 36 | "@react-navigation/native-stack": "^6.9.0", 37 | "@tsconfig/react-native": "^2.0.2", 38 | "@types/react": "^18.0.21", 39 | "dotenv-cli": "^6.0.0", 40 | "eslint": "^8.24.0", 41 | "husky": "^8.0.1", 42 | "npm-run-all": "^4.1.5", 43 | "prettier": "^2.7.1", 44 | "pretty-quick": "^3.1.3", 45 | "release-it": "^15.4.2", 46 | "rimraf": "^3.0.2", 47 | "typescript": "^4.8.4" 48 | }, 49 | "husky": { 50 | "hooks": { 51 | "pre-commit": "pretty-quick --staged" 52 | } 53 | }, 54 | "keywords": [ 55 | "react-native", 56 | "react-navigation", 57 | "react-native-navigation", 58 | "expo", 59 | "navio", 60 | "rn-navio" 61 | ], 62 | "license": "MIT" 63 | } 64 | -------------------------------------------------------------------------------- /src/types.tsx: -------------------------------------------------------------------------------- 1 | import React, {PropsWithChildren} from 'react'; 2 | import {BottomTabNavigationOptions} from '@react-navigation/bottom-tabs'; 3 | import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; 4 | import {NavigationContainer, ParamListBase, RouteProp} from '@react-navigation/native'; 5 | import {DrawerNavigationOptions} from '@react-navigation/drawer'; 6 | import {NativeStackNavigatorProps} from '@react-navigation/native-stack/lib/typescript/src/types'; 7 | 8 | // Container is anything from Stack, Tabs, Drawer, Modal 9 | 10 | export type Keys = keyof T; 11 | export type ContainerLayoutKeys = Keys; 12 | 13 | export type RootSetAs = keyof Pick; 14 | 15 | // Options 16 | export type BaseOptionsProps = 17 | | {route?: RouteProp; navigation?: any} 18 | | undefined; 19 | export type BaseOptions = 20 | | Return 21 | | ((props?: BaseOptionsProps) => Return); 22 | type ScreenOptions = BaseOptions; 23 | export type StackScreenOptions = ScreenOptions; 24 | export type ModalScreenOptions = StackScreenOptions; 25 | export type TabScreenOptions = BaseOptions; 26 | export type DrawerScreenOptions = BaseOptions; 27 | 28 | // Navigator options 29 | type StackNavigatorProps = Omit; // omitting required children prop 30 | type ModalNavigatorProps = StackNavigatorProps; 31 | type TabNavigatorProps = any; // TODO BottomTabNavigatorProps doesn't exist :( 32 | type DrawerNavigatorProps = any; // TODO DrawerNavigatorProps doesn't exist :( 33 | 34 | // Definitions 35 | export type TStackDefinition = 36 | | StackName 37 | | ScreenName[] 38 | | TStackDataObj; 39 | export type TDrawerDefinition = DrawerName; // maybe smth else will be added 40 | export type TTabsDefinition = TabName; // maybe smth else will be added 41 | export type TModalsDefinition = ModalName; // maybe smth else will be added 42 | 43 | // Data 44 | export type TScreenData = 45 | | NavioScreen 46 | | { 47 | component: NavioScreen; 48 | options?: ScreenOptions; 49 | }; 50 | // Stack 51 | export type TStackDataObj = { 52 | screens: ScreenName[]; 53 | options?: StackScreenOptions; 54 | navigatorProps?: StackNavigatorProps; 55 | }; 56 | export type TStackData = ScreenName[] | TStackDataObj; 57 | // Tabs 58 | export type TTabLayoutValue = { 59 | stack?: TStackDefinition; 60 | drawer?: TDrawerDefinition; 61 | options?: TabScreenOptions; 62 | }; 63 | export type TTabsData = { 64 | layout: Record>; 65 | options?: TabScreenOptions; 66 | navigatorProps?: TabNavigatorProps; 67 | }; 68 | // Drawer 69 | export type TDrawerLayoutValue = { 70 | stack?: TStackDefinition; 71 | tabs?: TTabsDefinition; 72 | options?: DrawerScreenOptions; 73 | }; 74 | export type TDrawersData = { 75 | layout: Record>; 76 | options?: DrawerScreenOptions; 77 | navigatorProps?: DrawerNavigatorProps; 78 | }; 79 | // Modal 80 | export type TModalData = { 81 | stack?: TStackDefinition; 82 | options?: ModalScreenOptions; 83 | // navigatorProps?: ModalNavigatorProps; // we don't need it because we build Navigator.Screen instead of Drawer.Navigator 84 | }; 85 | 86 | export type TRootName< 87 | StackName extends string, 88 | TabsName extends string, 89 | DrawersName extends string, 90 | > = `tabs.${TabsName}` | `stacks.${StackName}` | `drawers.${DrawersName}`; 91 | export type ExtractProps = Type extends React.FC ? X : never; 92 | 93 | // Layout 94 | export type Layout< 95 | Screens = any, 96 | Stacks = any, 97 | Tabs = any, 98 | Modals = any, 99 | Drawers = any, 100 | RootName = any, 101 | > = { 102 | /** 103 | * `(required)` 104 | * Screens of the app. Navigate to by using `navio.push('...')` method. 105 | */ 106 | screens: Screens; 107 | 108 | /** 109 | * `(optional)` 110 | * Stacks of the app. Navigate to by using `navio.pushStack('...Stack')` method. Good to use if you want to hide tabs on the specific screens. 111 | */ 112 | stacks?: Stacks; 113 | 114 | /** 115 | * `(optional)` 116 | * Tabs app. Navigate to by using `navio.jumpTo('...Tab')` method. 117 | */ 118 | tabs?: Tabs; 119 | 120 | /** 121 | * `(optional)` 122 | * Modals of the app. Navigate to by using `navio.show('...Modal')` method. 123 | */ 124 | modals?: Modals; 125 | 126 | /** 127 | * `(optional)` 128 | * Drawers of the app. Navigate to by using `navio.drawer.open('...Drawer')` method. 129 | */ 130 | drawers?: Drawers; 131 | 132 | /** 133 | * `(optional)` 134 | * Root name to start the app with. Possible values `any of tabs, stacks, drawers names`. 135 | */ 136 | root?: RootName; 137 | 138 | /** 139 | * `(optional)` 140 | * List of hooks that will be run on each generated stack or tab navigators. Useful for dark mode or language change. 141 | */ 142 | hooks?: Function[]; 143 | 144 | /** 145 | * `(optional)` 146 | * Default options to be applied per each stack's, tab's or drawer's screens generated within the app. 147 | */ 148 | defaultOptions?: DefaultOptions; 149 | }; 150 | export type DefaultOptions = { 151 | stacks?: { 152 | screen?: StackScreenOptions; 153 | container?: ScreenOptions; 154 | }; 155 | tabs?: { 156 | screen?: TabScreenOptions; 157 | container?: ScreenOptions; 158 | }; 159 | drawers?: { 160 | screen?: DrawerScreenOptions; 161 | container?: ScreenOptions; 162 | }; 163 | modals?: { 164 | container?: ScreenOptions; 165 | }; 166 | }; 167 | export type NavioScreen = React.FC> & { 168 | options?: StackScreenOptions; 169 | }; 170 | 171 | // Layouts 172 | export type RootProps = { 173 | navigationContainerProps?: Omit, 'children'>; 174 | root?: RootName; 175 | }; 176 | 177 | // Tunnel (Event Emitter) 178 | export type TunnelEvent$UpdateOptions$Params = { 179 | name: Name; 180 | options: Options; 181 | }; 182 | 183 | export type TunnelEvent = 'tabs.updateOptions' | 'drawer.updateOptions'; 184 | export type TunnelParams = T; 185 | export type TunnelListener = (params: TunnelParams) => void; 186 | export type TunnelEvents = Partial>; 187 | -------------------------------------------------------------------------------- /docs/layout-examples.md: -------------------------------------------------------------------------------- 1 | # Navio layouts 2 | 3 | Below you will find some options of different app layouts and most used approaches which can be achieved with Navio in easy way and without any boilerplate code. 4 | 5 | In the examples, we suppose that we already have our screen components ready under the `@app/screens` folder. 6 | 7 | ## Content 8 | 9 | - [Stacks](#stacks) 10 | - [App with 2 screens](#app-with-2-screens) 11 | - [Tabs](#tabs) 12 | - [App with 3 tabs](#app-with-3-tabs) 13 | - [Tab-based app with drawer](#tab-based-app-with-drawer) 14 | - [Hide tabs](#hide-tabs) 15 | - [Drawer](#drawer) 16 | - [Drawer app and 3 pages](#drawer-app-and-3-pages) 17 | - [Drawer app and tabs inside of one page](#drawer-app-and-tabs-inside-of-one-page) 18 | - [Drawer with custom content](#drawer-with-custom-content) 19 | - [Auth flow](#drawer) 20 | - [Static](#static) 21 | - [Dynamic](#dynamic) 22 | 23 | ## Stacks 24 | 25 | ### App with 2 screens 26 | 27 | ```tsx 28 | // App.tsx 29 | import {Navio} from 'rn-navio'; 30 | import {Home, Profile} from '@app/screens'; 31 | 32 | const navio = Navio.build({ 33 | screens: {Home, Profile}, 34 | stacks: { 35 | HomeStack: ['Home', 'Profile'], 36 | }, 37 | root: 'stacks.HomeStack', 38 | }); 39 | 40 | export default () => ; 41 | 42 | // Now you can push Profile screen from Home screen 43 | navio.push('Profile'); 44 | ``` 45 | 46 | ## Tabs 47 | 48 | ### App with 3 tabs 49 | 50 | ```tsx 51 | // App.tsx 52 | import {Navio} from 'rn-navio'; 53 | import {Home, Discover, Settings, Profile} from '@app/screens'; 54 | 55 | const navio = Navio.build({ 56 | screens: {Home, Discover, Settings, Profile}, 57 | stacks: { 58 | HomeStack: ['Home'], 59 | DiscoverStack: ['Discover'], 60 | SettingsStack: ['Settings', 'Profile'], 61 | }, 62 | tabs: { 63 | AppTabs: { 64 | layout: { 65 | HomeTab: { 66 | stack: 'HomeStack', 67 | options: () => ({ 68 | title: 'Home', 69 | }), 70 | }, 71 | DiscoverTab: { 72 | stack: 'DiscoverStack', 73 | options: () => ({ 74 | title: 'Discover', 75 | }), 76 | }, 77 | SettingsTab: { 78 | stack: 'SettingsStack', 79 | options: () => ({ 80 | title: 'Settings', 81 | }), 82 | }, 83 | }, 84 | }, 85 | }, 86 | root: 'tabs.AppTabs', 87 | }); 88 | 89 | export default () => ; 90 | ``` 91 | 92 | ### Tab-based app with drawer 93 | 94 | Builds an app with 2 tabs and a drawer inside one of the tab. It can be used for showing product categories or similar cases. 95 | 96 | ```tsx 97 | // App.tsx 98 | import {Navio} from 'rn-navio'; 99 | import {Home, Discover, Settings, Profile} from '@app/screens'; 100 | 101 | const navio = Navio.build({ 102 | screens: {Home, Settings}, 103 | stacks: { 104 | HomeStack: ['Home'], 105 | }, 106 | tabs: { 107 | AppTabs: { 108 | layout: { 109 | HomeTab: { 110 | stack: HomeStack, 111 | }, 112 | SettingsTab: { 113 | drawer: 'SettingsDrawer', 114 | }, 115 | }, 116 | }, 117 | }, 118 | drawers: { 119 | SettingsDrawer: { 120 | layout: { 121 | SettingsPage: { 122 | stack: ['Settings'], 123 | options: { 124 | title: 'Settings', 125 | drawerPosition: 'right', 126 | }, 127 | }, 128 | ProfilePage: { 129 | stack: ['Profile'], 130 | options: { 131 | title: 'Profile', 132 | drawerPosition: 'right', 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | root: 'tabs.AppTabs', 139 | }); 140 | 141 | export default () => ; 142 | ``` 143 | 144 | ### Hide tabs 145 | 146 | Hide tabs on a specific screen. 147 | 148 | As React Navigation suggests in the [docs](https://reactnavigation.org/docs/hiding-tabbar-in-screens/), we need to define a stack that we want to "push over" tabs. 149 | 150 | ```tsx 151 | // App.tsx 152 | import {Navio} from 'rn-navio'; 153 | import {Home, Product, Settings} from '@app/screens'; 154 | 155 | const navio = Navio.build({ 156 | screens: { 157 | Home, 158 | Settings, 159 | 160 | ProductPage: { 161 | component: Product, 162 | options: { 163 | headerShown: false, 164 | }, 165 | }, 166 | }, 167 | stacks: { 168 | ProductPageStack: { 169 | screens: ['ProductPage'], 170 | containerOptions: { 171 | headerShown: true, 172 | title: 'Product page', 173 | }, 174 | }, 175 | }, 176 | tabs: { 177 | AppTabs: { 178 | layout: { 179 | Home: ['Home'], 180 | Settings: ['Settings'], 181 | }, 182 | }, 183 | }, 184 | root: 'tabs.AppTabs', 185 | }); 186 | 187 | export default () => ; 188 | 189 | // Now you can push `ProductPageStack` from tabs and it will be without tabs. 190 | navio.stacks.push('ProductPageStack'); 191 | ``` 192 | 193 | ## Drawer 194 | 195 | ### Drawer app and 3 pages 196 | 197 | ```tsx 198 | // App.tsx 199 | import {Navio} from 'rn-navio'; 200 | import {Home, Discover, Settings, Profile} from '@app/screens'; 201 | 202 | const navio = Navio.build({ 203 | screens: {Home, Discover, Settings, Profile}, 204 | stacks: { 205 | HomeStack: ['Home'], 206 | SettingsStack: ['Settings', 'Profile'], 207 | }, 208 | drawers: { 209 | AppDrawer: { 210 | layout: { 211 | MainPage: 'HomeStack', 212 | DiscoverPage: ['Discover'], 213 | SettingsPage: {stack: 'SettingsStack'}, 214 | }, 215 | }, 216 | }, 217 | root: 'drawers.AppDrawer', 218 | }); 219 | 220 | export default () => ; 221 | ``` 222 | 223 | ### Drawer app and tabs inside of one page 224 | 225 | Builds an app with main drawer and tabs inside one of the drawer pages. It can be used to build Twitter app alike layout. 226 | 227 | ```tsx 228 | // App.tsx 229 | import {Navio} from 'rn-navio'; 230 | import {Home, ProductMain, ProductReviews, Settings} from '@app/screens'; 231 | 232 | const navio = Navio.build({ 233 | screens: {Home, ProductMain, ProductReviews, Settings}, 234 | tabs: { 235 | ProductTabs: { 236 | layout: { 237 | MainTab: { 238 | stack: ['ProductMain'], 239 | }, 240 | ReviewsTab: { 241 | stack: ['ProductReviews'], 242 | }, 243 | }, 244 | }, 245 | }, 246 | drawers: { 247 | AppDrawer: { 248 | layout: { 249 | MainPage: { 250 | stack: ['Home'], 251 | }, 252 | ProductPage: { 253 | tabs: 'ProductTabs', 254 | }, 255 | SettingsPage: ['Settings'], 256 | }, 257 | }, 258 | }, 259 | root: 'drawers.AppDrawer', 260 | }); 261 | 262 | export default () => ; 263 | ``` 264 | 265 | ### Drawer with custom content 266 | 267 | Builds an app with main drawer and custom content. 268 | 269 | ```tsx 270 | // App.tsx 271 | import {Navio} from 'rn-navio'; 272 | import {Home, Settings} from '@app/screens'; 273 | 274 | const navio = Navio.build({ 275 | screens: {Home, Settings}, 276 | drawers: { 277 | AppDrawer: { 278 | content: { 279 | HomePage: { 280 | stack: ['Home'], 281 | }, 282 | SettingsPage: { 283 | stack: ['Settings'], 284 | }, 285 | }, 286 | 287 | navigatorProps: { 288 | drawerContent: (props: any) => ( 289 | 290 | 291 | 292 | 293 | navio.drawers.close()} /> 294 | 295 | ), 296 | }, 297 | }, 298 | }, 299 | root: 'drawers.AppDrawer', 300 | }); 301 | 302 | export default () => ; 303 | ``` 304 | 305 | ## Auth flow 306 | 307 | There are two ways of handling `Auth` flow with Navio: Static and Dynamic. 308 | 309 | ### Static 310 | 311 | ```tsx 312 | // App.tsx 313 | import {Navio} from 'rn-navio'; 314 | import {Main, SignIn, SignUp} from '@app/screens'; 315 | 316 | const navio = Navio.build({ 317 | screens: {Main, SignIn, SignUp}, 318 | stacks: { 319 | MainApp: ['Main'], 320 | Auth: ['SignIn', 'SignUp'], 321 | }, 322 | root: 'stacks.MainApp', 323 | }); 324 | 325 | export default () => ; 326 | 327 | // Let's say you show `MainApp` in the beginning with limited functionality 328 | // and have some screen with "Sign in" button. After pressing "Sign in", 329 | // you can show `Auth` flow. 330 | const Main = () => { 331 | const {navio} = useServices(); 332 | 333 | const onSignInPressed = () => { 334 | navio.setRoot('stacks', 'Auth'); 335 | }; 336 | 337 | return <>{Content}; 338 | }; 339 | 340 | // After `Auth` flow is successfully finished, you can show `MainApp`. 341 | navio.setRoot('stacks', 'MainApp'); 342 | ``` 343 | 344 | ### Dynamic 345 | 346 | ```tsx 347 | // App.tsx 348 | import {Navio} from 'rn-navio'; 349 | import {Main, SignIn, SignUp} from '@app/screens'; 350 | 351 | const navio = Navio.build({ 352 | screens: {Main, SignIn, SignUp}, 353 | stacks: { 354 | MainApp: ['Main'], 355 | Auth: ['SignIn', 'SignUp'], 356 | }, 357 | root: 'stacks.MainApp', 358 | }); 359 | 360 | // Let's say you want to react on changes from auth provider (stores, hook, etc.) 361 | // and show root app depending on that value. 362 | export default (): JSX.Element => { 363 | const {authData} = useAuth(); 364 | const isLoggedIn = !!authData; 365 | 366 | return ( 367 | 368 | 369 | 370 | ); 371 | }; 372 | ``` 373 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧭 Navio 2 | 3 | [![React Native Compatible](https://img.shields.io/badge/React%20Native-Compatible-brightgreen)](https://snack.expo.dev/@kanzitelli/rn-navio-snack) 4 | [![Expo Compatible](https://img.shields.io/badge/𝝠%20Expo-Compatible-brightgreen)](https://snack.expo.dev/@kanzitelli/rn-navio-snack) 5 | [![Expo Snack](https://img.shields.io/badge/𝝠%20Expo-Snack-blue)](https://snack.expo.dev/@kanzitelli/rn-navio-snack) 6 | 7 | Navio is a navigation library for React Native built on top of [React Navigation](https://github.com/react-navigation/react-navigation). The main goal is to improve DX by building the app layout in one place and using the power of TypeScript to provide autocompletion and other features. 8 | 9 | Navio lets you easily create different kinds of apps: bottom tabs-based, simple single-screen, and apps with drawer layouts. It takes care of all boilerplate code configuration for Navigators, Screens, Stacks, Tabs, and Drawers under the hood, so you can focus on developing your app's business logic. 10 | 11 | > If `Navio` helped you in a way, support it with ⭐️ 12 | 13 | ☣️ Navio is still a young library and may have breaking changes in the future. Check out if [Navio is production-ready](#is-navio-production-ready) 14 | 15 | ## Quickstart 16 | 17 | ### Install dependencies 18 | 19 | ```bash 20 | yarn add rn-navio 21 | ``` 22 | 23 |
24 | React Navigation dependencies 25 | 26 | As Navio is built on top of [React Navigation](https://github.com/react-navigation/react-navigation), you will need to have the following libraries installed: 27 | 28 | ```bash 29 | yarn add @react-navigation/stack @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs @react-navigation/drawer 30 | ``` 31 | 32 | For more information, please check the [installation steps](https://reactnavigation.org/docs/getting-started/#installation). 33 | 34 |
35 | 36 | ### Create your first Navio layout 37 | 38 | This code will build a simple app with one screen. 39 | 40 | ```tsx 41 | // App.tsx 42 | import {Text} from 'react-native'; 43 | import {Navio} from 'rn-navio'; 44 | 45 | const Home = () => { 46 | return Home page; 47 | }; 48 | 49 | const navio = Navio.build({ 50 | screens: {Home}, 51 | stacks: { 52 | HomeStack: ['Home'], 53 | }, 54 | root: 'stacks.HomeStack', 55 | }); 56 | 57 | export default () => ; 58 | ``` 59 | 60 |
61 | Tab-based app with 2 tabs 62 | 63 | ```tsx 64 | // App.tsx 65 | import {Text} from 'react-native'; 66 | import {Navio} from 'rn-navio'; 67 | 68 | const Home = () => { 69 | return Home page; 70 | }; 71 | const Settings = () => { 72 | return Settings page; 73 | }; 74 | 75 | const navio = Navio.build({ 76 | screens: {Home, Settings}, 77 | stacks: { 78 | HomeStack: ['Home'], 79 | SettingsStack: ['Settings'], 80 | }, 81 | tabs: { 82 | AppTabs: { 83 | layout: { 84 | HomeTab: {stack: 'HomeStack'}, 85 | SettingsTab: {stack: 'SettingsStack'}, 86 | }, 87 | }, 88 | }, 89 | root: 'tabs.AppTabs', 90 | }); 91 | 92 | export default () => ; 93 | ``` 94 | 95 |
96 | 97 | If you'd like to see more complex and exotic example, please follow [this link](/docs/layout-examples.md). 98 | 99 | ## Playground 100 | 101 | ### React Native Starter 102 | 103 | You can bootstrap a new project with Navio from [expo-starter](https://github.com/kanzitelli/expo-starter): 104 | 105 | ```bash 106 | npx cli-rn new app 107 | ``` 108 | 109 | ### Expo Snack 110 | 111 | Play with the library in the [Expo Snack](https://snack.expo.dev/@kanzitelli/rn-navio-snack). 112 | 113 | ## Navigation API 114 | 115 | Navio provides a colleciton of actions to perform navigation within the app. Suppose, you have a `navio` object: 116 | 117 | ### Common 118 | 119 | - `.N` 120 | 121 | Current navigation object from React Navigation. You can perform any of [these actions](https://reactnavigation.org/docs/navigation-actions). 122 | 123 | - `.push(name, params?)` 124 | 125 | Adds a route on top of the stack and navigates forward to it. 126 | 127 | - `.goBack()` 128 | 129 | Allows to go back to the previous route in history. 130 | 131 | - `.setParams(name, params)` 132 | 133 | Allows to update params for a certain route. 134 | 135 | - `.setRoot(as, rootName)` 136 | 137 | Sets a new app root. It can be used to switch between `Stacks`, `Tabs`, and `Drawers`. 138 | 139 | ### Stacks 140 | 141 | Stacks-related actions. 142 | 143 | - `.stacks.push(name)` 144 | 145 | Adds a route on top of the stack and navigates forward to it. It can hide tab bar. 146 | 147 | - `.stacks.pop(count?)` 148 | 149 | Takes you back to a previous screen in the stack. 150 | 151 | - `.stacks.popToTop()` 152 | 153 | Takes you back to the first screen in the stack, dismissing all the others. 154 | 155 | - `.stacks.setRoot(name)` 156 | 157 | Sets a new app root from stacks. 158 | 159 | ### Tabs 160 | 161 | Tabs-related actions. 162 | 163 | - `.tabs.jumpTo(name)` 164 | 165 | Used to jump to an existing route in the tab navigator. 166 | 167 | - `.tabs.updateOptions(name, options)` 168 | 169 | Updates options for a given tab. Used to change badge count. 170 | 171 | - `.tabs.setRoot(name)` 172 | 173 | Sets a new app root from tabs. 174 | 175 | ### Drawers 176 | 177 | Drawers-related actions. 178 | 179 | - `.drawers.open()` 180 | 181 | Used to open the drawer pane. 182 | 183 | - `.drawers.close()` 184 | 185 | Used to close the drawer pane. 186 | 187 | - `.drawers.toggle()` 188 | 189 | Used to open the drawer pane if closed, or close if open. 190 | 191 | - `.drawers.jumpTo(name)` 192 | 193 | Used to jump to an existing route in the drawer navigator. 194 | 195 | - `.drawers.updateOptions(name, options)` 196 | 197 | Updates options for a given drawer menu content. Used to change its title. 198 | 199 | - `.drawers.setRoot(name)` 200 | 201 | Sets a new app root from drawers. 202 | 203 | ### Modals 204 | 205 | Modals-related actions. 206 | 207 | - `.modals.show(name, params)` 208 | 209 | Used to show an existing modal and pass params. 210 | 211 | - `.modals.getParams(name)` 212 | 213 | Returns params passed for modal on .show() method. 214 | 215 | ### Hooks 216 | 217 | Useful hooks. 218 | 219 | - `.useN()` 220 | 221 | Duplicate of `useNavigation()` hook from React Navigation. Used for convenience inside screens to get access to navigation object. [Docs](https://reactnavigation.org/docs/use-navigation/). 222 | 223 | - `.useR()` 224 | 225 | Duplicate of `useRoute()` hook from React Navigation. Used to convenience inside screens to get access to route object. [Docs](https://reactnavigation.org/docs/use-route) 226 | 227 | - `.useParams()` 228 | 229 | Used for quick access to navigation route params. Used to convenience inside screens when passing params. 230 | 231 | ## Layout structure 232 | 233 | Navio requires `screens` and at least one `stacks` to build an app layout. `tabs`, `drawers`, `modals`, `root`, `hooks` and `defaultOptions` are optional and used for more advanced app layouts. 234 | 235 | ### Screens 236 | 237 | These are main bricks of your app with Navio. You can reuse them for any stack you want to build. 238 | 239 | A screen can be defined by passing a plain React component. If you'd like to pass some options which describe the screen, then you can pass an object as well. 240 | 241 |
242 | Example 243 | 244 | ```tsx 245 | import {Screen1, Screen3} from '@app/screens'; 246 | 247 | const navio = Navio.build({ 248 | screens: { 249 | One: Screen1, 250 | Two: () => { 251 | return <>; 252 | } 253 | Three: { 254 | component: Screen3, 255 | options: (props) => ({ 256 | title: 'ThreeOne' 257 | }) 258 | } 259 | }, 260 | }); 261 | ``` 262 | 263 |
264 | 265 | ### Stacks 266 | 267 | Stacks are built using `Screens` that have been defined before. IDEs should help with autocompletion for better DX. 268 | 269 | A stack can be defined by passing an array of `Screens`. If you'd like to pass some options down to stack navigator, then you can pass an object. 270 | 271 |
272 | Example 273 | 274 | ```tsx 275 | const navio = Navio.build({ 276 | // screens are taken from previous step 277 | stacks: { 278 | MainStack: ['One', 'Two'], 279 | ExampleStack: { 280 | screens: ['Three'], 281 | navigatorProps: { 282 | screenListeners: { 283 | focus: () => {}, 284 | }, 285 | }, 286 | }, 287 | }, 288 | }); 289 | ``` 290 | 291 |
292 | 293 | ### Tabs 294 | 295 | Tabs are built using `Screens`, `Stacks`, and `Drawers` that have been defined before. 296 | 297 | Tabs can be defined by passing an object with `content` and, optionally, props for navigator. 298 | 299 | Content can take as a value one of `Stacks`, `Drawers`, array of `Screens`, or an object that describes stack and options for bottom tab (describing title, icon, etc.). 300 | 301 |
302 | Example 303 | 304 | ```tsx 305 | const navio = Navio.build({ 306 | // screens and stacks are taken from previous step 307 | tabs: { 308 | AppTabs: { 309 | layout: { 310 | MainTab: { 311 | stack: ['One', 'Two'], 312 | // or drawer: 'SomeDrawer', 313 | options: () => ({ 314 | title: 'Main', 315 | }), 316 | }, 317 | ExampleTab: { 318 | stack: 'ExampleStack', 319 | // or drawer: 'SomeDrawer', 320 | options: () => ({ 321 | title: 'Example', 322 | }), 323 | }, 324 | }, 325 | options: { ... }, // optional 326 | navigatorProps: { ... }, // optional 327 | }, 328 | }, 329 | }); 330 | ``` 331 | 332 |
333 | 334 | ### Drawers 335 | 336 | Drawers are built using `Screens`, `Stacks`, and `Tabs` that have been defined before. 337 | 338 | Drawers can be defined by passing an object with `content` and, optionally, props for navigator. 339 | 340 | Content can take as a value one of `Stacks`, `Tabs`, array of `Screens`, or an object that describes stack and options for bottom tab (describing title, icon, etc.). 341 | 342 |
343 | Example 344 | 345 | ```tsx 346 | const navio = Navio.build({ 347 | // screens and stacks are taken from previous step 348 | drawers: { 349 | MainDrawer: { 350 | layout: { 351 | Main: 'MainStack', 352 | Example: 'ExampleStack', 353 | Playground: ['One', 'Two', 'Three'], 354 | }, 355 | options: { ... }, // optional 356 | navigatorProps: { ... }, // optional 357 | }, 358 | }, 359 | }); 360 | ``` 361 | 362 |
363 | 364 | ### Modals 365 | 366 | Modals are built using `Screens` and `Stacks` that have been defined before. You can show/present them at any point of time while using the app. 367 | 368 | A modal can be defined by passing an array of `Screens` or a name of `Stacks`. 369 | 370 |
371 | Example 372 | 373 | ```tsx 374 | const navio = Navio.build({ 375 | // screens and stacks are taken from previous step 376 | modals: { 377 | ExampleModal: { 378 | stack: 'ExampleStack', 379 | options: { ... }, // optional 380 | }, 381 | }, 382 | }); 383 | ``` 384 | 385 |
386 | 387 | ### Root 388 | 389 | This is a root name of the app. It can be one of `Stacks`, `Tabs` or `Drawers`. 390 | 391 | You can change the root of the app later by `navio.setRoot('drawers', 'AppDrawer')` or by changing `initialRouteName` of `` component. 392 | 393 |
394 | Example 395 | 396 | ```tsx 397 | const navio = Navio.build({ 398 | // stacks, tabs and drawers are taken from previous examples 399 | root: 'tabs.AppTabs', // or 'stacks.MainStack', or 'drawers.AppDrawer' 400 | }); 401 | ``` 402 | 403 |
404 | 405 | ### Hooks 406 | 407 | List of hooks that will be run on each generated `Stacks`, `Tabs` or `Drawers` navigators. Useful for dark mode or language change. 408 | 409 |
410 | Example 411 | 412 | ```tsx 413 | const navio = Navio.build({ 414 | hooks: [useTranslation], 415 | }); 416 | ``` 417 | 418 |
419 | 420 | ### Default options 421 | 422 | Default options that will be applied per each `Stacks`'s, `Tabs`'s, `Drawer`'s, or `Modal`'s screens and containers generated within the app. 423 | 424 | `Note` All containers and `Tabs`'s and `Drawer`'s screens options have `headerShown: false` by default (in order to hide unnecessary navigation headers). You can always change them which might be useful if you want to have a native `< Back` button when hiding tabs (pushing new `Stack`). 425 | 426 |
427 | Example 428 | 429 | ```tsx 430 | const navio = Navio.build({ 431 | defaultOptions: { 432 | stacks: { 433 | screen: { 434 | headerShadowVisible: false, 435 | headerTintColor: Colors.primary, 436 | }, 437 | container: { 438 | headerShown: true, 439 | }, 440 | }, 441 | tabs: { 442 | screen: tabDefaultOptions, 443 | }, 444 | drawer: { 445 | screen: drawerDefaultOptions, 446 | }, 447 | }, 448 | }); 449 | ``` 450 | 451 |
452 | 453 | ### App 454 | 455 | Navio generates root component for the app after the layout is defined. It can be used to directly pass it to `registerRootComponent()` or to wrap with extra providers or add more logic before the app's start up. 456 | 457 | ```tsx 458 | const navio = Navio.build({...}); 459 | 460 | export default () => 461 | ``` 462 | 463 | You can change the root of the app by `navio.setRoot('drawers', 'AppDrawer')` or by changing `initialRouteName` of `` component. 464 | 465 | ## FAQs 466 | 467 | ### Passing params to a modal 468 | 469 | This is most frequently asked question ([here](https://github.com/kanzitelli/rn-navio/issues/19), [here](https://github.com/kanzitelli/rn-navio/issues/20) and [here](https://github.com/kanzitelli/rn-navio/issues/28)). Below you can find two solutions: 470 | 471 | #### Old approach using React Navigation object 472 | 473 | ```tsx 474 | // Use .navigate method of React Navigation object and pass params 475 | navio.N.navigate('MyModal', {screen: 'ScreenName', params: {userId: 'someid'}}); 476 | 477 | // Access params on a screen 478 | const Screen = () => { 479 | const {userId} = navio.useParams(); 480 | }; 481 | ``` 482 | 483 | #### New approach with Navio `v0.1.+` 484 | 485 | ```tsx 486 | // Use .modals.show method of Navio and pass params 487 | navio.modals.show('MyModal', {userId: 'someid'}); 488 | 489 | // Access params on a screen 490 | const Screen = () => { 491 | const {userId} = navio.modals.getParams('MyModal'); 492 | }; 493 | ``` 494 | 495 | ### What is the difference between Expo Router, Navio, and React Navigation? 496 | 497 | [Expo Router](https://docs.expo.dev/router/introduction/) is a routing library designed for Universal React Native applications using Expo. It operates on a file-based routing system, making it an excellent choice for developers looking to create applications for both native (iOS and Android) and web platforms using a single codebase. 498 | 499 | Navio, on the other hand, adopts a static configuration approach, similar to the layout building method seen in [React Native Navigation](https://github.com/wix/react-native-navigation). Navio primarily targets native platforms (iOS and Android), with less emphasis on web compatibility optimisation. In Navio, the application layout is configured within a single file. 500 | 501 | Both libraries are built on top of the [React Navigation](https://github.com/react-navigation/react-navigation) and can be used in conjunction with it. This means all the hooks, actions, deep linking capabilities, and other features from [React Navigation](https://github.com/react-navigation/react-navigation) are expected to work seamlessly with both. Notably, [React Navigation](https://github.com/react-navigation/react-navigation) introduces [Static Configuration in v7](https://reactnavigation.org/docs/7.x/static-configuration) (which has yet to be released). 502 | 503 | ## Is Navio production-ready? 504 | 505 | Navio has been essential for the [BUDDY Marketplace (iOS app)](https://buddify.app/get/buddy-ios), helping us launch it in just 2-3 months. Its use in the app, which is gaining users daily and needs new features fast, allows us to focus more on creating valuable business logic instead of dealing with basic setup tasks. 506 | 507 | However, Navio is still a young library and lacks some features, like [seamless Deep Linking integration](https://github.com/kanzitelli/expo-starter/issues/29), which are important for its full effectiveness in production apps. Since it's part of a live app, I plan to update it regularly, adding new functionalities. You can see what's coming by checking the [Future Plans section](#future-plans). 508 | 509 | If you're using Navio in your app, I'd love to hear from you, and if there are additional features you're looking for. 510 | 511 | ## Future plans 512 | 513 | Navio began as an experimental (and a bit weird) project aimed at minimizing repetitive code in app layout using [React Navigation](https://github.com/react-navigation/react-navigation). I like the concept of static configuration, where the entire app layout setup is condensed into a single file. After implementing it within the expo-starter and receiving positive feedback, I decided to integrate it into the [active mobile app](https://buddify.app/get/buddy-ios). There are additional features I'd like to integrate into Navio. One of the most exciting goals is to merge [React Navigation](https://github.com/react-navigation/react-navigation) and [React Native Navigation](https://github.com/wix/react-native-navigation) functionalities into a unified API, streamlining the development process even further. 514 | 515 | ### Enhancements 516 | 517 | There are still some things I would like to add to the library: 518 | 519 | - [x] `.updateOptions()` for specific tab and drawer. 520 | - [x] Tabs can be placed inside Drawer and vice versa. 521 | - [x] Pass props to Modals. 522 | - [ ] Make deeplinking easier by providing `linking` prop to screens. [Issue](https://github.com/kanzitelli/expo-starter/issues/29). 523 | - [ ] Make Navio universal by adding [RNN](https://github.com/wix/react-native-navigation) and [rnn-screens](https://github.com/kanzitelli/rnn-screens). 524 | - [ ] Extend Navio funtionality and app layout. 525 | - [ ] Easy integration of Navio with React Navigation (eg. navio.Stack()) 526 | - [ ] TypeScript issues @ `index.tsx` file. 527 | 528 | Feel free to open an issue for any suggestions. 529 | 530 | ## License 531 | 532 | This project is [MIT licensed](/LICENSE.md) 533 | -------------------------------------------------------------------------------- /src/navio.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo, useState} from 'react'; 2 | import {createBottomTabNavigator, BottomTabNavigationOptions} from '@react-navigation/bottom-tabs'; 3 | import { 4 | CommonActions, 5 | createNavigationContainerRef, 6 | DrawerActions, 7 | NavigationContainer, 8 | NavigationContainerRef, 9 | NavigationContainerRefWithCurrent, 10 | StackActions, 11 | TabActions, 12 | useNavigation, 13 | useRoute, 14 | } from '@react-navigation/native'; 15 | import { 16 | createNativeStackNavigator, 17 | NativeStackNavigationOptions, 18 | } from '@react-navigation/native-stack'; 19 | import {createDrawerNavigator, DrawerNavigationOptions} from '@react-navigation/drawer'; 20 | import { 21 | TScreenData, 22 | TStackData, 23 | TTabsData, 24 | TModalData, 25 | TRootName, 26 | Layout, 27 | TStackDefinition, 28 | NavioScreen, 29 | BaseOptions, 30 | RootProps, 31 | ContainerLayoutKeys, 32 | TStackDataObj, 33 | TDrawersData, 34 | TDrawerDefinition, 35 | TTabsDefinition, 36 | RootSetAs, 37 | TTabLayoutValue, 38 | TDrawerLayoutValue, 39 | DefaultOptions, 40 | TunnelEvent$UpdateOptions$Params, 41 | TModalsDefinition, 42 | } from './types'; 43 | import {NavioTunnel} from './tunnel'; 44 | import {safeOpts} from './help'; 45 | 46 | // Navio 47 | export class Navio< 48 | ScreenName extends string, 49 | StackName extends string, 50 | TabsName extends string, 51 | ModalName extends string, 52 | DrawersName extends string, 53 | // 54 | ScreenData extends TScreenData, 55 | StackData extends TStackData, 56 | TabsData extends TTabsData, 57 | ModalData extends TModalData, 58 | DrawersData extends TDrawersData, 59 | // 60 | TabsLayoutName extends ContainerLayoutKeys = ContainerLayoutKeys, 61 | DrawersLayoutName extends ContainerLayoutKeys = ContainerLayoutKeys, 62 | // 63 | RootName extends TRootName = TRootName< 64 | StackName, 65 | TabsName, 66 | DrawersName 67 | >, 68 | RootSetAsNames extends Record = { 69 | stacks: StackName; 70 | tabs: TabsName; 71 | drawers: DrawersName; 72 | }, 73 | > { 74 | static build< 75 | ScreenName extends string, 76 | StackName extends string, 77 | TabsName extends string, 78 | ModalName extends string, 79 | DrawersName extends string, 80 | // 81 | ScreenData extends TScreenData, 82 | StackData extends TStackData, 83 | TabsData extends TTabsData, 84 | ModalData extends TModalData, 85 | DrawersData extends TDrawersData, 86 | // 87 | TabsLayoutName extends ContainerLayoutKeys = ContainerLayoutKeys, 88 | DrawersLayoutName extends ContainerLayoutKeys = ContainerLayoutKeys, 89 | // 90 | RootName extends TRootName = TRootName< 91 | StackName, 92 | TabsName, 93 | DrawersName 94 | >, 95 | >( 96 | data: Layout< 97 | Record, 98 | Record, 99 | Record, 100 | Record, 101 | Record, 102 | RootName 103 | >, 104 | ) { 105 | const _navio = new Navio< 106 | ScreenName, 107 | StackName, 108 | TabsName, 109 | ModalName, 110 | DrawersName, 111 | ScreenData, 112 | StackData, 113 | TabsData, 114 | ModalData, 115 | DrawersData, 116 | TabsLayoutName, 117 | DrawersLayoutName, 118 | RootName 119 | >(data); 120 | return _navio; 121 | } 122 | 123 | // ======== 124 | // | Vars | 125 | // ======== 126 | private layout: Layout< 127 | Record, 128 | Record, 129 | Record, 130 | Record, 131 | Record, 132 | RootName 133 | >; 134 | 135 | // react navigation related 136 | private navRef: NavigationContainerRefWithCurrent; 137 | private navIsReadyRef: React.MutableRefObject; 138 | 139 | // for data transfer 140 | private tunnel: NavioTunnel; 141 | 142 | // updated options for tabs and drawers. used to store data during session 143 | private __tabsUpdatedOptions: Record = {}; 144 | private __drawerUpdatedOptions: Record = {}; 145 | 146 | // params for modals. used to easier transfer data to modal 147 | private __modalParams: Record = {}; 148 | 149 | // ======== 150 | // | Init | 151 | // ======== 152 | constructor( 153 | data: Layout< 154 | Record, 155 | Record, 156 | Record, 157 | Record, 158 | Record, 159 | RootName 160 | >, 161 | ) { 162 | // Layout 163 | this.layout = data; 164 | 165 | // Navigation 166 | this.navRef = createNavigationContainerRef(); 167 | this.navIsReadyRef = React.createRef(); 168 | 169 | // Tunnel (event emitter) 170 | this.tunnel = new NavioTunnel(); 171 | } 172 | 173 | // =========== 174 | // | Getters | 175 | // =========== 176 | get N() { 177 | return this.navRef; 178 | } 179 | 180 | private get navIsReady() { 181 | return ( 182 | !!this.navIsReadyRef && this.navIsReadyRef.current && !!this.navRef && !!this.navRef.current 183 | ); 184 | } 185 | 186 | // =========== 187 | // | Methods | 188 | // =========== 189 | private log(message: string, type: 'log' | 'warn' | 'error' = 'log') { 190 | console[type](`[navio] ${message}`); 191 | } 192 | 193 | private __setRoot(routeName: string) { 194 | const {stacks, tabs, drawers} = this.layout; 195 | 196 | if (stacks && stacks[routeName as StackName]) { 197 | this.stacks.setRoot(routeName as StackName); 198 | } 199 | if (tabs && tabs[routeName as TabsName]) { 200 | this.tabs.setRoot(routeName as TabsName); 201 | } 202 | if (drawers && drawers[routeName as DrawersName]) { 203 | this.drawers.setRoot(routeName as DrawersName); 204 | } 205 | } 206 | 207 | private getSafeRoot(name: RootName | undefined): StackName | TabsName | DrawersName | undefined { 208 | if (!name) return undefined; 209 | const {stacks, tabs, drawers} = this.layout; 210 | 211 | const split = name.split('.'); 212 | const type = split[0]; // tabs, stacks, drawers 213 | const routeName = split.slice(1).join(':'); 214 | 215 | if (type === 'tabs') { 216 | const rName = routeName as TabsName; 217 | if (!!tabs && !tabs[rName]) { 218 | this.log('Wrong app root', 'warn'); 219 | } 220 | return rName; 221 | } 222 | if (type === 'stacks') { 223 | const rName = routeName as StackName; 224 | if (!!stacks && !stacks[rName]) { 225 | this.log('Wrong app root', 'warn'); 226 | } 227 | return rName; 228 | } 229 | if (type === 'drawers') { 230 | const rName = routeName as DrawersName; 231 | if (!!drawers && !drawers[rName]) { 232 | this.log('Wrong app root', 'warn'); 233 | } 234 | return rName; 235 | } 236 | } 237 | 238 | private getCustomDefaultOptions(): DefaultOptions { 239 | return { 240 | stacks: { 241 | container: { 242 | headerShown: false, 243 | }, 244 | }, 245 | tabs: { 246 | container: { 247 | headerShown: false, 248 | }, 249 | screen: { 250 | headerShown: false, 251 | }, 252 | }, 253 | drawers: { 254 | container: { 255 | headerShown: false, 256 | }, 257 | screen: { 258 | headerShown: false, 259 | }, 260 | }, 261 | modals: { 262 | container: { 263 | headerShown: false, 264 | }, 265 | }, 266 | }; 267 | } 268 | 269 | protected navigate = < 270 | T extends ScreenName | StackName | TabsName | ModalName, 271 | Params extends object | undefined, 272 | >( 273 | name: T, 274 | params?: Params, 275 | ): void => { 276 | if (this.navIsReady) { 277 | this.navRef.current?.dispatch( 278 | CommonActions.navigate({ 279 | name: name as string, 280 | params, 281 | }), 282 | ); 283 | } 284 | }; 285 | 286 | // =========== 287 | // | Actions | 288 | // =========== 289 | /** 290 | * `push(...)` action adds a route on top of the stack and navigates forward to it. 291 | * 292 | * @param name ScreenName 293 | * @param params Params 294 | */ 295 | push(name: T, params?: Params) { 296 | if (this.navIsReady) { 297 | this.navRef.current?.dispatch(StackActions.push(name as string, params)); 298 | } 299 | } 300 | 301 | /** 302 | * `goBack()` action creator allows to go back to the previous route in history. 303 | */ 304 | goBack() { 305 | if (this.navIsReady) { 306 | this.navRef.current?.goBack(); 307 | } 308 | } 309 | 310 | /** 311 | * `setParams(...)` action allows to update params for a certain route. 312 | * 313 | * @param name all available navigation keys. Leave `undefined` if applying for the focused route. 314 | * @param params object 315 | */ 316 | setParams(name: T, params: Params) { 317 | if (this.navIsReady) { 318 | this.navRef.current?.dispatch({ 319 | ...CommonActions.setParams(params), 320 | source: name as string, 321 | }); 322 | } 323 | } 324 | 325 | /** 326 | * `setRoot(as, name)` action sets a new app root. 327 | * 328 | * Tips: It can be used to switch between Tabs, Drawers, and Stacks. 329 | * 330 | * @param as used to define the type of the app layout. Possible values: 'stacks' | 'tabs' | 'drawers'. 331 | * @param name will be autocompleted based on `as` value and current layout configuration. 332 | */ 333 | setRoot( 334 | as: SetAs, 335 | routeName: RouteName, 336 | ) { 337 | if (as) { 338 | this.__setRoot(routeName); 339 | } 340 | } 341 | 342 | /** 343 | * `stacks` contains navigation actions for stack-based navigators. 344 | * 345 | * Available methods: 346 | * 347 | * `push`, `pop`, `popToTop`, `setRoot` 348 | * 349 | */ 350 | get stacks() { 351 | // local copy of current instance 352 | const self = this; 353 | 354 | return { 355 | /** 356 | * `push(...)` action adds a route on top of the stack and navigates forward to it. 357 | * 358 | * Tips: It will "hide" tabs. 359 | * 360 | * @param name StackName 361 | */ 362 | push(name: T) { 363 | if (self.navIsReady) { 364 | self.navigate(name); 365 | } 366 | }, 367 | 368 | /** 369 | * `pop(...)` action takes you back to a previous screen in the stack. 370 | * 371 | * @param count number 372 | */ 373 | pop(count?: number) { 374 | if (self.navIsReady) { 375 | self.navRef.current?.dispatch(StackActions.pop(count)); 376 | } 377 | }, 378 | 379 | /** 380 | * `popToPop()` action takes you back to the first screen in the stack, dismissing all the others. 381 | */ 382 | popToTop() { 383 | if (self.navIsReady) { 384 | self.navRef.current?.dispatch(StackActions.popToTop()); 385 | } 386 | }, 387 | 388 | /** 389 | * `setRoot(...)` action sets a new app root from stacks. 390 | * 391 | * Tips: It can be used to switch between Auth and App stacks. 392 | * 393 | * @param name StackName 394 | */ 395 | setRoot(name: T) { 396 | if (self.navIsReady) { 397 | self.navRef.current?.dispatch( 398 | CommonActions.reset({ 399 | routes: [{name}], 400 | }), 401 | ); 402 | } 403 | }, 404 | }; 405 | } 406 | 407 | /** 408 | * `tabs` contains navigation actions for tab-based navigators. 409 | * 410 | * Available methods: 411 | * 412 | * `jumpTo`, `setRoot` 413 | * 414 | */ 415 | get tabs() { 416 | // local copy of current instance 417 | const self = this; 418 | 419 | return { 420 | /** 421 | * `jumpTo(...)` action can be used to jump to an existing route in the tab navigator. 422 | * 423 | * @param name TabName 424 | */ 425 | jumpTo(name: T) { 426 | if (self.navIsReady) { 427 | self.navRef.current?.dispatch(TabActions.jumpTo(name as string)); 428 | } 429 | }, 430 | 431 | /** 432 | * `updateOptions(...)` action updates provided tab's options. 433 | * 434 | * Tips: It can be used to update badge count. 435 | * 436 | * @param name name of the tab 437 | * @param options `BottomTabNavigationOptions` options for the tab. 438 | */ 439 | updateOptions(name: T, options: BottomTabNavigationOptions) { 440 | if (self.navIsReady) { 441 | self.tunnel.echo('tabs.updateOptions', { 442 | name, 443 | options, 444 | } as TunnelEvent$UpdateOptions$Params); 445 | } 446 | }, 447 | 448 | /** 449 | * `setRoot(...)` action sets a new app root from tabs. 450 | * 451 | * Tips: It can be used to switch between Auth and Tabs. 452 | * 453 | * @param name TabsName 454 | */ 455 | setRoot(name: T) { 456 | if (self.navIsReady) { 457 | self.navRef.current?.dispatch( 458 | CommonActions.reset({ 459 | routes: [{name}], 460 | }), 461 | ); 462 | } 463 | }, 464 | }; 465 | } 466 | 467 | /** 468 | * `modals` contains navigation actions for modals. 469 | * 470 | * Available methods: 471 | * 472 | * `show` 473 | * 474 | */ 475 | get modals() { 476 | // local copy of current instance 477 | const self = this; 478 | 479 | return { 480 | /** 481 | * `show(...)` action can be used to show an existing modal. 482 | * 483 | * @param name ModalName 484 | */ 485 | show(name: ModalName, params?: Params) { 486 | if (self.navIsReady) { 487 | // adding params to modals params data 488 | if (!!params) { 489 | self.__modalParams[name] = params; 490 | } 491 | 492 | self.navigate(name); 493 | } 494 | }, 495 | 496 | /** 497 | * `getParams(...)` action can be used to get params passed to the modal. 498 | * 499 | * @param name ModalName 500 | */ 501 | getParams(name: ModalName) { 502 | return self.__modalParams[name] as Params; 503 | }, 504 | }; 505 | } 506 | 507 | /** 508 | * `drawers` contains navigation actions for drawer-based navigators. 509 | * 510 | * Available methods: 511 | * 512 | * `open`, `close`, `toggle`, `jumpTo`, `setRoot` 513 | * 514 | */ 515 | get drawers() { 516 | // local copy of current instance 517 | const self = this; 518 | 519 | return { 520 | /** 521 | * `open()` action can be used to open the drawer pane. 522 | */ 523 | open() { 524 | if (self.navIsReady) { 525 | self.navRef.current?.dispatch(DrawerActions.openDrawer()); 526 | } 527 | }, 528 | 529 | /** 530 | * `close()` action can be used to close the drawer pane. 531 | */ 532 | close() { 533 | if (self.navIsReady) { 534 | self.navRef.current?.dispatch(DrawerActions.closeDrawer()); 535 | } 536 | }, 537 | 538 | /** 539 | * `toggle()` action can be used to open the drawer pane if closed, or close if open. 540 | */ 541 | toggle() { 542 | if (self.navIsReady) { 543 | self.navRef.current?.dispatch(DrawerActions.toggleDrawer()); 544 | } 545 | }, 546 | 547 | /** 548 | * `jumpTo(...)` action can be used to jump to an existing route in the drawer navigator. 549 | * 550 | * @param name StacksName 551 | */ 552 | jumpTo(name: T) { 553 | if (self.navIsReady) { 554 | self.navRef.current?.dispatch(DrawerActions.jumpTo(name as string)); 555 | } 556 | }, 557 | 558 | /** 559 | * `updateOptions(...)` action updates provided drawer's options. 560 | * 561 | * @param name name of the drawer layout 562 | * @param options `DrawerNavigationOptions` options for the drawer. 563 | */ 564 | updateOptions(name: T, options: DrawerNavigationOptions) { 565 | if (self.navIsReady) { 566 | self.tunnel.echo('drawer.updateOptions', { 567 | name, 568 | options, 569 | } as TunnelEvent$UpdateOptions$Params); 570 | } 571 | }, 572 | 573 | /** 574 | * `setRoot(...)` action sets a new app root from drawers. 575 | * 576 | * Tips: It can be used to switch between Auth and Drawers. 577 | * 578 | * @param name DrawersName 579 | */ 580 | setRoot(name: T) { 581 | if (self.navIsReady) { 582 | self.navRef.current?.dispatch( 583 | CommonActions.reset({ 584 | routes: [{name}], 585 | }), 586 | ); 587 | } 588 | }, 589 | }; 590 | } 591 | 592 | // ========= 593 | // | Hooks | 594 | // ========= 595 | /** 596 | * `useN()` is the duplicate of `useNavigation()` hook from React Navigation. 597 | * 598 | */ 599 | useN() { 600 | return useNavigation(); 601 | } 602 | 603 | /** 604 | * `useR()` is the duplicate of `useRoute()` hook from React Navigation. 605 | * 606 | */ 607 | useR() { 608 | return useRoute(); 609 | } 610 | 611 | /** 612 | * `useParams()` is used to quickly extract params from the React Navigation route. 613 | * 614 | */ 615 | useParams() { 616 | return useRoute()?.params as Params; 617 | } 618 | 619 | // =========== 620 | // | Layouts | 621 | // =========== 622 | // | Stacks | 623 | // some getters for Stack 624 | private __StackGetNavigatorProps = ( 625 | definition: TStackDefinition | undefined, 626 | ) => { 627 | const {stacks} = this.layout; 628 | if (stacks === undefined) return {}; 629 | 630 | return Array.isArray(definition) 631 | ? // if definition is ScreenName[] 632 | {} 633 | : // if stackDev is TStacksDataObj 634 | typeof definition === 'object' 635 | ? (definition as TStackDataObj).navigatorProps ?? {} 636 | : // if stackDev is StacksName -> look into stacks[...] 637 | typeof definition === 'string' 638 | ? // if stacks[name] is ScreenName[] 639 | Array.isArray(stacks[definition]) 640 | ? {} 641 | : // if stacks[name] is TStacksDataObj 642 | typeof stacks[definition] === 'object' 643 | ? (stacks[definition] as TStackDataObj).navigatorProps ?? {} 644 | : {} 645 | : {}; 646 | }; 647 | private __StackGetContainerOpts = ( 648 | definition: TStackDefinition | undefined, 649 | ) => { 650 | const {stacks} = this.layout; 651 | if (stacks === undefined) return {}; 652 | 653 | return Array.isArray(definition) 654 | ? // if definition is ScreenName[] 655 | {} 656 | : // if stackDev is TStacksDataObj 657 | typeof definition === 'object' 658 | ? (definition as TStackDataObj).options ?? {} 659 | : // if stackDev is StacksName -> look into stacks[...] 660 | typeof definition === 'string' 661 | ? // if stacks[name] is ScreenName[] 662 | Array.isArray(stacks[definition]) 663 | ? {} 664 | : // if stacks[name] is TStacksDataObj 665 | typeof stacks[definition] === 'object' 666 | ? (stacks[definition] as TStackDataObj).options ?? {} 667 | : {} 668 | : {}; 669 | }; 670 | private __StackGetScreens = (definition: TStackDefinition | undefined) => { 671 | const {stacks} = this.layout; 672 | if (stacks === undefined) return []; 673 | 674 | return Array.isArray(definition) 675 | ? // if definition is ScreenName[] 676 | (definition as ScreenName[]) 677 | : // if definition is TStacksDataObj 678 | typeof definition === 'object' 679 | ? (definition as TStackDataObj).screens ?? [] 680 | : // if definition is StacksName -> look into stacks[...] 681 | typeof definition === 'string' 682 | ? // if stacks[name] is ScreenName[] 683 | Array.isArray(stacks[definition]) 684 | ? (stacks[definition] as ScreenName[]) 685 | : // if stacks[name] is TStacksDataObj 686 | typeof stacks[definition] === 'object' 687 | ? (stacks[definition] as TStackDataObj).screens ?? [] 688 | : [] 689 | : []; 690 | }; 691 | 692 | private StackScreen: React.FC<{ 693 | StackNavigator: ReturnType; 694 | name: ScreenName; 695 | }> = ({StackNavigator, name}) => { 696 | const {screens, defaultOptions: globalDefaultOptions} = this.layout; 697 | 698 | const screen = screens[name]; 699 | 700 | // component 701 | // -- handling when screen is a component or object{component,options} 702 | let sComponent: NavioScreen; 703 | let sOptions: BaseOptions; 704 | if (typeof screen === 'object') { 705 | if (screen.component) { 706 | // {component,options} 707 | sComponent = screen.component; 708 | sOptions = screen.options ?? {}; 709 | } else { 710 | // component 711 | // this might happen if a screen is provided as wrapped component, for ex. const Main: React.FC = observer(() => {}); (observer from mobx) 712 | sComponent = screen as any; 713 | sOptions = {}; 714 | } 715 | } else { 716 | // component 717 | sComponent = screen; 718 | sOptions = {}; 719 | } 720 | const C = sComponent; 721 | 722 | // options 723 | const customDefaultOptions = this.getCustomDefaultOptions()?.stacks?.screen ?? {}; 724 | const defaultOptions = globalDefaultOptions?.stacks?.screen ?? {}; 725 | const Opts: BaseOptions = props => ({ 726 | ...safeOpts(customDefaultOptions)(props), // [!] custom default options 727 | ...safeOpts(defaultOptions)(props), // navio.defaultOptions.stacks.screen 728 | ...safeOpts(sOptions)(props), // navio.screens.[].options 729 | ...safeOpts(C.options)(props), // component-based options 730 | }); // must be function. merge options from buildNavio and from component itself. also providing default options 731 | 732 | // screen 733 | return ; 734 | }; 735 | 736 | private Stack: React.FC<{ 737 | definition: TStackDefinition | undefined; 738 | }> = ({definition}) => { 739 | if (!definition) return null; 740 | const {screens, stacks, hooks} = this.layout; 741 | 742 | // -- running hooks 743 | if (hooks) for (const h of hooks) if (h) h(); 744 | 745 | if (!screens) { 746 | this.log('No screens registered'); 747 | return <>; 748 | } 749 | if (!stacks) { 750 | this.log('No stacks registered'); 751 | return <>; 752 | } 753 | 754 | // -- building navigator 755 | const Stack = createNativeStackNavigator(); 756 | const StackScreensMemo = useMemo(() => { 757 | return this.__StackGetScreens(definition).map(sk => 758 | this.StackScreen({StackNavigator: Stack, name: sk}), 759 | ); 760 | }, [definition, screens, stacks]); 761 | 762 | // -- getting navigator props 763 | const navigatorProps = this.__StackGetNavigatorProps(definition); 764 | 765 | return {StackScreensMemo}; 766 | }; 767 | 768 | private StackContainer: React.FC<{ 769 | Navigator: ReturnType; 770 | name: StackName; 771 | definition: TStackDefinition | undefined; 772 | }> = ({Navigator, definition, name}) => { 773 | const {defaultOptions: globalDefaultOptions} = this.layout; 774 | 775 | // component 776 | const C = () => this.Stack({definition}); 777 | 778 | // options 779 | const customDefaultOptions = this.getCustomDefaultOptions()?.stacks?.container ?? {}; 780 | const defaultOptions = globalDefaultOptions?.tabs?.container ?? {}; 781 | const options = this.__StackGetContainerOpts(definition); 782 | const Opts: BaseOptions = props => ({ 783 | ...safeOpts(customDefaultOptions)(props), // ! custom default options 784 | ...safeOpts(defaultOptions)(props), // navio.defaultOptions.tabs.container 785 | ...safeOpts(options)(props), // navio.stacks.[].options 786 | }); // must be function. merge options from buildNavio. also providing default options 787 | 788 | return ( 789 | 790 | {(props: any) => } 791 | 792 | ); 793 | }; 794 | 795 | // | Tabs | 796 | private __TabsGet = (definition: TTabsDefinition | undefined) => { 797 | const {tabs} = this.layout; 798 | if (tabs === undefined) return undefined; 799 | 800 | const currentTabs: TTabsData | undefined = 801 | typeof definition === 'string' ? tabs[definition] : undefined; 802 | 803 | return currentTabs; 804 | }; 805 | 806 | private TabScreen: React.FC<{ 807 | TabNavigator: ReturnType; 808 | name: string; // TabsLayoutName 809 | layout: TTabLayoutValue; 810 | }> = ({TabNavigator, name, layout}) => { 811 | if (!layout.stack && !layout.drawer) { 812 | this.log(`Either 'stack' or 'drawer' must be provided for "${name}" tabs layout.`); 813 | return null; 814 | } 815 | 816 | // component 817 | const C = () => 818 | layout.stack 819 | ? this.Stack({definition: layout.stack}) 820 | : layout.drawer 821 | ? this.Drawer({definition: layout.drawer}) 822 | : null; 823 | 824 | return ; 825 | }; 826 | 827 | private Tabs: React.FC<{ 828 | definition: TTabsDefinition | undefined; 829 | }> = ({definition}) => { 830 | const {tabs, hooks, defaultOptions: globalDefaultOptions} = this.layout; 831 | 832 | // -- pre-checks 833 | if (!tabs) { 834 | this.log('No tabs registered'); 835 | return <>; 836 | } 837 | 838 | const currentTabs = this.__TabsGet(definition); 839 | if (!currentTabs) { 840 | this.log('No tabs defined found'); 841 | return <>; 842 | } 843 | 844 | // -- internal state 845 | const [updatedOptions, setUpdatedOptions] = useState< 846 | Record 847 | >({}); 848 | 849 | // -- internal effects 850 | useEffect(() => { 851 | this.tunnel.on( 852 | 'tabs.updateOptions', 853 | (params: TunnelEvent$UpdateOptions$Params) => { 854 | const tcname = params.name; 855 | const tcopts = params.options; 856 | this.__tabsUpdatedOptions = { 857 | ...this.__tabsUpdatedOptions, 858 | [tcname]: {...this.__tabsUpdatedOptions[tcname], ...tcopts}, 859 | }; 860 | setUpdatedOptions(this.__tabsUpdatedOptions); 861 | }, 862 | ); 863 | }, [definition]); 864 | 865 | // -- internal memos 866 | const currentTabsLayout = useMemo(() => currentTabs.layout, [currentTabs]); 867 | const currentTabsLayoutKeys = useMemo( 868 | () => Object.keys(currentTabsLayout), 869 | [currentTabsLayout], 870 | ); 871 | 872 | // -- running hooks 873 | if (hooks) for (const h of hooks) if (h) h(); 874 | 875 | // -- building navigator 876 | const Tabs = useMemo(() => createBottomTabNavigator(), [tabs]); 877 | const TabScreensMemo = useMemo( 878 | () => 879 | currentTabsLayoutKeys.map(key => 880 | this.TabScreen({ 881 | name: key, 882 | TabNavigator: Tabs, 883 | layout: currentTabs.layout[key as string], 884 | }), 885 | ), 886 | [Tabs, currentTabsLayoutKeys], 887 | ); 888 | 889 | // options 890 | const Opts: BaseOptions = props => { 891 | const rName = props?.route?.name; 892 | if (!rName) return {}; 893 | 894 | const customDefaultOptions = this.getCustomDefaultOptions()?.tabs?.screen ?? {}; 895 | const defaultOpts = globalDefaultOptions?.tabs?.screen ?? {}; 896 | const navigatorScreenOptions = currentTabs?.navigatorProps?.screenOptions ?? {}; 897 | const options = (currentTabs?.layout[rName] as any)?.options ?? {}; 898 | const _updatedOptions = updatedOptions[rName] ?? {}; 899 | return { 900 | ...safeOpts(customDefaultOptions)(props), // [!] custom default options 901 | ...safeOpts(defaultOpts)(props), // navio.defaultOptions.tabs.screen 902 | ...safeOpts(navigatorScreenOptions)(props), // navio.tabs.[].navigatorProps.screenOptions -- because we override it below 903 | ...safeOpts(options)(props), // tab-based options 904 | ...safeOpts(_updatedOptions)(props), // upddated options (navio.tabs.updateOptions()) 905 | }; 906 | }; // must be function. merge options from buildNavio. also providing default options 907 | 908 | return ( 909 | 910 | {TabScreensMemo} 911 | 912 | ); 913 | }; 914 | 915 | private TabsContainer: React.FC<{ 916 | Navigator: ReturnType; 917 | name: TabsName; 918 | definition: TTabsDefinition | undefined; 919 | }> = ({Navigator, definition, name}) => { 920 | const {defaultOptions: globalDefaultOptions} = this.layout; 921 | 922 | // component 923 | const C = () => this.Tabs({definition}); 924 | 925 | // options 926 | const customDefaultOptions = this.getCustomDefaultOptions()?.tabs?.container ?? {}; 927 | const defaultOptions = globalDefaultOptions?.tabs?.container ?? {}; 928 | const options = this.__TabsGet(definition)?.options ?? {}; 929 | const Opts: BaseOptions = props => ({ 930 | ...safeOpts(customDefaultOptions)(props), // [!] custom default options 931 | ...safeOpts(defaultOptions)(props), // navio.defaultOptions.tabs.container 932 | ...safeOpts(options)(props), // navio.tabs.[].options 933 | }); // must be function. merge options from buildNavio. also providing default options 934 | 935 | return ; 936 | }; 937 | 938 | // | Drawers | 939 | private __DrawerGet = (definition: TDrawerDefinition | undefined) => { 940 | const {drawers} = this.layout; 941 | if (drawers === undefined) return undefined; 942 | 943 | const current: TDrawersData | undefined = 944 | typeof definition === 'string' ? drawers[definition] : undefined; 945 | 946 | return current; 947 | }; 948 | 949 | private DrawerScreen: React.FC<{ 950 | DrawerNavigator: ReturnType; 951 | name: string; 952 | layout: TDrawerLayoutValue; 953 | }> = ({DrawerNavigator, name, layout}) => { 954 | if (!layout.stack && !layout.tabs) { 955 | this.log(`Either 'stack' or 'tabs' must be provided for "${name}" drawer layout.`); 956 | return null; 957 | } 958 | 959 | // component 960 | const C = () => 961 | layout.stack 962 | ? this.Stack({definition: layout.stack}) 963 | : layout.tabs 964 | ? this.Tabs({definition: layout.tabs}) 965 | : null; 966 | 967 | // screen 968 | return ; 969 | }; 970 | 971 | private Drawer: React.FC<{ 972 | definition: TDrawerDefinition | undefined; 973 | }> = ({definition}) => { 974 | const {drawers, defaultOptions: globalDefaultOptions, hooks} = this.layout; 975 | 976 | if (!drawers) { 977 | this.log('No drawers registered'); 978 | return <>; 979 | } 980 | 981 | const currentDrawer = this.__DrawerGet(definition); 982 | if (!currentDrawer) { 983 | this.log('No drawer found'); 984 | return <>; 985 | } 986 | 987 | // -- internal state 988 | const [updatedOptions, setUpdatedOptions] = useState>( 989 | {}, 990 | ); 991 | 992 | // -- internal effects 993 | useEffect(() => { 994 | this.tunnel.on( 995 | 'drawer.updateOptions', 996 | (params: TunnelEvent$UpdateOptions$Params) => { 997 | const name = params.name; 998 | const opts = params.options; 999 | this.__drawerUpdatedOptions = { 1000 | ...this.__drawerUpdatedOptions, 1001 | [name]: {...this.__drawerUpdatedOptions[name], ...opts}, 1002 | }; 1003 | setUpdatedOptions(this.__drawerUpdatedOptions); 1004 | }, 1005 | ); 1006 | }, [definition]); 1007 | 1008 | // -- internal memos 1009 | const currentDrawerLayout = useMemo(() => currentDrawer.layout, [currentDrawer]); 1010 | const currentDrawerLayoutKeys = useMemo( 1011 | () => Object.keys(currentDrawerLayout), 1012 | [currentDrawerLayout], 1013 | ); 1014 | 1015 | // -- running hooks 1016 | if (hooks) for (const h of hooks) if (h) h(); 1017 | 1018 | // -- building navigator 1019 | const Drawer = useMemo(() => createDrawerNavigator(), [drawers]); 1020 | const DrawerScreensMemo = useMemo(() => { 1021 | return currentDrawerLayoutKeys.map(key => 1022 | this.DrawerScreen({ 1023 | name: key, 1024 | DrawerNavigator: Drawer, 1025 | layout: currentDrawer.layout[key], 1026 | }), 1027 | ); 1028 | }, [Drawer, currentDrawerLayoutKeys]); 1029 | 1030 | // options 1031 | const Opts: BaseOptions = props => { 1032 | const rName = props?.route?.name; 1033 | if (!rName) return {}; 1034 | 1035 | const customDefaultOptions = this.getCustomDefaultOptions()?.drawers?.screen ?? {}; 1036 | const defaultOptions = globalDefaultOptions?.drawers?.screen ?? {}; 1037 | const navigatorScreenOptions = currentDrawer?.navigatorProps?.screenOptions ?? {}; 1038 | const options = (currentDrawer?.layout[rName] as any)?.options ?? {}; 1039 | const _updatedOptions = updatedOptions[rName] ?? {}; 1040 | return { 1041 | ...safeOpts(customDefaultOptions)(props), // [!] custom default options 1042 | ...safeOpts(defaultOptions)(props), // navio.defaultOptions.drawers.screen 1043 | ...safeOpts(navigatorScreenOptions)(props), // navio.drawers.[].navigatorProps.screenOptions -- because we override it below 1044 | ...safeOpts(options)(props), // tab-based options 1045 | ...safeOpts(_updatedOptions)(props), // upddated options (navio.drawers.updateOptions()) 1046 | }; 1047 | }; // must be function. merge options from buildNavio. also providing default options 1048 | 1049 | return ( 1050 | 1051 | {DrawerScreensMemo} 1052 | 1053 | ); 1054 | }; 1055 | 1056 | private DrawerContainer: React.FC<{ 1057 | Navigator: ReturnType; 1058 | name: DrawersName; 1059 | definition: TDrawerDefinition | undefined; 1060 | }> = ({Navigator, definition, name}) => { 1061 | const {defaultOptions: globalDefaultOptions} = this.layout; 1062 | 1063 | // component 1064 | const C = () => this.Drawer({definition}); 1065 | 1066 | // options 1067 | const customDefaultOptions = this.getCustomDefaultOptions()?.drawers?.container ?? {}; 1068 | const defaultOptions = globalDefaultOptions?.drawers?.container ?? {}; 1069 | const options = this.__DrawerGet(definition)?.options ?? {}; 1070 | const Opts: BaseOptions = props => ({ 1071 | ...safeOpts(customDefaultOptions)(props), // [!] custom default options 1072 | ...safeOpts(defaultOptions)(props), // navio.defaultOptions.tabs.container 1073 | ...safeOpts(options)(props), // navio.stacks.[].options 1074 | }); // must be function. merge options from buildNavio. also providing default options 1075 | 1076 | return ; 1077 | }; 1078 | 1079 | // | Modals | 1080 | private __ModalGet = (definition: TModalsDefinition | undefined) => { 1081 | const {modals} = this.layout; 1082 | if (modals === undefined) return undefined; 1083 | 1084 | const currentModal: TModalData | undefined = 1085 | typeof definition === 'string' ? modals[definition] : undefined; 1086 | 1087 | return currentModal; 1088 | }; 1089 | 1090 | private ModalContainer: React.FC<{ 1091 | Navigator: ReturnType; 1092 | name: ModalName; 1093 | definition: TModalsDefinition | undefined; 1094 | }> = ({Navigator, definition, name}) => { 1095 | const {defaultOptions: globalDefaultOptions} = this.layout; 1096 | 1097 | const currentModal = this.__ModalGet(definition); 1098 | if (!currentModal) { 1099 | this.log('No modal found'); 1100 | return <>; 1101 | } 1102 | 1103 | // methods 1104 | const clearParams = (name: string) => { 1105 | this.__modalParams[name] = undefined; 1106 | }; 1107 | 1108 | // component 1109 | const C = () => this.Stack({definition: currentModal?.stack}); 1110 | 1111 | // options 1112 | const customDefaultOptions = this.getCustomDefaultOptions()?.modals?.container ?? {}; 1113 | const defaultOptions = globalDefaultOptions?.modals?.container ?? {}; 1114 | const options = currentModal?.options ?? {}; 1115 | const Opts: BaseOptions = props => ({ 1116 | ...safeOpts(customDefaultOptions)(props), // [!] custom default options 1117 | ...safeOpts(defaultOptions)(props), // navio.defaultOptions.tabs.container 1118 | ...safeOpts(options)(props), // navio.modals.[].options 1119 | }); // must be function. merge options from buildNavio. also providing default options 1120 | 1121 | return ( 1122 | ({ 1129 | blur: e => clearParams(route?.name), 1130 | })} 1131 | /> 1132 | ); 1133 | }; 1134 | 1135 | /** 1136 | * Generates `` component for provided layout. Returns Stack Navigator. 1137 | */ 1138 | private Root: React.FC> = ({root: parentRoot}) => { 1139 | const {stacks, tabs, modals, drawers, root} = this.layout; 1140 | const AppNavigator = createNativeStackNavigator(); 1141 | const appRoot = this.getSafeRoot(parentRoot ?? root); 1142 | 1143 | if (!appRoot) { 1144 | this.log('No modal found'); 1145 | return <>; 1146 | } 1147 | 1148 | // Effects 1149 | useEffect(() => { 1150 | // -- changing route if `root` was changed 1151 | if (!!appRoot) { 1152 | this.__setRoot(appRoot); 1153 | } 1154 | // listening to changes of parentRoot, but setting appRoot value 1155 | }, [parentRoot]); 1156 | 1157 | // UI Methods 1158 | // -- app stacks 1159 | const AppStacks = useMemo(() => { 1160 | if (!stacks) return null; 1161 | 1162 | const stacksKeys = Object.keys(stacks) as StackName[]; 1163 | return stacksKeys.map(key => 1164 | this.StackContainer({Navigator: AppNavigator, name: key, definition: stacks[key]}), 1165 | ); 1166 | }, [stacks]); 1167 | 1168 | // -- app tabs 1169 | const AppTabs = useMemo(() => { 1170 | if (!tabs) return null; 1171 | 1172 | const tabsKeys = Object.keys(tabs) as TabsName[]; 1173 | return tabsKeys.map(key => 1174 | this.TabsContainer({Navigator: AppNavigator, name: key, definition: key}), 1175 | ); 1176 | }, [tabs]); 1177 | 1178 | // -- app drawers 1179 | const AppDrawers = useMemo(() => { 1180 | if (!drawers) return null; 1181 | 1182 | const drawersKeys = Object.keys(drawers) as DrawersName[]; 1183 | return drawersKeys.map(key => 1184 | this.DrawerContainer({Navigator: AppNavigator, name: key, definition: key}), 1185 | ); 1186 | }, [drawers]); 1187 | 1188 | // -- app modals 1189 | const AppModals = useMemo(() => { 1190 | if (!modals) return null; 1191 | 1192 | const modalsKeys = Object.keys(modals) as ModalName[]; 1193 | return modalsKeys.map(key => 1194 | this.ModalContainer({Navigator: AppNavigator, name: key, definition: key}), 1195 | ); 1196 | }, [modals]); 1197 | 1198 | // -- app root 1199 | const AppRoot = useMemo(() => { 1200 | return ( 1201 | 1202 | {/* Stacks */} 1203 | {AppStacks} 1204 | 1205 | {/* Tabs */} 1206 | {AppTabs} 1207 | 1208 | {/* Drawers */} 1209 | {AppDrawers} 1210 | 1211 | {/* Modals */} 1212 | 1213 | {AppModals} 1214 | 1215 | 1216 | ); 1217 | }, [appRoot]); 1218 | 1219 | return AppRoot; 1220 | }; 1221 | 1222 | /** 1223 | * Generates your app's root component for provided layout. 1224 | * Can be used as `` 1225 | */ 1226 | App: React.FC> = ({navigationContainerProps, root: parentRoot}) => { 1227 | // Navigation-related methods 1228 | const _navContainerRef = (instance: NavigationContainerRef<{}> | null) => { 1229 | this.navRef.current = instance; 1230 | }; 1231 | 1232 | const _navContainerOnReady = () => { 1233 | this.navIsReadyRef.current = true; 1234 | 1235 | if (navigationContainerProps?.onReady) { 1236 | navigationContainerProps?.onReady(); 1237 | } 1238 | }; 1239 | 1240 | return ( 1241 | 1246 | 1247 | 1248 | ); 1249 | }; 1250 | } 1251 | --------------------------------------------------------------------------------