React-Leaflet:将地图控件组件放置在地图之外?
React-Leaflet: Placing map control components outside of map?
这是我的另一个问题的更一般化的版本:。 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 在我的另一个问题中的解决方案是简单地从头开始重建缩放组件并将其提供给地图上下文。这适用于简单的缩放组件,但对于更复杂的组件,我无法重建整个框架。比如我做了一个快速的react-leaflet组件版本 esri-leaflet's geosearch:
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 template 可以开始搞乱。感谢阅读。
您可以使用 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在 and 中的回答,我准备在这里写一个答案。这确实是 kboul 的回答,但我想通过将其写出来在我的大脑中巩固它,并让任何偶然经过的人都可以使用它。
首先,我们需要创建一个上下文 object,并为该上下文创建一个提供程序。我们将在目录中创建两个新文件,以便于从其他文件访问:
/src
-App.js
-Map.js
-MapContext.js
-MapProvider.js
-Sibling.js
-Niece.js
/Components
-ZoomControl.js
-OtherControls.js
创建空上下文object:
// MapContext.jsx
import { createContext } from "react";
const MapContext = createContext();
export default MapContext
接下来,我们使用MapContext object 创建一个MapProvider。 MapProvider 有自己的状态,其中包含一个将成为地图引用的空白引用。它还有一个方法 setMap
可以在其状态中设置地图引用。它提供空白参考作为其值,以及设置地图参考的方法。最后,它呈现 children:
// 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;
现在,在地图组件中,我们将导出包裹在 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
在这最后一步中,我们不导出地图,而是导出包装在提供程序中的地图,使用 MapProvider 的 {value}
作为地图的道具。这样,在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
渲染 运行s,并且 LeafletMaps
componentDidMount 运行s,setMap
运行s,以及 map
值是提供者可用。所以在 App
中,我们等到 componentDidMount 运行s,此时 setMap
已经 运行。我们在加载地图的 App
中设置状态,我们 Sibling
的条件渲染语句将渲染 Sibling
,及其所有 children,以及 MapContext object 正确引用地图。现在我们可以在组件中使用它了。例如,我重写了 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 object,并将其 HTML 和关联的处理程序存储在变量 searchContainer
中,但没有将其添加到地图中。相反,我们在 DOM 树中的所需位置创建一个容器 div,然后在 componentDidMount 上,我们在该容器 div 内渲染 HTML。所以我们有一个在应用程序中的预期位置编写和呈现的组件,它与地图正确通信。
抱歉阅读时间过长,但我想写下答案以巩固我自己的知识,并为将来可能遇到相同情况的任何人提供一个相当规范的答案。功劳 100% 归功于 kboul,我只是将他的答案综合到一个地方。他有一个工作示例 here。如果您喜欢这个答案,请为他的答案点赞。
在这种情况下可能没有帮助,但我使用 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。
这是我的另一个问题的更一般化的版本:<ZoomControl />
、<LayersControl />
等。但是为了让这些组件与地图实例正确通信,它们必须写成<Map />
组件的子组件,像这样:
<Map center={...} zoom={...}>
<ZoomControl />
<LayersControl />
<MyCustomComponent />
</Map>
我想要创建的是这样一种情况,即不是地图的直接子项的地图组件可以与地图正确通信。例如:
<App>
<Map />
<Sibling>
<Niece>
<ZoomControl />
<OtherControls />
</Niece>
</Sibling>
</App>
显然这里的问题在于,当这些控件不再是 <Map />
的子控件或后代控件时,它们不会通过提供程序接收地图实例。正如您在 componentDidMount
中,如下所示:
// Niece.js
componentDidMount(){
const newZoomHome = document.querySelector('#Niece')
const leafletZoomControl= document.querySelector('.leaflet-control-zoom')
newZoomHome.appendChild(leafletZoomControl)
}
我真的很讨厌这个,因为现在我的通用组件结构不能反映应用程序结构。我的 Zoom 需要写成我的地图的一部分,但最终会出现在我的 Neice 组件中。
Kboul 在我的另一个问题中的解决方案是简单地从头开始重建缩放组件并将其提供给地图上下文。这适用于简单的缩放组件,但对于更复杂的组件,我无法重建整个框架。比如我做了一个快速的react-leaflet组件版本 esri-leaflet's geosearch:
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 template 可以开始搞乱。感谢阅读。
您可以使用 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在
首先,我们需要创建一个上下文 object,并为该上下文创建一个提供程序。我们将在目录中创建两个新文件,以便于从其他文件访问:
/src
-App.js
-Map.js
-MapContext.js
-MapProvider.js
-Sibling.js
-Niece.js
/Components
-ZoomControl.js
-OtherControls.js
创建空上下文object:
// MapContext.jsx
import { createContext } from "react";
const MapContext = createContext();
export default MapContext
接下来,我们使用MapContext object 创建一个MapProvider。 MapProvider 有自己的状态,其中包含一个将成为地图引用的空白引用。它还有一个方法 setMap
可以在其状态中设置地图引用。它提供空白参考作为其值,以及设置地图参考的方法。最后,它呈现 children:
// 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;
现在,在地图组件中,我们将导出包裹在 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
在这最后一步中,我们不导出地图,而是导出包装在提供程序中的地图,使用 MapProvider 的 {value}
作为地图的道具。这样,在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
渲染 运行s,并且 LeafletMaps
componentDidMount 运行s,setMap
运行s,以及 map
值是提供者可用。所以在 App
中,我们等到 componentDidMount 运行s,此时 setMap
已经 运行。我们在加载地图的 App
中设置状态,我们 Sibling
的条件渲染语句将渲染 Sibling
,及其所有 children,以及 MapContext object 正确引用地图。现在我们可以在组件中使用它了。例如,我重写了 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 object,并将其 HTML 和关联的处理程序存储在变量 searchContainer
中,但没有将其添加到地图中。相反,我们在 DOM 树中的所需位置创建一个容器 div,然后在 componentDidMount 上,我们在该容器 div 内渲染 HTML。所以我们有一个在应用程序中的预期位置编写和呈现的组件,它与地图正确通信。
抱歉阅读时间过长,但我想写下答案以巩固我自己的知识,并为将来可能遇到相同情况的任何人提供一个相当规范的答案。功劳 100% 归功于 kboul,我只是将他的答案综合到一个地方。他有一个工作示例 here。如果您喜欢这个答案,请为他的答案点赞。
在这种情况下可能没有帮助,但我使用 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。