React-Leaflet: Размещение компонентов управления картой вне карты? - PullRequest
0 голосов
/ 15 января 2020

Это более обобщенная версия моего другого вопроса: Снять элемент управления масштабированием с карты в ответной листовке . React-листовка поставляется с несколькими компонентами, которые контролируют карту - то есть <ZoomControl />, <LayersControl />, et c. Но для того, чтобы эти компоненты должным образом взаимодействовали с экземпляром карты, они должны быть записаны как дочерний элемент компонента <Map />, например:

<Map center={...} zoom={...}>

  <ZoomControl />
  <LayersControl />
  <MyCustomComponent />

</Map>

Я пытаюсь создать ситуацию, в которой Компоненты карты, которые не являются прямыми дочерними элементами карты, могут правильно взаимодействовать с картой. Например:

<App>
  <Map />
  <Sibling>
    <Niece>
      <ZoomControl />
      <OtherControls />
    </Niece>
  </Sibling>
</App>

Очевидно, что проблема здесь в том, что, когда эти элементы управления больше не являются потомками или потомками <Map />, они не получают экземпляр карты через провайдера. Как вы можете видеть в моем другом вопросе , я попытался создать новый объект Context и использовать его для предоставления карты смещенным элементам управления. Это не сработало, и я не уверен почему. До сих пор мое решение состояло в том, чтобы использовать vanilla javascript для изменения этих элементов управления в componentDidMount их предполагаемого родителя, например так:

// Niece.js
componentDidMount(){
  const newZoomHome = document.querySelector('#Niece')
  const leafletZoomControl= document.querySelector('.leaflet-control-zoom')
  newZoomHome.appendChild(leafletZoomControl)
}

Я действительно ненавижу это, потому что теперь моя общая структура компонентов делает не отражает структуру приложения. Мой Zoom должен быть написан как часть моей карты, но заканчивается в моем компоненте Neice.

Решением Kboul в моем другом вопросе было просто перестроить компонент zoom с нуля и передать его контексту карты. Это прекрасно работает для простого компонента масштабирования, но для более сложных компонентов я не могу перестраивать целые фреймворки. Например, я сделал версию компонента esri-leaflet для быстрого реагирования :

import { withLeaflet, MapControl } from "react-leaflet";
import * as ELG from "esri-leaflet-geocoder";

class GeoSearch extends MapControl {
  createLeafletElement(props) {
    const searchOptions = {
       ...props,
      providers: props.providers ? props.providers.map( provider => ELG[provider]()) : null
    };

    const GeoSearch = new ELG.Geosearch(searchOptions);
    // Author can specify a callback function for the results event
    if (props.onResult){
      GeoSearch.addEventListener('results', props.onResult)
    }
    return GeoSearch;
  }

  componentDidMount() {
    const { map } = this.props.leaflet;
    this.leafletElement.addTo(map);
  }
}

export default withLeaflet(GeoSearch);

Это относительно просто и прекрасно работает, когда объявлено внутри компонента <Map />. Но я хочу переместить его в отдельное место в приложении, и я не хочу перекодировать весь esri Geosearch. Как я могу использовать функционирующие компоненты управления Reaction-leaflet вне компонента <Map /> при правильной привязке его к экземпляру карты?

Вот быстрый шаблон codsandbox , с которым можно начать связываться, если вы ' так склонен. Спасибо за чтение.

Ответы [ 2 ]

1 голос
/ 17 января 2020

Вы можете использовать метод onAdd , чтобы создать контейнер для вашего плагина за пределами карты, а затем с помощью ссылок добавить элемент в DOM следующим образом:

class Map extends React.Component {
  mapRef = createRef();
  plugin = createRef();

  componentDidMount() {
    // map instance
    const map = this.mapRef.current.leafletElement;

    const searchcontrol = new ELG.Geosearch();
    const results = new L.LayerGroup().addTo(map);
    searchcontrol.on("results", function(data) {
      results.clearLayers();
      for (let i = data.results.length - 1; i >= 0; i--) {
        results.addLayer(L.marker(data.results[i].latlng));
      }
    });
    const searchContainer = searchcontrol.onAdd(map);
    this.plugin.current.appendChild(searchContainer);
  }

  render() {
    return (
      <>
        <LeafletMap
          zoom={4}
          style={{ height: "50vh" }}
          ref={this.mapRef}
          center={[33.852169, -100.5322]}
        >
          <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
        </LeafletMap>
        <div ref={this.plugin} />
      </>
    );
  }
}

Демо

0 голосов
/ 20 января 2020

Благодаря ответу kboul в этом и моем другом вопросе я собираюсь выписать ответ здесь. Это на самом деле ответ kboul, но я хочу закрепить его в своем мозгу, написав его, и сделать его доступным для всех, кто запинается.

Во-первых, нам нужно создать объект контекста и поставщика для этот контекст. Мы создадим два новых файла в каталоге для легкого доступа из других файлов:

/src
  -App.js
  -Map.js
  -MapContext.js
  -MapProvider.js
  -Sibling.js
  -Niece.js
  /Components
    -ZoomControl.js
    -OtherControls.js

Создайте пустой объект контекста:

// MapContext.jsx
import { createContext } from "react";

const MapContext = createContext();

export default MapContext

Далее мы используем объект MapContext для создания MapProvider. MapProvider имеет свое собственное состояние, которое содержит пустую ссылку, которая станет ссылкой на карту. Он также имеет метод setMap для установки ссылки на карту в своем состоянии. Он предоставляет в качестве значения пустую ссылку, а также метод для установки ссылки на карту. Наконец, он отображает свои дочерние элементы:

// MapProvider.jsx

import React from "react";
import MapContext from "./MapContext";

class MapProvider extends React.Component {
  state = { map: null };

  setMap = map => {
    this.setState({ map });
  };

  render() {
    return (
      <MapContext.Provider value={{ map: this.state.map, setMap: this.setMap }}>
        {this.props.children}
      </MapContext.Provider>
    );
  }
}

export default MapProvider;

Теперь в компоненте Map мы экспортируем карту, обернутую в MapProvider.

// Map.jsx

import React from "react";
import { Map as MapComponent, TileLayer, Marker, etc } from 'react-leaflet'
import MapContext from './MapContext'

class Map extends React.Component{

  mapRef = React.createRef(null);

  componentDidMount() {
    const map = this.mapRef.current.leafletElement;
    this.props.setMap(map);
  }

  render(){

    return (

      <MapComponent 
         center={[centerLat, centerLng]} 
         zoom={11.5} 
         ...all the props
         ref={this.mapRef} >

      </MapComponent>

    );
  }
}

const LeafletMap = props =>  (
  <MapContext.Consumer>
    {({ setMap }) => <Map {...props} setMap={setMap} />}
  </MapContext.Consumer>
)

export default LeafletMap

На этом последнем шаге мы не будем Экспорт карты, а мы экспортируем карту, завернутую в поставщика, с {value} MapProvider в качестве реквизита карты. Таким образом, когда LeafletMap вызывается в компоненте App, в componentDidMount функция setMap будет вызываться как опора, вызывая функцию MapProvider setMap. Это устанавливает состояние MapProvider, чтобы иметь ссылку на карту. Но этого не произойдет, пока карта не будет отображена в App:

// App.js

class App extends React.Component{

  state = { mapLoaded: false }

  componentDidMount(){
    this.setState({ mapLoaded:true })

  }

  render(){
    return (
      <MapProvider>
        <LeafletMap  />
        {this.state.mapLoaded && <Sibling/>}
      </MapProvider>
    )
  }

}

Обратите внимание, что метод MapProvider setMap не вызывается до тех пор, пока не сработает LeafletMap componentDidMount. Таким образом, при рендеринге в App значение контекста еще не существует, и у любого компонента в Sibling, который пытается получить доступ к контексту, его еще не будет. Но после запуска рендеринга App и запуска LeafletMaps componentDidMount запускается setMap, и значение map означает, что поставщик доступен. Поэтому в App мы ждем, пока будет запущен componentDidMount, и в этот момент setMap уже запущен. Мы устанавливаем состояние в App, в которое загружена карта, наш оператор условного рендеринга для Sibling будет отображать Sibling со всеми его дочерними объектами с объектом MapContext, правильно ссылающимся на карту. Теперь мы можем использовать его в компоненте. Например, я переписал компонент GeoSearch, чтобы он работал так (благодаря предложению kboul):

// GeoSearch

import React from 'react'
import MapContext from '../Context'
import * as ELG from "esri-leaflet-geocoder";

class EsriGeoSearch extends React.Component {

   componentDidMount() {

      const map = this.mapReference

      const searchOptions = {
         ...this.props,
        providers: this.props.providers ? this.props.providers.map( provider => ELG[provider]()) : null
      };
      const GeoSearch = new ELG.Geosearch(searchOptions);

      const searchContainer = GeoSearch.onAdd(map);
      document.querySelector('geocoder-control-wrapper').appendChild(searchContainer);

   }


  render() {
     return (
        <MapContext.Consumer>
           { ({map}) => {
              this.mapReference = map
              return <div className='geocoder-control-wrapper' EsriGeoSearch`} />
           }}
        </MapContext.Consumer>
     )
  }
}

export default EsriGeoSearch;

Итак, все, что мы здесь сделали, - это создали объект Esri GeoSearch и сохранили его HTML и связанные с ним обработчики в переменная searchContainer, но не добавила ее на карту. Вместо этого мы создаем контейнер div, где мы хотим его в нашем дереве DOM, а затем на componentDidMount мы визуализируем HTML внутри этого контейнера div. Таким образом, у нас есть компонент, который написан и отрисован в предназначенном для него месте в приложении, который правильно связывается с картой.

Извините за долгое чтение, но я хотел выписать ответ, чтобы укрепить свои собственные знания и иметь достаточно канонический ответ для любого, кто может оказаться в такой же ситуации в будущем. Кредит идет на 100%, я просто обобщаю его ответы в одном месте. У него есть рабочий пример здесь . Если вам нравится этот ответ, пожалуйста, проголосуйте за его недовольство.

...