I have 3 files:
1.
PolylineMeasure.jsx
import { MapControl, withLeaflet } from "react-leaflet";
import * as L from "leaflet";
class PolylineMeasure extends MapControl {
createLeafletElement() {
return L.control.polylineMeasure({
position: "topleft",
unit: "metres",
showBearings: true,
clearMeasurementsOnStop: false,
showClearControl: true,
showUnitControl: true,
});
}
componentDidMount() {
const { map } = this.props.leaflet;
const polylineMeasure = this.leafletElement;
polylineMeasure.addTo(map);
}
}
export default withLeaflet(PolylineMeasure);
Map.jsx
import { Map, TileLayer } from "react-leaflet";
import PolylineMeasure from "./PolylineMeasure";
import "leaflet/dist/leaflet.css";
import "leaflet/dist/leaflet.css";
import "leaflet.polylinemeasure/Leaflet.PolylineMeasure.css";
import "leaflet.polylinemeasure/Leaflet.PolylineMeasure";
const Leaflet = () => {
return (
<>
<Map
center={[52.11, 19.21]}
zoom={6}
scrollWheelZoom={true}
style={{ height: 600, width: "50%" }}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<PolylineMeasure />
</Map>
</>
);
};
export default Leaflet;
I'm using nextjs so I had to import without SSR.
home.js
import dynamic from "next/dynamic";
function HomePage() {
const Map = dynamic(() => import("../components/Map"), {
loading: () => <p>A map is loading</p>,
ssr: false,
});
return <Map />;
}
export default HomePage;
https://github.com/ppete2/Leaflet.PolylineMeasure
Using demos in link above, I was able to log an array of coorfinates like this:
{ ... }
polylineMeasure.addTo(map);
function debugevent() {
polylineMeasure._arrPolylines[0].arrowMarkers.map((el) => {
console.log(el._latlng);
});
}
map.on("polylinemeasure:toggle", debugevent);
How can I access these coordinates in nextjs (home.js file)?
How to render PolylineMeasure (Map.jsx file) already with coordinates by passing down an array as props?
So this is about 2 things: lifting up state, and capturing Leaflet.Polyline's internal events.
First, let's keep track of a state variable in Home.js, and pass its setter down into the map component:
function HomePage() {
const [pointarray, setPointarray] = useState()
const Map = dynamic(() => import("../components/Map"), {...})
return <Map setPointarray={setPointarray} />;
}
Now in Map, we need to get a reference to the underlying leaflet map so that we can attach some event handlers. You're using createLeafletElement and withLeaflet, so I assume you're using reat-leaflet version 2. (I recommend updating to v3 when you can).
const Leaflet = ({ setPointarray }) => {
const mapRef = React.useRef()
useEffect(() => {
if (mapRef && mapRef.current){
mapRef.current.leafletElement.on(
'polylinemeasure:finish',
currentLine => setPointarray(currentLine.getLatLngs())
)
}
}, [mapRef])
return (
<>
<Map
ref={mapRef}
...
>
<TileLayer ... />
<PolylineMeasure />
</Map>
</>
);
};
What happens here is that a ref is attached to your Map component, which references the underlying leaflet L.map instance. When that ref is ready, the code inside the useEffect if statement runs. It gets the map instance from mapRef.current.leafletElement, and attaches an event handler based on Leaflet.PolylineMeasure's events, specifically the event of when a drawing is complete. When that happens, it saves the drawn line to the state variable, which lives in the Home component.
There are a lot of variations on this, it just depends on what you're trying to do exactly. As far as feeding preexisting polyline coordinates down to PolylineMeasurer as props, I couldn't find any examples of that even with the vanilla leaflet PolylineMeasurer. I found a comment from the plugin author saying that "restoring of drawed measurements is not possible", which is essentially what we're talking about doing by passing props down to that component. I'm sure it can be done by digging into the source code and programmatically drawing a polyline, but I've run out of time, I'll try to revisit that later.
react-leaflet version 3 answer
As per request, here's how to do this with react-leaflet v3, while initializing the polylinemeasurer with data passed down as props.
Create custom react-leaflet v3 control
Creating custom components with react-leaflet is easier than ever. Take a look at createcontrolcomponent. If you're not used to reading these docs, it boils down to this: to create a custom control component, you need to make a function that returns the leaflet instance of the control you want to make. You feed that function to createcontrolcomponent, and that's it:
import { createControlComponent } from "#react-leaflet/core";
const createPolylineMeasurer = (props) => {
return L.control.polylineMeasure({ ...props });
};
const PolylineMeasurer = createControlComponent(createPolylineMeasurer);
export default PolylineMeasurer;
Altering the original plugin to seed data
However, in our case, we want to add some extra logic to pre-seed the PolylineMeasurer with some latlngs that we pass down as a prop. I put in a pull request to the original plugin to add a .seed method. However, in the case of react-leaflet, we need to be more careful than using the code I put there. A lot of the methods required to draw polylines are only available after the L.Control.PolylineMeasure has been added to the map. I spent probably way too much time trying to figure out where in the react/react-leaflet lifecyle to intercept the instance of the polylineMeasure after it had been added to the map, so my eventual solution was to alter the source code of Leaflet.PolylineMeasure.
In the onAdd method, after all the code has run, we add in this code, which says that if you use a seedData option, it will draw that seed data once the control is added to the map:
// inside L.Control.PolylineMeasure.onAdd:
onAdd: function(map) {
// ... all original Leaflet.PolylineMeasure code here ...
if (this.options.seedData) {
const { seedData } = this.options;
seedData.forEach((polyline) => {
// toggle draw state on:
this._toggleMeasure();
// start line with first point of each polyline
this._startLine(polyline[0]);
// add subsequent points:
polyline.forEach((point, ind) => {
const latLng = L.latLng(point);
this._mouseMove({ latLng });
this._currentLine.addPoint(latLng);
// on last point,
if (ind === polyline.length - 1) {
this._finishPolylinePath();
this._toggleMeasure();
}
});
});
}
return this._container;
}
This code programatically calls all the same events that would be called if a user turned on the control, clicked around, and drew their lines that way.
Tying it together
So now our <PolylineMeasurer /> component takes as its props the options that would be fed to L.control.polylineMeasure, in addition to a new optional prop called seedData which will cause the map to be rendered with that seedData:
const Map = () => {
return (
<MapContainer {...mapContainerProps}>
<TileLayer url={url} />
<PolylineMeasurer
position="topleft"
clearMeasurementsOnStop={false}
seedData={seedData}
/>
</MapContainer>
);
};
Working Codesandbox
Caveat
If by some other mechanism in your app the seedData changes, you cannot expect the PolylineMeasurer component to react in the same way that normal React components do. In create leaflet, this control is added to the map once with the options you feed it, and that's it. While some react-leaflet-v3 component factory functions come with an update paramter, createcontrolcomponent does not (i.e. its first argument is a function which creates a control instance, but it does not accept a second argument to potentially update the control instance like, say, createlayercomponent does).
That being said, you can apply a key prop to the PolylineMeasurer component, and if your seedData is changed somewhere else in the app, also change the key, and the PolylineMeasurer will be forced to rerender and draw your new data.
Related
Following the offical reference for Higher Level Component Factory to update props for a Control Component
The core APIs export other high-level component factories that can be
used in a similar way.
I've mimicked the example - but I get a syntax error for the following:
import L from "leaflet";
import "leaflet-routing-machine";
import { createControlComponent } from "#react-leaflet/core";
import 'leaflet-routing-machine/dist/leaflet-routing-machine.css'
function setWaypoints(props)
{
return {
waypoints: [
L.latLng(props.startLat, props.startLng),
L.latLng(props.endLat, props.endLng)
],
lineOptions: {
styles: [{ color: "#0500EE", weight: 4 }]
},
show: false,
addWaypoints: false,
routeWhileDragging: true,
draggableWaypoints: true,
fitSelectedRoutes: true,
showAlternatives: false,
createMarker: function() { return null; },
}
}
function createRoutingMachine(props, context)
{
const instance = new L.Routing.control(setWaypoints(props))
return
{
instance, context: { ...context, overlayContainer: instance }
}
}
function updateRoutingMachine(instance, props, prevProps)
{
if (props.endLat !== prevProps.endLat || props.endLng !== prevProps.endLng)
{
instance.setWaypoints(props)
}
}
const RoutingMachine = createControlComponent(createRoutingMachine, updateRoutingMachine)
export default RoutingMachine;
Missing semicolon. (35:25)
33 | return 34 | {
35 | instance, context: { ...context, overlayContainer: instance }
| ^ 36 | }
If I change this to:
function createRoutingMachine(props)
{
const instance = new L.Routing.control(setWaypoints(props))
return instance
}
The compiler is happy, but the component never updates.
I know I'm creating the Control Component incorrectly, but I can't find the information for the correct implementation.
Related:
How to use Leaflet Routing Machine with React-Leaflet 3?
How to extend TileLayer component in react-leaflet v3?
You will notice that in the docs, createcontrolcomponent lists only one argument, which is a function to create the instance. You are expecting it to behave like createlayercomponent, which takes two arguments. In createlayercomponent, the second argument is a function to update the layer component when the props change. However, createcontrolcomponent offers no such functionality. react-leaflet is assuming, much like vanilla leaflet, that once your control is added to the map, you won't need to alter it directly.
This gets a bit confusing in terms of leaflet-routing-machine, because you don't need to change the instance of the control, but rather you need to call a method on it which affects the map presentation.
IMO, the best way to go is to use a state variable to keep track of whether or not your waypoints have changed, and use a ref to access the underlying leaflet instance of the routing machine, and call setWayPoints on that:
// RoutineMachine.jsx
const createRoutineMachineLayer = (props) => {
const { waypoints } = props;
const instance = L.Routing.control({
waypoints,
...otherOptions
});
return instance;
};
// Takes only 1 argument:
const RoutingMachine = createControlComponent(createRoutineMachineLayer);
// Map.jsx
const Map = (props) => {
// create a ref
const rMachine = useRef();
// create some state variable, any state variable, to track changes
const [points, setPoints] = useState(true);
const pointsToUse = points ? points1 : points2;
// useEffect which responds to changes in waypoints state variable
useEffect(() => {
if (rMachine.current) {
rMachine.current.setWaypoints(pointsToUse);
}
}, [pointsToUse, rMachine]);
return (
<MapContainer {...props}>
<RoutineMachine ref={rMachine} waypoints={pointsToUse} />
<button onClick={() => setPoints(!points)}>
Toggle Points State and Props
</button>
</MapContainer>
);
};
Working Codesandbox
Bonus: a cheap and easy way to force a rerender on your <RoutineMachine> component (or any react component) is to assign it a key prop, and change that key prop when you want to rerender it. This might be a uuid, or even a unique set of waypoints ran through JSON.stringify. Just an idea.
I'm using the react-google-maps/api library, and I have an application where I need the user to edit a Polyline.
The problem I'm having is grabbing the path of the polyline after the user has finished editing.
If I use native react components, the path returned on the props from the polyline is the original path of the line - not the one edited by the user.
The code below is a cutdown version of where I'm trying to get the path of the line from the react component. If you try it and edit the line, the return array is the original path. I've seen some examples using the getPath() method, but I just can't seem to get this to work on the React component (ie polylineRef.current.getPath() returns a no function error.
How should I be getting the path information of the edited line?
import React, { Fragment, useRef } from "react";
import { GoogleMap, Polyline, useLoadScript } from "#react-google-maps/api";
const MapTest = (props) => {
const polylineRef = useRef();
const mapRef = useRef();
const mapContainerStyle = {
width: "80vw",
height: "80vh",
};
const showPath = () => {
console.log(polylineRef.current.props.path); //What should be here to show the edited path if its possible to access?
};
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.REACT_APP_GOOGLE_KEY,
});
const centre = { lat: 51.999889, lng: -0.98807 };
if (loadError) return "Error loading Google Map";
if (!isLoaded) return "Loading Maps....";
console.log(polylineRef.current.props.path);
return (
<Fragment>
<GoogleMap
mapContainerStyle={mapContainerStyle}
ref={mapRef}
zoom={10}
center={centre}
>
<Polyline
ref={polylineRef}
key={1}
path={[
{ lat: 51.9298274729133, lng: -1.0446431525421085 },
{ lat: 51.98483618577529, lng: -1.2423970587921085 },
]}
options={{ editable: true, strokeColor: "#ff0000" }}
/>
</GoogleMap>
<button
onClick={(event) => {
showPath(event);
}}
>
Show Path in Console
</button>
</Fragment>
);
};
export default MapTest;
If I use the native google API, then I can see the updated path, but I can't get a reference to the map created by the map to place the polyline onto.
If I can't access the edited path through the react component, how should I provide a reference to the google maps native API, so when I do
polyline = new google.maps.Polyline(//polyline options)
polyline.setMap(map) //Where do I get the handle for this map to put it on the map above?
/*I've tried using mapRef.current (not a map instance) and
mapRef.current.getInstance() - this makes the original map disappear, for reasons I don't understand*/
When I build this using the native API, I can access the edited path using the getPath() method, but I can't render this polyline on the component rendered above.
Other than building the map out of the native API I'm struggling to do this at the moment - but the benefits of the ease of rendering of React make me want to continue down this path for a while longer - is anyone able to help please?
I think this is what you are trying to achieve:
https://codesandbox.io/s/snowy-night-ony59?file=/src/App.js
My answer is based on:
https://codesandbox.io/s/reactgooglemapsapi-editing-a-polygon-popr2?file=/src/index.js:2601-2845
which I found by googling: react-google-maps-api editable polygon
Basically just copying and pasting the code referred by Daniele Cordano
import React, { useState, useRef, useCallback } from "react";
import ReactDOM from "react-dom";
import { LoadScript, GoogleMap, Polygon } from "#react-google-maps/api";
import "./styles.css";
// This example presents a way to handle editing a Polygon
// The objective is to get the new path on every editing event :
// - on dragging the whole Polygon
// - on moving one of the existing points (vertex)
// - on adding a new point by dragging an edge point (midway between two vertices)
// We achieve it by defining refs for the google maps API Polygon instances and listeners with `useRef`
// Then we bind those refs to the currents instances with the help of `onLoad`
// Then we get the new path value with the `onEdit` `useCallback` and pass it to `setPath`
// Finally we clean up the refs with `onUnmount`
function App() {
// Store Polygon path in state
const [path, setPath] = useState([
{ lat: 52.52549080781086, lng: 13.398118538856465 },
{ lat: 52.48578559055679, lng: 13.36653284549709 },
{ lat: 52.48871246221608, lng: 13.44618372440334 }
]);
// Define refs for Polygon instance and listeners
const polygonRef = useRef(null);
const listenersRef = useRef([]);
// Call setPath with new edited path
const onEdit = useCallback(() => {
if (polygonRef.current) {
const nextPath = polygonRef.current
.getPath()
.getArray()
.map(latLng => {
return { lat: latLng.lat(), lng: latLng.lng() };
});
setPath(nextPath);
}
}, [setPath]);
// Bind refs to current Polygon and listeners
const onLoad = useCallback(
polygon => {
polygonRef.current = polygon;
const path = polygon.getPath();
listenersRef.current.push(
path.addListener("set_at", onEdit),
path.addListener("insert_at", onEdit),
path.addListener("remove_at", onEdit)
);
},
[onEdit]
);
// Clean up refs
const onUnmount = useCallback(() => {
listenersRef.current.forEach(lis => lis.remove());
polygonRef.current = null;
}, []);
console.log("The path state is", path);
return (
<div className="App">
<LoadScript
id="script-loader"
googleMapsApiKey=""
language="en"
region="us"
>
<GoogleMap
mapContainerClassName="App-map"
center={{ lat: 52.52047739093263, lng: 13.36653284549709 }}
zoom={12}
version="weekly"
on
>
<Polygon
// Make the Polygon editable / draggable
editable
draggable
path={path}
// Event used when manipulating and adding points
onMouseUp={onEdit}
// Event used when dragging the whole Polygon
onDragEnd={onEdit}
onLoad={onLoad}
onUnmount={onUnmount}
/>
</GoogleMap>
</LoadScript>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
For some reason, the useRef hook doesn't accept the function getPath/getPaths() with the typescript error : TS2339: Property 'getPath' does not exist on type 'MutableRefObject'
const handleNewPolygonPath = useCallback(() => {
const newPath = polygonRef.getPath();
dispatch(setNewPolygonPath(newPath));
console.log(newPolygonPath);
}, [dispatch, newPolygonPath]);
I am using Tabulator under React, with the react-tabulator module.
I am missing something very basic, likely due to my new knowledge of React. Implementing this module, I know how to connect a tabulator callback to a javascript function. But I don't know how to call a tabulator method. For instance:
const options = {
height: '100%',
ajaxURL: 'http://example.com',
ajaxProgressiveLoad: 'scroll',
ajaxError: (error) => {
console.log('ajaxError ', error);
},
};
...
<ReactTabulator
columns={columns}
layout="fitColumns"
data={[]}
options={options}
/>
Here the ajaxError callback is passed to ReactTabulator, and called when appropriate.
Now, the tabulator module has lots of methods, for instance setData. Outside of React, this would be used as follows:
var table = new Tabulator("#example-table", {
ajaxURL:"http://www.getmydata.com/now", //ajax URL
});
...
table.setData("http://www.getmydata.com/now");
How do I translate this into the React world (in a hook environment), since I don't have direct access to the equivalent of a 'table' object? Do I need to get to my tabulator object by using getElementById or something similar?
I believe that the solution is to use a ref as described here:
https://reactjs.org/docs/refs-and-the-dom.html
, in the section:
Refs and Function Components
By default, you may not use the ref attribute on function components because they don’t have instances:
...
You can, however, use the ref attribute inside a function component as long as you refer to a DOM element or a class component:
Using this approach I am able to access my tabulator setData method.
const tableDisplay = (props) => {
const tableRef = createRef();
// I can now use setData in various effects:
// if ((tableRef.current) && (tableRef.current.table)) {
// tableRef.current.table.setData();
return (
<ReactTabulator
ref={tableRef}
columns={columns}
layout="fitColumns"
data={[]}
options={options}
/>
);
};
This works just fine, I am just not sure this is the "clean" way to go about it?
You need to fetch the api from useEffect and useState its result. Heres an example
import React,{ useState, useEffect }from "react"
import { ReactTabulator } from "react-tabulator"
const Dashboard = props => {
const [data,setData] = useState([])
const getData = async () => {
const result = await fetch("http://www.getmydata.com/now") // this is where you call the api and get its result
setState(result.data) // this is where you set the result to your state
}
useEffect(()=>{
getData()
},[])
const columns=[
{
title:"name",
field:"name",
},
{
title:"Number of Request"
field:"noOfRequest",
}
]
return (
<ReactTabulator
columns={columns}
layout="fitColumns"
data={data} // here is the state of the table
options={options}
/>
)
}
export default Dashboard
This is how you get data from an api in React.
I am rewriting an existing web app to a React app. In the original, leaflet is used for maps, along with OverlappingMarkerSpiderfier
to create a distinction between map markers. In the new app, I'm using react-leaflet. All the other plugins used are available to react, but I want to create a leaflet custom component for OverlappingMarkerSpiderfier. I am a bit out of my depth with creating this component.
Ideally, I'd like to call the component like this in the React render loop.
return (
<Map>
<Spiderfy >
<Marker>
<Popup>
</Popup>
</Marker>
<Marker>
<Popup>
</Popup>
</Marker>
</Spiderfy>
</Map>
)
The OverlappingMarkerSpiderfier example shows an instance of the object being created, an event listener is added to the map, and all the Markers are added to the OMS instance. I've tried squeezing this into a ReactLeaflet class but had no success so far.
class _Spiderfy extends MapLayer {
constructor(props) {
super(props)
this.oms = null
}
createLeafletElement ({children, leaflet: {map, ...props}}) {
let newLayer = L.featureGroup()
this.oms = new OverlappingMarkerSpiderfier(map);
var popup = new L.Popup();
this.oms.addListener('click', function(marker) {
popup.setContent(marker.desc);
popup.setLatLng(marker.getLatLng());
map.openPopup(popup);
});
this.oms.addListener('spiderfy', function(markers) {
map.closePopup();
});
return newLayer
}
}
export default withLeaflet(_Spiderfy);
This is the wrapper I currently have. It renders the children (all the markers and their popups), but the event listener doesn't execute when the map is clicked. I also don't know how to get the Marker objects from the children list and add them to the oms instance.
Does anyone have some advice to create this class or some other library which does something very similar?
You was close enough, here is an updated component version which demonstrates how to integrate OverlappingMarkerSpiderfier-Leaflet into React-Leaflet
class Spiderfy extends MapLayer {
createLeafletElement(props) {
const { map } = props.leaflet;
this.oms = this.createOverlappingMarkerSpiderfier(map);
const el = L.layerGroup();
this.contextValue = { ...props.leaflet, layerContainer: el };
return el;
}
componentDidMount() {
super.componentDidMount();
this.leafletElement.eachLayer(layer => {
if (layer instanceof L.Marker) {
this.oms.addMarker(layer);
}
});
}
createOverlappingMarkerSpiderfier(map) {
const oms = new window.OverlappingMarkerSpiderfier(map);
oms.addListener("spiderfy", markers => {
markers.forEach(m => m.closePopup())//force to close popup
if (this.props.onSpiderfy) this.props.onSpiderfy(markers);
});
oms.addListener("unspiderfy", markers => {
if (this.props.onUnspiderfy) this.props.onUnspiderfy(markers);
});
oms.addListener("click", marker => {
if (this.props.onClick) this.props.onClick(marker);
});
return oms;
}
}
export default withLeaflet(Spiderfy);
The list of missing parts:
initialization from markers list via OverlappingMarkerSpiderfier.addMarker method
event handlers implementation
And here is a demo for your reference
I am wondering if it is possible to render a react component within a mapboxgl.Popup(). Something like this:
componentDidMount() {
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`<div>${<MapPopup />}<p>${moreText}</p></div>`)
//.setDOMContent(`${<MapPopup />}`) ?????
.addTo(this.props.mapboxMap)
})
Or should this be done using ReactDOM.render?
ReactDOM.render(<MapPopup />, document.getElementById('root'))
This project will have buttons and inputs in the popup that connect to a redux store.
Thanks for any input!
This works:
addPopup(el: JSX.Element, lat: number, lng: number) {
const placeholder = document.createElement('div');
ReactDOM.render(el, placeholder);
const marker = new MapboxGl.Popup()
.setDOMContent(placeholder)
.setLngLat({lng: lng, lat: lat})
.addTo(map);
}
(Where I've used typescript to illustrate types, but you can just leave these out for pure js.) Use it as
addPopup(<h1>Losers of 1966 World Cup</h1>, 52.5, 13.4);
You can try to implement React component:
export const Popup = ({ children, latitude, longitude, ...mapboxPopupProps }) => {
// this is a mapbox map instance, you can pass it via props
const { map } = useContext(MapboxContext);
const popupRef = useRef();
useEffect(() => {
const popup = new MapboxPopup(mapboxPopupProps)
.setLngLat([longitude, latitude])
.setDOMContent(popupRef.current)
.addTo(map);
return popup.remove;
}, [children, mapboxPopupProps, longitude, latitude]);
return (
/**
* This component has to have 2 divs.
* Because if you remove outter div, React has some difficulties
* with unmounting this component.
* Also `display: none` is solving that map does not jump when hovering
* ¯\_(ツ)_/¯
*/
<div style={{ display: 'none' }}>
<div ref={popupRef}>
{children}
</div>
</div>
);
};
After some testing, I have realized that Popup component was not rendering properly on the map. And also unmounting the component was unsuccessful. That is why there are two divs in return. However, it may happen only in my environment.
See https://docs.mapbox.com/mapbox-gl-js/api/#popup for additional mapboxPopupProps
useEffect dependencies make sure that MapboxPopup gets re-created every time something of that list changes & cleaning up the previous popup instance with return popup.remove;
I've been battling with this as well. One solution I found was using ReactDOM.render(). I created an empty popup then use the container generated by mapboxgl to render my React component.
marker.setPopup(new mapboxgl.Popup({ offset: 18 }).setHTML(''));
markerEl.addEventListener('mouseenter', () => {
markerEl.classList.add('enlarge');
if (!marker.getPopup().isOpen()) {
marker.getPopup().addTo(this.getMap());
ReactDOM.render(
component,
document.querySelector('.mapboxgl-popup-content')
);
}
});
const mapCardNode = document.createElement("div");
mapCardNode.className = "css-class-name";
ReactDOM.render(
<YourReactPopupComponent / > ,
mapCardNode
);
//if you have a popup then we remove it from the map
if (popupMarker.current) popupMarker.current.remove();
popupBox.current = new mapboxgl.Popup({
closeOnClick: false,
anchor: "center",
maxWidth: "240px",
})
.setLngLat(coordinates)
.setDOMContent(mapCardNode)
.addTo(map);
I used MapBox GL's map and popup events (to improve upon #Jan Dockal solution) which seemed to improve reliability. Also, removed the extra div wrapper.
import { useWorldMap as useMap } from 'hooks/useWorldMap'
import mapboxgl from 'mapbox-gl'
import { FC, useRef, useEffect } from 'react'
export const Popup: FC<{
layerId: string
}> = ({ layerId, children }) => {
const map = useMap() // Uses React Context to get a mapboxgl map (could possibly be null)
const containerRef = useRef<HTMLDivElement>(null)
const popupRef = useRef<mapboxgl.Popup>()
const handleClick = (
e: mapboxgl.MapMouseEvent & {
features?: mapboxgl.MapboxGeoJSONFeature[] | undefined
} & mapboxgl.EventData
) => {
// Bail early if there is no map or container
if (!map || !containerRef.current) {
return
}
// Remove the previous popup if it exists (useful to prevent multiple popups)
if (popupRef.current) {
popupRef.current.remove()
popupRef.current = undefined
}
// Create the popup and add it to the world map
const popup = new mapboxgl.Popup()
.setLngLat(e.lngLat) // could also use the coordinates from a feature geometry if the source is in geojson format
.setDOMContent(containerRef.current)
.addTo(map)
// Keep track of the current popup
popupRef.current = popup
// Remove the tracked popup with the popup is closed
popup.on('close', () => {
popupRef.current = undefined
})
}
useEffect(() => {
if (map && layerId) {
// Listen for clicks on the specified layer
map?.on('click', layerId, handleClick)
// Clean up the event listener
return () => {
map?.off('click', layerId, handleClick)
popupRef.current?.remove()
popupRef.current = undefined
}
}
}, [map, layerId])
return <div ref={containerRef}>{children}</div>
}
Try to do with onClick event, instead of creating a button. After that put your react component in onClick events add event listener refrence link
[1]: https://stackoverflow.com/a/64182029/15570982