Merge pull request #894 from sharetribe/autocomplete-default-searches

Default location suggestions for LocationAutocompleteInput
This commit is contained in:
Kimmo Puputti 2018-08-13 09:51:00 +03:00 committed by GitHub
commit fd4c39d01d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 25 deletions

View file

@ -13,6 +13,12 @@ way to update this template, but currently, we follow a pattern:
---
## Upcoming version 2018-08-XX
* [change] Add support for default locations in the
LocationAutocompleteInput component. Common searches can be
configured to show when the input has focus. This reduces typing and
Google Places geolocation API usage. The defaults can be configured
in `src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js`.
[#894](https://github.com/sharetribe/flex-template-web/pull/894)
* [change] Removed the `country` parameter from the search page as it
was not used anywhere.
[#893](https://github.com/sharetribe/flex-template-web/pull/893)

View file

@ -25,3 +25,14 @@ local development, you can add the variable in the Gitignored `.env` file in the
```
REACT_APP_GOOGLE_MAPS_API_KEY=my-key-here
```
## Setup common locations to reduce typing
The location autocomplete input in the landing page and the topbar can
be configured to have specific locations shown by default when the
user focuses on the input and hasn't yet typed in any searches. This
reduces the typing required for common searches, and also reduces the
need to use the Google Places geolocation API that much.
The default locations can be configured in
[src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js](../src/components/LocationAutocompleteInput/GeocoderGoogleMaps.js).

View file

@ -3,6 +3,30 @@ import { getPlacePredictions, getPlaceDetails } from '../../util/googleMaps';
import css from './LocationAutocompleteInput.css';
// A list of default predictions that can be shown when the user
// focuses on the autocomplete input without typing a search. This can
// be used to reduce typing and Geocoding API calls for common
// searches.
//
// Example:
//
// [
// {
// place_id: 'Place ID from Google Maps Places API',
// description: 'Place name to show in the autocomplete dropdown',
// },
// ]
//
// To know which values to set as defaults, log a real prediction
// object from the Places API call and copy the Place ID from the
// response.
export const defaultPredictions = [
// {
// place_id: 'ChIJkQYhlscLkkYRY_fiO4S9Ts0',
// description: 'Helsinki, Finland',
// },
];
// When displaying data from the Google Maps Places API, and
// attribution is required next to the results.
// See: https://developers.google.com/places/web-service/policies#powered
@ -44,6 +68,13 @@ class GeocoderGoogleMaps {
});
}
/**
* Get the ID of the given prediction.
*/
getPredictionId(prediction) {
return prediction.place_id;
}
/**
* Get the address text of the given prediction.
*/

View file

@ -3,6 +3,8 @@ import config from '../../config';
const { LatLng: SDKLatLng, LatLngBounds: SDKLatLngBounds } = sdkTypes;
const placeId = prediction => prediction.id;
const placeAddress = prediction => prediction.place_name;
const placeOrigin = prediction => {
@ -24,6 +26,34 @@ const placeBounds = prediction => {
return null;
};
// A list of default predictions that can be shown when the user
// focuses on the autocomplete input without typing a search. This can
// be used to reduce typing and Geocoding API calls for common
// searches.
//
// Example:
//
// [
// {
// id: 'any unique id',
// place_name: 'Place name to show in the autocomplete dropdown',
// center: [longitude, latitude],
// bbox: [minX, minY, maxX, maxY],
// },
// ]
//
// To know which values to set as defaults, log a real prediction
// object from the Geocoding API call and check the values from the
// response.
export const defaultPredictions = [
// {
// id: 'default-helsinki',
// place_name: 'Helsinki, Finland',
// center: [24.94861, 60.17333],
// bbox: [24.82617, 60.075361, 25.313112, 60.297839],
// },
];
export const GeocoderAttribution = () => null;
/**
@ -66,6 +96,13 @@ class GeocoderMapbox {
});
}
/**
* Get the ID of the given prediction.
*/
getPredictionId(prediction) {
return placeId(prediction);
}
/**
* Get the address text of the given prediction.
*/

View file

@ -3,7 +3,8 @@ import { any, arrayOf, bool, func, number, shape, string, oneOfType, object } fr
import classNames from 'classnames';
import debounce from 'lodash/debounce';
import { propTypes } from '../../util/types';
import Geocoder, { GeocoderAttribution } from './GeocoderGoogleMaps';
import Geocoder, { GeocoderAttribution, defaultPredictions } from './GeocoderGoogleMaps';
// import Geocoder, { GeocoderAttribution, defaultPredictions } from './GeocoderMapbox';
import css from './LocationAutocompleteInput.css';
@ -69,12 +70,12 @@ const LocationPredictionsList = props => {
return (
<li
className={isHighlighted ? css.highlighted : null}
key={prediction.id}
key={geocoder.getPredictionId(prediction)}
onTouchStart={e => onSelectStart(getTouchCoordinates(e.nativeEvent))}
onMouseDown={() => onSelectStart()}
onTouchMove={e => onSelectMove(getTouchCoordinates(e.nativeEvent))}
onTouchEnd={() => onSelectEnd(index)}
onMouseUp={() => onSelectEnd(index)}
onTouchEnd={() => onSelectEnd(prediction)}
onMouseUp={() => onSelectEnd(prediction)}
>
{geocoder.getPredictionAddress(prediction)}
</li>
@ -148,8 +149,9 @@ class LocationAutocompleteInputImpl extends Component {
this.geocoder = new Geocoder();
this.currentPredictions = this.currentPredictions.bind(this);
this.changeHighlight = this.changeHighlight.bind(this);
this.selectItem = this.selectItem.bind(this);
this.selectPrediction = this.selectPrediction.bind(this);
this.selectItemIfNoneSelected = this.selectItemIfNoneSelected.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onChange = this.onChange.bind(this);
@ -164,6 +166,13 @@ class LocationAutocompleteInputImpl extends Component {
this.predict = debounce(this.predict.bind(this), DEBOUNCE_WAIT_TIME, { leading: true });
}
currentPredictions() {
const { search, predictions: fetchedPredictions } = currentValue(this.props);
const hasFetchedPredictions = fetchedPredictions && fetchedPredictions.length > 0;
return !search && !hasFetchedPredictions ? defaultPredictions : fetchedPredictions;
}
// Interpret input key event
onKeyDown(e) {
if (e.keyCode === KEY_CODE_ARROW_UP) {
@ -175,9 +184,9 @@ class LocationAutocompleteInputImpl extends Component {
e.preventDefault();
this.changeHighlight(DIRECTION_DOWN);
} else if (e.keyCode === KEY_CODE_ENTER) {
const { search, selectedPlace } = currentValue(this.props);
const { selectedPlace } = currentValue(this.props);
if (search && !selectedPlace) {
if (!selectedPlace) {
// Prevent form submit, try to select value instead.
e.preventDefault();
e.stopPropagation();
@ -193,7 +202,7 @@ class LocationAutocompleteInputImpl extends Component {
// Handle input text change, fetch predictions if the value isn't empty
onChange(e) {
const onChange = this.props.input.onChange;
const { predictions } = currentValue(this.props);
const predictions = this.currentPredictions();
const newValue = e.target.value;
// Clear the current values since the input content is changed
@ -220,7 +229,7 @@ class LocationAutocompleteInputImpl extends Component {
// (DIRECTION_UP or DIRECTION_DOWN)
changeHighlight(direction) {
this.setState((prevState, props) => {
const { predictions } = currentValue(props);
const predictions = this.currentPredictions();
const currentIndex = prevState.highlightedIndex;
let index = currentIndex;
@ -244,16 +253,7 @@ class LocationAutocompleteInputImpl extends Component {
// Select the prediction in the given item. This will fetch/read the
// place details and set it as the selected place.
selectItem(index) {
if (index < 0) {
return;
}
const { predictions } = currentValue(this.props);
if (index >= predictions.length) {
return;
}
const prediction = predictions[index];
selectPrediction(prediction) {
this.props.input.onChange({
...this.props.input,
selectedPlace: null,
@ -278,10 +278,14 @@ class LocationAutocompleteInputImpl extends Component {
});
}
selectItemIfNoneSelected() {
const { search, selectedPlace } = currentValue(this.props);
if (search && !selectedPlace) {
const { selectedPlace } = currentValue(this.props);
const predictions = this.currentPredictions();
if (!selectedPlace) {
const index = this.state.highlightedIndex !== -1 ? this.state.highlightedIndex : 0;
this.selectItem(index);
if (index >= 0 && index < predictions.length) {
this.selectPrediction(predictions[index]);
}
}
}
predict(search) {
@ -350,7 +354,7 @@ class LocationAutocompleteInputImpl extends Component {
});
}
handlePredictionsSelectEnd(index) {
handlePredictionsSelectEnd(prediction) {
let selectAndFinalize = false;
this.setState(
prevState => {
@ -361,7 +365,7 @@ class LocationAutocompleteInputImpl extends Component {
},
() => {
if (selectAndFinalize) {
this.selectItem(index);
this.selectPrediction(prediction);
this.finalizeSelection();
}
}
@ -383,9 +387,10 @@ class LocationAutocompleteInputImpl extends Component {
inputRef,
} = this.props;
const { name, onFocus } = input;
const { search, predictions } = currentValue(this.props);
const { search } = currentValue(this.props);
const { touched, valid } = meta || {};
const isValid = valid && touched;
const predictions = this.currentPredictions();
const handleOnFocus = e => {
this.setState({ inputHasFocus: true });