From 4fa231b2b888af53ad4831fc9831273bf59c4bb5 Mon Sep 17 00:00:00 2001 From: alvaro Date: Mon, 9 May 2016 13:36:10 +0200 Subject: [PATCH] v0.2.0 Updated README Added flow --- .eslintrc | 2 - .flowconfig | 96 +++++++++++++++++++++++++++++++++ README.md | 38 +++++++------ index.js | 2 + lib/NavBar.android.js | 10 +++- lib/NavBar.ios.js | 5 +- lib/NavBarBackButton.android.js | 11 ++-- lib/NavBarBackButton.ios.js | 15 +++--- lib/NavigatorWrapper.js | 67 +++++++++++++++-------- lib/RouteMapper.js | 33 +++++++++--- lib/TopNavigatorWrapper.js | 69 +++++++++++++----------- package.json | 4 +- 12 files changed, 258 insertions(+), 94 deletions(-) create mode 100644 .flowconfig diff --git a/.eslintrc b/.eslintrc index 430f354..01a2c88 100644 --- a/.eslintrc +++ b/.eslintrc @@ -157,11 +157,9 @@ "no-lonely-if": 0, "no-new-object": 1, "no-spaced-func": 1, - "semi-spacing": 1, "no-ternary": 0, "no-trailing-spaces": 1, "no-underscore-dangle": 0, - "no-extra-parens": 0, "no-mixed-spaces-and-tabs": 1, "quotes": [1, "single", "avoid-escape"], "quote-props": 0, diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000..f56848d --- /dev/null +++ b/.flowconfig @@ -0,0 +1,96 @@ +[ignore] + +# We fork some components by platform. +.*/*.web.js +.*/*.android.js + +# Some modules have their own node_modules with overlap +.*/node_modules/node-haste/.* + +# Ugh +.*/node_modules/babel.* +.*/node_modules/babylon.* +.*/node_modules/invariant.* + +# Ignore react and fbjs where there are overlaps, but don't ignore +# anything that react-native relies on +.*/node_modules/fbjs/lib/Map.js +.*/node_modules/fbjs/lib/fetch.js +.*/node_modules/fbjs/lib/ExecutionEnvironment.js +.*/node_modules/fbjs/lib/ErrorUtils.js + +# Flow has a built-in definition for the 'react' module which we prefer to use +# over the currently-untyped source +.*/node_modules/react/react.js +.*/node_modules/react/lib/React.js +.*/node_modules/react/lib/ReactDOM.js + +.*/__mocks__/.* +.*/__tests__/.* + +.*/commoner/test/source/widget/share.js + +# Ignore commoner tests +.*/node_modules/commoner/test/.* + +# See https://github.com/facebook/flow/issues/442 +.*/react-tools/node_modules/commoner/lib/reader.js + +# Ignore jest +.*/node_modules/jest-cli/.* + +# Ignore Website +.*/website/.* + +# Ignore generators +.*/local-cli/generator.* + +# Ignore BUCK generated folders +.*\.buckd/ + +.*/node_modules/is-my-json-valid/test/.*\.json +.*/node_modules/iconv-lite/encodings/tables/.*\.json +.*/node_modules/y18n/test/.*\.json +.*/node_modules/spdx-license-ids/spdx-license-ids.json +.*/node_modules/spdx-exceptions/index.json +.*/node_modules/resolve/test/subdirs/node_modules/a/b/c/x.json +.*/node_modules/resolve/lib/core.json +.*/node_modules/jsonparse/samplejson/.*\.json +.*/node_modules/json5/test/.*\.json +.*/node_modules/ua-parser-js/test/.*\.json +.*/node_modules/builtin-modules/builtin-modules.json +.*/node_modules/binary-extensions/binary-extensions.json +.*/node_modules/url-regex/tlds.json +.*/node_modules/joi/.*\.json +.*/node_modules/isemail/.*\.json +.*/node_modules/tr46/.*\.json + + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js +node_modules/react-native/flow +flow/ + +[options] +module.system=haste + +esproposal.class_static_fields=enable +esproposal.class_instance_fields=enable + +munge_underscores=true + +module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\)$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-3]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-3]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy + +[version] +0.23.0 diff --git a/README.md b/README.md index 00580dc..a29e898 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ A React Native Navigator component wrapper that implements nested navigators for both push and modal transitions. +

+Wrapper example +

+ +## Disclaimer +This component uses the JS `Navigator` implementation of React Native. Future versions of the component will use the actual `NavigatorExperimental`. + ## Installation You can install this component through ``npm``: @@ -9,9 +16,9 @@ You can install this component through ``npm``: npm i react-native-navigator-wrapper --save ``` -You also need to install the awesome +Configure the awesome [``react-native-vector-icons``](https://github.com/oblador/react-native-vector-icons#installation) -from Joel Oblador (in order to use the back button arrow icon) and include the +from Joel Oblador to display the back button icons. Remember to include the ``Ionicons.ttf`` font in your project. All the components of the library are written in ES6/ES7 style. @@ -48,8 +55,7 @@ modal component. ## Usage This library can be used in several ways. It's composed from a couple of different components that interact with each other. In short, it has a navigation bar that -mimics the iOS navigation bar and two navigation wrappers. Expect an Android -style navigation bar soon. +mimics the iOS and Android navigation bar and two navigation wrappers. ### Nested navigation with ``TopNavigatorWrapper`` You can use ``TopNavigatorWrapper`` component to bring the nested navigator @@ -57,34 +63,29 @@ strategy just importing the component and wrapping whatever you want to render inside it: ```js -import React from 'react-native' +import React from 'react' import { TopNavigatorWrapper } from 'react-native-navigator-wrapper' import MyComponent from './MyComponent' class MyApp extends React.Component { render () { return ( - + + + ) } } ``` -You component ``MyComponent`` will have two props, **navigator** and -**topNavigator**. They will let you to push new components from right using -the first one or open a modal pushing from the second one. +You component ``MyComponent`` will have a **topNavigator** prop. It will let you to push new components in a modal-style, opening from bottom to top. ### Navigation with ``NavigatorWrapper`` If you just want to use the navigation bar inside a navigator, use the ``NavigatorWrapper`` component: ```js -import React from 'react-native' +import React from 'react' import { NavigatorWrapper } from 'react-native-navigator-wrapper' class MyComponent extends React.Component { @@ -109,12 +110,15 @@ that will let you to keep pushing components in the stack. The React Native ``Navigator.NavigatorBar`` component has an object called ``routeMapper`` that configures the three components that can be displayed inside the navigation bar: ``LeftButton``, ``RightButton`` and ``Title``. -This library auto-generates a default route mapper object that displays an iOS -style back button, a title and accepts a right element to render. +This library auto-generates a default route mapper object that displays an iOS & Android style back button, a title and accepts a right element to render. It also provides functions to generate each of the route mapper components so you can build a completely custom navigation bar for each ``NavigatorWrapper``. See the source code for more information. +## 🚧 Roadmap + +- [ ] Handle several hardware back button actions with multiple navigators (Android). + ## License MIT. diff --git a/index.js b/index.js index b0cdc6a..bae946c 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,5 @@ +/* @flow */ + import NavBarBackButton from './lib/NavBarBackButton' import NavBar from './lib/NavBar' import NavigatorWrapper from './lib/NavigatorWrapper' diff --git a/lib/NavBar.android.js b/lib/NavBar.android.js index 362f979..07ce843 100644 --- a/lib/NavBar.android.js +++ b/lib/NavBar.android.js @@ -1,11 +1,17 @@ -import React, { +/* @flow */ + +import React from 'react' +import { Navigator, StyleSheet, } from 'react-native' const stylesAndroid = StyleSheet.create({ navBar: { - backgroundColor: 'white', + backgroundColor: '#f5f5f5', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', } }) diff --git a/lib/NavBar.ios.js b/lib/NavBar.ios.js index 8c59751..853c4f1 100644 --- a/lib/NavBar.ios.js +++ b/lib/NavBar.ios.js @@ -1,4 +1,7 @@ -import React, { Navigator, StyleSheet, PixelRatio } from 'react-native' +/* @flow */ + +import React from 'react' +import { Navigator, StyleSheet, PixelRatio } from 'react-native' class NavBar extends React.Component { updateProgress (progress, fromIndex, toIndex) { diff --git a/lib/NavBarBackButton.android.js b/lib/NavBarBackButton.android.js index a3ec67a..5796195 100644 --- a/lib/NavBarBackButton.android.js +++ b/lib/NavBarBackButton.android.js @@ -1,6 +1,7 @@ -import React, { TouchableOpacity, Text, PropTypes, StyleSheet } from 'react-native' -import StyleSheetPropType from 'react-native/Libraries/StyleSheet/StyleSheetPropType' -import TextStylePropTypes from 'react-native/Libraries/Text/TextStylePropTypes' +/* @flow */ + +import React, { PropTypes } from 'react' +import { TouchableOpacity, Text, StyleSheet } from 'react-native' import Ionicon from 'react-native-vector-icons/Ionicons' class NavBarBackButton extends React.Component { @@ -32,7 +33,7 @@ class NavBarBackButton extends React.Component { - {this._renderBackTitle.bind(this)} @@ -44,7 +45,7 @@ NavBarBackButton.propTypes = { ...TouchableOpacity.propTypes, tintColor: PropTypes.string, children: PropTypes.string.isRequired, - style: StyleSheetPropType(TextStylePropTypes), + style: Text.propTypes.style, showBackTitle: PropTypes.bool, } diff --git a/lib/NavBarBackButton.ios.js b/lib/NavBarBackButton.ios.js index d74067b..90df14a 100644 --- a/lib/NavBarBackButton.ios.js +++ b/lib/NavBarBackButton.ios.js @@ -1,10 +1,13 @@ -import React, { TouchableOpacity, Text, PropTypes, StyleSheet } from 'react-native' -import StyleSheetPropType from 'react-native/Libraries/StyleSheet/StyleSheetPropType' -import TextStylePropTypes from 'react-native/Libraries/Text/TextStylePropTypes' +/* @flow */ + +import React, { PropTypes } from 'react' +import { TouchableOpacity, Text, StyleSheet } from 'react-native' import Ionicon from 'react-native-vector-icons/Ionicons' class NavBarBackButton extends React.Component { - constructor (props) { + state: Object; + + constructor (props: Object) { super(props) this.state = { tintColor: props.tintColor || 'black' @@ -34,7 +37,7 @@ class NavBarBackButton extends React.Component { style={styles.container}> - {this._renderBackTitle.bind(this)} + {this._renderBackTitle()} ) } @@ -44,7 +47,7 @@ NavBarBackButton.propTypes = { ...TouchableOpacity.propTypes, tintColor: PropTypes.string, children: PropTypes.string.isRequired, - style: StyleSheetPropType(TextStylePropTypes), + style: Text.propTypes.style, showBackTitle: PropTypes.bool, } diff --git a/lib/NavigatorWrapper.js b/lib/NavigatorWrapper.js index 3c3b6cd..628e346 100644 --- a/lib/NavigatorWrapper.js +++ b/lib/NavigatorWrapper.js @@ -1,15 +1,23 @@ -import React, { +/* @flow */ + +import React, { PropTypes } from 'react' +import { View, - PropTypes, Navigator, BackAndroid, - Platform + Platform, } from 'react-native' -import NavBar from './NavBar' import { defaultRouteMapper } from './RouteMapper' +import NavBar from './NavBar' class NavigatorWrapper extends React.Component { static isAndroid = Platform.OS !== 'ios'; + static androidToolbarHeight = 56; + static iosStatusAndNavbarHeight = 64; + + navigator: Object; + firstComponentInStack: boolean; + bindedBackFunction: Function; _handleAndroidBackButton () { if (this.navigator && !this.firstComponentInStack) { @@ -19,15 +27,14 @@ class NavigatorWrapper extends React.Component { return false } - constructor (props) { + constructor (props: Object) { super(props) - this.navigator = undefined this.firstComponentInStack = true } - componentDidMount() { + componentDidMount () { // Automatically handle back button under Android platform - if (NavigatorWrapper.isAndroid && this.props.initialRoute.handleBackAndroid) { + if (NavigatorWrapper.isAndroid && this.props.initialRoute.handleBackAndroid !== false) { this.bindedBackFunction = this._handleAndroidBackButton.bind(this) BackAndroid.addEventListener('hardwareBackPress', this.bindedBackFunction) } @@ -35,23 +42,27 @@ class NavigatorWrapper extends React.Component { componentWillUnmount () { if (NavigatorWrapper.isAndroid) { - BackAndroid.removeEventListener('hardwareBackPress', this.bindedBackFunction) + BackAndroid.removeEventListener( + 'hardwareBackPress', + this.bindedBackFunction + ) } } - renderScene (route, navigator) { - let marginTop = 64 - this.firstComponentInStack = route.handleBackAndroid + renderScene (route: Object, navigator: Object) { + let marginTop = NavigatorWrapper.iosStatusAndNavbarHeight + this.firstComponentInStack = (navigator.state.routeStack.length === 1) if (NavigatorWrapper.isAndroid) { // Save navigator to handle back button under Android if (!this.navigator) { this.navigator = navigator } - marginTop = 56 + // Configure right navbar height + marginTop = NavigatorWrapper.androidToolbarHeight } const RenderComponent = route.component return ( - + return ( navAnimation} + configureScene={(route, routeStack) => this.props.navigationBarAnimation || navAnimation} initialRoute={this.props.initialRoute} initialRouteStack={this.props.initialRouteStack} - navigationBar={ - - } + navigationBar={NavigationBar} renderScene={this.renderScene.bind(this)} /> ) @@ -96,6 +104,7 @@ NavigatorWrapper.propTypes = { leftElement: PropTypes.node, textElement: PropTypes.node, rightElement: PropTypes.node, + // Set to false to disable the back button under Android handleBackAndroid: PropTypes.bool, }), initialRouteStack: PropTypes.arrayOf(PropTypes.object), @@ -111,11 +120,27 @@ NavigatorWrapper.propTypes = { */ navBarStyle: View.propTypes.style, + /** + * Defines the navigator style. Useful for changing the background color + * while transitioning + */ + style: View.propTypes.style, + /** * A ``routeMapper`` object to customize Left, Title and Right components for * the ``NavigationBar``. */ routeMapper: PropTypes.object, + + /** + * Optional prop to hide the navigation bar + */ + hideNavBar: PropTypes.bool, + + /** + * Optional navigation scene config animation + */ + navigationBarAnimation: PropTypes.object, } export default NavigatorWrapper diff --git a/lib/RouteMapper.js b/lib/RouteMapper.js index fa78a2c..a7fe815 100644 --- a/lib/RouteMapper.js +++ b/lib/RouteMapper.js @@ -1,14 +1,22 @@ -import React, { View, Text } from 'react-native' +/* @flow */ + +import React from 'react' +import { View, Text, StyleSheet, Platform } from 'react-native' import NavBarBackButton from './NavBarBackButton' export function leftButtonRouteMapperGenerator (BackComponent, styles, tintColor, topNavigator) { return { LeftButton: (route, navigator, index, navState) => { if (route.leftElement) { - return React.cloneElement(route.leftElement, { + const leftElement = React.cloneElement(route.leftElement, { navigator: navigator, topNavigator: topNavigator, }) + return ( + + {leftElement} + + ) } else if (index > 0) { const previousRoute = navState.routeStack[index - 1] return ( @@ -16,7 +24,8 @@ export function leftButtonRouteMapperGenerator (BackComponent, styles, tintColor navigator.pop()} style={[{flex: 1}, styles]} - tintColor={tintColor}> + tintColor={tintColor} + showBackTitle={true}> {previousRoute.title} @@ -31,10 +40,15 @@ export function rightButtonRouteMapperGenerator (RightComponent, topNavigator) { return { RightButton: (route, navigator, index, navState) => { if (route.rightElement) { - return React.cloneElement(route.rightElement, { + const rightElement = React.cloneElement(route.rightElement, { navigator: navigator, topNavigator: topNavigator, }) + return ( + + {rightElement} + + ) } else if (RightComponent) { return } @@ -68,10 +82,10 @@ export class CenteredText extends React.Component { } } -const defaultStyles = React.StyleSheet.create({ +const defaultStyles = StyleSheet.create({ back: { flex: 1, - color: 'black', + color: (Platform.OS === 'ios') ? '#0076ff' : '#607D8B', }, navFont: { fontSize: 17, @@ -79,11 +93,16 @@ const defaultStyles = React.StyleSheet.create({ navText: { flex: 1, }, + elementContainer: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + }, }) export function defaultRouteMapper () { return { - ...leftButtonRouteMapperGenerator(NavBarBackButton, [defaultStyles.navFont, defaultStyles.back], 'black'), + ...leftButtonRouteMapperGenerator(NavBarBackButton, [defaultStyles.navFont, defaultStyles.back], (Platform.OS === 'ios') ? '#0076ff' : '#607D8B'), ...rightButtonRouteMapperGenerator(), ...titleRouteMapperGenerator(CenteredText, [defaultStyles.navFont, defaultStyles.navText]) } diff --git a/lib/TopNavigatorWrapper.js b/lib/TopNavigatorWrapper.js index 780280a..1d3fca0 100644 --- a/lib/TopNavigatorWrapper.js +++ b/lib/TopNavigatorWrapper.js @@ -1,23 +1,27 @@ -import React, { View, Navigator, PropTypes, Platform } from 'react-native' +/* @flow */ + +import React, { PropTypes } from 'react' +import { View, Navigator, Platform } from 'react-native' import NavigatorWrapper from './NavigatorWrapper' +import { defaultRouteMapper } from './RouteMapper' class TopNavigatorWrapper extends React.Component { static isAndroid = Platform.OS !== 'ios'; - _renderScene (route, navigator) { - // Render the inner component or the modal. This component will be the one - // with push-like transitions. + _renderScene (route: Object, navigator: Object) { + // Render the inner component or the modal. This is basically a container + // that can handle anything, usually a TabBarIOS with more NavigatorWrappers + // inside. if (route.id === 'mainComponent') { + // Inject the top navigator into the inner component in order to be able + // to open a modal from anywhere + const children = React.cloneElement(this.props.children, { + topNavigator: navigator + }) return ( - + + {children} + ) } @@ -27,17 +31,17 @@ class TopNavigatorWrapper extends React.Component { // // By generating the routeMapper from a function, we can pass the outer // modal navigator into the route mapper. + const modalRouteMapper = this.props.modalRouteMapper || defaultRouteMapper return ( ) } @@ -50,6 +54,7 @@ class TopNavigatorWrapper extends React.Component { renderScene={(route, navigator) => (this._renderScene(route, navigator))} initialRoute={{id: 'mainComponent'}} configureScene={(route, routeStack) => modalAnimation} + style={this.props.modalStyle} /> ) } @@ -57,27 +62,29 @@ class TopNavigatorWrapper extends React.Component { TopNavigatorWrapper.propTypes = { /** - * Provide the initial route or the initial route stack. + * Optional style for the default modal navigation bar. */ - initialRoute: PropTypes.shape({ - component: PropTypes.func.isRequired, - title: PropTypes.string.isRequired, - passProps: PropTypes.object, - }), - initialRouteStack: PropTypes.arrayOf(PropTypes.object), + modalNavBarStyle: View.propTypes.style, /** - * Optional style for the default navigation bar. + * Route mapper for the modal component */ - navBarStyle: View.propTypes.style, + modalRouteMapper: PropTypes.func, /** - * Optional style for the default modal navigation bar. + * The style of the inner container */ - modalNavBarStyle: View.propTypes.style, + containerStyle: View.propTypes.style, - routeMapper: PropTypes.func, - modalRouteMapper: PropTypes.func, + /** + * The style of the modal transition + */ + modalStyle: View.propTypes.style, + + /** + * Hides the modal navigation bar + */ + hideNavBar: PropTypes.bool, } export default TopNavigatorWrapper diff --git a/package.json b/package.json index 4789f67..481a692 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-navigator-wrapper", - "version": "0.1.0", + "version": "0.2.0", "description": "A React Native Navigator component wrapper that implements nested navigators for both push and modal transitions.", "main": "index.js", "scripts": { @@ -30,6 +30,6 @@ "eslint-plugin-react": "^3.7.1" }, "dependencies": { - "react-native-vector-icons": ">=1.0.4" + "react-native-vector-icons": "^2.0.0" } }