Combining google-maps-react with react-router - javascript

So I'm rendering a bunch of <Marker /> that are giving by google-maps-react
The problem is that it don't seems like google-maps-react likes it when I add a <Link /> that comes from react-router-dom
This is how I put it together :
render() {
return (
<div>
<Map
google={this.props.google}
zoom={14}
styles={this.props.mapStyles}
disableDefaultUI={true}
onClick={this.saveCoords}
>
{this.state.data.map(m => {
return (
<Link to={`/c/contribution/${m.id}`}>
<Marker position={{ lat: m.x, lng: m.y }} title={m.title} />
</Link>
)
})}
</Map>
</div>
)
}
I tried using window.location instead but this reloads the page and I don't want that.
I get this error with the code above, it don't really makes sense for me :
Warning: React does not recognize the `mapCenter` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `mapcenter` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
With this I try to achieve a clickable <Marker/> that will render another element. This specific elemend can be accessed by going to the Route present in the code example.
The route used :
<Route path='/c/contribution/:id' component={Contribution} />

Unfortunately, you cannot wrap markers with google-maps-react like that because of how the map's children are rendered. I didn't look into that too deep, so why exactly it works the way it does is unclear to me.
With google-maps-react I think the way to go is to attach click handlers, but that makes working with react-router a bit more complicated.
For maybe an easier path, you could try the google-map-react package. It works a bit differently, but makes it easy to render almost anything on the map.

You can use the onClick event on the markers for the redirect. Set up a state property for the path you want to redirect to, and if that property is not an empty string , instead of renering your normal component, return a <Redirect/> element.
constructor(props){
super(props)
this.state = {
...
redirectTo:"
};
}
this.setRedirect = (path)=>{
this.setState({
redirectTo:path
}
}
render(){
if(this.state.redirectTo !== "")
return <Redirect to={this.state.redirectTo}/>
else
return (
<div>
<Map
google={this.props.google}
zoom={14}
styles={this.props.mapStyles}
disableDefaultUI={true}
onClick={this.saveCoords}
>
{this.state.data.map(m => {
return (
<Marker onClick={()=>this.setRedirect(`/c/contribution/${m.id}`)} position={{ lat: m.x, lng: m.y }} title={m.title} />
)
})}
</Map>
</div>
)
}

Related

how to integrate google maps in react using package google-maps-react?

When I use google-maps-react to integrate maps, map loads correctly and even shows the correct markers but I am having some issues.
1- A styling issue normally map component overlays the content I think because it has position absolute but when I change its position to relative it does not show. I have placed it in a div everything above the div shows but it overlays the content below the div.
2- it is giving errors in the console (I am listing them below)
Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. Move data fetching code or side effects to componentDidUpdate.If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps.
Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to this.state directly or define a state = {}; class property with the desired state in the Wrapper component.
My mapComponent:
import React from "react";
import { Map, Marker, GoogleApiWrapper } from "google-maps-react";
export function MapContainer({ google, captains = [] }) {
const mapStyles = {
width: "85%",
height: "100%",
};
return (
<Map
google={google}
// containerStyle={{
// position: "absolute",
// width: "85%",
// height: "40%",
// }}
// style={mapStyles}
containerStyle={mapStyles}
initialCenter={{
lat: captains[0].location.coordinates[1],
lng: captains[0].location.coordinates[0],
}}
zoom={captains.length === 1 ? 18 : 13}
>
{captains.map((captain) => (
<Marker
key={captain._id}
position={{
lat: captain.location.coordinates[1],
lng: captain.location.coordinates[0],
}}
/>
))}
</Map>
);
}
export default GoogleApiWrapper({
apiKey: "#############################",
})(MapContainer);
Here is where I am using it:
import MapContainer from "../components/MapContainer";
function Captains({ captains }) {
return (
<div className=" h-screen overflow-auto">
<div className="pt-24 pl-20 flex flex-col">
<othercontent ..... />
<div className="visible h-1/3 w-10/12 mx-5">
<MapContainer captains={captains} />
</div>
<PaginationTable
.....
/>
</div>
</div>
</>
);
}

How to add "outside" event listener to Markers in Google Maps (react-google-maps/api)

I have a list of hikes stored in state and I rendered the location of those hikes as Markers on the Google Map Component like so:
{hikes.map(hike =>
<Marker
position={{lat: hike.coordinates.latitude, lng: hike.coordinates.longitude}}
icon = {
{ url:"https://static.thenounproject.com/png/29961-200.png",
scaledSize : new google.maps.Size(50,50)
}
}
onClick={()=>{console.log(hike.name)}}
/>
I also display the list of hikes and its other details in its own BusinessCard Component like so:
export const Businesses = (props)=>{
const {hikes, addToTrip} = props
return(<>
<div className="businessesColumn">
{hikes.map(hike=>(
<BusinessCard.../>
))}
When I hover over each of the BusinessCard components, I want the corresponding marker to animate "bounce." I tried manipulate google.maps.event.addListener but I think I was doing it wrong. I'm not sure if it can detect events outside of the GoogleMap component? Any ideas on how should I approach this problem?
Okay so I don't know exactly how your components are structured, but try adding state activeMarker inside the component where your Markers are located. Then pass down setActiveMarker as a prop to the Business component. And inside the Business component, pass down hike.coordinates.latitude, hike.coordinates.longitude and setActiveMarker as props to the BusinessCard components. Inside BusinessCard, add onHover={() => props.setActiveMarker({ lat: props.latitude, lng: props.longitude })}
Something like this...
Component where Markers are located
const [activeMarker, setActiveMarker] = useState(null)
return (
<>
<GoogleMap>
{hikes.map(hike => (
<Marker
position={{lat: hike.coordinates.latitude, lng: hike.coordinates.longitude}}
icon = {{
url:"https://static.thenounproject.com/png/29961-200.png",
scaledSize : new google.maps.Size(50,50)
}}
animation={
(hike.coordinates.latitude === activeMarker.lat
&& hike.coordinates.longitude === activeMarker.lng)
? google.maps.Animation.BOUNCE : undefined
}
/>
))}
</GoogleMap>
<Business setActiveMarker={setActiveMarker} />
</>
)
Business component
return(
<div className="businessesColumn">
{hikes.map(hike => (
<BusinessCard
latitude={hike.coordinates.latitude}
longitude={hike.coordinates.longitude}
setActiveMarker={props.setActiveMarker}
/>
))}
</div>
)
BusinessCard component
return (
<div
className="business-card"
onMouseEnter={() => props.setActiveMarker({ lat: props.latitude, lng: props.longitude })}
>
{/* Whatever */}
</div>
)

Why isn't this React iteration of images working?

I'm learning React and calling Dog API. I got it to work for rendering an image, but when I tried to render multiple images, it doesn't work. For context, the API call link is https://dog.ceo/api/breeds/image/random/5 where "/5" specifies the number of images. I'm pretty sure that the state is being set because when I put console.log instead of setting the state, it's giving me the JSON with the urls to the images. I'm getting back an error message of "Cannot read property 'map' of undefined".
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount() {
fetch("https://dog.ceo/api/breeds/image/random/5")
.then(response => response.json())
.then(json => this.setState({data: json}));
}
render() {
return (
<div>
{this.state.data.message.map((item, id) => (
<img src={item} key={id} alt="dog" />
))}
</div>
)
}
}
export default App;
this.state.data.message doesn't exist when the component first loads. Instead, you've set this.state.data to an empty array, then later replace it with an object. It's best to keep the types consistent.
The simplest change is probably just to set up this.state.data.message to be an empty array:
constructor(props) {
super(props);
this.state = {
data: {
message: []
},
}
}
From there, taking note of the asynchronous nature of the AJAX operation, you might also consider implementing a "loading state" for when there are no images to display. (Perhaps even a meaningful empty state for when the operation has completed and there are still no images? An error state? etc.)
Check if the data has been manipulated or not. If not yet set the state by the API call then there is nothing this.state.data.message.
Note that, the ?. sign is called Optional Chaining. Optional chaining is used to check if an object has it's key or not in the deep level & also the Optional Chaining does not have the IE support.
render() {
return (
<div>
{this.state.data?.message?.length > 0 && this.state.data.message.map((item, id) => (
<img src={item} key={id} alt="dog" />
))}
</div>
);
}
It happened because when the page initially rendered, the data state is just an empty array. You have to add ? as an optional chaining which basically 'ignore' the error when you are accessing a property of something undefined.
Your code should be something like this
render() {
return (
<div>
{this.state.data.message?.map((item, id) => (
<img src={item} key={id} alt="dog" />
))}
</div>
)
}
So when the data state is empty it would not render anything.
Another way to do it is to check whether the state has content with
render() {
return (
<div>
{this.state.data.message &&
this.state.data.map((item, id) => (
<img src={item} key={id} alt="dog" />
))}
</div>
)
This code will check whether the data.message is truthy and will render the component, otherwise it will render nothing. More info here
}

Efficient way to make multiple clickable areas on a static image component (in React)

I'm trying to figure out the best way of making clickable areas for a static image (which is a component). The goal is not to show users those clickable areas with any click effects. After users click the correct areas, the score will increment.
What I'm thinking to make divs that overlay the image. But there are many random objects that I need to consider. I might need to a lot of math (mainly ratio problems) for each div I'm making.
I'm also trying to use html map tag. So far, I don't see anything clickable. I don't get errors either.
What's the most efficient way of approaching this problem?
Here is my code:
//my static image component
import React from "react";
const BackgroundImage = ({ src, name }) => {
return (
<div>
<img className="backgroundImage" src={src} useMap={name} alt={name} />
</div>
);
};
export default BackgroundImage;
//the map components that I can pass in different values to make different clickable areas
import React from "react";
import BackgroundImage from "./BackgroundImage";
const Mapper = ({
src,
name,
target = "_self",
alt = "none",
title,
coords,
shape,
}) => {
return (
<div>
<BackgroundImage src={src} useMap={name} />
<map name={name}>
<area
target={target}
alt={alt}
title={title}
coords={coords}
shape={shape}
/>
</map>
</div>
);
};
export default Mapper;
//the component with game logic: upon clicking correct clickable areas, users will earn scores (i.e. 3 areas to click to get it right; scores ranging from 0/3 to 3/3) after 10 seconds passed, then it automatically move on to the next page
import React from "react";
import zoomcall from "../images/zoomcall.png";
import BackgroundImage from "./BackgroundImage";
import Timer from "./Timer";
import LeftArrow from "./LeftArrow";
import { Link } from "react-router-dom";
import CorrectClicks from "./CorrectClicks";
import Mapper from "./Mapper";
const ZoomCall1Countdown = () => {
return (
<div>
{/* <BackgroundImage src={zoomcall} usemap="zoomcall" /> */}
<Timer />
<CorrectClicks />
<Link to="/readingtheroom">
<LeftArrow />
</Link>
<Mapper
src={zoomcall}
name="zoomcall"
title="11"
coords="0,22,295,196"
shape="rect"
style={{ backgroundColor: "red", border: "5px,solid,red" }}
/>
</div>
);
};
export default ZoomCall1Countdown;
For Mapper, I'm thinking replicate this component to add more clickable areas. Right now, it's only one to try out.
Map should be perfect to add clickable areas on top of image.
<map> should be rendered before <BackgroundImage> producing HTML similar to the MDN docs.
const Mapper = ({
src,
name,
href = 'about:blank',
target = "_self",
alt = "none",
title,
coords,
shape,
}) => {
return (
<div>
<map name={name}>
<area
href={href} // URL to open when clicked
target={target}
alt={alt}
title={title}
coords={coords}
shape={shape}
/>
</map>
<BackgroundImage src={src} name={name} />
</div>
);
};
href prop was added for URL to open when area is clicked.
And also BackgroundImage should reference map by using attribute like usemap="#zoomcall":
const BackgroundImage = ({ src, name }) => {
return (
<div>
<img className="backgroundImage" src={src} usemap={`#${name}`} alt={name} />
</div>
);
};
P.S.
Alternatively SVG vector image can be used instead of img+map because it supports hyperlinks in it.
Read this article to learn how to add links to SVG.
Or use SVG editor that supports it.
For example this SVG with links was created using Google Drawings.

Remove Zoom control from map in react-leaflet

I am building a react-leaflet application, and I am trying to separate the zoom control from the map itself. The same question in a vanilla Leaflet context was asked here: Placing controls outside map container with Leaflet?. This is what I'm trying to accomplish within the react-leaflet framework. Here is the general outline of my project:
import UIWindow from './UIWindow'
import Map from './MapRL'
class App extends React.Component {
render () {
return (
<div className="App">
<Map />
<UIWindow />
</div>
)
}
}
export default App;
My map component looks like this:
import React from 'react'
import { Map as LeafletMap, TileLayer } from 'react-leaflet'
class Map extends React.Component {
render () {
return(
<LeafletMap
className="sidebar-map"
center={...}
zoom={...}
zoomControl={false}
id="mapId" >
<TileLayer
url="..."
attribution="...
/>
</LeafletMap>
)
}
}
export default Map;
Then my UIWindow looks like this:
class UIWindow extends React.Component {
render () {
return(
<div className="UIWindow">
<Header />
<ControlLayer />
</div>
)
}
}
And finally, my ControlLayer (where I want my ZoomControl to live) should look something like this:
class ControlLayer extends React.Component {
render () {
return (
<div className="ControlLayer">
<LeftSidebar />
<ZoomControl />
{this.props.infoPage && <InfoPage />}
</div>
)
}
}
Of course with this current code, putting ZoomControl in the ControlLayer throws an error: TypeError: Cannot read property '_zoom' of undefined, with some more detailed writeup of what's wrong, with all the references the Leaflet's internal code regarding the zoom functionality.
(DomUtil.removeClass(this._zoomInButton, className);, etc.)
I expected an error, because the ZoomControl is no longer a child of the <Map /> component, but rather a grandchild of the <Map />'s sibling. I know react-leaflet functions on its context provider and consumer, LeafletProvider and LeafletConsumer. When I try to call on my LeafletConsumer from within my <ControlLayer />, I get nothing back. For example:
<LeafletConsumer>
{context => console.log(context)}
</LeafletConsumer>
This logs an empty object. Clearly my LeafletConsumer from my ControlLayer is not properly hooked into the LeaflerProvider from my <Map />. Do I need to export the context from the Map somehow using LeafletProvider? I am a little new to React Context API, so this is not yet intuitive for me. (Most of the rest of the app will be using React Redux to manage state changes and communication between components. This is how I plan to hook up the contents of the sidebar to the map itself. My sidebar doesn't seem to have any problem with being totally disconnected from the <Map />).
How can I properly hook this ZoomControl up to my Map component?
UPDATE:
I tried capturing the context in my redux store, and then serving it to my externalized ZoomControl. Like so:
<LeafletConsumer>
{ context => {
this.props.captureContext(context)
}}
</LeafletConsumer>
This captures the context as part of my redux store. Then I use this as a value in a new context:
// ControlLayer.js
const MapContext = React.createContext()
<MapContext.Provider value={this.props.mapContext}>
<LeftSidebar />
<MapContext.Consumer>
{context => {
if (context){
console.log(ZoomControl);
}
}}
</MapContext.Consumer>
</MapContext.Provider>
Where this.props.mapContext is brought in from my redux matchStateToProps, and its exactly the context captured by the captureContext function.
Still, this is not working. My react dev tools show that the MapContent.Consumer is giving the exact same values as react-leaflet's inherent '' gives when the ZoomControl is within the Map component. But I still get the same error message. Very frustrated over here.
Here is the same approach without hooks:
the Provider should look like this:
class Provider extends Component {
state = { map: null };
setMap = map => {
this.setState({ map });
};
render() {
return (
<Context.Provider value={{ map: this.state.map, setMap: this.setMap }}>
{this.props.children}
</Context.Provider>
);
}
}
Leaflet component will be:
class Leaflet extends Component {
mapRef = createRef(null);
componentDidMount() {
const map = this.mapRef.current.leafletElement;
this.props.setMap(map);
}
render() {
return (
<Map
style={{ width: "80vw", height: "60vh" }}
ref={this.mapRef}
center={[50.63, 13.047]}
zoom={13}
zoomControl={false}
minZoom={3}
maxZoom={18}
>
<TileLayer
attribution='&copy OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?"
/>
</Map>
);
}
}
and now to access the setMap function to the compoenntDidMount you need to do the following:
export default props => (
<Context.Consumer>
{({ setMap }) => <Leaflet {...props} setMap={setMap} />}
</Context.Consumer>
);
For the rest take a look here: Demo
I am not sure how to achieve that using your approach where react-leaflet's wrapper ZoomControl is not a child of Map wrapper when you try to place it outside the Map wrapper.
However, for a small control like the ZoomControl, an easy solution would be to create a custom Zoom component, identical to the original, construct it easily using the native css style and after accessing the map element, invoke the zoom in and out methods respectively.
In the below example I use react-context to save the map element after the map loads:
useEffect(() => {
const map = mapRef.current.leafletElement;
setMap(map);
}, [mapRef, setMap]);
and then here use the map reference to make a custom Zoom component identical to the native (for css see the demo):
const Zoom = () => {
const { map } = useContext(Context);
const zoomIn = e => {
e.preventDefault();
map.setZoom(map.getZoom() + 1);
};
const zoomOut = e => {
e.preventDefault();
map.setZoom(map.getZoom() - 1);
};
return (
<div className="leaflet-bar">
<a
className="leaflet-control-zoom-in"
href="/"
title="Zoom in"
role="button"
aria-label="Zoom in"
onClick={zoomIn}
>
+
</a>
<a
className="leaflet-control-zoom-out"
href="/"
title="Zoom out"
role="button"
aria-label="Zoom out"
onClick={zoomOut}
>
−
</a>
</div>
);
};
and then place it wherever you like:
const App = () => {
return (
<Provider>
<div style={{ display: "flex", flexDirection: "row" }}>
<Leaflet />
<Zoom />
</div>
</Provider>
);
};
Demo

Categories

Resources