react-native-google-places-autocomplete
Version:
Customizable Google Places autocomplete component for iOS and Android React-Native apps
817 lines (742 loc) • 23.4 kB
JavaScript
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
TextInput,
View,
FlatList,
ScrollView,
Image,
Text,
StyleSheet,
Dimensions,
TouchableHighlight,
Platform,
ActivityIndicator,
PixelRatio
} from 'react-native';
import Qs from 'qs';
import debounce from 'lodash.debounce';
const WINDOW = Dimensions.get('window');
const defaultStyles = {
container: {
flex: 1,
},
textInputContainer: {
backgroundColor: '#C9C9CE',
height: 44,
borderTopColor: '#7e7e7e',
borderBottomColor: '#b5b5b5',
borderTopWidth: 1 / PixelRatio.get(),
borderBottomWidth: 1 / PixelRatio.get(),
flexDirection: 'row',
},
textInput: {
backgroundColor: '#FFFFFF',
height: 28,
borderRadius: 5,
paddingTop: 4.5,
paddingBottom: 4.5,
paddingLeft: 10,
paddingRight: 10,
marginTop: 7.5,
marginLeft: 8,
marginRight: 8,
fontSize: 15,
flex: 1
},
poweredContainer: {
justifyContent: 'flex-end',
alignItems: 'center',
backgroundColor: '#FFFFFF',
},
powered: {},
listView: {
// flex: 1,
},
row: {
padding: 13,
height: 44,
flexDirection: 'row',
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#c8c7cc',
},
description: {},
loader: {
// flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
height: 20,
},
androidLoader: {
marginRight: -15,
},
};
export default class GooglePlacesAutocomplete extends Component {
constructor (props) {
super(props);
this._isMounted = false;
this._results = [];
this._requests = [];
this.state = this.getInitialState.call(this);
this.getInitialState = this.getInitialState.bind(this);
this.setAddressText = this.setAddressText.bind(this);
this.buildRowsFromResults = this.buildRowsFromResults.bind(this);
this._abortRequests = this._abortRequests.bind(this);
this.triggerFocus = this.triggerFocus.bind(this);
this.triggerBlur = this.triggerBlur.bind(this);
this.getCurrentLocation = this.getCurrentLocation.bind(this);
this._enableRowLoader = this._enableRowLoader.bind(this);
this._disableRowLoaders = this._disableRowLoaders.bind(this);
this._onPress = this._onPress.bind(this);
this._getPredefinedPlace = this._getPredefinedPlace.bind(this);
this._filterResultsByTypes = this._filterResultsByTypes.bind(this);
this._requestNearby = this._requestNearby.bind(this);
this._request = this._request.bind(this);
this._onChangeText = this._onChangeText.bind(this);
this._handleChangeText = this._handleChangeText.bind(this);
this._renderRowData = this._renderRowData.bind(this);
this._renderDescription = this._renderDescription.bind(this);
this._renderLoader = this._renderLoader.bind(this);
this._renderRow = this._renderRow.bind(this);
this._renderSeparator = this._renderSeparator.bind(this);
this._onBlur = this._onBlur.bind(this);
this._onFocus = this._onFocus.bind(this);
this._renderPoweredLogo = this._renderPoweredLogo.bind(this);
this._shouldShowPoweredLogo = this._shouldShowPoweredLogo.bind(this);
this._renderLeftButton = this._renderLeftButton.bind(this);
this._renderRightButton = this._renderRightButton.bind(this);
this._getFlatList = this._getFlatList.bind(this);
}
getInitialState() {
return {
text: this.props.getDefaultValue(),
dataSource: this.buildRowsFromResults([]),
listViewDisplayed: this.props.listViewDisplayed === 'auto' ? false : this.props.listViewDisplayed,
};
}
setAddressText(address) {
this.setState({
text: address
})
}
getAddressText() {
return this.state.text
}
buildRowsFromResults(results) {
var res = null;
if (results.length === 0 || this.props.predefinedPlacesAlwaysVisible === true) {
res = [...this.props.predefinedPlaces];
if (this.props.currentLocation === true) {
res.unshift({
description: this.props.currentLocationLabel,
isCurrentLocation: true,
});
}
} else {
res = [];
}
res = res.map(function(place) {
return {
...place,
isPredefinedPlace: true,
}
});
return [...res, ...results];
}
componentWillMount() {
this._request = this.props.debounce
? debounce(this._request, this.props.debounce)
: this._request;
}
componentDidMount() {
// This will load the default value's search results after the view has
// been rendered
this._isMounted = true;
this._onChangeText(this.state.text);
}
componentWillReceiveProps(nextProps) {
if (nextProps.listViewDisplayed !== 'auto') {
this.setState({
listViewDisplayed: nextProps.listViewDisplayed,
});
}
}
componentWillUnmount() {
this._abortRequests();
this._isMounted = false;
}
_abortRequests() {
for (let i = 0; i < this._requests.length; i++) {
this._requests[i].abort();
}
this._requests = [];
}
/**
* This method is exposed to parent components to focus on textInput manually.
* @public
*/
triggerFocus() {
if (this.refs.textInput) this.refs.textInput.focus();
}
/**
* This method is exposed to parent components to blur textInput manually.
* @public
*/
triggerBlur() {
if (this.refs.textInput) this.refs.textInput.blur();
}
getCurrentLocation() {
let options = null;
if (this.props.enableHighAccuracyLocation) {
options = (Platform.OS === 'android') ? {
enableHighAccuracy: true,
timeout: 20000
} : {
enableHighAccuracy: true,
timeout: 20000,
maximumAge: 1000
};
}
navigator.geolocation.getCurrentPosition(
(position) => {
if (this.props.nearbyPlacesAPI === 'None') {
let currentLocation = {
description: this.props.currentLocationLabel,
geometry: {
location: {
lat: position.coords.latitude,
lng: position.coords.longitude
}
}
};
this._disableRowLoaders();
this.props.onPress(currentLocation, currentLocation);
} else {
this._requestNearby(position.coords.latitude, position.coords.longitude);
}
},
(error) => {
this._disableRowLoaders();
alert(error.message);
},
options
);
}
_onPress(rowData) {
if (rowData.isPredefinedPlace !== true && this.props.fetchDetails === true) {
if (rowData.isLoading === true) {
// already requesting
return;
}
this._abortRequests();
// display loader
this._enableRowLoader(rowData);
// fetch details
const request = new XMLHttpRequest();
this._requests.push(request);
request.timeout = this.props.timeout;
request.ontimeout = this.props.onTimeout;
request.onreadystatechange = () => {
if (request.readyState !== 4) {
return;
}
if (request.status === 200) {
const responseJSON = JSON.parse(request.responseText);
if (responseJSON.status === 'OK') {
if (this._isMounted === true) {
const details = responseJSON.result;
this._disableRowLoaders();
this._onBlur();
this.setState({
text: this._renderDescription( rowData ),
});
delete rowData.isLoading;
this.props.onPress(rowData, details);
}
} else {
this._disableRowLoaders();
if (this.props.autoFillOnNotFound) {
this.setState({
text: this._renderDescription( rowData ),
});
delete rowData.isLoading;
}
if (!this.props.onNotFound)
console.warn('google places autocomplete: ' + responseJSON.status);
else
this.props.onNotFound(responseJSON);
}
} else {
this._disableRowLoaders();
if (!this.props.onFail)
console.warn('google places autocomplete: request could not be completed or has been aborted');
else
this.props.onFail();
}
};
request.open('GET', 'https://maps.googleapis.com/maps/api/place/details/json?' + Qs.stringify({
key: this.props.query.key,
placeid: rowData.place_id,
language: this.props.query.language,
}));
if (this.props.query.origin !== null) {
request.setRequestHeader('Referer', this.props.query.origin)
}
request.send();
} else if (rowData.isCurrentLocation === true) {
// display loader
this._enableRowLoader(rowData);
this.setState({
text: this._renderDescription( rowData ),
});
this.triggerBlur(); // hide keyboard but not the results
delete rowData.isLoading;
this.getCurrentLocation();
} else {
this.setState({
text: this._renderDescription( rowData ),
});
this._onBlur();
delete rowData.isLoading;
let predefinedPlace = this._getPredefinedPlace(rowData);
// sending predefinedPlace as details for predefined places
this.props.onPress(predefinedPlace, predefinedPlace);
}
}
_enableRowLoader(rowData) {
let rows = this.buildRowsFromResults(this._results);
for (let i = 0; i < rows.length; i++) {
if ((rows[i].place_id === rowData.place_id) || (rows[i].isCurrentLocation === true && rowData.isCurrentLocation === true)) {
rows[i].isLoading = true;
this.setState({
dataSource: rows,
});
break;
}
}
}
_disableRowLoaders() {
if (this._isMounted === true) {
for (let i = 0; i < this._results.length; i++) {
if (this._results[i].isLoading === true) {
this._results[i].isLoading = false;
}
}
this.setState({
dataSource: this.buildRowsFromResults(this._results),
});
}
}
_getPredefinedPlace(rowData) {
if (rowData.isPredefinedPlace !== true) {
return rowData;
}
for (let i = 0; i < this.props.predefinedPlaces.length; i++) {
if (this.props.predefinedPlaces[i].description === rowData.description) {
return this.props.predefinedPlaces[i];
}
}
return rowData;
}
_filterResultsByTypes(responseJSON, types) {
if (types.length === 0) return responseJSON.results;
var results = [];
for (let i = 0; i < responseJSON.results.length; i++) {
let found = false;
for (let j = 0; j < types.length; j++) {
if (responseJSON.results[i].types.indexOf(types[j]) !== -1) {
found = true;
break;
}
}
if (found === true) {
results.push(responseJSON.results[i]);
}
}
return results;
}
_requestNearby(latitude, longitude) {
this._abortRequests();
if (latitude !== undefined && longitude !== undefined && latitude !== null && longitude !== null) {
const request = new XMLHttpRequest();
this._requests.push(request);
request.timeout = this.props.timeout;
request.ontimeout = this.props.onTimeout;
request.onreadystatechange = () => {
if (request.readyState !== 4) {
return;
}
if (request.status === 200) {
const responseJSON = JSON.parse(request.responseText);
this._disableRowLoaders();
if (typeof responseJSON.results !== 'undefined') {
if (this._isMounted === true) {
var results = [];
if (this.props.nearbyPlacesAPI === 'GoogleReverseGeocoding') {
results = this._filterResultsByTypes(responseJSON, this.props.filterReverseGeocodingByTypes);
} else {
results = responseJSON.results;
}
this.setState({
dataSource: this.buildRowsFromResults(results),
});
}
}
if (typeof responseJSON.error_message !== 'undefined') {
console.warn('google places autocomplete: ' + responseJSON.error_message);
}
} else {
// console.warn("google places autocomplete: request could not be completed or has been aborted");
}
};
let url = '';
if (this.props.nearbyPlacesAPI === 'GoogleReverseGeocoding') {
// your key must be allowed to use Google Maps Geocoding API
url = 'https://maps.googleapis.com/maps/api/geocode/json?' + Qs.stringify({
latlng: latitude + ',' + longitude,
key: this.props.query.key,
...this.props.GoogleReverseGeocodingQuery,
});
} else {
url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json?' + Qs.stringify({
location: latitude + ',' + longitude,
key: this.props.query.key,
...this.props.GooglePlacesSearchQuery,
});
}
request.open('GET', url);
if (this.props.query.origin !== null) {
request.setRequestHeader('Referer', this.props.query.origin)
}
request.send();
} else {
this._results = [];
this.setState({
dataSource: this.buildRowsFromResults([]),
});
}
}
_request(text) {
this._abortRequests();
if (text.length >= this.props.minLength) {
const request = new XMLHttpRequest();
this._requests.push(request);
request.timeout = this.props.timeout;
request.ontimeout = this.props.onTimeout;
request.onreadystatechange = () => {
if (request.readyState !== 4) {
return;
}
if (request.status === 200) {
const responseJSON = JSON.parse(request.responseText);
if (typeof responseJSON.predictions !== 'undefined') {
if (this._isMounted === true) {
this._results = responseJSON.predictions;
this.setState({
dataSource: this.buildRowsFromResults(responseJSON.predictions),
});
}
}
if (typeof responseJSON.error_message !== 'undefined') {
console.warn('google places autocomplete: ' + responseJSON.error_message);
}
} else {
// console.warn("google places autocomplete: request could not be completed or has been aborted");
}
};
request.open('GET', 'https://maps.googleapis.com/maps/api/place/autocomplete/json?&input=' + encodeURIComponent(text) + '&' + Qs.stringify(this.props.query));
if (this.props.query.origin !== null) {
request.setRequestHeader('Referer', this.props.query.origin)
}
request.send();
} else {
this._results = [];
this.setState({
dataSource: this.buildRowsFromResults([]),
});
}
}
_onChangeText(text) {
this._request(text);
this.setState({
text: text,
listViewDisplayed: true,
});
}
_handleChangeText(text) {
this._onChangeText(text);
const onChangeText = this.props
&& this.props.textInputProps
&& this.props.textInputProps.onChangeText;
if (onChangeText) {
onChangeText(text);
}
}
_getRowLoader() {
return (
<ActivityIndicator
animating={true}
size="small"
/>
);
}
_renderRowData(rowData) {
if (this.props.renderRow) {
return this.props.renderRow(rowData);
}
return (
<Text style={[{flex: 1}, defaultStyles.description, this.props.styles.description, rowData.isPredefinedPlace ? this.props.styles.predefinedPlacesDescription : {}]}
numberOfLines={1}
>
{this._renderDescription(rowData)}
</Text>
);
}
_renderDescription(rowData) {
if (this.props.renderDescription) {
return this.props.renderDescription(rowData);
}
return rowData.description || rowData.formatted_address || rowData.name;
}
_renderLoader(rowData) {
if (rowData.isLoading === true) {
return (
<View
style={[defaultStyles.loader, this.props.styles.loader]}
>
{this._getRowLoader()}
</View>
);
}
return null;
}
_renderRow(rowData = {}, sectionID, rowID) {
return (
<ScrollView
style={{ flex: 1 }}
scrollEnabled={this.props.isRowScrollable}
keyboardShouldPersistTaps={this.props.keyboardShouldPersistTaps}
horizontal={true}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}>
<TouchableHighlight
style={{ width: WINDOW.width }}
onPress={() => this._onPress(rowData)}
underlayColor={this.props.listUnderlayColor || "#c8c7cc"}
>
<View style={[defaultStyles.row, this.props.styles.row, rowData.isPredefinedPlace ? this.props.styles.specialItemRow : {}]}>
{this._renderRowData(rowData)}
{this._renderLoader(rowData)}
</View>
</TouchableHighlight>
</ScrollView>
);
}
_renderSeparator(sectionID, rowID) {
if (rowID == this.state.dataSource.length - 1) {
return null
}
return (
<View
key={ `${sectionID}-${rowID}` }
style={[defaultStyles.separator, this.props.styles.separator]} />
);
}
_onBlur() {
this.triggerBlur();
this.setState({
listViewDisplayed: false
});
}
_onFocus() {
this.setState({
listViewDisplayed: true
});
}
_renderPoweredLogo() {
if (!this._shouldShowPoweredLogo()) {
return null
}
return (
<View
style={[defaultStyles.row, defaultStyles.poweredContainer, this.props.styles.poweredContainer]}
>
<Image
style={[defaultStyles.powered, this.props.styles.powered]}
resizeMode={Image.resizeMode.contain}
source={require('./images/powered_by_google_on_white.png')}
/>
</View>
);
}
_shouldShowPoweredLogo() {
if (!this.props.enablePoweredByContainer || this.state.dataSource.length == 0) {
return false
}
for (let i = 0; i < this.state.dataSource.length; i++) {
let row = this.state.dataSource[i];
if (!row.hasOwnProperty('isCurrentLocation') && !row.hasOwnProperty('isPredefinedPlace')) {
return true
}
}
return false
}
_renderLeftButton() {
if (this.props.renderLeftButton) {
return this.props.renderLeftButton()
}
}
_renderRightButton() {
if (this.props.renderRightButton) {
return this.props.renderRightButton()
}
}
_getFlatList() {
if ((this.state.text !== '' || this.props.predefinedPlaces.length || this.props.currentLocation === true) && this.state.listViewDisplayed === true) {
return (
<FlatList
style={[defaultStyles.listView, this.props.styles.listView]}
data={this.state.dataSource}
keyExtractor={(item) => item.description}
extraData={[this.state.dataSource, this.props]}
ItemSeparatorComponent={this._renderSeparator}
renderItem={({ item }) => this._renderRow(item)}
ListFooterComponent={this._renderPoweredLogo}
{...this.props}
/>
);
}
return null;
}
render() {
let {
onFocus,
...userProps
} = this.props.textInputProps;
return (
<View
style={[defaultStyles.container, this.props.styles.container]}
>
<View
style={[defaultStyles.textInputContainer, this.props.styles.textInputContainer]}
>
{this._renderLeftButton()}
<TextInput
ref="textInput"
returnKeyType={this.props.returnKeyType}
autoFocus={this.props.autoFocus}
style={[defaultStyles.textInput, this.props.styles.textInput]}
onChangeText={this._handleChangeText}
value={this.state.text}
placeholder={this.props.placeholder}
placeholderTextColor={this.props.placeholderTextColor}
onFocus={onFocus ? () => {this._onFocus(); onFocus()} : this._onFocus}
clearButtonMode="while-editing"
underlineColorAndroid={this.props.underlineColorAndroid}
{ ...userProps }
/>
{this._renderRightButton()}
</View>
{this._getFlatList()}
{this.props.children}
</View>
);
}
}
GooglePlacesAutocomplete.propTypes = {
placeholder: PropTypes.string,
placeholderTextColor: PropTypes.string,
underlineColorAndroid: PropTypes.string,
returnKeyType: PropTypes.string,
onPress: PropTypes.func,
onNotFound: PropTypes.func,
onFail: PropTypes.func,
minLength: PropTypes.number,
fetchDetails: PropTypes.bool,
autoFocus: PropTypes.bool,
autoFillOnNotFound: PropTypes.bool,
getDefaultValue: PropTypes.func,
timeout: PropTypes.number,
onTimeout: PropTypes.func,
query: PropTypes.object,
GoogleReverseGeocodingQuery: PropTypes.object,
GooglePlacesSearchQuery: PropTypes.object,
styles: PropTypes.object,
textInputProps: PropTypes.object,
enablePoweredByContainer: PropTypes.bool,
predefinedPlaces: PropTypes.array,
currentLocation: PropTypes.bool,
currentLocationLabel: PropTypes.string,
nearbyPlacesAPI: PropTypes.string,
enableHighAccuracyLocation: PropTypes.bool,
filterReverseGeocodingByTypes: PropTypes.array,
predefinedPlacesAlwaysVisible: PropTypes.bool,
enableEmptySections: PropTypes.bool,
renderDescription: PropTypes.func,
renderRow: PropTypes.func,
renderLeftButton: PropTypes.func,
renderRightButton: PropTypes.func,
listUnderlayColor: PropTypes.string,
debounce: PropTypes.number,
isRowScrollable: PropTypes.bool
}
GooglePlacesAutocomplete.defaultProps = {
placeholder: 'Search',
placeholderTextColor: '#A8A8A8',
isRowScrollable: true,
underlineColorAndroid: 'transparent',
returnKeyType: 'default',
onPress: () => {},
onNotFound: () => {},
onFail: () => {},
minLength: 0,
fetchDetails: false,
autoFocus: false,
autoFillOnNotFound: false,
keyboardShouldPersistTaps: 'always',
getDefaultValue: () => '',
timeout: 20000,
onTimeout: () => console.warn('google places autocomplete: request timeout'),
query: {
key: 'missing api key',
language: 'en',
types: 'geocode',
},
GoogleReverseGeocodingQuery: {},
GooglePlacesSearchQuery: {
rankby: 'distance',
types: 'food',
},
styles: {},
textInputProps: {},
enablePoweredByContainer: true,
predefinedPlaces: [],
currentLocation: false,
currentLocationLabel: 'Current location',
nearbyPlacesAPI: 'GooglePlacesSearch',
enableHighAccuracyLocation: true,
filterReverseGeocodingByTypes: [],
predefinedPlacesAlwaysVisible: false,
enableEmptySections: true,
listViewDisplayed: 'auto',
debounce: 0
}
// this function is still present in the library to be retrocompatible with version < 1.1.0
const create = function create(options = {}) {
return React.createClass({
render() {
return (
<GooglePlacesAutocomplete
ref="GooglePlacesAutocomplete"
{...options}
/>
);
},
});
};
module.exports = {
GooglePlacesAutocomplete,
create
};