Render React component as a custom marker icon in react-leaflet with working events/interaction
B

1

1
    const IconGen = ({ incomingData }) => {
     
      const [dataMap, setDataMap] = useState({});
      const dataMapTemp = {}; 
      incomingData.BandData.forEach((each) => {
        dataMapTemp[each.AntennaX].list.push(each);
        dataMapTemp[each.AntennaX].angle= each.angle;
        dataMapTemp[each.AntennaX].sId = each.SID;
          });
      if (!dataMap[1]) {
 
        setDataMap(dataMapTemp);
       
      }

      return (
         <div class="container_Sector"  >
             <div class="circle" style={{backgroundColor : "#c31605"></div> 
                 <div 
              id = {dataMap && dataMap[1]?.sId } 
              
              style={{
                rotate: `${dataMap[1]?.angle}deg`,
              }}
              onClick={() => {
                alert("You clicked the sector!");
              }
              }
            >
              {dataMap[1]?.list.map((each) => generateSvg(each))}
            </div>
                 
            <div
              style={{
                rotate: `${dataMap[2]?.angle}deg`,
              }}
            >
              {dataMap[2]?.list.map((each) => generateSvg(each))}
            </div>
            <div 
              style={{
                rotate: `${dataMap[3]?.angle}deg`,
              }}
            >
              {dataMap[3]?.list.map((each) => generateSvg(each))}
            </div>
          </div>
        
      );
    };
    export default IconGen;

    //Parent Component

    <MapContainer>
       <Marker
               key={data.SiteID}
               position={[data.Latitude, data.Longitude]}
               icon = <IconGen
                incomingData={data}
     
              />
               
             >
     
             </Marker>
    </Mapcontainer>

I am able to render custom icon using icon={L.divIcon({ className: "custom icon", html: ReactDOMServer.renderToString( <MyComponent/> ) })}.

However the onClick within the custom icon component does not trigger. onClick is not working due to rendering the MyComponent using ReactDOMServer.renderToString.

I need the onClick event inside the custom component to function correctly.

Bili answered 25/1 at 18:5 Comment(6)
Why are you using ReactDOMServer.renderToString()? What is the variable L? If you render to string the react runtime is not controlling that component and so any interactions defined within that component will not work, as you are seeing.Handgun
L is leaflet. I need customized marker in the Map. So i have designed separate component with svg images. My single marker designed with 3 images. Onclick of 1 image i have open a dialog/popup in Map. To render my customozed icon in html format , i have used ReactDOMServer.renderToString().Bili
And i tried with react-leaflet-enhanced-marker to render Marker using static React Component. <Map center={this.state.center} zoom={this.state.zoom}> <TileLayer /> <Marker icon={<MyComponent/>} position={this.state.center} /> </Map>Bili
So the marker has different clickable elements inside? Its not practically possible this way. The problem is, by design, leaflet restricts the icon to static HTML. The renderToString() trick (which ` react-leaflet-enhanced-marker` also uses under the hood) will allow for a HTML string to be produced from a react component -- but crucially, it will not be a fully interactible component. The react runtime is not managing that content or even aware of it beyond the initial call to renderToString. However, please share the code of this icon. It may be possible to workaround this.Handgun
Give me some time to put an answer together, I have a working solutionHandgun
i have added the snippetBili
H
4

I have now published this solution as a library at @adamscybot/react-leaflet-component-marker.

The reason that the onClick handler does not work is that renderToString means the component isn't truly mounted, in the sense that React is not aware of it as an ongoing concern. Running renderToString reduces the component to whatever the DOM looks like for that component on its first render, and nothing else will change that from the point renderToString is called.

The base problem here is that the react-leaflet library doesn't support this out of the box. However, we could get around this by:

  1. Using L.divIcon to render a dummy div as the icon containing nothing. We will assign a unique ID for that div for each marker, which will come in handy later.
  2. Using the Marker components add event to detect when the icon has actually been rendered to the DOM.
  3. When this happens, render the real React component inside the dummy marker by using a React portal that targets the div with the aforementioned unique ID.
  4. Additionally detect when the marker is removed and remove the portal.

We can encapsulate this behaviour in an EnhancedMarker component, for ease of use.

Here is a working CodeSandbox of the proof of concept. In this proof of concept, I am rendering two buttons as a marker, each with click events that work.

The below includes the generic code that can be applied to any situation:

import React, { useState, useId, useMemo } from "react";
import { createPortal } from "react-dom";
import { MapContainer, TileLayer, Marker } from "react-leaflet";

// `EnhancedMarker` has the same API as `Marker`, apart from the `icon` can be a React component.
const EnhancedMarker = ({
  eventHandlers,
  icon: providedIcon,
  ...otherProps
}) => {
  const [markerRendered, setMarkerRendered] = useState(false);
  const id = "marker-" + useId();

  const icon = useMemo(
    () =>
      L.divIcon({
        html: `<div id="${id}"></div>`,
      }),
    [id]
  );

  return (
    <>
      <Marker
        {...otherProps}
        eventHandlers={{
          ...eventHandlers,
          add: (...args) => {
            setMarkerRendered(true);
            if (eventHandlers?.add) eventHandlers.add(...args);
          },
          remove: (...args) => {
            setMarkerRendered(false);
            if (eventHandlers?.remove) eventHandlers.remove(...args);
          },
        }}
        icon={icon}
      />
      {markerRendered &&
        createPortal(providedIcon, document.getElementById(id))}
    </>
  );
};

const MarkerIconExample = () => {
  return (
    <>
      <button onClick={() => console.log("button 1 clicked")}>Button 1</button>
      <button onClick={() => console.log("button 2 clicked")}>Button 2</button>
    </>
  );
};

const CENTER = [51.505, -0.091];
const ZOOM = 13;
const App = () => {
  return (
    <MapContainer center={CENTER} zoom={ZOOM}>
      <TileLayer
        attribution='&amp;copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
      />
      <EnhancedMarker position={CENTER} icon={<MarkerIconExample />} />
    </MapContainer>
  );
};


For your example, you should be able to:

  1. Copy in the new EnhancedMarker component.
  2. Change existing usages of <Marker> in your use case to <EnhancedMarker>.
  3. Simply use <IconGen /> in the <EnhancedMarker> icon prop.
Handgun answered 25/1 at 21:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.