The animation prop in this Marker component rendering gets a marker to bounce if there's one and drop if there are multiple, using the marker's array:
<Marker
key={index}
position={{lat: marker.lat, lng: marker.lng}}
onClick={() => props.handleMarkerClick(marker)}
animation={array.length === 1 ? google.maps.Animation.BOUNCE : google.maps.Animation.DROP}
>
However, my goal is to set a 750ms timeout for the bounce (based on an answer to another SO question). I can't do this with the conditional ternary operator, so I made two attempts at creating a function for this, replacing the animation property in the Marker component with animation={array.length === 1 ? bounceMarker() : google.maps.Animation.DROP} and both had issues.
This one I thought would work, but using google.maps.Animation.BOUNCE; throws an ESLint error "Expected an assignment or function call and instead saw an expression."
const bounceMarker = () => {
google.maps.Animation.BOUNCE;
setTimeout(() => {
marker.setAnimation(null);
}, 750);
};
This function is based on Google's markers tutorial, and caused the error "marker.getAnimation is not a function":
const bounceMarker = () => {
if (marker.getAnimation() !== null) {
marker.setAnimation(null);
} else {
marker.setAnimation(google.maps.Animation.BOUNCE);
setTimeout(() => {
marker.setAnimation(null);
}, 750);
}
};
In case it's useful here's my full code from this file, with the two functions commented out:
// Specifies global variable to ESLint (bundled with create-react-app), circumventing no-undef rule. See https://eslint.org/docs/user-guide/configuring#specifying-globals
/* global google */
// This component's code is from react-google-maps implementation instructions https://tomchentw.github.io/react-google-maps/#installation
import React, { Component , Fragment } from 'react';
import { withScriptjs , withGoogleMap, GoogleMap, Marker, InfoWindow } from 'react-google-maps';
const MyMapComponent = withScriptjs(
withGoogleMap(props => (
<GoogleMap
zoom={props.zoom}
defaultCenter={{ lat: 47.6093, lng: -122.3309 }}
>
{/* If there are markers, filters all visible markers (creating new array) then maps over newly created array taking the marker and marker's array index as arguments, rendering each Marker component with the marker index set as the key and the marker's lat and long as the position */}
{props.markers &&
props.markers.filter(marker => marker.isVisible)
// "Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity ... When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort" https://reactjs.org/docs/lists-and-keys.html
.map((marker, index, array) => {
// const bounceMarker = () => {
// if (marker.getAnimation() !== null) {
// marker.setAnimation(null);
// } else {
// marker.setAnimation(google.maps.Animation.BOUNCE);
// setTimeout(() => {
// marker.setAnimation(null);
// }, 750);
// }
// };
// const bounceMarker = () => {
// google.maps.Animation.BOUNCE;
// setTimeout(() => {
// marker.setAnimation(null);
// }, 750);
// };
const venueInfo = props.venues.find(venue => venue.id === marker.id);
return (
<Marker
key={index}
position={{lat: marker.lat, lng: marker.lng}}
// Marker click event listener, defined in App component class
onClick={() => props.handleMarkerClick(marker)}
animation={array.length === 1 ? bounceMarker() : google.maps.Animation.DROP}
>
{/* Show marker's InfoWindow when its isOpen state is set to true (set in app.js) */}
{marker.isOpen &&
<InfoWindow>
{/* If a venueInfo is not falsey and: 1) if there's a name and bestPhoto property, return the venue photo and name; 2) if there's only a name property, display the name only; 3) if there's only a photo property, display the photo only. If neither are available and/or venueInfo is falsy display text indicating no info available. See SO question about multiple ternary operators https://stackoverflow.com/questions/7757549/multiple-ternary-operators */}
{venueInfo && venueInfo.name && venueInfo.bestPhoto ?
<Fragment>
<p>{venueInfo.name}</p>
<img src={`${venueInfo.bestPhoto.prefix}200x200${venueInfo.bestPhoto.suffix}`}
// Screen readers already announce as image; don't need the word "image", "photo", etc.
alt={"Venue"}
/>
</Fragment> : venueInfo && venueInfo.name ? <p>{venueInfo.name}</p> : venueInfo && venueInfo.bestPhoto ? <img src={`${venueInfo.bestPhoto.prefix}200x200${venueInfo.bestPhoto.suffix}`}
// Screen readers already announce as image; don't need the word "image", "photo", etc.
alt={"Venue"}
/> : <p>No info available</p>}
</InfoWindow>
}
</Marker>
);
})}
</GoogleMap>
))
);
export default class Map extends Component {
render() {
return (
<MyMapComponent
// This is making the this.setState passed into Map component (as its prop) inside App's component class's render method available to MyMapComponent, which is how props from this.setState are eventually included inside MyMapComponent class (such as zoom={props.zoom})
{...this.props}
isMarkerShown
googleMapURL="https://maps.googleapis.com/maps/api/js?v=3.exp&key=AIzaSyDjl8LxY7Edfulq6t_VDaQsYY4ymPjwN0w"
// CSS declarations are placed in double curly braces because attributes accept JS objects; this is how to include an object literal. See https://stackoverflow.com/questions/22671582/what-is-the-purpose-of-double-curly-braces-in-reacts-jsx-syntax
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `100%`, width: `75%` }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
);
}
}
You are getting this error
marker.getAnimation is not a function
since marker is not the actual google.maps.Marker object.
Instead of rendering markers and afterwards updating Marker.animation property, how about to render the marker with a timeout (basically the same way as it is demonstrated in Marker Animations With setTimeout() example)?
For that purpose it is proposed to introduce a state for keeping animated markers
class MarkersAnimation extends React.Component {
constructor(props) {
super(props);
this.state = {
markers: []
};
}
render() {
return (
<GoogleMap
defaultZoom={this.props.zoom}
defaultCenter={this.props.center}
>
{this.state.markers.map((place, i) => {
return (
<Marker
animation={google.maps.Animation.BOUNCE}
id={place.id}
key={place.id}
position={{ lat: place.lat, lng: place.lng }}
/>
);
})}
</GoogleMap>
);
}
componentDidMount() {
const { markers, delay } = this.props;
this.interval = setInterval(() => {
this.setState(prev => ({
markers: prev.markers.concat([markers.shift()])
}));
if (!markers.length) clearInterval(this.interval);
}, delay);
}
componentWillUnmount() {
clearInterval(this.interval);
}
}
Here is a demo
Related
I have a map component with two childs - GSearchVehicle & MapView - with a positions prop. On the zoomInVehicle function the positions always returns with a empty array, but on the renderCustomCluster function it returns full.
The zoomInVehicle is a function triggered by a custom onPress on GSearchVehicle, it returns the vehicle id that I want to use on the zoomInVehicle. The problem is the positions prop not returning filled.
This is bugging me 'cause positions is filled on all places of the GMap component, but only on that function it returns empty... and it's not a problem about waiting the positions request because it's already filled by the time you call that zoomInVehicle function.
What is the meaning of this behavior? Something with the scope of GSearchVehicle and MapView? I can't see why positions is empty only on that function.
const GMap: React.FC<GMapProps> = ({ positions, selectMarker, selectedMarker }) => {
const mapRef = useRef<MapViewProps | any>(null);
console.log(positions); // [{...}] <--- NOT EMPTY!
const zoomInVehicle = (vehicleId: number): void => {
console.log(vehicleId); // 15486
console.log(positions); // [] <--- EMPTY!
// ...
};
const renderCustomCluster = (cluster: any) => {
console.log(positions) // [{...}] <--- NOT EMPTY!
// ...
}
return (
<>
<GSearchVehicle
placeholder="Digite a placa ou descrição"
onPress={zoomInVehicle}
/>
<MapView
ref={mapRef}
style={{ flex: 1 }}
initialRegion={INITIAL_REGION}
provider={null}
showsCompass={false}
renderCluster={renderCustomCluster}
rotateEnabled={false}
onRegionChangeComplete={getRegionZoom}
>
<UrlTile urlTemplate={TILE_URL} shouldReplaceMapContent />
{/* Draw vehicle marker */}
{ positions.map((position) => (
<Marker
key={position.adhesion.id}
tracksViewChanges={false}
coordinate={position.coordinates}
onPress={() => selectMarker(position)}
>
<GVehicleMarker
key={position.adhesion.id}
position={position}
/>
</Marker>
))}
{/* ... */}
</MapView>
</>
);
};
I found the problem, on the GSearchVehicle component my memoized functions were missing the positions prop on it's dependencies.
const memoizedItems = useMemo(() => renderItem, [vehiclesGroup, positions]);
const memoizedHeader = useMemo(() => renderSectionHeader, [vehiclesGroup, positions]);
I have been using a library called #react-google-maps/api and I want to store the map center as react state and I want the user to be able to drag the map while the marker always stays at the center of the map (uber style location selection)
The Problem is when I call the onCenterChange of the component, it returns me undefined
and when after store the map instance (recieved on the onLoad callback) as react state. The map instance returns the exact same center everytime (I guess the state save is static)
<GoogleMap
id={id}
zoom={zoom}
center={center}
options={options}
mapContainerStyle={{ width, height }}
onLoad={m => {
if (!map) setMap(m);
}}
onCenterChanged={e => {
console.log(map);
if (map) {
console.log(parseCoords(map.getCenter()));
}
}}
>
{children}
</GoogleMap>
Indeed, onCenterChanged event does not accept any arguments in #react-google-maps/api:
onCenterChanged?: () => void;
Instead map instance could be retrieved via onLoad event handler and map center saved in state like this:
function Map(props) {
const mapRef = useRef(null);
const [position, setPosition] = useState({
lat: -25.0270548,
lng: 115.1824598
});
function handleLoad(map) {
mapRef.current = map;
}
function handleCenterChanged() {
if (!mapRef.current) return;
const newPos = mapRef.current.getCenter().toJSON();
setPosition(newPos);
}
return (
<GoogleMap
onLoad={handleLoad}
onCenterChanged={handleCenterChanged}
zoom={props.zoom}
center={props.center}
id="map"
>
</GoogleMap>
);
}
Here is a demo which demonstrates how to keep the marker centered of the map while dragging the map.
I am currently using react-google-maps and I am able to display the marker and InfoWindow correctly by clicking on the marker (from the map or from the list). But once I click on a marker to open, the previous InfoWindow stays open even when I select a new one.
I am able to close the InfoWindow if I click on the 'x' or marker from list. But I'm not sure how to close other InfoWindows automatically and only open the current marker's InfoWindow.
I have looked at several similar questions to get an idea: When displaying multiple markers on a map, how to open just one info window, when clicking on a marker?, React-Google-Map multiple Info window open
.
But I'm still having trouble with passing and checking for unique marker so that only the InfoWindow which matches the marker ID is displayed.
class MarkerInfoList extends Component {
constructor(props){
super(props);
this.state = {
isOpen: false
};
}
handleToggleOpen = (markerId) => {
this.setState({
isOpen: true
});
}
handleToggleClose = (markerId) => {
this.setState({
isOpen: false
});
}
render() {
const isOpen = this.state.isOpen;
return (
<div>
{this.state.isOpen ? (
<ul>
<li onClick={() => this.handleToggleClose()}>{this.props.venue}</li>
<Marker
key={this.props.index}
id={this.props.index}
position={{ lat: this.props.lat, lng: this.props.lng}}
defaultAnimation={google.maps.Animation.DROP}
onClick={() => this.handleToggleOpen(this.props.index)}>
<InfoWindow
id = {this.props.index}
onCloseClick={() => this.setState({isOpen: false})}>
<div key={this.props.index}>
<h4>{this.props.venue}</h4>
</div>
</InfoWindow>
</Marker>
</ul>
) : (
<ul>
<li onClick={() => this.handleToggleOpen()}>{this.props.venue}</li>
</ul>
)
}
</div>
)
}
}
export default MarkerInfoList;
I wanted to toggle individual windows but also needed to know if the locations were "active" on a parent component. So in the parent component I have state the with an active key. I am passing that down to the Map as activeKey component as well as the initial list with all the location data as locations.
<GoogleMap defaultZoom={16} defaultCenter={this.state.currentLocation}>
{this.props.locations.map((location, i) => (
<Marker
key={location.key}
position={{
lat: location.latitude,
lng: location.longitude
}}
onClick={() => {
this.props.toggleLocationsActive(location.key);
// this.setCurrentLocation(location.latitude, location.longitude);
}}
>
{location.key === this.props.activeKey && (
<InfoWindow onCloseClick={props.onToggleOpen}>
<div>
{location.orgName}
{location.orgName !== location.programName &&
"/" + location.programName}
</div>
</InfoWindow>
)}
</Marker>
))}
</GoogleMap>
In the parent component I had state = {activeKey: ""} and the following method:
toggleLocationsActive = locationKey => {
this.setState({
activeKey: locationKey
});
};
Both are are passed down in props:
<Map
activeKey={this.state.activeKey}
toggleLocationsActive={this.toggleLocationsActive}
/>
Please note that I if you don't require this state to exist in the parent component you could do this within the map component.
I have a React app in which I am using Leaflet through react-leaflet, both super useful libraries.
In this app, I have a group of coordinates that need to be rendered as follows:
When zoomed out, cluster the coordinates into Marker Clusters like so
When zoomed in, each Marker needs to have
A dynamic countdown timer under it
A dynamic SVG countdown clock around it like so
For the clustering, I am using the react-leaflet-markercluster plugin, which works great for showing static content.
But when I need to show any dynamic content within each marker, I have no option of sending in JSX, there's only provision for static HTML as can been seen from the example available here.
// Template for getting popup html MarkerClusterGroup
// IMPORTANT: that function returns string, not JSX
function getStringPopup(name) {
return (`
<div>
<b>Hello world!</b>
<p>I am a ${name} popup.</p>
</div>
`);
}
// that function returns Leaflet.Popup
function getLeafletPopup(name) {
return L.popup({ minWidth: 200, closeButton: false })
.setContent(`
<div>
<b>Hello world!</b>
<p>I am a ${name} popup.</p>
</div>
`);
}
Is there a way to handle this situation? How can I make a JSX marker instead of a static HTML marker?
PS: I have tried using ReactDOM.renderToStringalready, but it's an ugly hack and involves re-rendering the markers every time.
TIA!!
Here's a sample WebpackBin for you to play around with if you have a solution in mind
I now figured out some working code for rendering custom JSX as Marker.
It's a 95% copy of https://jahed.dev/2018/03/20/react-portals-and-leaflet/ and 5% inspiration from https://github.com/PaulLeCam/react-leaflet/blob/master/packages/react-leaflet/src/Marker.tsx
I'm sure some things can be optimized further.
import * as React from 'react';
import { createPortal } from "react-dom";
import { DivIcon, marker } from "leaflet";
import * as RL from "react-leaflet";
import { MapLayer } from "react-leaflet";
import { difference } from "lodash";
const CustomMarker = (RL as any).withLeaflet(class extends MapLayer<any> {
leafletElement: any;
contextValue: any;
createLeafletElement(props: any) {
const { map, layerContainer, position, ...rest } = props;
// when not providing className, the element's background is a white square
// when not providing iconSize, the element will be 12x12 pixels
const icon = new DivIcon({ ...rest, className: '', iconSize: undefined });
const el = marker(position, { icon: icon, ...rest });
this.contextValue = { ...props.leaflet, popupContainer: el };
return el;
}
updateLeafletElement(fromProps: any, toProps: any) {
const { position: fromPosition, zIndexOffset: fromZIndexOffset, opacity: fromOpacity, draggable: fromDraggable, className: fromClassName } = fromProps;
const { position: toPosition, zIndexOffset: toZIndexOffset, toOpacity, draggable: toDraggable, className: toClassName } = toProps;
if(toPosition !== fromPosition) {
this.leafletElement.setLatLng(toPosition);
}
if(toZIndexOffset !== fromZIndexOffset) {
this.leafletElement.setZIndexOffset(toZIndexOffset);
}
if(toOpacity !== fromOpacity) {
this.leafletElement.setOpacity(toOpacity);
}
if(toDraggable !== fromDraggable) {
if(toDraggable) {
this.leafletElement.dragging.enable();
} else {
this.leafletElement.dragging.disable();
}
}
if(toClassName !== fromClassName) {
const fromClasses = fromClassName.split(" ");
const toClasses = toClassName.split(" ");
this.leafletElement._icon.classList.remove(
...difference(fromClasses, toClasses)
);
this.leafletElement._icon.classList.add(
...difference(toClasses, fromClasses)
);
}
}
componentWillMount() {
if(super.componentWillMount) {
super.componentWillMount();
}
this.leafletElement = this.createLeafletElement(this.props);
this.leafletElement.on("add", () => this.forceUpdate());
}
componentDidUpdate(fromProps: any) {
this.updateLeafletElement(fromProps, this.props);
}
render() {
const { children } = this.props;
const container = this.leafletElement._icon;
if(!container) {
return null;
}
const portal = createPortal(children, container);
const LeafletProvider = (RL as any).LeafletProvider;
return children == null || portal == null || this.contextValue == null ? null : (
<LeafletProvider value={this.contextValue}>{portal}</LeafletProvider>
)
}
});
And then just use it in you component like this:
<Map ...>
<CustomMarker position={[50, 10]}>
<Tooltip>
tooltip
</Tooltip>
<Popup>
popup
</Popup>
<div style={{ backgroundColor: 'red' }} onClick={() => console.log("CLICK")}>
CUSTOM MARKER CONTENT
</div>
MORE CONTENT
</CustomMarker>
</Map>
If you don't use TypeScript. Just remove the as any and : any stuff.
EDIT: Something automatically sets width: 12px; and height: 12px;. I'm not yet sure, how to prevent this. Everything else seems to work fine!
EDIT2: Fixed it! Using iconSize: undefined
EDIT3: There's also this: https://github.com/OpenGov/react-leaflet-marker-layer
Haven't tested it, but the example code looks good.
Here is a 2023 answer based on react 18, with functional components, in typescript:
import React, { useState } from "react";
import { Marker, MarkerProps } from "react-leaflet";
import ReactDOM from "react-dom/client";
import L from "leaflet";
interface Props extends MarkerProps {
/**
* Options to pass to the react-lefalet L.divIcon that is used as the marker's custom icon
*/
iconOptions?: L.DivIconOptions;
}
/**
* React-leaflet marker that allows for fully interactive JSX in icon
*/
export const JSXMarker = React.forwardRef<L.Marker, Props>(
({ children, iconOptions, ...rest }, refInParent) => {
const [ref, setRef] = useState<L.Marker>();
const node = React.useMemo(
() => (ref ? ReactDOM.createRoot(ref.getElement()) : null),
[ref]
);
return (
<>
{React.useMemo(
() => (
<Marker
{...rest}
ref={(r) => {
setRef(r as L.Marker);
if (refInParent) {
// #ts-expect-error fowardref ts defs are tricky
refInParent.current = r;
}
}}
icon={L.divIcon(iconOptions)}
/>
),
[]
)}
{ref && node.render(children)}
</>
);
}
);
And how you would use it:
import React from "react";
import { MapContainer } from "react-leaflet";
import { Marker } from "./JSXMarker";
export const Map: React.FC = () => {
return (
<MapContainer>
<Marker
position={[20.27, -157]}
iconOptions={{
className: "jsx-marker",
iconSize: [100, 100],
iconAnchor: [50, 50]
}}
>
<div>
{/* Fully functional, interactive JSX goes here */}
</div>
</Marker>
</MapContainer>
);
};
Working codesandbox
What I wan't to do is to show the location picked from some mobile devices on the Map.
Data about the locations are there..
What I need here is to add Markers on the map depending on the data received from the server.
Assume I have set the location data ({Lat,Lang}) to the state markers
Then How can I add this to show in Map.
My Map Code is as follows!
import React, { Component } from 'react';
import GoogleMapReact from 'google-map-react';
const AnyReactComponent = ({ text }) => <div>{text}</div>;
class MyClass extends Component {
constructor(props){
super(props);
}
render() {
return (
<GoogleMapReact
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
style={{height: '300px'}}
>
<AnyReactComponent
lat={59.955413}
lng={30.337844}
text={'Google Map'}
/>
</GoogleMapReact>
);
}
}
MyClass.defaultProps = {
center: {lat: 59.95, lng: 30.33},
zoom: 11
};
export default MyClass;
This Code is from the answer Implementing google maps with react
Used npm package :- google-map-react
You may try:
import React, { Component } from 'react';
import GoogleMapReact from 'google-map-react';
const AnyReactComponent = ({ img_src }) => <div><img src={img_src} className="YOUR-CLASS-NAME" style={{}} /></div>;
class MyClass extends Component {
constructor(props){
super(props);
this.state = {
markers: [],
}
}
componentDidMount(){
// or you can set markers list somewhere else
// please also set your correct lat & lng
// you may only use 1 image for all markers, if then, remove the img_src attribute ^^
this.setState({
markers: [{lat: xxxx, lng: xxxx, img_src: 'YOUR-IMG-SRC'},{lat: xxxx, lng: xxxx, img_src: 'YOUR-IMG-SRC' },{lat: xxxx, lng: xxxx, img_src: 'YOUR-IMG-SRC'}],
});
}
render() {
return (
<GoogleMapReact
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
style={{height: '300px'}}
>
{this.state.markers.map((marker, i) =>{
return(
<AnyReactComponent
lat={marker.lat}
lng={marker.lng}
img_src={marker.img_src}
/>
)
})}
</GoogleMapReact>
);
}
}
MyClass.defaultProps = {
center: {lat: 59.95, lng: 30.33},
zoom: 11
};
If this has error, please show here too, then we can fix it later
===========
ADDED EXAMPLE FOR CLICK-EVENT ON MARKERS
markerClicked(marker) {
console.log("The marker that was clicked is", marker);
// you may do many things with the "marker" object, please see more on tutorial of the library's author:
// https://github.com/istarkov/google-map-react/blob/master/API.md#onchildclick-func
// Look at their examples and you may have some ideas, you can also have the hover effect on markers, but it's a bit more complicated I think
}
render() {
return (
<GoogleMapReact
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
style={{height: '300px'}}
>
{this.state.markers.map((marker, i) =>{
return(
<AnyReactComponent
lat={marker.lat}
lng={marker.lng}
img_src={marker.img_src}
onChildClick={this.markerClicked.bind(this, marker)}
/>
)
})}
</GoogleMapReact>
);
}
Once again, post here some errors if any ^^ !
Be careful. You said react-google-map but you are using google-map-react. Those are 2 different packages. Do not mix up their documentation.