diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js index 12bb5664361c35..d264ff6fc2af65 100644 --- a/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js +++ b/Examples/UIExplorer/NavigationExperimental/NavigationExperimentalExample.js @@ -34,6 +34,7 @@ const View = require('View'); */ const EXAMPLES = { 'NavigationCardStack Example': require('./NavigationCardStack-example'), + 'Header + Scenes + Tabs Example': require('./NavigationHeaderScenesTabs-example'), }; const EXAMPLE_STORAGE_KEY = 'NavigationExperimentalExample'; diff --git a/Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js b/Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js new file mode 100644 index 00000000000000..7d8efd0c950917 --- /dev/null +++ b/Examples/UIExplorer/NavigationExperimental/NavigationHeaderScenesTabs-example.js @@ -0,0 +1,478 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +'use strict'; + +const NavigationExampleRow = require('./NavigationExampleRow'); +const React = require('react'); +const ReactNative = require('react-native'); + +/** + * Basic example that shows how to use to build + * an app with composite navigation system. + */ + +const { + Component, + PropTypes, +} = React; + +const { + NavigationExperimental, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} = ReactNative; + +const { + CardStack: NavigationCardStack, + Header: NavigationHeader, + PropTypes: NavigationPropTypes, + StateUtils: NavigationStateUtils, +} = NavigationExperimental; + +// First Step. +// Define what app navigation state will look like. +function createAppNavigationState(): Object { + return { + // Three tabs. + tabs: { + index: 0, + routes: [ + {key: 'apple'}, + {key: 'banana'}, + {key: 'orange'}, + ], + }, + // Scenes for the `apple` tab. + apple: { + index: 0, + routes: [{key: 'Apple Home'}], + }, + // Scenes for the `banana` tab. + banana: { + index: 0, + routes: [{key: 'Banana Home'}], + }, + // Scenes for the `orange` tab. + orange: { + index: 0, + routes: [{key: 'Orange Home'}], + }, + }; +} + +// Next step. +// Define what app navigation state shall be updated. +function updateAppNavigationState( + state: Object, + action: Object, +): Object { + let {type} = action; + if (type === 'BackAction') { + type = 'pop'; + } + + switch (type) { + case 'push': { + // Push a route into the scenes stack. + const route: Object = action.route; + const {tabs} = state; + const tabKey = tabs.routes[tabs.index].key; + const scenes = state[tabKey]; + const nextScenes = NavigationStateUtils.push(scenes, route); + if (scenes !== nextScenes) { + return { + ...state, + [tabKey]: nextScenes, + }; + } + break; + } + + case 'pop': { + // Pops a route from the scenes stack. + const {tabs} = state; + const tabKey = tabs.routes[tabs.index].key; + const scenes = state[tabKey]; + const nextScenes = NavigationStateUtils.pop(scenes); + if (scenes !== nextScenes) { + return { + ...state, + [tabKey]: nextScenes, + }; + } + break; + } + + case 'selectTab': { + // Switches the tab. + const tabKey: string = action.tabKey; + const tabs = NavigationStateUtils.jumpTo(state.tabs, tabKey); + if (tabs !== state.tabs) { + return { + ...state, + tabs, + }; + } + } + } + return state; +} + +// Next step. +// Defines a helper function that creates a HOC (higher-order-component) +// which provides a function `navigate` through component props. The +// `navigate` function will be used to invoke navigation changes. +// This serves a convenient way for a component to navigate. +function createAppNavigationContainer(ComponentClass) { + const key = '_yourAppNavigationContainerNavigateCall'; + + class Container extends Component { + static contextTypes = { + [key]: PropTypes.func, + }; + + static childContextTypes = { + [key]: PropTypes.func.isRequired, + }; + + static propTypes = { + navigate: PropTypes.func, + }; + + getChildContext(): Object { + return { + [key]: this.context[key] || this.props.navigate, + }; + } + + render(): ReactElement { + const navigate = this.context[key] || this.props.navigate; + return ; + } + } + + return Container; +} + +// Next step. +// Define a component for your application that owns the navigation state. +class YourApplication extends Component { + + static propTypes = { + onExampleExit: PropTypes.func, + }; + + // This sets up the initial navigation state. + constructor(props, context) { + super(props, context); + // This sets up the initial navigation state. + this.state = createAppNavigationState(); + this._navigate = this._navigate.bind(this); + } + + render(): ReactElement { + // User your own navigator (see next step). + return ( + + ); + } + + // This public method is optional. If exists, the UI explorer will call it + // the "back button" is pressed. Normally this is the cases for Android only. + handleBackAction(): boolean { + return this._onNavigate({type: 'pop'}); + } + + // This handles the navigation state changes. You're free and responsible + // to define the API that changes that navigation state. In this exmaple, + // we'd simply use a `updateAppNavigationState` to update the navigation + // state. + _navigate(action: Object): void { + if (action.type === 'exit') { + // Exits the example. `this.props.onExampleExit` is provided + // by the UI Explorer. + this.props.onExampleExit && this.props.onExampleExit(); + return; + } + + const state = updateAppNavigationState( + this.state, + action, + ); + + // `updateAppNavigationState` (which uses NavigationStateUtils) gives you + // back the same `state` if nothing has changed. You could use + // that to avoid redundant re-rendering. + if (this.state !== state) { + this.setState(state); + } + } +} + +// Next step. +// Define your own controlled navigator. +const YourNavigator = createAppNavigationContainer(class extends Component { + static propTypes = { + appNavigationState: PropTypes.shape({ + apple: NavigationPropTypes.navigationState.isRequired, + banana: NavigationPropTypes.navigationState.isRequired, + orange: NavigationPropTypes.navigationState.isRequired, + tabs: NavigationPropTypes.navigationState.isRequired, + }), + navigate: PropTypes.func.isRequired, + }; + + // This sets up the methods (e.g. Pop, Push) for navigation. + constructor(props: any, context: any) { + super(props, context); + this._renderHeader = this._renderHeader.bind(this); + this._renderScene = this._renderScene.bind(this); + } + + // Now use the `NavigationCardStack` to render the scenes. + render(): ReactElement { + const {appNavigationState} = this.props; + const {tabs} = appNavigationState; + const tabKey = tabs.routes[tabs.index].key; + const scenes = appNavigationState[tabKey]; + + return ( + + + + + ); + } + + // Render the header. + // The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition` + // as type `NavigationSceneRendererProps`. + _renderHeader(sceneProps: Object): ReactElement { + return ( + + ); + } + + // Render a scene for route. + // The detailed spec of `sceneProps` is defined at `NavigationTypeDefinition` + // as type `NavigationSceneRendererProps`. + _renderScene(sceneProps: Object): ReactElement { + return ( + + ); + } +}); + +// Next step. +// Define your own header. +const YourHeader = createAppNavigationContainer(class extends Component { + static propTypes = { + ...NavigationPropTypes.SceneRendererProps, + navigate: PropTypes.func.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + this._renderTitleComponent = this._renderTitleComponent.bind(this); + } + + render(): ReactElement { + return ( + + ); + } + + _renderTitleComponent(): ReactElement { + return ( + + {this.props.scene.route.key} + + ); + } +}); + +// Next step. +// Define your own scene. +const YourScene = createAppNavigationContainer(class extends Component { + static propTypes = { + ...NavigationPropTypes.SceneRendererProps, + navigate: PropTypes.func.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + this._exit = this._exit.bind(this); + this._popRoute = this._popRoute.bind(this); + this._pushRoute = this._pushRoute.bind(this); + } + + render(): ReactElement { + return ( + + + + + + ); + } + + _pushRoute(): void { + // Just push a route with a new unique key. + const route = {key: '[' + this.props.scenes.length + ']-' + Date.now()}; + this.props.navigate({type: 'push', route}); + } + + _popRoute(): void { + this.props.navigate({type: 'pop'}); + } + + _exit(): void { + this.props.navigate({type: 'exit'}); + } +}); + +// Next step. +// Define your own tabs. +const YourTabs = createAppNavigationContainer(class extends Component { + static propTypes = { + navigationState: NavigationPropTypes.navigationState.isRequired, + navigate: PropTypes.func.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + } + + render(): ReactElement { + return ( + + {this.props.navigationState.routes.map(this._renderTab, this)} + + ); + } + + _renderTab(route: Object, index: number): ReactElement { + return ( + + ); + } +}); + +// Next step. +// Define your own Tab +const YourTab = createAppNavigationContainer(class extends Component { + + static propTypes = { + navigate: PropTypes.func.isRequired, + route: NavigationPropTypes.navigationRoute.isRequired, + selected: PropTypes.bool.isRequired, + }; + + constructor(props: Object, context: any) { + super(props, context); + this._onPress = this._onPress.bind(this); + } + + render(): ReactElement { + const style = [styles.tabText]; + if (this.props.selected) { + style.push(styles.tabSelected); + } + return ( + + + {this.props.route.key} + + + ); + } + + _onPress() { + this.props.navigate({type: 'selectTab', tabKey: this.props.route.key}); + } +}); + +const styles = StyleSheet.create({ + navigator: { + flex: 1, + }, + navigatorCardStack: { + flex: 20, + }, + scrollView: { + marginTop: 64 + }, + tabs: { + flex: 1, + flexDirection: 'row', + }, + tab: { + alignItems: 'center', + backgroundColor: '#fff', + flex: 1, + justifyContent: 'center', + }, + tabText: { + color: '#222', + fontWeight: '500', + }, + tabSelected: { + color: 'blue', + }, +}); + +module.exports = YourApplication;