Skip to content

Commit 77edef5

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 72fda3a + 4679886 commit 77edef5

4 files changed

Lines changed: 208 additions & 4 deletions

File tree

docs/content/scripts/google-maps/1.guides/6.marker-clustering.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,36 @@ const locations = [
135135
</template>
136136
```
137137

138+
## Custom Cluster Renderer
139+
140+
Use the `#renderer` slot on the clusterer to fully customize cluster visuals with Vue templates.
141+
142+
```vue
143+
<template>
144+
<ScriptGoogleMaps :center="{ lat: -33.863, lng: 151.210 }" :zoom="13">
145+
<ScriptGoogleMapsMarkerClusterer>
146+
<template #renderer="{ cluster }">
147+
<div
148+
class="bg-blue-600 text-white rounded-full w-10 h-10 flex items-center justify-center font-bold text-sm shadow-lg"
149+
>
150+
{{ cluster.count }}
151+
</div>
152+
</template>
153+
154+
<ScriptGoogleMapsMarker
155+
v-for="loc in locations"
156+
:key="loc.id"
157+
:position="loc.position"
158+
>
159+
<ScriptGoogleMapsInfoWindow>
160+
<strong>{{ loc.name }}</strong>
161+
</ScriptGoogleMapsInfoWindow>
162+
</ScriptGoogleMapsMarker>
163+
</ScriptGoogleMapsMarkerClusterer>
164+
</ScriptGoogleMaps>
165+
</template>
166+
```
167+
138168
## Clustering with Custom Markers
139169

140170
Custom HTML markers cluster the same way as default pins.

docs/content/scripts/google-maps/2.api/4.marker-clusterer.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ function addLocation(position: google.maps.LatLngLiteral) {
7171
</template>
7272
```
7373

74-
### Custom Algorithm & Renderer
74+
### Custom Algorithm
7575

76-
Pass `algorithm` and `renderer` through `options` to customize clustering behavior. See the [`@googlemaps/markerclusterer` documentation](https://googlemaps.github.io/js-markerclusterer/) for available algorithms.
76+
Pass `algorithm` through `options` to customize clustering behavior. See the [`@googlemaps/markerclusterer` documentation](https://googlemaps.github.io/js-markerclusterer/) for available algorithms.
7777

7878
```vue
7979
<script setup lang="ts">
@@ -97,6 +97,44 @@ const clustererOptions = {
9797
</template>
9898
```
9999

100+
### Custom Cluster Renderer
101+
102+
Use the `#renderer` slot to customize how clusters look using Vue templates. The slot receives `cluster` (with `count`, `position`, `markers`), `stats`, and `map` (the Google Maps instance).
103+
104+
```vue
105+
<template>
106+
<ScriptGoogleMaps api-key="your-api-key">
107+
<ScriptGoogleMapsMarkerClusterer>
108+
<template #renderer="{ cluster }">
109+
<div
110+
style="
111+
background: #4285f4;
112+
color: white;
113+
border-radius: 50%;
114+
width: 40px;
115+
height: 40px;
116+
display: flex;
117+
align-items: center;
118+
justify-content: center;
119+
font-weight: bold;
120+
"
121+
>
122+
{{ cluster.count }}
123+
</div>
124+
</template>
125+
126+
<ScriptGoogleMapsMarker
127+
v-for="loc in locations"
128+
:key="loc.id"
129+
:position="loc.position"
130+
/>
131+
</ScriptGoogleMapsMarkerClusterer>
132+
</ScriptGoogleMaps>
133+
</template>
134+
```
135+
136+
The `#renderer` slot overrides the `renderer` option. Each cluster gets its own `AdvancedMarkerElement` with the slot content as its visual.
137+
100138
::callout
101139
The clusterer only renders its default slot after the clusterer instance is ready. This prevents child markers from mounting before the clusterer can receive them.
102140
::

packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMapsMarkerClusterer.vue

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import type { InjectionKey, ShallowRef } from 'vue'
3-
import { inject, provide, shallowRef, watch } from 'vue'
3+
import { getCurrentInstance, h, inject, onBeforeUnmount, provide, shallowRef, useSlots, render as vueRender, watch } from 'vue'
44
import { bindGoogleMapsEvents, MAP_INJECTION_KEY, useGoogleMapsResource } from './useGoogleMapsResource'
55
66
// Inline types to avoid requiring @googlemaps/markerclusterer as a build-time dependency
@@ -19,6 +19,22 @@ export interface MarkerClustererOptions {
1919
onClusterClick?: unknown
2020
}
2121
22+
export interface Cluster {
23+
marker: google.maps.marker.AdvancedMarkerElement
24+
markers?: google.maps.marker.AdvancedMarkerElement[]
25+
position: google.maps.LatLng
26+
bounds: google.maps.LatLngBounds | undefined
27+
count: number
28+
}
29+
30+
export interface ClusterStats {
31+
markers: { sum: number }
32+
clusters: {
33+
count: number
34+
markers: { min: number, max: number, mean: number, sum: number }
35+
}
36+
}
37+
2238
export interface MarkerClustererContext {
2339
markerClusterer: ShallowRef<MarkerClustererInstance | undefined>
2440
requestRerender: () => void
@@ -59,17 +75,64 @@ const markerClustererEvents = [
5975
'clusteringend',
6076
] as const
6177
78+
const slots = useSlots()
79+
const instance = getCurrentInstance()
6280
const mapContext = inject(MAP_INJECTION_KEY, undefined)
6381
const clusteringVersion = shallowRef(0)
6482
83+
// Track containers for Vue-rendered cluster content so we can unmount them
84+
const renderedContainers: HTMLElement[] = []
85+
86+
function cleanupRenderedClusters() {
87+
for (const container of renderedContainers) {
88+
vueRender(null, container)
89+
}
90+
renderedContainers.length = 0
91+
}
92+
93+
onBeforeUnmount(cleanupRenderedClusters)
94+
6595
const markerClusterer = useGoogleMapsResource<MarkerClustererInstance>({
66-
async create({ map }) {
96+
async create({ map, mapsApi }) {
6797
const { MarkerClusterer } = await import('@googlemaps/markerclusterer')
98+
99+
// Pre-load marker library so the synchronous renderer can use AdvancedMarkerElement
100+
if (slots.renderer) {
101+
await mapsApi.importLibrary('marker')
102+
}
103+
68104
const clusterer = new MarkerClusterer({
69105
map,
70106
...props.options,
107+
...(slots.renderer
108+
? {
109+
renderer: {
110+
render(cluster: Cluster, stats: ClusterStats) {
111+
const container = document.createElement('div')
112+
const vnode = h({
113+
render: () => slots.renderer?.({ cluster, stats, map }),
114+
})
115+
if (instance) {
116+
vnode.appContext = instance.appContext
117+
}
118+
vueRender(vnode, container)
119+
renderedContainers.push(container)
120+
121+
const marker = new mapsApi.marker.AdvancedMarkerElement({
122+
position: cluster.position,
123+
content: container.firstElementChild as HTMLElement || container,
124+
})
125+
return marker
126+
},
127+
},
128+
}
129+
: {}),
71130
} as any) as MarkerClustererInstance
131+
72132
bindGoogleMapsEvents(clusterer, emit, { withPayload: markerClustererEvents })
133+
clusterer.addListener('clusteringbegin', () => {
134+
cleanupRenderedClusters()
135+
})
73136
clusterer.addListener('clusteringend', () => {
74137
clusteringVersion.value++
75138
})
@@ -78,6 +141,7 @@ const markerClusterer = useGoogleMapsResource<MarkerClustererInstance>({
78141
cleanup(clusterer, { mapsApi }) {
79142
mapsApi.event.clearInstanceListeners(clusterer)
80143
clusterer.setMap(null)
144+
cleanupRenderedClusters()
81145
},
82146
})
83147

test/unit/google-maps-components.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,78 @@ describe('google Maps SFC Components Logic', () => {
228228
})
229229
})
230230

231+
describe('marker Clusterer #renderer Slot', () => {
232+
it('should create clusterer with custom renderer when #renderer slot is provided', async () => {
233+
const { MarkerClusterer } = await import('@googlemaps/markerclusterer')
234+
235+
// Simulate what the component does when #content slot exists
236+
const mockMap = {}
237+
const rendererFn = vi.fn((_cluster: any, _stats: any) => {
238+
const container = document.createElement('div')
239+
container.innerHTML = '<span>5</span>'
240+
return container
241+
})
242+
243+
const renderer = {
244+
render(cluster: any, stats: any) {
245+
const content = rendererFn(cluster, stats)
246+
const marker = new mocks.mockMapsApi.marker.AdvancedMarkerElement({
247+
position: cluster.position,
248+
content,
249+
})
250+
return marker
251+
},
252+
}
253+
254+
// eslint-disable-next-line no-new
255+
new MarkerClusterer({
256+
map: mockMap,
257+
renderer,
258+
})
259+
260+
expect(MarkerClusterer).toHaveBeenCalledWith(
261+
expect.objectContaining({ renderer }),
262+
)
263+
264+
// Simulate a cluster render call
265+
const cluster = { position: { lat: 1, lng: 2 }, count: 5, markers: [] }
266+
const stats = { clusters: [], markers: [] }
267+
const marker = renderer.render(cluster, stats)
268+
269+
expect(rendererFn).toHaveBeenCalledWith(cluster, stats)
270+
expect(mocks.mockMapsApi.marker.AdvancedMarkerElement).toHaveBeenCalledWith(
271+
expect.objectContaining({ position: cluster.position }),
272+
)
273+
expect(marker).toBeDefined()
274+
})
275+
276+
it('should pre-load marker library when #renderer slot is used', async () => {
277+
// Simulate the pre-loading that happens when slots.content exists
278+
await mocks.mockMapsApi.importLibrary('marker')
279+
280+
expect(mocks.mockMapsApi.importLibrary).toHaveBeenCalledWith('marker')
281+
})
282+
283+
it('should clean up rendered containers on clusteringbegin', () => {
284+
// Simulate the cleanup pattern used in the component
285+
const containers: HTMLElement[] = []
286+
287+
// Create some containers (as would happen during render)
288+
for (let i = 0; i < 3; i++) {
289+
const container = document.createElement('div')
290+
container.innerHTML = `<span>${i}</span>`
291+
containers.push(container)
292+
}
293+
294+
expect(containers).toHaveLength(3)
295+
296+
// Simulate clusteringbegin cleanup
297+
containers.length = 0
298+
299+
expect(containers).toHaveLength(0)
300+
})
301+
})
302+
231303
describe('google Maps API Types and Integration', () => {
232304
it('should work with LatLng objects', () => {
233305
const lat = -33.8688

0 commit comments

Comments
 (0)