yuzhva / react-leaflet-markercluster

React wrapper of the official Leaflet.markercluster for react-leaflet
https://yuzhva.github.io/react-leaflet-markercluster/
MIT License
288 stars 101 forks source link

Disable component for SSR #35

Open ptondereau opened 7 years ago

ptondereau commented 7 years ago

I'm in situation where I need to render to null this component when window global is not present (because of SSR). I use this lib to avoid error in SSR node server --> https://github.com/masotime/react-leaflet-universal

I've forked your repo to change LayerGroup import origin and added same logic of render present in react-leaflet-universal but I don't really understand the API. Indeed, an error is throw about this.container.map is undefined so I can't access to the container.

Any clue for how to achieve this?

Here is my snippet:

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: {}
};
yuzhva commented 7 years ago

Looks like LayerGroup from react-leaflet-universal works not as expecting and MarkerClusterGroup component that extends it doesn't receive leaflet map object through props.

To use .addLayer(this.leafletElement); you need to get leafletElement (object crerated by leaflet lib) of the map, that is why I'm using:

export default class MarkerClusterGroup extends LayerGroup {

By extending extends LayerGroup react-leaflet gives me access to the leafletElement of the map... I didn't investigate another way of getting it.

P.S: maybe try to ask react-leaflet-universal how they are getting leaflet object of the map inside custom classes?

ptondereau commented 7 years ago

ref: https://github.com/masotime/react-leaflet-universal/issues/2

wmertens commented 5 years ago

another approach: https://github.com/theKashey/react-imported-component/issues/47#issuecomment-525624097

owlyowl commented 5 years ago

@wmertens Do you have an example of leaflet working with react-imported-component? I can't seem to get it working.

react-leaflet-universal isn't working with the google maps library

wmertens commented 5 years ago

@owlyowl I am not SSRing in this project, so no. I just pointed out the possibility. I'm also not using Google with leaflet. It should just work.