Skip to content

Commit ca1ecdb

Browse files
feat(google-maps): declarative SFC API (#510)
Co-authored-by: Damian Glowala <[email protected]>
1 parent 1edcd3d commit ca1ecdb

21 files changed

+2106
-23
lines changed

docs/content/scripts/content/google-maps.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,278 @@ By providing your own placeholder slot you will disable the default placeholder
380380
</template>
381381
```
382382

383+
## Google Maps SFC Components
384+
385+
Nuxt Scripts provides individual Single File Components (SFCs) for different Google Maps elements. These components allow you to declaratively compose complex maps using Vue's template syntax.
386+
387+
### Installation
388+
389+
To use marker clustering functionality, you'll need to install the required peer dependency:
390+
391+
```bash
392+
npm install @googlemaps/markerclusterer
393+
# or
394+
yarn add @googlemaps/markerclusterer
395+
# or
396+
pnpm add @googlemaps/markerclusterer
397+
```
398+
399+
### Available Components
400+
401+
All Google Maps SFC components must be used within a `<ScriptGoogleMaps>` component:
402+
403+
- `<ScriptGoogleMapsMarker>` - Classic markers with icon support
404+
- `<ScriptGoogleMapsAdvancedMarkerElement>` - Modern advanced markers with HTML content
405+
- `<ScriptGoogleMapsPinElement>` - Customizable pin markers (use within AdvancedMarkerElement)
406+
- `<ScriptGoogleMapsInfoWindow>` - Information windows that appear on click
407+
- `<ScriptGoogleMapsMarkerClusterer>` - Groups nearby markers into clusters
408+
- `<ScriptGoogleMapsCircle>` - Circular overlays
409+
- `<ScriptGoogleMapsPolygon>` - Polygon shapes
410+
- `<ScriptGoogleMapsPolyline>` - Line paths
411+
- `<ScriptGoogleMapsRectangle>` - Rectangular overlays
412+
- `<ScriptGoogleMapsHeatmapLayer>` - Heatmap visualization
413+
414+
### Basic Usage
415+
416+
```vue
417+
<template>
418+
<ScriptGoogleMaps
419+
:center="{ lat: -34.397, lng: 150.644 }"
420+
:zoom="8"
421+
api-key="your-api-key"
422+
>
423+
<!-- Add markers -->
424+
<ScriptGoogleMapsMarker
425+
:options="{ position: { lat: -34.397, lng: 150.644 } }"
426+
>
427+
<!-- Info window appears on marker click -->
428+
<ScriptGoogleMapsInfoWindow>
429+
<div>
430+
<h3>Sydney, Australia</h3>
431+
<p>A great city!</p>
432+
</div>
433+
</ScriptGoogleMapsInfoWindow>
434+
</ScriptGoogleMapsMarker>
435+
436+
<!-- Advanced marker with custom pin -->
437+
<ScriptGoogleMapsAdvancedMarkerElement
438+
:options="{ position: { lat: -34.407, lng: 150.654 } }"
439+
>
440+
<ScriptGoogleMapsPinElement
441+
:options="{ scale: 1.5, background: '#FF0000' }"
442+
/>
443+
</ScriptGoogleMapsAdvancedMarkerElement>
444+
445+
<!-- Circle overlay -->
446+
<ScriptGoogleMapsCircle
447+
:options="{
448+
center: { lat: -34.397, lng: 150.644 },
449+
radius: 1000,
450+
fillColor: '#FF0000',
451+
fillOpacity: 0.35
452+
}"
453+
/>
454+
</ScriptGoogleMaps>
455+
</template>
456+
```
457+
458+
### Component Composition Patterns
459+
460+
**Marker Clustering**
461+
462+
```vue
463+
<template>
464+
<ScriptGoogleMaps api-key="your-api-key">
465+
<ScriptGoogleMapsMarkerClusterer>
466+
<ScriptGoogleMapsMarker
467+
v-for="location in locations"
468+
:key="location.id"
469+
:options="{ position: location.position }"
470+
>
471+
<ScriptGoogleMapsInfoWindow>
472+
<div>{{ location.name }}</div>
473+
</ScriptGoogleMapsInfoWindow>
474+
</ScriptGoogleMapsMarker>
475+
</ScriptGoogleMapsMarkerClusterer>
476+
</ScriptGoogleMaps>
477+
</template>
478+
```
479+
480+
**Heatmap with Data Points**
481+
482+
```vue
483+
<script setup>
484+
const heatmapData = ref([])
485+
486+
onMounted(() => {
487+
// Populate heatmap data with google.maps.LatLng objects
488+
})
489+
</script>
490+
491+
<template>
492+
<ScriptGoogleMaps api-key="your-api-key">
493+
<ScriptGoogleMapsHeatmapLayer
494+
:options="{ data: heatmapData }"
495+
/>
496+
</ScriptGoogleMaps>
497+
</template>
498+
```
499+
500+
**See the [SFC Playground Example](https://nuxt-scripts-playground.stackblitz.io/third-parties/google-maps/sfcs) for a comprehensive demonstration.**
501+
502+
### Component Details
503+
504+
#### ScriptGoogleMapsMarker
505+
506+
Classic Google Maps marker with icon support.
507+
508+
**Props:**
509+
- `options` - `google.maps.MarkerOptions` (excluding `map`)
510+
511+
**Events:**
512+
- Standard marker events: `click`, `mousedown`, `mouseover`, etc.
513+
514+
#### ScriptGoogleMapsAdvancedMarkerElement
515+
516+
Modern advanced markers that support HTML content and better customization.
517+
518+
**Props:**
519+
- `options` - `google.maps.marker.AdvancedMarkerElementOptions` (excluding `map`)
520+
521+
**Events:**
522+
- Standard marker events: `click`, `drag`, `position_changed`, etc.
523+
524+
#### ScriptGoogleMapsInfoWindow
525+
526+
Information windows that display content when triggered.
527+
528+
**Props:**
529+
- `options` - `google.maps.InfoWindowOptions`
530+
531+
**Behavior:**
532+
- Automatically opens on parent marker click
533+
- Can be used standalone with explicit position
534+
- Supports custom HTML content via default slot
535+
536+
#### ScriptGoogleMapsMarkerClusterer
537+
538+
Groups nearby markers into clusters for better performance and UX.
539+
540+
**Props:**
541+
- `options` - `MarkerClustererOptions` (excluding `map`)
542+
543+
**Dependencies:**
544+
- Requires `@googlemaps/markerclusterer` peer dependency
545+
546+
#### Other Components
547+
548+
- **ScriptGoogleMapsPinElement**: Use within AdvancedMarkerElement for customizable pins
549+
- **ScriptGoogleMapsCircle**: Circular overlays with radius and styling
550+
- **ScriptGoogleMapsPolygon/Polyline**: Shape and line overlays
551+
- **ScriptGoogleMapsRectangle**: Rectangular overlays
552+
- **ScriptGoogleMapsHeatmapLayer**: Data visualization with heatmaps
553+
554+
All components support:
555+
- Reactive `options` prop that updates the underlying Google Maps object
556+
- Automatic cleanup on component unmount
557+
- TypeScript support with Google Maps types
558+
559+
### Best Practices
560+
561+
#### Performance Considerations
562+
563+
**Use MarkerClusterer for Many Markers**
564+
```vue
565+
<!-- ✅ Good: Use clusterer for >10 markers -->
566+
<ScriptGoogleMapsMarkerClusterer>
567+
<ScriptGoogleMapsMarker v-for="marker in manyMarkers" />
568+
</ScriptGoogleMapsMarkerClusterer>
569+
570+
<!-- ❌ Avoid: Many individual markers -->
571+
<ScriptGoogleMapsMarker v-for="marker in manyMarkers" />
572+
```
573+
574+
**Prefer AdvancedMarkerElement for Modern Apps**
575+
```vue
576+
<!-- ✅ Recommended: Better performance and styling -->
577+
<ScriptGoogleMapsAdvancedMarkerElement :options="options">
578+
<ScriptGoogleMapsPinElement :options="{ background: '#FF0000' }" />
579+
</ScriptGoogleMapsAdvancedMarkerElement>
580+
581+
<!-- ⚠️ Legacy: Use only when advanced markers aren't supported -->
582+
<ScriptGoogleMapsMarker :options="options" />
583+
```
584+
585+
#### Component Hierarchy
586+
587+
Components must follow this nesting structure:
588+
589+
```
590+
ScriptGoogleMaps (root)
591+
├── ScriptGoogleMapsMarkerClusterer (optional)
592+
│ └── ScriptGoogleMapsMarker/AdvancedMarkerElement
593+
│ └── ScriptGoogleMapsInfoWindow (optional)
594+
├── ScriptGoogleMapsAdvancedMarkerElement
595+
│ ├── ScriptGoogleMapsPinElement (optional)
596+
│ └── ScriptGoogleMapsInfoWindow (optional)
597+
└── Other overlays (Circle, Polygon, etc.)
598+
```
599+
600+
#### Reactive Data Patterns
601+
602+
**Reactive Marker Updates**
603+
```vue
604+
<script setup>
605+
const markers = ref([
606+
{ id: 1, position: { lat: -34.397, lng: 150.644 }, title: 'Sydney' }
607+
])
608+
609+
// Markers automatically update when data changes
610+
function addMarker() {
611+
markers.value.push({
612+
id: Date.now(),
613+
position: getRandomPosition(),
614+
title: 'New Location'
615+
})
616+
}
617+
</script>
618+
619+
<template>
620+
<ScriptGoogleMaps>
621+
<ScriptGoogleMapsMarker
622+
v-for="marker in markers"
623+
:key="marker.id"
624+
:options="{ position: marker.position, title: marker.title }"
625+
/>
626+
</ScriptGoogleMaps>
627+
</template>
628+
```
629+
630+
#### Error Handling
631+
632+
Always provide error fallbacks and loading states:
633+
634+
```vue
635+
<script setup>
636+
const mapError = ref(false)
637+
</script>
638+
639+
<template>
640+
<ScriptGoogleMaps
641+
@error="mapError = true"
642+
api-key="your-api-key"
643+
>
644+
<template #error>
645+
<div class="p-4 bg-red-100">
646+
Failed to load Google Maps
647+
</div>
648+
</template>
649+
650+
<!-- Your components -->
651+
</ScriptGoogleMaps>
652+
</template>
653+
```
654+
383655
## useScriptGoogleMaps
384656

385657
The `useScriptGoogleMaps` composable lets you have fine-grain control over the Google Maps SDK. It provides a way to load the Google Maps SDK and interact with it programmatically.

eslint.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export default createConfigForNuxt({
3636
'vue/no-multiple-template-root': 'off',
3737
// NOTE: Disable this style rules if stylistic is not enabled
3838
'vue/max-attributes-per-line': 'off',
39+
// Disabled for Google Maps SFC components that use conditional rendering without root elements
40+
'vue/valid-template-root': 'off',
3941
},
4042
})
4143
.append({

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@
7272
]
7373
},
7474
"peerDependencies": {
75-
"@stripe/stripe-js": "^7.0.0",
75+
"@googlemaps/markerclusterer": "^2.6.2",
7676
"@paypal/paypal-js": "^8.1.2",
77+
"@stripe/stripe-js": "^7.0.0",
7778
"@types/google.maps": "^3.58.1",
7879
"@types/vimeo__player": "^2.18.3",
7980
"@types/youtube": "^0.1.0",
@@ -124,6 +125,7 @@
124125
"@paypal/paypal-js": "^8.4.2",
125126
"@types/semver": "^7.7.1",
126127
"@typescript-eslint/typescript-estree": "^8.44.0",
128+
"@vue/test-utils": "^2.4.6",
127129
"acorn-loose": "^8.5.2",
128130
"bumpp": "^10.2.3",
129131
"changelogen": "^0.6.2",

0 commit comments

Comments
 (0)