【问题标题】:React-Leaflet: Placing map control components outside of map?React-Leaflet:将地图控制组件放置在地图之外?
【发布时间】:2020-05-01 17:26:03
【问题描述】:

这是我的另一个问题的更通用版本:Remove Zoom control from map in react-leaflet。 React-leaflet 带有一些控制地图的组件 - 即<ZoomControl /><LayersControl /> 等。但是为了使这些组件与地图实例正确通信,它们必须编写为 @ 的子987654327@组件,像这样:

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

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

</Map>

我正在尝试创建一种情况,即不是地图直接子级的地图组件可以与地图正确通信。例如:

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

显然这里的问题是当这些控件不再是&lt;Map /&gt; 的子控件或后代时,它们不会通过提供程序接收地图实例。正如您在my other question 中看到的那样,我尝试创建一个新的 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's geosearch的quick 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);

当在&lt;Map /&gt; 组件中声明时,它相对简单并且效果很好。但我想将它移动到应用程序中的单独位置,并且我不想重新编码整个 esri Geosearch。如何在 &lt;Map /&gt; 组件之外使用功能正常的 react-leaflet 控制组件,同时将其正确链接到地图实例?

如果您愿意,这里有一个快速的codsandbox template 开始搞乱。感谢阅读。

【问题讨论】:

    标签: javascript reactjs custom-component react-context react-leaflet


    【解决方案1】:

    您可以使用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} />
          </>
        );
      }
    }
    

    Demo

    【讨论】:

    • 这并没有完全回答我的问题,因为您是说将 GeoSearch 组件添加到地图的 componentDidMount 中。您在other question 中的回答解释了如何将地图上下文带到地图之外。这个答案给出了如何创建传单自定义控件并将其呈现在 DOM 中的任何位置(在外部化上下文的保护伞下)的关键要点。我将在这里和那里写下您的答案的综合,以帮助我理解并将这个难题标记为已解决。非常感谢!
    • 你说''因为你说地理搜索组件被添加到地图的 componentDidMount 中。 “那又怎样?这有什么限制?它实际上给出了一个答案,因为你说你不想从头开始构建整个库。如果你想重建像缩放这样的小组件,我提到的其他答案是合适的并且避免在任何地方传递任何上下文。
    • 从“它有效吗?”观点,没有限制,这是一个很好的解决方案。但我的问题主要是“如何在&lt;Map /&gt; 组件之外使用功能正常的 react-leaflet 控制组件,同时将其正确链接到地图实例?”这个答案并没有真正做到,因为控件是在 Map 组件中创建的。我正在处理包含许多子组件的地图,并且以这种方式编写它们会变得一团糟。我猜这是风格/组织上的差异。
    • 我将您的答案标记为答案,因为它非常有帮助,但再次感谢您
    【解决方案2】:

    感谢 kboul 在thismy other question 的回答,我将在这里写下一个答案。这确实是 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,而是导出封装在提供程序中的 Map,将 MapProvider 的 {value} 作为 Map 的 props。这样,当LeafletMapApp组件中被调用时,在componentDidMount上,setMap函数会作为prop被调用,回调MapProvidersetMap函数。这会将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,我只是将他的答案综合到一个地方。他有一个工作示例here。如果您喜欢这个答案,请为他的答案投票。

    【讨论】:

      【解决方案3】:

      在这种情况下可能没有帮助,但我使用 redux 来定义地图的状态,然后使用普通操作和 reducer 从应用程序的任何位置更新地图。

      所以你的操作看起来像这样

      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。

      【讨论】:

      • 感谢您对此的看法。这基本上就是我在使用 redux 的大型应用程序中所做的事情。但它并不能真正使我能够在&lt;Map /&gt; 实例之外使用prebuilt 地图组件,同时仍与它进行通信。例如,为了获得&lt;GeoSearch /&gt; 的功能,我必须编写哪些操作集。正如我所提到的,“我不想重新编码整个 esri Geosearch。”您的答案非常适合孤立的动作片段,但不适用于将上下文传递给整个组件。谢谢你的想法。
      猜你喜欢
      • 1970-01-01
      • 2023-03-22
      • 1970-01-01
      • 2022-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-02-05
      相关资源
      最近更新 更多