Created
December 8, 2016 13:11
-
-
Save jaxxreal/1f75269d7cd492f514e2853bac424cdd to your computer and use it in GitHub Desktop.
React component for Google Places Autocomplete
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from "react"; | |
import { Component, ValidationMap, FormEvent, PropTypes } from "react"; | |
import { includes, isFunction } from "lodash"; | |
// components | |
import { TextField } from "../material"; | |
// interfaces | |
import { UserAddress } from "../../interfaces/user"; | |
import { GeoQuery } from "../../interfaces/google"; | |
// utils | |
import { getCannyAddress } from "../utils"; | |
// styles | |
import { floatingLabelStyle } from "../../styles/text-field"; | |
declare let google: any; | |
declare let __CLIENT__: boolean; | |
export interface GooglePlaceProps { | |
id: string; | |
proposals?: UserAddress[]; | |
errorText?: string; | |
labelText?: string; | |
onlyCoords?: boolean; | |
latitude?: number; | |
longitude?: number; | |
onChange(address: any): void; | |
onError?(address: any): void; | |
} | |
export interface Address { | |
city?: string; | |
state?: string; | |
zip?: string; | |
formattedAddress?: string; | |
latitude?: number; | |
longitude?: number; | |
} | |
interface GooglePlaceState { | |
formattedAddress?: string; | |
predictions?: any[]; | |
errorText?: string; | |
showUserAddress?: boolean; | |
selectedAddress?: any; | |
} | |
let googleMap: any; | |
export class GooglePlace extends Component<GooglePlaceProps, GooglePlaceState> { | |
public static propTypes: ValidationMap<GooglePlaceProps> = { | |
id: PropTypes.string.isRequired, | |
proposals: PropTypes.array, | |
errorText: PropTypes.string, | |
labelText: PropTypes.string, | |
onChange: PropTypes.func, | |
onError: PropTypes.func, | |
onlyCoords: PropTypes.bool, | |
latitude: PropTypes.number, | |
longitude: PropTypes.number, | |
}; | |
public state: GooglePlaceState = { | |
formattedAddress: "", | |
predictions: [], | |
errorText: "", | |
showUserAddress: false, | |
selectedAddress: {} | |
}; | |
private autocompleteService: any; | |
private placesService: any; | |
private geoQuery: GeoQuery = { | |
types: ["geocode"], | |
componentRestrictions: { country: "us" }, | |
bounds: null | |
}; | |
private style = { | |
fontFamily: "'Open Sans'", | |
fontSize: "16px", | |
}; | |
constructor(props: GooglePlaceProps) { | |
super(props); | |
if (__CLIENT__) { | |
googleMap = new google.maps.Map(document.createElement("div")); | |
/** | |
* https://developers.google.com/maps/documentation/javascript/places-autocomplete | |
* https://developers.google.com/maps/documentation/javascript/reference#AutocompleteService | |
* @type {google.maps.places.AutocompleteService} | |
*/ | |
this.autocompleteService = new google.maps.places.AutocompleteService(); | |
this.placesService = new google.maps.places.PlacesService(googleMap); | |
const { latitude, longitude } = props; | |
this.getAddressFromCoordinates({ latitude, longitude }); | |
} | |
} | |
public componentWillReceiveProps(nextProps: GooglePlaceProps) { | |
if (nextProps.errorText) { | |
this.setState({ errorText: nextProps.errorText }); | |
} | |
const { latitude, longitude } = nextProps; | |
if (latitude !== this.props.latitude && longitude !== this.props.longitude) { | |
this.getAddressFromCoordinates({ latitude, longitude }); | |
} | |
} | |
public render() { | |
const { labelText } = this.props; | |
return ( | |
<div className="dropdown"> | |
<TextField | |
id={ this.props.id } | |
value={ this.state.formattedAddress } | |
hintText="Street, City, State" | |
floatingLabelText={ labelText ? labelText : null } | |
errorText={ this.state.errorText } | |
fullWidth={ true } | |
onChange={ this.onChange } | |
onFocus={ this.onFocus } | |
onBlur={ this.onBlur } | |
style={ this.style } | |
floatingLabelStyle={ floatingLabelStyle } | |
/> | |
<ul className="dropdown__body"> | |
{ this.state.predictions.map((addr: any, idx: number) => ( | |
<li | |
key={ idx } | |
className="dropdown__item" | |
title={ addr.description } | |
onMouseDown={ this.selectAddress.bind(this, addr) } | |
> | |
{ addr.description } | |
</li> | |
))} | |
{ this.renderProposals() } | |
</ul> | |
</div> | |
); | |
} | |
private renderProposals = () => { | |
const { proposals = [] } = this.props; | |
const { showUserAddress } = this.state; | |
if (showUserAddress && proposals.length) { | |
return proposals.map((addr: UserAddress, idx: number) => ( | |
<li | |
key={ idx } | |
title={ addr.formattedAddress } | |
className="dropdown__item" | |
onMouseDown={ this.selectProposal.bind(this, idx) } | |
> | |
{ addr.formattedAddress } | |
</li> | |
)); | |
} else { | |
return null; | |
} | |
}; | |
private onChange = (e: FormEvent) => { | |
const el = (e.target as HTMLInputElement); | |
this.setState({ | |
formattedAddress: el.value.substr(0, 100), | |
showUserAddress: false | |
}); | |
if (!el.value.length) { | |
return this.setState({ | |
predictions: [], | |
errorText: "" | |
}); | |
} | |
const cb = (predictions: any[], status: string) => { | |
if (status === google.maps.places.PlacesServiceStatus.OK) { | |
this.setState({ predictions }); | |
} | |
}; | |
this.autocompleteService.getPlacePredictions(Object.assign({ input: el.value }, this.geoQuery), cb); | |
}; | |
private selectAddress = (addr: any) => { | |
const { onlyCoords = false } = this.props; | |
if (includes(addr.types, onlyCoords ? "geocode" : "street_address")) { | |
this.setState({ | |
formattedAddress: addr.description, | |
errorText: "", | |
predictions: [] | |
}); | |
const cb = (place: any, status: string) => { | |
if (status === google.maps.places.PlacesServiceStatus.OK) { | |
this.notify(getCannyAddress(place)); | |
this.setState({ selectedAddress: getCannyAddress(place) }); | |
} | |
}; | |
this.placesService.getDetails({ placeId: addr.place_id }, cb); | |
} else { | |
if (!onlyCoords) { | |
this.setState({ | |
formattedAddress: addr.description, | |
errorText: "Please, enter a house number", | |
predictions: [] | |
}); | |
if (this.props.onError) { | |
this.props.onError(addr); | |
} | |
} | |
} | |
}; | |
private selectProposal = (idx: number) => { | |
const addr = this.props.proposals[idx]; | |
this.setState({ | |
formattedAddress: addr.formattedAddress, | |
selectedAddress: addr, | |
showUserAddress: false, | |
errorText: "" | |
}); | |
this.notify(addr); | |
}; | |
private onFocus = (e: FormEvent) => { | |
const el = (e.target as HTMLInputElement); | |
setTimeout(() => { | |
if (el.select) { | |
el.select(); | |
el.setSelectionRange(0, 9999); | |
} | |
}, 0); | |
this.setState({ showUserAddress: true }); | |
}; | |
private onBlur = () => { | |
const { selectedAddress: { formattedAddress = "" } } = this.state; | |
this.setState({ showUserAddress: false }); | |
if (!this.state.formattedAddress.length) { | |
this.setState({ formattedAddress }); | |
} | |
}; | |
private handleGeocode = (results: any, status: any) => { | |
if (status === google.maps.GeocoderStatus.OK) { | |
if (results.length) { | |
const [address] = results; | |
this.setState({ | |
selectedAddress: getCannyAddress(address), | |
formattedAddress: address.formatted_address | |
}); | |
} else { | |
console.log("No results found"); | |
} | |
} else { | |
console.log(`Geocoder failed due to: ${status}`); | |
} | |
}; | |
private getAddressFromCoordinates = ({ latitude = 0, longitude = 0 }: { latitude: number, longitude: number }) => { | |
if (typeof google !== null) { | |
const geocoder = new google.maps.Geocoder(); | |
geocoder.geocode( | |
{ latLng: new google.maps.LatLng(latitude, longitude) }, | |
this.handleGeocode | |
); | |
const circle = new google.maps.Circle({ | |
center: { | |
lat: latitude, | |
lng: longitude | |
}, | |
radius: 50 | |
}); | |
this.geoQuery.bounds = circle.getBounds(); | |
} | |
}; | |
private notify = (addr: any) => { | |
if (isFunction(this.props.onChange)) { | |
this.props.onChange(addr); | |
} else { | |
console.log(addr); | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment