簡體   English   中英

React-Leaflet:將地圖控制組件放置在地圖之外?

[英]React-Leaflet: Placing map control components outside of map?

這是我的另一個問題的更通用版本: Remove Zoom control from map in react-leaflet React-leaflet 附帶了一些控制地圖的組件 - 即<ZoomControl /><LayersControl />等。但是為了讓這些組件與地圖實例正確通信,它們必須被編寫為<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 在我的另一個問題中的解決方案只是從頭開始重建縮放組件並將其提供給地圖上下文。 這適用於簡單的縮放組件,但對於更復雜的組件,我無法重建整個框架。 例如,我制作了esri-leaflet 的 geosearch的快速 react-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。 如何在<Map />組件之外使用功能正常的 react-leaflet 控件組件,同時將其正確鏈接到地圖實例?

如果您願意,這里有一個快速的codsandbox 模板可以開始使用。 謝謝閱讀。

您可以使用onAdd方法在地圖外為您的插件創建一個容器,然后使用 refs 將元素添加到 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} />
      </>
    );
  }
}

演示

感謝 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

在這最后一步中,我們不導出 Map,而是導出包裝在 provider 中的 Map,將 MapProvider 的{value}作為 Map 的 props。 這樣,當App組件中調用LeafletMap時,在componentDidMount上, setMap函數會作為prop調用,回調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>
    )
  }

}

請注意,在LeafletMap componentDidMount 觸發之前,不會調用 MapProvider 的setMap方法。 因此,在App渲染時,還沒有上下文值,並且Sibling中嘗試訪問上下文的任何組件都還沒有它。 但是一旦App的渲染運行,並且LeafletMaps componentDidMount 運行, setMap運行,並且map值是 Provider 可用。 所以在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 ,但沒有將其添加到地圖中。 相反,我們在 DOM 樹中我們想要的位置創建一個容器 div,然后在 componentDidMount 上,我們在該容器 div 內呈現該 HTML。 因此,我們有一個在應用程序中預期位置編寫和呈現的組件,它與地圖正確通信。

抱歉閱讀太長了,但我想寫出答案來鞏固我自己的知識,並為將來可能遇到相同情況的任何人提供一個相當規范的答案。 功勞 100% 歸功於 kboul,我只是將他的答案綜合到一個地方。 在這里有一個工作示例。 如果你喜歡這個答案,請給他的答案點贊。

在這種情況下可能沒有幫助,但我使用 redux 來定義地圖的狀態,然后使用普通操作和減速器從應用程序的任何地方更新地圖。

所以你的動作看起來像這樣

export const setCenterMap = (payload) => ({
  type: CENTER_MAP,
  payload,
})

和一個基本的減速器:

const initialState = {
centerMap: false,
}

export const reducer = (state = initialState, action) => {
    switch (action.type) {
        case (CENTER_MAP) : {
            return ({
                ...state,
                centerMap: action.payload
            })
        }
        default: return state
    }
}

然后將其連接到地圖組件

const mapStateToProps = state => ({
  centerMap: state.app.centerMap,
})

const mapDispatchToProps = dispatch => ({
  setCenterMap: (centerMap) => dispatch(setCenterMap(centerMap)),
})

您現在可以在 Leaflet 組件之外操作地圖。

        <LeafletMap
            center={centerMap}
            sites={event.sites && [...event.sites, ...event.workingGroups]}
        />
        <button onClick={() => setCenterMap([5.233, 3.342])} >SET MAP CENTER</button>

其中大部分是偽代碼,因此您必須將其用於自己的用途,但我發現從 LeafletMap 組件外部添加一些基本地圖控件是一種相當輕松的方法,尤其是如果您已經在使用 redux。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM