I am trying to implement google maps in my app using react-google-maps package. In map, I am showing multiple Marker and used MarkerCluster.
Until now I have no issue and easily implemented from the doc. But now I want to show InfoWindow if the marker is clicked.
So, I thought of making a function to get the click event and pass the markerId, so I can call the API and get the relevant data for that marker and then put it in infowindow in a tabular manner.
Now, the problem I am facing is:
1) Calling onToggleOpen from onMarkerClick
2) how to set data in infowindow object in onMarkerClick
All this problem I am facing is because i am using HOC i.e recompose.
I am used to Class implement but tried functional implement trying to make it purely stateless.
Reference link: https://tomchentw.github.io/react-google-maps/#infowindow
Following is my code:
import React, { Component } from "react";
import Header from "./Header.js";
import Sidebar from "./Sidebar.js";
import axios from "axios";
import imgmapcluster from "./pins/iconmapcluster.png";
import user from "./pins/user1copy.png";
import { compose, withProps, withHandlers } from "recompose";
import {
withScriptjs,
withGoogleMap,
GoogleMap,
Marker,
InfoWindow
} from "react-google-maps";
// const fetch = require("isomorphic-fetch");
const {
MarkerClusterer
} = require("react-google-maps/lib/components/addons/MarkerClusterer");
const MapWithAMarkerClusterer = compose(
withProps({
googleMapURL:
"https://maps.googleapis.com/maps/api/js?key=AIzaSyCHi5ryWgN1FcZI-Hmqw3AdxJQmpopYJGk&v=3.exp&libraries=geometry,drawing,places",
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `90vh` }} />,
mapElement: <div style={{ height: `100%` }} />
}),
withHandlers(
{
onMarkerClustererClick: () => markerClusterer => {
// console.log("markerCluster", markerClusterer);
const clickedMarkers = markerClusterer.getMarkers();
// console.log(`Current clicked markers length: ${clickedMarkers.length}`);
// console.log(clickedMarkers);
},
onMarkerClick: (props) => markerss => {
//calling api and setting info window object
props.isOpen=!props.isOpen //showing error
},
onToggleOpen: ({ isOpen }) => () => ({
isOpen: !isOpen
})
}
),
withScriptjs,
withGoogleMap
)(props => (
<GoogleMap
defaultZoom={5}
defaultCenter={{ lat: 22.845625996700075, lng: 78.9629 }}
>
<MarkerClusterer
onClick={props.onMarkerClustererClick}
averageCenter
styles={[
{
textColor: 'white',
url: imgmapcluster,
height: 68,
lineHeight: 3,
width: 70
}
]}
enableRetinaIcons
gridSize={50}
>
{props.markers.map((marker, index) => (
<Marker
key={index}
icon={user}
onClick={props.onMarkerClick.bind(props,marker)}
position={{ lat: marker.latitude, lng: marker.longitude }}
/>
))}
{props.isOpen && (
<InfoWindow
// position={{ lat: props.infowindow.lat, lng: props.infowindow.lng }}
onCloseClick={props.onToggleOpen}
>
<h4>hello</h4>
</InfoWindow>
)}
</MarkerClusterer>
</GoogleMap>
));
class DemoApp extends React.PureComponent {
componentWillMount() {
this.setState({ markers: [],isOpen:false,infowindow:{} });
}
componentDidMount() {
axios({
url: "http://staging.clarolabs.in:6067/farmerinfo/farmercoordinates",
method: "POST",
data: {
temp: "temp"
},
headers: {
"Content-Type": "application/json"
}
}).then(res => {
this.setState({ markers: res.data.data.list });
});
}
render() {
return <MapWithAMarkerClusterer markers={this.state.markers} isOpen={this.state.isOpen} InfoWindowobject={this.state.InfoWindowobject}/>;
}
}
In order to call a withHandler from another, you need to separate them within two handlers. Also you can make use of withStateHandler and store the infoWindow state
withStateHandlers(
{ infoWindow: null },
{
setInfoWindow: () => (value) => ({ infoWindow: value)
}
),
withHandlers({
onToggleOpen: ({ isOpen }) => () => ({
isOpen: !isOpen
})
}),
withHandlers(
{
onMarkerClustererClick: () => markerClusterer => {
// console.log("markerCluster", markerClusterer);
const clickedMarkers = markerClusterer.getMarkers();
// console.log(`Current clicked markers length: ${clickedMarkers.length}`);
// console.log(clickedMarkers);
},
onMarkerClick: (props) => markerss => {
const { setInfoWindow, onToggleOpen} = props;
//calling api and setting info window object
setInfoWindow({lat: res.lat, lng: res.lng}) // set infoWindow object here
onToggleOpen() // Toggle open state
}
}
),
You probably should use withStateHandlers to handle state. Also, you can write withHandlers more than once, so handlers inside following withHandlers will have access to other handlers.
compose(
// .....
withStateHandlers(
{ isOpen: false },
{
toggleOpen: (state) => () => ({ isOpen: !state.isOpen }),
// or
setOpen: () => (value) => ({ isOpen: value }),
}
),
withHandlers(
// .....
{
onMarkerClick: (props) => markerss => {
// .....
props.toggleOpen();
// .....
},
},
// .....
),
// .....
)
Related
I'm trying to add a similar feature (an interactive map with popups) to a react application I'm working on. I'm trying to change this into a function component with hooks but have been having some problems (specifically with handleResults and addMarker). I'm relatively new to coding, sorry if this seems really trivial.
import React, { Component } from 'react';
import ReactMapGl, {Marker, Popup} from 'react-map-gl'
import Geocoder from 'react-map-gl-geocoder'
import 'mapbox-gl/dist/mapbox-gl.css';
import 'react-map-gl-geocoder/dist/mapbox-gl-geocoder.css'
const mapboxToken = 'Your API Key'
class Map3 extends Component {
constructor() {
super()
this.state = {
viewport: {
width: '100vw',
height: '100vh',
latitude: 49.28,
longitude: -123.12,
zoom: 14
},
currMarker: null,
markers: [],
selectedMarker: null
}
this.handleViewportChange = this.handleViewportChange.bind(this)
this.handleGeocoderViewportChange = this.handleGeocoderViewportChange.bind(this)
this.handleResult = this.handleResult.bind(this)
this.addMarker = this.addMarker.bind(this)
this.handleClose = this.handleClose.bind(this)
this.handleMarkerClick = this.handleMarkerClick.bind(this)
}
mapRef = React.createRef()
handleViewportChange(viewport) {
this.setState(prevState => ({
viewport: {...prevState.viewport, ...viewport}
}))
}
handleGeocoderViewportChange(viewport) {
const geocoderDefaultOverrides = {transitionDuration: 1000}
return this.handleViewportChange({
...viewport,
...geocoderDefaultOverrides
})
}
handleResult (result) {
this.setState({
currMarker: {
name: result.result.place_name,
latitude: result.result.center[1],
longitude: result.result.center[0]
}
})
}
addMarker() {
const {currMarker} = this.state
this.setState(prevState => ({
markers: [...prevState.markers, currMarker],
currMarker: null
}))
}
handleMarkerClick(marker) {
this.setState({
selectedMarker: marker
})
}
handleClose = () => {
this.setState({
selectedMarker: null
})
}
render() {
const {viewport, markers, selectedMarker} = this.state
return (
<ReactMapGl
ref={this.mapRef}
{...viewport}
onViewportChange={viewport => this.setState({viewport})}
mapboxApiAccessToken={mapboxToken}
mapStyle="mapbox://styles/mapbox/streets-v10"
>
<button className="add-btn" onClick={this.addMarker}>Add</button>
{markers.map((marker, idx) => {
return (
<Marker
key={idx}
latitude={marker.latitude}
longitude={marker.longitude}
onClick={() => this.handleMarkerClick(marker)}
>
<img src="pin.png" alt="marker"/>
</Marker>
)
})
}
<Geocoder
onSelected={this.handleResult}
mapRef={this.mapRef}
placeholder="Search here!"
onViewportChange={this.handleGeocoderViewportChange}
onResult={this.handleResult}
mapboxApiAccessToken={mapboxToken}
position="top-right"
/>
{this.state.selectedMarker &&
<Popup
mapRef={this.mapRef}
latitude={selectedMarker.latitude}
longitude={selectedMarker.longitude}
closeButton={true}
closeOnClick={false}
onClose={this.handleClose}
>
<h3>{selectedMarker.name}</h3>
</Popup>
}
</ReactMapGl>
)
}
}
export default Map3;
I am using #react-google-maps/api and loading the script from cdn to my public directory index.html file.
I get InvalidValueError: setMap: not an instance of Map; and not an instance of StreetViewPanorama and the markers do not appear on my map. Weird thing is that it is totally random to get this error. Sometimes it works without any problem. That is what I do not understand in the first place.
import React, { useEffect } from 'react';
import { GoogleMap, Marker } from '#react-google-maps/api';
import mapStyles from './mapStyles';
//google map height 100
import './MapDisplay.css';
import { connect } from 'react-redux';
const mapContainerStyle = {
height: '100%',
width: '100%',
};
const options = {
styles: mapStyles,
disableDefaultUI: true,
zoomControl: true,
};
const MapDisplay = ({
userLocation,
mouseHoverIdR,
selectedInfoR,
searchedResults,
unAuthNoSearchResults,
selectedLocationInfoWindowS,
}) => {
const onClickMarker = (e) => {
selectedLocationInfoWindowS({
selectedInfoR: e,
type: 'infoWindowLocations',
});
};
const onClickMap = () => {
selectedLocationInfoWindowS({
type: '',
selectedInfoR: {
_id: '',
},
});
};
return (
<div id='map'>
<GoogleMap
id='map_canvas'
mapContainerStyle={mapContainerStyle}
zoom={12}
center={userLocation}
options={options}
onClick={() => onClickMap()}
>
{!unAuthNoSearchResults &&
searchedResults.map((searchedResult) => {
if (
mouseHoverIdR === searchedResult._id ||
selectedInfoR._id === searchedResult._id
) {
var a = window.google.maps.Animation.BOUNCE;
}
return (
<Marker
position={searchedResult.coordinates}
key={searchedResult._id}
clickable
onClick={() => onClickMarker(searchedResult)}
animation={a}
id={'hello'}
/>
);
})}
</GoogleMap>
</div>
);
};
How can I fix the error that I get when I try to display the marker on the map?
I am trying to show different locations of stores that exists in a database in the based on user search, but at first i need to center the map using user's current latitude and longitude.
I did set the lat and lng to 0 in the state and updated the state values when the component is mounted.
The problem is, the map component does not re-render when this state value is updated.
import React, { Component } from 'react'
import { Map, GoogleApiWrapper, InfoWindow, Marker } from 'google-maps-react';
import { GeneConsumer } from '../../store';
const mapStyles = {
width: '100% !important',
height: '100% !important'
};
class LocationMap extends Component {
constructor(props) {
super(props);
this.state = {
currentLatLng: {
lat: 0,
lng: 0
},
showingInfoWindow: false, //Hides or the shows the infoWindow
activeMarker: {}, //Shows the active marker upon click
selectedPlace: {} //Shows the infoWindow to the selected place upon a marker
}
}
componentDidMount() {
navigator.geolocation.getCurrentPosition(
(position) => {
this.setState({
currentLatLng: {
lat: position.coords.latitude,
lng: position.coords.longitude
}
});
},
(error) => {
this.setState({
currentLatLng: {
lat: 6.4494007,
lng: 3.4498444
}
});
}
)
}
render() {
console.log(this.state.currentLatLng.lat)
console.log(this.state.currentLatLng.lng)
return(
<GeneConsumer>
{value => {
const {locations} = value;
const displayMarkers = () => {
if(!locations){
return null;
}
return locations.map((location, index) => {
return <Marker key={index} id={index} position={{
lat: location.latitude,
lng: location.longitude
}}
onClick={() => onMarkerClick(location)}
name = {location.name}
/>
})
}
const onMarkerClick = (props, marker, e) =>
this.setState({
selectedPlace: props,
activeMarker: marker,
showingInfoWindow: true
});
const onClose = props => {
if (this.state.showingInfoWindow) {
this.setState({
showingInfoWindow: false,
activeMarker: null
});
}
};
return (
<Map
google={this.props.google}
zoom={10}
style={mapStyles}
initialCenter={{ lat: this.state.currentLatLng.lat, lng: this.state.currentLatLng.lng}}
>
{displayMarkers()}
<InfoWindow
marker={this.state.activeMarker}
visible={this.state.showingInfoWindow}
onClose={onClose()}
>
<div>
<h4>{this.state.selectedPlace.name}</h4>
</div>
</InfoWindow>
</Map>
);
}}
</GeneConsumer>
);
}
}
export default GoogleApiWrapper({
apiKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
})(LocationMap);
This is by design, updating initialCenter prop does not cause map to re-render:
initalCenter: Takes an object containing latitude and longitude
coordinates. Sets the maps center upon loading.
To re-center the map use center prop along with initialCenter prop. The following example demonstrates how to re-center the map on map click event:
class MapExample extends Component {
state = {
currentCenter: this.props.center //default center
};
handleMapClick = (ref, map, ev) => {
this.setState({ currentCenter: ev.latLng });
};
render() {
return (
<Map
google={this.props.google}
className={"map"}
initialCenter={this.props.center}
center={this.state.currentCenter}
zoom={this.props.zoom}
onClick={this.handleMapClick}
>
<Marker position={this.state.currentCenter} />
</Map>
);
}
}
export default GoogleApiWrapper({
apiKey: "--YOUR-KEY-GOES-HERE--"
})(MapExample);
Am trying to fetch nearby restaurants using google maps API with react-google-maps.
import React from 'react';
import { compose, withState, withProps, withStateHandlers, lifecycle } from 'recompose';
import { withScriptjs, withGoogleMap, GoogleMap, Marker, InfoWindow } from 'react-google-maps';
import dotenv from 'dotenv';
import { HOME_MAP } from './MapNavigationConstants';
import MapSearch from './MapSearch';
dotenv.config();
const MapWithInfoWindow = compose(
withProps({
googleMapURL: HOME_MAP,
loadingElement: <div style={{ height: `100%` }} />,
containerElement:<div style={{ height: `720px` }} />,
mapElement:<div style={{ height: `100%` }} />,
}),
withState('mapUrl', 'setMapUrl', ''),
withState('bounds', 'setBounds', null),
withState('center', 'setCenter', {
lat: 53.3498, lng: -6.2603
}),
withState('markers', 'setMarkers', [
{
position: {
lat: () => 53.3498,
lng: () => -6.2603
}
}
]),
withState('places', 'updatePlaces', ''),
withStateHandlers(() => ({
isOpen: false,
isExploreOn: false,
}), {
onToggleOpen: ({ isOpen }) => () => ({
isOpen: !isOpen,
})
}
),
lifecycle({
componentWillMount() {
const refs = {}
this.setState({
onMapMounted: ref => {
refs.map = ref;
},
onBoundsChanged: (bounds, center, markers) => {
this.props.setBounds(!bounds ? this.props.bounds : bounds);
this.props.setCenter(!center ? this.props.center : center);
this.props.setMarkers(!markers ? this.props.markers : markers);
},
fetchPlaces: () => {
this.props.setMapUrl('places');
const bounds = refs.map.getBounds();
const map = refs.map;
const service = new window.google.maps.places.PlacesService(map);
console.log(service);
const request = {
bounds,
type: ['restaurants', 'cafe','bars']
};
service.nearBySearch(request, (results, status) => {
if (status === window.google.maps.places.PlacesServiceStatus.OK) {
this.props.updatePlaces(results);
}
});
}
})
},
}),
withScriptjs,
withGoogleMap)((props) =>
<GoogleMap
ref={props.onMapMounted}
defaultCenter = {props.center}
defaultZoom = { 13 }
center={props.center}
bounds={props.bounds}
options={{gestureHandling: 'cooperative',
scrollwheel: false,
disableDefaultUI: true,
}}
bootstrapURLKeys={{libraries: props.mapUrl}}
onBoundsChanged={props.onBoundsChanged}
>
<MapSearch
onBoundsChanged={(bounds, center, markers) => props.onBoundsChanged(bounds, center, markers)}
fetchPlaces={props.fetchPlaces}
/>
{
props.markers && props.markers.length > 0 && props.markers.map((marker, index) => (
<Marker
key={index}
position={{ lat: marker.position.lat(), lng:marker.position.lng() }}
onClick={props.onToggleOpen}
>
{props.isOpen && <InfoWindow onCloseClick={props.onToggleOpen}>
{props.children}
</InfoWindow>}
</Marker>
))
}{
props.places && props.places.length > 0 && props.places.map((place, index) => (
<Marker
key={index}
position={{ lat: place.location.lat(), lng:place.location.lng() }}
onClick={props.onToggleOpen}
>
{props.isOpen && <InfoWindow onCloseClick={props.onToggleOpen}>
{props.children}
</InfoWindow>}
</Marker>
))
}
</GoogleMap>
)
export default MapWithInfoWindow;
here HOME_MAP = https://maps.googleapis.com/maps/api/js?key=${KEY}&v=3.exp&libraries=geometry,drawing,places
Inside fetchplaces method, new window.google.maps.places.PlacesService(map) always returns null and service.nearBySearch throws not a function error.
Please help.
There are at least two issues with your example
const map = refs.map;
const service = new window.google.maps.places.PlacesService(map);
^^^
map object here corresponds to the instance of Map component while google.maps.places.PlacesService class expects Google Maps object instead. In case of react-google-maps library PlacesService could be instantiated like this:
mapMounted(element) {
const mapObject = element.context[MAP];
const service = new google.maps.places.PlacesService(map);
//...
}
where
<GoogleMap
ref={this.mapMounted}
defaultZoom={this.props.zoom}
defaultCenter={this.props.center}/>
There is also a typo at line:
service.nearBySearch(request, (results, status) => {
^^^^^^^^^^^^
function should be renamed to nearbySearch
Here is a demo that demonstrates how to utilize Places Service with react-google-maps library.
I am building a Zillow-like map with real estate property points in page body, and filters in page header.
What I need is to center AND zoom react-google-maps map, when selecting Google place from external Google places filter. Currently I am able to center map on place select, but not zoom in (that is because i need to refetch data on every zoom in/out, so need to control zoom, see code below).
I tried to use fitBounds from here, but ref in my code is always undefined.
I have API that fetches points, based on filters, with params like this:
viewPort: {
northEast: { lat: 57.04834755670983, lng: 24.3255340332031 },
southWest: { lat: 56.83635114109457, lng: 23.93826596679685 }
},
priceMin: 0,
priceMax: 500,
propType: "RENT",
numRooms: 3,
...
In header section there is also address filter input, that has Google places autocomplete. When selected, Google provides info about place:
"geometry" : {
...
"viewport" : {
"northeast" : {
"lat" : 56.95662478029149,
"lng" : 24.2054917802915
},
"southwest" : {
"lat" : 56.9539268197085,
"lng" : 24.2027938197085
}
}
...
Here is my Map component:
import React, { Component } from 'react';
import { compose, withProps, withState, withHandlers } from "recompose";
import {
withGoogleMap,
GoogleMap,
Marker,
} from "react-google-maps";
import { MarkerClusterer } from "react-google-maps/lib/components/addons/MarkerClusterer";
const defaultMapOptions = {
fullscreenControl: false,
streetViewControl: false,
};
const PropertyMap = compose(
withProps({
loadingElement: <div>Loading...</div>,
containerElement: <div style={{ height: "500px" }} />,
mapElement: <div style={{ height: `100%` }} />,
}),
withState('zoom', 'onZoomChange', 11),
withHandlers(() => {
const refs = {
map: undefined,
}
return {
onMapMounted: () => ref => {
refs.map = ref
},
onZoomChanged: ({ onZoomChange, onMapBoundsChange }) => () => {
onZoomChange(refs.map.getZoom())
onMapBoundsChange(refs.map.getBounds());
},
onDragEnd: ({ onMapBoundsChange }) => () => {
onMapBoundsChange(refs.map.getBounds());
},
onMarkerClustererClick: () => (markerClusterer) => {
const clickedMarkers = markerClusterer.getMarkers()
console.log(`Current clicked markers length: ${clickedMarkers.length}`)
console.log(clickedMarkers)
}
}
}),
withGoogleMap
)(props =>
<GoogleMap
defaultCenter={props.defaultCenter ? props.defaultCenter : { lat: 56.9425, lng: 24.1319 }}
zoom={props.zoom}
ref={props.onMapMounted}
onZoomChanged={props.onZoomChanged}
onDragEnd={props.onDragEnd}
defaultOptions={defaultMapOptions}
>
<MarkerClusterer
onClick={props.onMarkerClustererClick}
averageCenter
enableRetinaIcons
gridSize={60}
key={props.mapUid}
>
{props.features !== null && props.features.map(feature => {
const coord = { lng: feature.geometry.coordinates[0], lat: feature.geometry.coordinates[1] };
return <Marker key={feature.id} position={coord} />;
})}
</MarkerClusterer>
</GoogleMap>
);
export default PropertyMap;
So I think I would like to make it possible to pass zoom param in props, like this:
<PropertyMap
mapUid={this.state.mapUid}
features={this.state.features}
key={this.state.defaultCenter ? `${this.state.defaultCenter.lat}` : '1'}
defaultCenter={this.state.defaultCenter}
defaultZoom={calculateZoomFromViewPort(this.state.viewPort)}
onMapBoundsChange={this.handleMapBoundsChange}
/>
This way I could calculateZoomFromViewPort(this.state.viewPort), but then I need to somehow set it to withState zoom default value.
So what I need is some how pass default zoom value to withState('zoom', 'onZoomChange', 11), (11 is hardcoded, currently). But I am new to recompose package, but is it heavily used in react-google-maps package examples.
If you have other ideas, how to zoom and center map based on viewport & center params from Google place, please share!
fitbounds() is the recommended way to achieve auto zoom. Remember refs always created after first render of the component. I have made changes in your code by adding fitBounds. There might be few syntax errors as I prefer to use react-google-maps like class based utility wrapper. It's easier to manage with redux and component lifecycles. Below is the latest code:
import React, { Component } from 'react';
import { compose, withProps, withState, withHandlers } from "recompose";
import {
withGoogleMap,
GoogleMap,
Marker,
} from "react-google-maps";
import { MarkerClusterer } from "react-google-maps/lib/components/addons/MarkerClusterer";
const defaultMapOptions = {
fullscreenControl: false,
streetViewControl: false,
};
const PropertyMap = compose(
withProps({
loadingElement: <div>Loading...</div>,
containerElement: <div style={{ height: "500px" }} />,
mapElement: <div style={{ height: `100%` }} />,
}),
withState('zoom', 'onZoomChange', 11),
withHandlers(() => {
const refs = {
map: undefined,
}
return {
onMarkersUpdate: ()=> () => {
if(refs.map){
const mapBounds = new google.maps.LatLngBounds();
//just add markers position into mapBounds
markers.forEach((marker)=>{
mapBounds.extend(marker.position)
});
this.refs.map.fitBounds(mapBbounds);
}
},
onZoomChanged: ({ onZoomChange, onMapBoundsChange }) => () => {
onZoomChange(refs.map.getZoom())
onMapBoundsChange(refs.map.getBounds());
},
onDragEnd: ({ onMapBoundsChange }) => () => {
onMapBoundsChange(refs.map.getBounds());
},
onMarkerClustererClick: () => (markerClusterer) => {
const clickedMarkers = markerClusterer.getMarkers()
console.log(`Current clicked markers length: ${clickedMarkers.length}`)
console.log(clickedMarkers)
}
}
}),
withGoogleMap
)(props =>
<GoogleMap
defaultCenter={props.defaultCenter ? props.defaultCenter : { lat: 56.9425, lng: 24.1319 }}
zoom={props.zoom}
ref='map'
onZoomChanged={props.onZoomChanged}
onDragEnd={props.onDragEnd}
defaultOptions={defaultMapOptions}
>
{props.onMarkersUpdate()}
<MarkerClusterer
onClick={props.onMarkerClustererClick}
averageCenter
enableRetinaIcons
gridSize={60}
key={props.mapUid}
>
{props.features !== null && props.features.map(feature => {
const coord = { lng: feature.geometry.coordinates[0], lat: feature.geometry.coordinates[1] };
return <Marker key={feature.id} position={coord} />;
})}
</MarkerClusterer>
</GoogleMap>
);
export default PropertyMap;
Please feel free to react out me. If you make a codesandbox for this. It will be easier to fix syntax errors. And Don't miss out fitbounds(). It makes things easier for us.