Skip to content

Commit

Permalink
feat(SearchBox): Support for Google Places API search box
Browse files Browse the repository at this point in the history
* Original commit: ab72761
* Original author: @eyebraus
* Closes #110
  • Loading branch information
eyebraus authored and tomchentw committed Sep 8, 2015
1 parent a615dc2 commit b06f5d6
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 3 deletions.
12 changes: 12 additions & 0 deletions examples/gh-pages/scripts/ReactRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ const DROPDOWN_ACTIONS = [
},
},
},
false,
{
key: "places__search-box",
displayName: "Adding a Places search box",
path: "#places/search-box",
component: {
componentClass: require("./components/places/SearchBox"),
componentRaw: {
__raw: require("!raw-loader!./components/places/SearchBox"),
},
}
},
];

const RIGHT_ACTIONS = [
Expand Down
100 changes: 100 additions & 0 deletions examples/gh-pages/scripts/components/places/SearchBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {default as React, Component} from "react";

import {default as GoogleMap} from "../../../../../src/GoogleMap";
import {default as Marker} from "../../../../../src/Marker";
import {default as SearchBox} from "../../../../../src/SearchBox";

/*
* https://developers.google.com/maps/documentation/javascript/examples/places-searchbox
*
* Add <script src="https://maps.googleapis.com/maps/api/js"></script> to your HTML to provide google.maps reference
*/
export default class SearchBoxExample extends Component {

static inputStyle = {
"border": "1px solid transparent",
"borderRadius": "1px",
"boxShadow": "0 2px 6px rgba(0, 0, 0, 0.3)",
"boxSizing": "border-box",
"MozBoxSizing": "border-box",
"fontSize": "14px",
"height": "32px",
"marginTop": "27px",
"outline": "none",
"padding": "0 12px",
"textOverflow": "ellipses",
"width": "400px"
}

static mapCenter = {
lat: 47.6205588,
lng: -122.3212725
}

state = {
bounds: null,
center: SearchBoxExample.mapCenter,
markers: []
}

_handle_bounds_changed = () => {
this.setState({
bounds: this.refs.map.getBounds(),
center: this.refs.map.getCenter()
});
}

_handle_places_changed = () => {
const places = this.refs.searchBox.getPlaces();
const markers = [];

// Add a marker for each place returned from search bar
places.forEach(function (place) {
markers.push({
position: place.geometry.location
});
});

// Set markers; set map center to first search result
const mapCenter = markers.length > 0 ? markers[0].position : this.state.center;

this.setState({
center: mapCenter,
markers: markers
});

return;
}

render () {

return (
<GoogleMap
center={this.state.center}
containerProps={{
...this.props,
style: {
height: "100%"
}
}}
defaultZoom={15}
onBoundsChanged={this._handle_bounds_changed}
ref="map">

<SearchBox
bounds={this.state.bounds}
controlPosition={google.maps.ControlPosition.TOP_LEFT}
onPlacesChanged={this._handle_places_changed}
ref="searchBox"
style={SearchBoxExample.inputStyle} />

{this.state.markers.map((marker, index) => (
<Marker position={marker.position} key={index} />
))}

</GoogleMap>
);

}

}
2 changes: 1 addition & 1 deletion examples/gh-pages/scripts/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class ReactHtml extends Component {
<head>
<title>React Google Maps | tomchentw</title>
{this._render_link_to_stylesheet_(clientAssets)}
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing" />
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places" />
<script type="text/javascript" src="prism.min.js" />
</head>
<body>
Expand Down
74 changes: 74 additions & 0 deletions src/SearchBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
default as React,
Component,
findDOMNode
} from "react";

import {
default as SearchBoxCreator,
searchBoxDefaultPropTypes,
searchBoxControlledPropTypes,
searchBoxEventPropTypes
} from "./creators/SearchBoxCreator";

/*
* Original author: @eyebraus
* Original PR: https://github.com/tomchentw/react-google-maps/pull/110
*/
export default class SearchBox extends Component {
static propTypes = {
// Uncontrolled default[props] - used only in componentDidMount
...searchBoxDefaultPropTypes,
// Controlled [props] - used in componentDidMount/componentDidUpdate
...searchBoxControlledPropTypes,
// Event [onEventName]
...searchBoxEventPropTypes,
}

// Public APIs
//
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox
//
// [].map.call($0.querySelectorAll("tr>td>code"), function(it){ return it.textContent; }).filter(function(it){ return it.match(/^get/) && !it.match(/Map$/); })
getBounds () { return this.state.searchBox.getBounds(); }

getPlaces () { return this.state.searchBox.getPlaces(); }
// END - Public APIs
//
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox

state = {}

componentDidMount () {
const {mapHolderRef, classes, style, ...searchBoxProps} = this.props;

// Cannot create input via component - Google Maps will mess with React's internal state by detaching/attaching.
// Allow developers to style the "hidden element" via inputClasses.
let domEl = document.createElement("input");
domEl.className = classes;
domEl.type = "text";

for (var propKey in style) {
if (style.hasOwnProperty(propKey)) {
domEl.style[propKey] = style[propKey];
}
}

const searchBox = SearchBoxCreator._createSearchBox(domEl, searchBoxProps);

this.setState({
inputElement: domEl,
searchBox: searchBox,
});
}

render () {
const {mapHolderRef, controlPosition} = this.props;

return this.state.searchBox ? (
<SearchBoxCreator controlPosition={controlPosition} inputElement={this.state.inputElement} mapHolderRef={mapHolderRef} searchBox={this.state.searchBox} {...this.props}>
{this.props.children}
</SearchBoxCreator>
) : (<noscript />);
}
}
84 changes: 84 additions & 0 deletions src/creators/SearchBoxCreator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
default as React,
PropTypes,
Component
} from "react";

import {default as SearchBoxEventList} from "../eventLists/SearchBoxEventList";
import {default as eventHandlerCreator} from "../utils/eventHandlerCreator";
import {default as defaultPropsCreator} from "../utils/defaultPropsCreator";
import {default as composeOptions} from "../utils/composeOptions";
import {default as componentLifecycleDecorator} from "../utils/componentLifecycleDecorator";

import {default as GoogleMapHolder} from "./GoogleMapHolder";

export const searchBoxControlledPropTypes = {
bounds: PropTypes.any,
};

export const searchBoxDefaultPropTypes = defaultPropsCreator(searchBoxControlledPropTypes);

const searchBoxUpdaters = {
bounds (bounds, component) { component.getSearchBox().setBounds(bounds); },
};

const {eventPropTypes, registerEvents} = eventHandlerCreator(SearchBoxEventList);

export const searchBoxEventPropTypes = eventPropTypes;

@componentLifecycleDecorator({
registerEvents,
instanceMethodName: "getSearchBox",
updaters: searchBoxUpdaters,
})
export default class SearchBoxCreator extends Component {

static propTypes = {
mapHolderRef: PropTypes.instanceOf(GoogleMapHolder).isRequired,
searchBox: PropTypes.object.isRequired,
}

static _createSearchBox (inputElement, searchBoxProps) {
const searchBox = new google.maps.places.SearchBox(inputElement, composeOptions(searchBoxProps, [
"bounds",
]));

return searchBox;
}

componentDidMount () {
this._mountComponentToMap(this.props.controlPosition);
}

componentDidUpdate (prevProps) {
if (this.props.controlPosition !== prevProps.controlPosition) {
this._unmountComponentFromMap(prevProps.controlPosition);
this._mountComponentToMap(this.props.controlPosition);
}
}

componentWillUnmount () {
this._unmountComponentFromMap(this.props.controlPosition);
}

_mountComponentToMap (controlPosition) {
const {mapHolderRef, inputElement} = this.props;

mapHolderRef.getMap().controls[controlPosition].push(inputElement);
}

_unmountComponentFromMap (controlPosition) {
const {mapHolderRef, inputElement} = this.props;

const index = mapHolderRef.getMap().controls[controlPosition].getArray().indexOf(inputElement);
mapHolderRef.getMap().controls[controlPosition].removeAt(index);
}

getSearchBox () {
return this.props.searchBox;
}

render () {
return (<noscript />);
}
}
5 changes: 5 additions & 0 deletions src/eventLists/SearchBoxEventList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// https://developers.google.com/maps/documentation/javascript/3.exp/reference#SearchBox
// [].map.call($0.querySelectorAll("tr>td>code"), function(it){ return it.textContent; })
export default [
"places_changed",
];
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export {default as OverlayView} from "./OverlayView";
export {default as Polygon} from "./Polygon";
export {default as Polyline} from "./Polyline";
export {default as Rectangle} from "./Rectangle";
export {default as SearchBox} from "./SearchBox";
22 changes: 20 additions & 2 deletions src/utils/componentLifecycleDecorator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,30 @@ export default function componentLifecycleDecorator ({registerEvents, instanceMe
this._unregisterEvents = null;
}

function noop () {}

// Stash component's own lifecycle methods to be invoked later
const componentDidMount = Component.prototype.hasOwnProperty("componentDidMount") ? Component.prototype.componentDidMount : noop;
const componentDidUpdate = Component.prototype.hasOwnProperty("componentDidUpdate") ? Component.prototype.componentDidUpdate : noop;
const componentWillUnmount = Component.prototype.hasOwnProperty("componentWillUnmount") ? Component.prototype.componentWillUnmount : noop;

Object.defineProperty(Component.prototype, "componentDidMount", {
enumerable: false,
configurable: true,
writable: true,
value: register,
value: function () {
// Hook into client's implementation, if it has any
componentDidMount.call(this);

register.call(this);
},
});

Object.defineProperty(Component.prototype, "componentDidUpdate", {
enumerable: false,
configurable: true,
writable: true,
value () {
value (prevProps) {
unregister.call(this);

for (const name in updaters) {
Expand All @@ -34,6 +46,9 @@ export default function componentLifecycleDecorator ({registerEvents, instanceMe
}
}

// Hook into client's implementation, if it has any
componentDidUpdate.call(this, prevProps);

register.call(this);
},
});
Expand All @@ -43,6 +58,9 @@ export default function componentLifecycleDecorator ({registerEvents, instanceMe
configurable: true,
writable: true,
value () {
// Hook into client's implementation, if it has any
componentWillUnmount.call(this);

unregister.call(this);
const instance = this[instanceMethodName]();
if ("setMap" in instance) {
Expand Down

0 comments on commit b06f5d6

Please sign in to comment.