上一个 SO 问题展示了我们如何使用 Vue2 组件作为 LeafletJS 弹出窗口的内容。我无法在 Vue3 中使用它。
提取我的代码的相关部分,我有:
<script setup lang="ts">
import { ref } from 'vue'
import L, { type Content } from 'leaflet'
import type { FeatureCollection, Feature } from 'geojson'
import LeafletPopup from '@/components/LeafletPopup.vue'
// This ref will be matched by Vue to the element with the same ref name
const popupDialogElement = ref(null)
function addFeaturePopup(feature:Feature, layer:L.GeoJSON) {
if (popupDialogElement?.value !== null) {
const content:Content = popupDialogElement.value as HTMLElement
layer.bindPopup(() => content.$el)
}
}
</script>
<template>
<div class="map-container">
<section id="map">
</section>
<leaflet-popup ref="popupDialogElement" v-show="false">
</leaflet-popup>
</div>
</template>
当我点击地图时,这确实会产生一个弹出窗口,但它没有内容。
如果我将第 14 行更改为:
layer.bindPopup(() => content.$el.innerHTML)
然后我do得到一个带有我期望的HTML标记的弹出窗口,但毫不奇怪的是我失去了我需要的所有Vue行为(事件处理等)。
在JS调试器中检查
addFeaturePopup
函数,content
似乎确实是HTMLElement
的实例,所以我不确定为什么将它传递给Leaflet的bindPopup
方法不起作用。我认为这与 Vue3 处理引用的方式有关,但到目前为止我还没有找到解决方法。
根据要求,这是
console.log
输出:我已将其放入要点,因为它很长
因此,为了记录我最终使用的解决方案,除了问题中概述的一般框架之外,我还需要添加额外的样式规则:
<style>
.leaflet-popup-content >* {
display: block !important;
}
</style>
这会覆盖通过
display:none
附加到 DOM 节点的 v-show=false
。不需要 !important
就好了,但我在实验中无法使规则具有足够的选择性。
总结
使用 Vue3 的 Teleport,将基于 Vue 的弹出窗口(具有完全反应性)渲染为空的 Leaflet 弹出窗口。这或多或少只是一种更干净的方式来完成人们多年来手动做的事情。
请参阅下面的代码和演示。
一些要点
1 - 在启用 Vue 组件(以及需要 DOM 目标的 Teleport)之前,原生 Leaflet 弹出 DOM 元素需要首先存在。因此,在创建 Leaflet 弹出窗口时,我们首先绑定它,显示 Vue 组件,当一切处理完毕后,然后实际上会显示 Leaflet 弹出窗口。
2 - 在我自己的应用程序中,我必须放置战略性的
setTimeout
,否则我不会在弹出窗口和/或控制台错误中呈现任何内容。在codesandbox演示中,这些超时似乎没有必要,我将它们注释掉了。或者,Vue nextTick()
可能是一个更优雅的东西。
3 - 解除弹出窗口的绑定很重要,否则你可能会遇到弹出窗口消失或其他奇怪的行为。
4 - 下面的工作假设一次只打开一个弹出窗口,并且仅依赖于现有的 Leaflet 弹出窗口 CSS 类。如果需要更多的特异性或处理多个弹出窗口,那么您必须在创建每个 Leaflet 弹出窗口时创建一个唯一的 id,并在 Teleport 中引用它。
https://codesandbox.io/p/devbox/leaflet-vue3-popup-using-teleport-gw4wkp
Vue3 + 打字稿
应用程序.vue
<script setup lang="ts">
import { onMounted, ref } from "vue"
import * as L from "leaflet"
import "leaflet/dist/leaflet.css"
import MyMapPopup from "./components/MapPopup.vue"
import MarkerImage from "leaflet/dist/images/marker-icon.png"
var map:L.Map
var myMarker: L.Marker
var map_id = "myMap"
const popupcard = ref()
const showTeleportContent = ref()
onMounted(() => {
initMap()
})
const closePopup = () => {
myMarker.closePopup()
}
const initMap = () => {
// MAP ELEMENT
let mapHTMLElement = document.getElementById(map_id) as HTMLElement
map = L.map(mapHTMLElement).setView([43.6532, -79.3832], 10);
// TILE LAYER L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
}).addTo(map)
// MARKER
var myIcon = L.icon({ iconUrl: MarkerImage, iconAnchor: [12, -10], popupAnchor: [0, -40] })
myMarker = L.marker([43.7, -79.4], { icon: myIcon })
myMarker.addTo(map)
// MARKER CLICK
myMarker.on('click', (e:L.LeafletMouseEvent) => {
// Possibly need a delay here (or Vue nextTick())
//setTimeout(() => {
// UNBIND ANY PREVIOUSLY OPENED POPUPS (just in case)
myMarker.unbindPopup()
// CREATE NEW LEAFLET POPUP
// First value is blank ('') because Leaflet needs _something_ to initialize the popup.
myMarker.bindPopup('', {
'maxWidth': 300,
'maxHeight': 100,
'className': 'myCustomPopupClass',
'autoPan': true,
'closeButton': false, // Hide native Leafet Popup close button; alternatively can use CSS
'closeOnEscapeKey': true
})
// SET CONTENT AND REVEAL THE POPUP
// Possibly need a delay here (or Vue nextTick())
//setTimeout(() => {
// Enable the Vue content (and trigger the Teleport)
showTeleportContent.value = true
// Reveal the popup to the user
myMarker.openPopup()
//}, 100)
// LISTENER FOR POPUP CLOSE
myMarker.getPopup()?.on('remove', () => {
myMarker.unbindPopup()
showTeleportContent.value = false
})
//}, 100)
})
}
</script>
<template>
<div id="myWrapper">
<div id="myMap"></div>
<!-- The Vue popup is only shown (v-if) when the Leaflet popup is initialized,
as Vue Teleport requires a DOM target to function. -->
<div ref="popupcard" id="popupcard" v-if="showTeleportContent">
<!-- Teleport is a Vue 3 feature; it basically appends the component
to any DOM target (:to). Here, we point it to the content class of the Leaflet popup. Since only one popup is open at a time (presumably) this is safe. Otherwise you'd need to create a unique ID when creating the Leaflet popup. -->
<Teleport :to="'.leaflet-popup-content'">
<component ref="popupcardInner"
:is="MyMapPopup"
v-model="count"
:message1="message1"
:message2="message2"
@close="closePopup()" />
</Teleport>
</div>
</div>
</template>
<style scoped>
#myWrapper {
width: 100%;
height: 100%;
}
#myMap {
width: 100%;
height: 100%;
}
</style>
<style>
/* UNSCOPED CSS (ie global) */
/* ------------------------------------ */
/* RESET LEAFLET POPUP defaults */
/* ------------------------------------ */
.leaflet-popup-content-wrapper {
padding: 0;
}
.leaflet-popup {
width: 300px;
height: 200px;
}
.leaflet-popup-content {
margin: 0;
line-height: inherit;
font-size: inherit;
}
/*
OPTIONAL: You can use this CSS to hide the native "x" close button instead of using the popup property method.
.leaflet-popup-close-button {
display: none;
}
*/
</style>