Nenad
Nenad

Reputation: 271

Need proper way to render jsx component inside Leaflet popup when using geojson pointToLayer function

Hi is there any way to pass jsx component to bindPopup function so I can push redux commands on button click?

 pointToLayer={(
      geoJsonPoint: Feature<Point, DeviceProperties>,
      latlng,
    ) => {
      const marker = L.marker(latlng);
      marker.setIcon(
        markerIcon({ variant: geoJsonPoint.properties.relation }),
      );

      const sddds = (
        <div className="font-quicksand">
          <h2>{geoJsonPoint.properties.id}</h2>
          <h2>{geoJsonPoint.properties.name}</h2>
          <p>{geoJsonPoint.properties.description}</p>
          <p>{geoJsonPoint.properties.ownerId}</p>
          <a
            onClick={() => {
              dispatch(setDevice(geoJsonPoint.properties));
            }}
          >
            Open device details
          </a>
        </div>
      );

      marker.bindPopup(renderToString(sddds));
      return marker;
    }}

I know I can use react leaflet component but that way I cant pass props into every marker options (I mean marker as layer).

Upvotes: 4

Views: 1686

Answers (2)

Nenad
Nenad

Reputation: 271

These are two solutions I have come to and want to share if someone is having same problem.

My problem with using leaflet-react Popup component is that it will not pass geojson properties to marker layer when I just map over geojson object because react-leaflet Marker does not have api for feature like geojson layer does and I need to access those properties via marker layers in other parts of map.

Solution 1:

Use ReactDOM.render() inside pointToLayer method, react will show warning about pure functions but it will work. You just shoud not render imported component because it will complain about store and redux provider, instead paste component code inside render. If you want to avoid warnings create another function / hook and render code inside its useEffect() to container (div or something).

Here is example:

const popup = L.popup();
const marker = L.marker(latlng);
const container = L.DomUtil.create('div');
render(
  <div>
    <h2>{props.id}</h2>
    <h2>{props.name}</h2>
    <p>{props.description}</p>
    <p>{props.ownerId}</p>
    <a onClick={() => dispatch(setDevice(geoJsonPoint.properties))}></a>
  </div>,
  container,
);
popup.setContent(container);
marker.bindPopup(popup);
return marker;

With custom hook / function:

const useRenderPopup = (props) => {
const container = L.DomUtil('div');
const dispatch = useAppDispatch()
useEffect(() => {
render(
  <div>
    <h2>{props.id}</h2>
    <h2>{props.name}</h2>
    <p>{props.description}</p>
    <p>{props.ownerId}</p>
    <a onClick={() => dispatch(setDevice(props.geoJsonPoint.properties))}></a>
  </div>,
  container,
);
},[])
return container;
}

and just call this function like popup.setContent(useRenderPopup(someprop)), this way there will be no warning.

Solution 2:

Render everything static with renderToString() and other stuff that need to trigger redux update attach event listeners.

const popup = L.popup();
const marker = L.marker(latlng);
const link = L.DomUtil.create('a');
const container = L.DomUtil.create('div');
const content = <DeviceSummary {...geoJsonPoint.properties} />;

marker.setIcon(markerIcon({ variant: geoJsonPoint.properties.relation }));

link.addEventListener('click', () =>
  dispatch(setDevice(geoJsonPoint.properties)),
);

link.innerHTML = 'Show device details';
container.innerHTML = renderToString(content);
container.appendChild(link);

popup.setContent(container);
marker.bindPopup(popup);
return marker;

Here DeviceSummary component is static so I render it as a string and later append link with redux callback added as event listener to it.

(both solutions except custom function example goes into pointToLatyer method inside geoJSON layer)

Upvotes: 5

Seth Lutske
Seth Lutske

Reputation: 10752

So this has been discussed a bit. There is an issue in the react-leaflet repo discussing this, whose conclusion is to simply use vanilla JS within the bindPopup method to create your popup. I don't like this solution at all, especially when you're trying to use very react oriented event handlers (like react-redux actions) from within a popup.

The question React-leaflet geojson onEachFeature popup with custom react component was asked, which you may have read, as you use react's renderToString method in your code. But as you've probably discovered, this does not maintain any interactivity or JS that your JSX may include. The answerer there came up with the idea of using a modal instead of a popup, but that doesn't exactly answer your question or truly using JSX in a popup based off of a point-layer geojson.

Ultimately, you will not be able to return JSX from the pointToLayer function that is interactive. I think this would be a nice feature that react-leaflet doesn't currently implement. Within the closure of the pointToLayer function, there's no good way to directly write fully functional JSX.

I played with this for a bit, trying to harness pointToLayer and save the feature of each iteration to state, and then render a Marker with Popup from that, but it got me thinking - why bother? Just ditch the GeoJSON component altogether and render your Markers and Popups directly from the JSON object. Like this:

{myGeoJson.features.map((feature, index) => {
  return (
    <Marker
      key={index}
      position={L.latLng(feature.geometry.coordinates.reverse())}
    >
      <Popup>
        <button
          onClick={() => { yourReduxAction() }}
        >
          Click meeee
        </button>
      </Popup>
    </Marker>
  );
})}

Working sandbox

In this way, you need to work a little harder by manually transforming your GeoJSON into Markers with Popups, but not nearly as hard as trying to bend over backwards by going from JSX (<GeoJSON />) to vanilla JS (pointToLayer) back to JSX (<Popup />).

Upvotes: 4

Related Questions