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
This commit is contained in:
Родитель
81b2d69575
Коммит
3ee3d2b4b2
|
@ -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 `<KeyboardSpacer />` 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 <activity> 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' ? <KeyboardSpacerIOS /> : null
|
||||||
|
)
|
||||||
|
|
||||||
|
class KeyboardSpacerIOS extends Component<Props, Props, State> {
|
||||||
|
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 <View style={{ height: this.state.keyboardHeight }} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyboardSpacer;
|
|
@ -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 (
|
||||||
|
<TouchableNativeFeedback onPress={onPress}>
|
||||||
|
{child}
|
||||||
|
</TouchableNativeFeedback>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<TouchableHighlight onPress={onPress} underlayColor='#ddd'>
|
||||||
|
{child}
|
||||||
|
</TouchableHighlight>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem = ({label, onPress}) => (
|
||||||
|
<Touchable onPress={onPress}>
|
||||||
|
<View style={styles.item}>
|
||||||
|
<Text style={styles.label}>{label}</Text>
|
||||||
|
</View>
|
||||||
|
</Touchable>
|
||||||
|
);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
item: {
|
||||||
|
height: 48,
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingLeft: 12,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: '#ddd',
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 16,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ListItem;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AppRegistry } from 'react-native';
|
||||||
|
|
||||||
|
import MainNavigator from './views/MainNavigator';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('ChatExample', () => MainNavigator);
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { AppRegistry } from 'react-native';
|
||||||
|
|
||||||
|
import MainNavigator from './views/MainNavigator';
|
||||||
|
|
||||||
|
AppRegistry.registerComponent('ChatExample', () => MainNavigator);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 }) => (
|
||||||
|
<Image
|
||||||
|
// Using react-native-vector-icons works here too
|
||||||
|
source={require('./chat-icon.png')}
|
||||||
|
style={[styles.icon, {tintColor: 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 (
|
||||||
|
<ListItem
|
||||||
|
label={name}
|
||||||
|
onPress={() => this.props.navigation.navigate('Chat', {name: name})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ListView
|
||||||
|
dataSource={this.state.dataSource}
|
||||||
|
renderRow={this._renderRow}
|
||||||
|
style={styles.listView}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
listView: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 30,
|
||||||
|
height: 26,
|
||||||
|
},
|
||||||
|
});
|
|
@ -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) => (
|
||||||
|
<View style={styles.bubble}>
|
||||||
|
<Text style={styles.name}>{message.name}</Text>
|
||||||
|
<Text>{message.text}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ListView
|
||||||
|
ref="listView"
|
||||||
|
dataSource={this.state.dataSource}
|
||||||
|
renderRow={this.renderRow}
|
||||||
|
style={styles.listView}
|
||||||
|
onLayout={this.scrollToBottom}
|
||||||
|
/>
|
||||||
|
<View style={styles.composer}>
|
||||||
|
<TextInput
|
||||||
|
ref='textInput'
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder='Type a message...'
|
||||||
|
text={this.state.myMessage}
|
||||||
|
onSubmitEditing={this.addMessage}
|
||||||
|
onChange={this.myMessageChange}
|
||||||
|
/>
|
||||||
|
{this.state.myMessage !== '' && (
|
||||||
|
<Button
|
||||||
|
title="Send"
|
||||||
|
onPress={this.addMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<KeyboardSpacer />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
listView: {
|
||||||
|
flex: 1,
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
},
|
||||||
|
bubble: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
backgroundColor: '#d6f3fc',
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 4,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
composer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 36,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
flex: 1,
|
||||||
|
borderColor: '#ddd',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 4,
|
||||||
|
height: 30,
|
||||||
|
fontSize: 13,
|
||||||
|
marginRight: 8,
|
||||||
|
}
|
||||||
|
});
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 2.4 KiB |
|
@ -0,0 +1,49 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
Image,
|
||||||
|
Platform,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
import ListItem from '../../components/ListItem';
|
||||||
|
|
||||||
|
export default class FriendListScreen extends Component {
|
||||||
|
|
||||||
|
static navigationOptions = {
|
||||||
|
title: 'Friends',
|
||||||
|
header: {
|
||||||
|
visible: Platform.OS === 'ios',
|
||||||
|
},
|
||||||
|
tabBar: {
|
||||||
|
icon: ({ tintColor }) => (
|
||||||
|
<Image
|
||||||
|
// Using react-native-vector-icons works here too
|
||||||
|
source={require('./friend-icon.png')}
|
||||||
|
style={[styles.icon, {tintColor: tintColor}]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Text>A list of friends here.</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
flex: 1,
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 30,
|
||||||
|
height: 26,
|
||||||
|
},
|
||||||
|
});
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 4.7 KiB |
|
@ -0,0 +1,48 @@
|
||||||
|
# App templates
|
||||||
|
|
||||||
|
This folder contains basic app templates. These get expanded by 'react-native init' when creating a new app to make it easier for anyone to get started.
|
||||||
|
|
||||||
|
# Chat Example
|
||||||
|
|
||||||
|
This is an example React Native app demonstrates ListViews, text input and
|
||||||
|
navigation between a few screens.
|
||||||
|
|
||||||
|
<img width="487" alt="screenshot 2017-01-13 17 24 37" src="https://cloud.githubusercontent.com/assets/346214/21950983/54d75cb4-d9b5-11e6-9d63-bd7edf51f4d4.png">
|
||||||
|
<img width="487" alt="screenshot 2017-01-13 17 24 40" src="https://cloud.githubusercontent.com/assets/346214/21950982/54d6797a-d9b5-11e6-829f-3e0f15dab0c1.png">
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
One problem with React Native is that it is not trivial to get started: `react-native init` creates a very simple app that renders some text. Everyone then has to figure out how to do very basic things such as adding a list of items fetched from a server, navigating to a screen when a list item is tapped, or handling text input.
|
||||||
|
|
||||||
|
This app is a template used by `react-native init` so it is easier for anyone to get up and running quickly by having an app with a few screens, a `ListView` and a `TextInput` that works well with the software keyboard.
|
||||||
|
|
||||||
|
## Best practices
|
||||||
|
|
||||||
|
Another purpose of this app is to define best practices such as:
|
||||||
|
- The folder structure of a standalone React Native app
|
||||||
|
- A style guide for JavaScript and React - for this we use the [AirBnb style guide](https://github.com/airbnb/javascript)
|
||||||
|
- Naming conventions
|
||||||
|
|
||||||
|
We need your feedback to settle on a good set of best practices. Have you built React Native apps? If so, please use the issues in the repo [mkonicek/ChatExample](https://github.com/mkonicek/ChatExample) to discuss what you think are the best practices that this example should be using.
|
||||||
|
|
||||||
|
## Running the app locally
|
||||||
|
|
||||||
|
```
|
||||||
|
cd ChatExample
|
||||||
|
yarn
|
||||||
|
react-native run-ios
|
||||||
|
react-native run-android
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
(In case you want to use react-navigation master):
|
||||||
|
|
||||||
|
```
|
||||||
|
# Install dependencies:
|
||||||
|
cd react-navigation
|
||||||
|
yarn
|
||||||
|
yarn pack --filename react-navigation-1.0.0-alpha.tgz
|
||||||
|
cd ChatExample
|
||||||
|
yarn
|
||||||
|
yarn add ~/code/react-navigation/react-navigation-1.0.0-alpha.tgz
|
||||||
|
```
|
Загрузка…
Ссылка в новой задаче