6
votes

There's a new react-leaflet version coming which can be found here: npm install react-leaflet/next docs: https://react-leaflet-v3.now.sh/docs/start-introduction

which is a complete re write using hooks.

Based on these new hooks i'm trying to write a custom layer control, which i mainly just want to split into groups and render my react components from material-ui to keep the map styling inline with the rest of my application. The hooks from my understanding get used like the folliwng:

basic control example

const MyControl = Control.extend({
  onAdd: map => {
    let container = DomUtil.create('div');
    container.innerHTML += 'My Control';

    return container;
  },
  // this one is optional
  onRemove: map => {},
});

const useControlElement = createElementHook(
  function createControl(_ref, ctx) {
    const context = useLeafletContext();
    var instance = new MyControl(position);
    return {
      instance: instance,
      context: Object.assign({}, ctx, {
        control: instance,
      }),
    };
  },
  function updateControl(control, props, prevProps) {},
);

const useControl = createControlHook(useControlElement);

const FinalControl = createContainerComponent(useControl);

function ControlComponent({ children }) {
  const controlRef = useRef();
  // if ref exits portal children through, note: using react scripts to support ?
   if(ref.?current?.getContainer()){
     ReactDOM.createPortal(children, ref.current.getContainer())
   }
  return <FinalControl ref={controlRef} />;
}

export default ControlComponent;

However the children are never rendered because controls are only added on componentDidMount.

I want to write this layer control in React/JSX/JS i don't want to have to completely build it using html i.e. L.Control, and using leaflets domutil as this would't work with my react styling anyway and provide restricted access to my redux store. So i'm trying to just portal my component through a basic control like how react-leaflet-control works

Am i approaching this in the right way? i'm just going around in circles atm trying to get this to work in a good way. I'm nearly at the point where i'm just going to render a react component as a child of the map and set the position to absolute (which i know is the wrong way, but does work)

i.e.

<MapContainer>
  <ReactComponent />
</MapContiner>

The documentation has an example for custom components but that's only for leaflet component that already exists, i.e. the square example in the "core architecture" section. Any help appreciated!

3

3 Answers

3
votes

Thanks to Zac's answer I could do it in this way.

Dependencies versions:

"react-leaflet": "^3.0.5",
"leaflet": "^1.7.1",

Here is a component that extends Leaflet control. You can use options to customize the component with received props.

//MyComponent.jsx
import React from 'react';
import {useLeafletContext} from '@react-leaflet/core';
import L from 'leaflet';

export const MyComponent = (props) => {
  const context = useLeafletContext();
  const control = L.control.Extend({
    //...options    
  });

  React.useEffect(() => {
    const container = context.layerContainer || context.map;
    container.addControl(control);

    return () => {
      container.removeControl(control);
    };
  });

  return null;
};

And here is the custom map component where you can use the component created before.

//MyMapComponent.jsx
import React from 'react';
import {MapContainer, TileLayer} from 'react-leaflet';
import {MyComponent} from './MyComponent';

export const MyMapComponent = (props) => {
  return (
    <MapContainer>
      <TileLayer url="whateverLayerURL" />
      <MyComponent {...props} />
    </MapContainer>
  );
};
3
votes

This can be simplified even further using createControlComponent hook

import { createControlComponent } from "@react-leaflet/core";
import { Control, DomUtil } from "leaflet";

Control.Watermark = Control.extend({
  onAdd: function (map) {
    const img = DomUtil.create("img");
    img.src = "./logo.png";
    img.style.width = "200px";
    return img;
  },

  onRemove: function (map) {},
});

export const WatermarkControl = createControlComponent(
  (props) => new Control.Watermark(props)
);
2
votes

I updated my answer. Not sure if that helps you, but hopefully it can provide you a hint.

I mocked up it from the a plain Leaflet example from here, which added a control to the left bottom of the map.

import { MapContainer, TileLayer } from 'react-leaflet';
import { useLeafletContext } from '@react-leaflet/core'
import L from 'leaflet';
import {useEffect} from 'react'

function CustomControl(props) {
    const context = useLeafletContext()

    L.Control.Watermark = L.Control.extend({
        onAdd: function (map) {
            var img = L.DomUtil.create('img');

            img.src = './logo.png';
            img.style.width = '200px';

            return img;
        },

        onRemove: function (map) {
            // Nothing to do here
        }
    });

    L.control.watermark = function (opts) {
        return new L.Control.Watermark(opts);
    }

    useEffect(() => {
        const container = context.layerContainer || context.map

        const control = L.control.watermark({ position: props.position })
        container.addControl(control)

        return () => {
            container.removeControl(control)
        }
    })

    return null
}


function MapCompenent() {

    return (
        <MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={true} className='map'>
            <TileLayer
                attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
           
            {/* the custom control */}
            <CustomControl position="bottomleft"></CustomControl>
        </MapContainer>
    )
}

Demo Screenshot