Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SearchBox): Support for Google Places API search box #110

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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