Skip to content

Instantly share code, notes, and snippets.

@jaxxreal
Created December 8, 2016 13:11
Show Gist options
  • Save jaxxreal/1f75269d7cd492f514e2853bac424cdd to your computer and use it in GitHub Desktop.
Save jaxxreal/1f75269d7cd492f514e2853bac424cdd to your computer and use it in GitHub Desktop.
React component for Google Places Autocomplete
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