From 3ee3d2b4b26e0febb4a7be4258c7706feb040516 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Thu, 2 Feb 2017 03:26:36 -0800 Subject: [PATCH] CLI: Add basic Navigation template (Chat) Summary: Basic template using 'react-navigation' to make it easy to get started. Not duplicating all the files in `android` and `ios` folders. These will be taken from the `HelloWorld` template. Let's not duplicate all of these files (it's a lot and they are large, especially the Xcode projects). **Test plan (required)** The app works locally. This PR is just a preparation for a next PR that will add support for 'react-native init --template Navigation'. Will have a proper test plan there. Closes https://github.com/facebook/react-native/pull/12153 Differential Revision: D4494776 Pulled By: mkonicek fbshipit-source-id: b43eafd7a1424477f9493a3eb4083ba4dd3d3846 --- .../components/KeyboardSpacer.js | 115 +++++++++++++++ .../HelloNavigation/components/ListItem.js | 52 +++++++ .../HelloNavigation/index.android.js | 5 + .../templates/HelloNavigation/index.ios.js | 5 + .../views/HomeScreenTabNavigator.js | 24 ++++ .../HelloNavigation/views/MainNavigator.js | 25 ++++ .../views/chat/ChatListScreen.js | 68 +++++++++ .../HelloNavigation/views/chat/ChatScreen.js | 132 ++++++++++++++++++ .../HelloNavigation/views/chat/chat-icon.png | Bin 0 -> 2458 bytes .../views/friends/FriendListScreen.js | 49 +++++++ .../views/friends/friend-icon.png | Bin 0 -> 4800 bytes local-cli/templates/README.md | 48 +++++++ 12 files changed, 523 insertions(+) create mode 100644 local-cli/templates/HelloNavigation/components/KeyboardSpacer.js create mode 100644 local-cli/templates/HelloNavigation/components/ListItem.js create mode 100644 local-cli/templates/HelloNavigation/index.android.js create mode 100644 local-cli/templates/HelloNavigation/index.ios.js create mode 100644 local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js create mode 100644 local-cli/templates/HelloNavigation/views/MainNavigator.js create mode 100644 local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js create mode 100644 local-cli/templates/HelloNavigation/views/chat/ChatScreen.js create mode 100644 local-cli/templates/HelloNavigation/views/chat/chat-icon.png create mode 100644 local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js create mode 100644 local-cli/templates/HelloNavigation/views/friends/friend-icon.png create mode 100644 local-cli/templates/README.md diff --git a/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js b/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js new file mode 100644 index 000000000..e728cad33 --- /dev/null +++ b/local-cli/templates/HelloNavigation/components/KeyboardSpacer.js @@ -0,0 +1,115 @@ +/* @flow */ + +import React, { PropTypes, Component } from 'react'; +import { + Platform, + View, + Keyboard, + LayoutAnimation, + UIManager, +} from 'react-native'; + +type Props = { + offset?: number; +} + +type State = { + keyboardHeight: number +} + +// Consider contributing this to the popular library: +// https://github.com/Andr3wHur5t/react-native-keyboard-spacer + +/** + * On iOS, the software keyboard covers the screen by default. + * This is not desirable if there are TextInputs near the bottom of the screen - + * they would be covered by the keyboard and the user cannot see what they + * are typing. + * To get around this problem, place a `` at the bottom + * of the screen, after your TextInputs. The keyboard spacer has size 0 and + * when the keyboard is shown it will grow to the same size as the keyboard, + * shifting all views above it and therefore making them visible. + * + * On Android, this component is not needed because resizing the UI when + * the keyboard is shown is supported by the OS. + * Simply set the `android:windowSoftInputMode="adjustResize"` attribute + * on the element in your AndroidManifest.xml. + * + * How is this different from KeyboardAvoidingView? + * The KeyboardAvoidingView doesn't work when used together with + * a ScrollView/ListView. + */ +const KeyboardSpacer = () => ( + Platform.OS === 'ios' ? : null +) + +class KeyboardSpacerIOS extends Component { + static propTypes = { + offset: PropTypes.number, + }; + + static defaultProps = { + offset: 0, + }; + + state: State = { + keyboardHeight: 0, + }; + + componentWillMount() { + this._registerEvents(); + } + + componentWillUnmount() { + this._unRegisterEvents(); + } + + _keyboardWillShowSubscription: { remove: Function }; + _keyboardWillHideSubscription: { remove: Function }; + + _registerEvents = () => { + this._keyboardWillShowSubscription = Keyboard.addListener( + 'keyboardWillShow', + this._keyboardWillShow + ); + this._keyboardWillHideSubscription = Keyboard.addListener( + 'keyboardWillHide', + this._keyboardWillHide + ); + }; + + _unRegisterEvents = () => { + this._keyboardWillShowSubscription.remove(); + this._keyboardWillHideSubscription.remove(); + }; + + _configureLayoutAnimation = () => { + // Any duration is OK here. The `type: 'keyboard defines the animation. + LayoutAnimation.configureNext({ + duration: 100, + update: { + type: 'keyboard', + } + }); + } + + _keyboardWillShow = (e: any) => { + this._configureLayoutAnimation(); + this.setState({ + keyboardHeight: e.endCoordinates.height - (this.props.offset || 0), + }); + }; + + _keyboardWillHide = () => { + this._configureLayoutAnimation(); + this.setState({ + keyboardHeight: 0, + }); + }; + + render() { + return ; + } +} + +export default KeyboardSpacer; diff --git a/local-cli/templates/HelloNavigation/components/ListItem.js b/local-cli/templates/HelloNavigation/components/ListItem.js new file mode 100644 index 000000000..a51db840f --- /dev/null +++ b/local-cli/templates/HelloNavigation/components/ListItem.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import { + Platform, + StyleSheet, + Text, + TouchableHighlight, + TouchableNativeFeedback, + View, +} from 'react-native'; + +/** + * Renders the right type of Touchable for the list item, based on platform. + */ +const Touchable = ({onPress, children}) => { + const child = React.Children.only(children); + if (Platform.OS === 'android') { + return ( + + {child} + + ); + } else { + return ( + + {child} + + ); + } +} + +const ListItem = ({label, onPress}) => ( + + + {label} + + +); + +const styles = StyleSheet.create({ + item: { + height: 48, + justifyContent: 'center', + paddingLeft: 12, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#ddd', + }, + label: { + fontSize: 16, + } +}); + +export default ListItem; diff --git a/local-cli/templates/HelloNavigation/index.android.js b/local-cli/templates/HelloNavigation/index.android.js new file mode 100644 index 000000000..b122bdc5d --- /dev/null +++ b/local-cli/templates/HelloNavigation/index.android.js @@ -0,0 +1,5 @@ +import { AppRegistry } from 'react-native'; + +import MainNavigator from './views/MainNavigator'; + +AppRegistry.registerComponent('ChatExample', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/index.ios.js b/local-cli/templates/HelloNavigation/index.ios.js new file mode 100644 index 000000000..b122bdc5d --- /dev/null +++ b/local-cli/templates/HelloNavigation/index.ios.js @@ -0,0 +1,5 @@ +import { AppRegistry } from 'react-native'; + +import MainNavigator from './views/MainNavigator'; + +AppRegistry.registerComponent('ChatExample', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js new file mode 100644 index 000000000..123ee3a78 --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js @@ -0,0 +1,24 @@ +import React, { Component } from 'react'; +import { + ListView, + Platform, + Text, +} from 'react-native'; +import { TabNavigator } from 'react-navigation'; + +import ChatListScreen from './chat/ChatListScreen'; +import FriendListScreen from './friends/FriendListScreen'; + +/** + * Screen with tabs shown on app startup. + */ +const HomeScreenTabNavigator = TabNavigator({ + Chats: { + screen: ChatListScreen, + }, + Friends: { + screen: FriendListScreen, + }, +}); + +export default HomeScreenTabNavigator; diff --git a/local-cli/templates/HelloNavigation/views/MainNavigator.js b/local-cli/templates/HelloNavigation/views/MainNavigator.js new file mode 100644 index 000000000..20a5fb11b --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/MainNavigator.js @@ -0,0 +1,25 @@ +/** + * This is an example React Native app demonstrates ListViews, text input and + * navigation between a few screens. + * https://github.com/facebook/react-native + */ + +import React, { Component } from 'react'; +import { StackNavigator } from 'react-navigation'; + +import HomeScreenTabNavigator from './HomeScreenTabNavigator'; +import ChatScreen from './chat/ChatScreen'; + +/** + * Top-level navigator. Renders the application UI. + */ +const MainNavigator = StackNavigator({ + Home: { + screen: HomeScreenTabNavigator, + }, + Chat: { + screen: ChatScreen, + }, +}); + +export default MainNavigator; diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js new file mode 100644 index 000000000..a29fa1443 --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js @@ -0,0 +1,68 @@ +import React, { Component } from 'react'; +import { + Image, + ListView, + Platform, + StyleSheet, +} from 'react-native'; +import ListItem from '../../components/ListItem'; + +export default class ChatListScreen extends Component { + + static navigationOptions = { + title: 'Chats', + header: { + visible: Platform.OS === 'ios', + }, + tabBar: { + icon: ({ tintColor }) => ( + + ), + }, + } + + constructor(props) { + super(props); + const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + this.state = { + dataSource: ds.cloneWithRows([ + 'Claire', 'John' + ]) + }; + } + + // Binding the function so it can be passed to ListView below + // and 'this' works properly inside _renderRow + _renderRow = (name) => { + return ( + this.props.navigation.navigate('Chat', {name: name})} + /> + ) + } + + render() { + return ( + + ); + } +} + +const styles = StyleSheet.create({ + listView: { + backgroundColor: 'white', + }, + icon: { + width: 30, + height: 26, + }, +}); diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js new file mode 100644 index 000000000..8b732f9a0 --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js @@ -0,0 +1,132 @@ +import React, { Component } from 'react'; +import { + Button, + ListView, + Platform, + StyleSheet, + Text, + TextInput, + View, +} from 'react-native'; +import KeyboardSpacer from '../../components/KeyboardSpacer'; + +export default class ChatScreen extends Component { + + static navigationOptions = { + title: (navigation) => `Chat with ${navigation.state.params.name}`, + } + + constructor(props) { + super(props); + const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + const messages = [ + { + name: props.navigation.state.params.name, + name: 'Claire', + text: 'I ❤️ React Native!', + }, + ]; + this.state = { + messages: messages, + dataSource: ds.cloneWithRows(messages), + myMessage: '', + } + } + + addMessage = () => { + this.setState((prevState) => { + if (!prevState.myMessage) return prevState; + const messages = [ + ...prevState.messages, { + name: 'Me', + text: prevState.myMessage, + } + ]; + return { + messages: messages, + dataSource: prevState.dataSource.cloneWithRows(messages), + myMessage: '', + } + }); + this.refs.textInput.clear(); + } + + myMessageChange = (event) => { + this.setState({myMessage: event.nativeEvent.text}); + } + + renderRow = (message) => ( + + {message.name} + {message.text} + + ) + + render() { + return ( + + + + + {this.state.myMessage !== '' && ( +