Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to get leaflet object of the map inside an extending class #2

Closed
ptondereau opened this issue Jul 28, 2017 · 4 comments
Closed

Comments

@ptondereau
Copy link

ptondereau commented Jul 28, 2017

how to get leaflet object of the map inside custom class?

Because I'm using https://github.com/YUzhva/react-leaflet-markercluster and I've forked it to use your component but it throws this error: Uncaught TypeError: Cannot read property '_container' of undefined.

It seems that LayerGroup can't access to the map container from the context.

Here is my code:

import React, {Children, cloneElement} from 'react';
import PropTypes from 'prop-types';
import {LayerGroup} from 'react-leaflet-universal';

let L;

export default class MarkerClusterGroup extends LayerGroup {

  constructor() {
    super();
    this.state = { loaded: false };
  }

  componentDidMount() {
	  L = require('leaflet');
	  require('leaflet.markercluster');
	  this.setState({ loaded: true }, () => {
		  // Override auto created leafletElement with L.markerClusterGroup element
		  this.leafletElement = L.markerClusterGroup(this.props.options);

		  console.log(this.context);

		  if (this.props.markers.length) {
			  this.addLayersWithMarkersFromProps(this.props.markers);
		  }

		  this.props.wrapperOptions.enableDefaultStyle && (
			  this.context.map._container.className += ' marker-cluster-styled'
		  );

		  !this.props.wrapperOptions.disableDefaultAnimation && (
			  this.context.map._container.className += ' marker-cluster-animated'
		  );

		  // Init listeners for markerClusterGroup leafletElement only once
		  this.initEventListeners(this.leafletElement);
    });
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.markers.length && !isArraysEqual(this.props.markers, nextProps.markers)) {
      // Remove layer from map with previously rendered clustered markers
      this.layerContainer.removeLayer(this.leafletElement);
      // Remove layers with markers from markerClusterGroup
      this.leafletElement.clearLayers();

      this.addLayersWithMarkersFromProps(nextProps.markers);
    }
  }

  removeMarkersWithSameCoordinates(markers) {
    // init filtered markers list with first marker from list
    let filteredMarkers = [markers[0]];

    markers.forEach((marker) => {
      if (!JSON.stringify(filteredMarkers).includes(JSON.stringify(marker))) {
        filteredMarkers.push(marker);
      }
    });

    return filteredMarkers;
  }

  addLayersWithMarkersFromProps(markers) {
    let markersOptions = this.props.markerOptions
      ? Object.assign({}, this.props.markerOptions)
      : {};

    let filteredMarkers = this.props.wrapperOptions.removeDuplicates
      ? this.removeMarkersWithSameCoordinates(markers)
      : markers;

    let leafletMarkers = [];

    filteredMarkers.forEach((marker) => {
      let currentMarkerOptions = marker.options
        ? Object.assign({}, marker.options)
        : null ;

      let leafletMarker = L.marker(
        [marker.lat, marker.lng],
        currentMarkerOptions || markersOptions
      );

      marker.popup && leafletMarker.bindPopup(marker.popup);
      marker.tooltip && leafletMarker.bindTooltip(marker.tooltip);

      leafletMarkers.push(leafletMarker);
    });

    // Add markers leafletElements to the markerClusterGroup
    this.leafletElement.addLayers(leafletMarkers);
    // Add clustered markers to the leaflet map
    !this.props.children && this.layerContainer.addLayer(this.leafletElement);
  }

  initEventListeners(markerClusterGroup) {
    this.props.onMarkerClick && (
      markerClusterGroup.on('click', (marker) => {
        this.props.onMarkerClick(marker.layer);
      })
    );

    this.props.onClusterClick && (
      markerClusterGroup.on('clusterclick', (cluster) => {
        this.props.onClusterClick(cluster.layer);
      })
    );

    this.props.onPopupClose && (
      markerClusterGroup.on('popupclose', (map) => {
        this.props.onPopupClose(map.popup);
      })
    );
  }

  addLayersWithReactLeafletMarkers() {
    const leafletMarkers = [];

    // Map through all react-leaflet Markers and clone them with ref prop
    // ref prop required to get leafletElement of Marker
    return Children.map(this.props.children, (reactLeafletMarker, index) => (
      cloneElement(reactLeafletMarker, {
        ref: (marker) => {
          if (marker) {
            leafletMarkers.push(marker.leafletElement);

            if (
              (index === (this.props.children.length - 1)) ||
              // addClusteredMarkersToMap when there is only one marker
              !Array.isArray(this.props.children)
            ) {
              // Add markers leafletElements to the markerClusterGroup
              this.leafletElement.addLayers(leafletMarkers);
              // Add clustered markers to the leaflet map
              this.layerContainer.addLayer(this.leafletElement);
            }
          }
        },
        key: `react-leaflet-marker-${index}`
      })
    ));
  }

  getLeafletElement() {
    return this.leafletElement;
  }

  render() {
	  if (!this.state.loaded) {
		  return null;
	  }

    return this.props.children
    ? (
      <section className="marker-cluster-group">
        {this.addLayersWithReactLeafletMarkers()}
      </section>
    )
    : null;
  }
}

function isArraysEqual(firstArray, secondArray) {
  return (JSON.stringify(firstArray) === JSON.stringify(secondArray));
}

MarkerClusterGroup.propTypes = {
  // List of markers with required lat and lng keys
  markers: PropTypes.arrayOf(PropTypes.object),
  // List of react-leaflet markers
  children: PropTypes.node,
  // All available options for Leaflet.markercluster
  options: PropTypes.object,
  // All available options for Leaflet.Marker
  markerOptions: PropTypes.object,
  // Options that are supporting by react-leaflet-markercluster wrapper
  wrapperOptions: PropTypes.object,
  // Events
  onMarkerClick: PropTypes.func,
  onClusterClick: PropTypes.func,
  onPopupClose: PropTypes.func
};

MarkerClusterGroup.defaultProps = {
  markers: [],
  wrapperOptions: {}
};
@ptondereau ptondereau changed the title How to get leaflet object of the map inside an extending classe How to get leaflet object of the map inside an extending class Jul 28, 2017
@bengoh
Copy link

bengoh commented Jul 29, 2017

@ptondereau I've looked into the code and the issue mostly stems from the fact that the react-leaflet-universal module exposes a higher-order-component rather than extending the class. As a result, it doesn't have access to the original properties on the class, i.e. it works poorly with inheritance. Unfortunately this is difficult to bypass because "universal inheritance" doesn't really make sense when an instance can only properly exist on the client.

I see you've already done some stuff like shifting code to componentDidMount, but additionally you will need to "copy" instance properties from the original wrapped component onto the "universal" one.

The solution should involving copying the this properties from the wrapped component onto the wrapper, but it is time sensitive in the sense that it can only be done when the component can be mounted (window is available). I will have to investigate this further.

@ptondereau
Copy link
Author

@bengoh thank you very much! We use SSR and such feature would be great!

@masotime
Copy link
Owner

masotime commented Dec 3, 2017

@ptondereau - first off, apologies for taking so long to get back on this. If you still need assistance on this, there is now support for "render-props" that should help make your situation easier.

You won't need to fork react-leaflet-markercluster - react-leaflet-universal (v1.2 onwards) now supports what is known as a "render prop" to make it possible to force react-leaflet-markercluster to mount only on the client. For example:

import React, { Fragment } from 'react';
import { Map, TileLayer } from 'react-leaflet-universal';

export default function MarkerClusterExample() {
  return (
    <Map
      style={
        {
          width: '500px',
          height: '500px'
        }
      }
      className="markercluster-map"
      center={[51.0, 19.0]} zoom={4} maxZoom={18}>
      {
        () => {
          const MarkerClusterGroup = require('react-leaflet-markercluster').default;
          return (
            <Fragment>
              <TileLayer
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
              />
              <MarkerClusterGroup
                markers={[
                  { position: [49.8397, 24.0297] },
                  { position: [52.2297, 21.0122] },
                  { position: [51.5074, -0.0901] },
                ]}
              />
            </Fragment>
          );
        }
      }
    </Map>
  );
}

Let me know if this is sufficient for your needs.

EDIT: Fragment is part of the recently introduced React 16.2. If you can't use 16.2, then just replace <Fragment> with <div> or other containing element.

@ptondereau
Copy link
Author

@masotime thank you very much!
We ended to exclude all leaflet libs from the webpack build used by SSR.
We're on react update feature and I'll let you know if it works!

@masotime masotime closed this as completed Sep 8, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants