From 3c8a0e92c8684ba373d9873a380cc7eadd17a44a Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Fri, 20 Mar 2026 15:27:49 +1100 Subject: [PATCH 1/6] feat(google-maps): DX overhaul for v1 OverlayView: - Marker anchoring: auto-inherits position when nested inside Marker/AdvancedMarkerElement - v-model:open for toggling visibility without remounting - Follows marker during drag via position_changed/drag event listeners - Fixed stale computed position bug (plain function instead of computed for Google Maps state) AdvancedMarkerElement: - #content slot replaces default pin with any Vue template - :position top-level prop (shorthand for options.position) Marker: - :position top-level prop - Dev deprecation warning (google.maps.Marker is deprecated by Google) Root ScriptGoogleMaps: - :zoom reactive prop (takes precedence over mapOptions.zoom) - resolveQueryToLatLng typo fix (was resolveQueryToLatLang) - Improved dev error message guiding users to registry config Shared: - bindGoogleMapsEvents utility extracts duplicated event setup across 7 components Docs: - Restructured guides: Performance, Programmatic API, Map Styling, Billing - All API docs use ::script-types directive for auto-generated props/events - Quick Start section in index.md - v1 release notes updated with Google Maps overhaul section --- .../scripts/google-maps/.navigation.yml | 1 + .../google-maps/1.guides/.navigation.yml | 2 + .../google-maps/1.guides/0.getting-started.md | 164 +++++++++++++ .../google-maps/1.guides/1.eager-loading.md | 22 ++ .../google-maps/1.guides/1.performance.md | 127 ++++++++++ .../google-maps/1.guides/2.marker-control.md | 21 ++ .../1.guides/2.programmatic-api.md | 130 +++++++++++ .../google-maps/1.guides/3.map-control.md | 35 +++ .../google-maps/1.guides/3.map-styling.md | 105 +++++++++ .../scripts/google-maps/1.guides/4.billing.md | 66 ++++++ .../1.guides/4.immediate-loading.md | 11 + .../google-maps/1.guides/5.map-styling.md | 31 +++ .../scripts/google-maps/2.api/.navigation.yml | 2 + .../google-maps/2.api/1.script-google-maps.md | 130 +++++++++++ .../scripts/google-maps/2.api/10.rectangle.md | 30 +++ .../google-maps/2.api/11.heatmap-layer.md | 32 +++ .../google-maps/2.api/12.overlay-view.md | 99 ++++++++ .../2.api/13.use-script-google-maps.md | 39 ++++ .../scripts/google-maps/2.api/2.marker.md | 43 ++++ .../2.api/3.advanced-marker-element.md | 70 ++++++ .../google-maps/2.api/4.pin-element.md | 29 +++ .../google-maps/2.api/5.info-window.md | 27 +++ .../google-maps/2.api/6.marker-clusterer.md | 38 +++ .../scripts/google-maps/2.api/7.circle.md | 28 +++ .../scripts/google-maps/2.api/8.polygon.md | 29 +++ .../scripts/google-maps/2.api/9.polyline.md | 29 +++ docs/content/scripts/google-maps/index.md | 221 ++++++++++++++++++ .../pages/third-parties/google-maps/sfcs.vue | 135 ++++++++++- .../GoogleMaps/ScriptGoogleMaps.vue | 23 +- .../ScriptGoogleMapsAdvancedMarkerElement.vue | 46 +++- .../GoogleMaps/ScriptGoogleMapsCircle.vue | 15 +- .../GoogleMaps/ScriptGoogleMapsInfoWindow.vue | 9 +- .../GoogleMaps/ScriptGoogleMapsMarker.vue | 31 ++- .../ScriptGoogleMapsOverlayView.vue | 194 +++++++++++++++ .../GoogleMaps/ScriptGoogleMapsPolygon.vue | 14 +- .../GoogleMaps/ScriptGoogleMapsPolyline.vue | 14 +- .../GoogleMaps/ScriptGoogleMapsRectangle.vue | 15 +- .../GoogleMaps/bindGoogleMapsEvents.ts | 22 ++ 38 files changed, 2001 insertions(+), 78 deletions(-) create mode 100644 docs/content/scripts/google-maps/.navigation.yml create mode 100644 docs/content/scripts/google-maps/1.guides/.navigation.yml create mode 100644 docs/content/scripts/google-maps/1.guides/0.getting-started.md create mode 100644 docs/content/scripts/google-maps/1.guides/1.eager-loading.md create mode 100644 docs/content/scripts/google-maps/1.guides/1.performance.md create mode 100644 docs/content/scripts/google-maps/1.guides/2.marker-control.md create mode 100644 docs/content/scripts/google-maps/1.guides/2.programmatic-api.md create mode 100644 docs/content/scripts/google-maps/1.guides/3.map-control.md create mode 100644 docs/content/scripts/google-maps/1.guides/3.map-styling.md create mode 100644 docs/content/scripts/google-maps/1.guides/4.billing.md create mode 100644 docs/content/scripts/google-maps/1.guides/4.immediate-loading.md create mode 100644 docs/content/scripts/google-maps/1.guides/5.map-styling.md create mode 100644 docs/content/scripts/google-maps/2.api/.navigation.yml create mode 100644 docs/content/scripts/google-maps/2.api/1.script-google-maps.md create mode 100644 docs/content/scripts/google-maps/2.api/10.rectangle.md create mode 100644 docs/content/scripts/google-maps/2.api/11.heatmap-layer.md create mode 100644 docs/content/scripts/google-maps/2.api/12.overlay-view.md create mode 100644 docs/content/scripts/google-maps/2.api/13.use-script-google-maps.md create mode 100644 docs/content/scripts/google-maps/2.api/2.marker.md create mode 100644 docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md create mode 100644 docs/content/scripts/google-maps/2.api/4.pin-element.md create mode 100644 docs/content/scripts/google-maps/2.api/5.info-window.md create mode 100644 docs/content/scripts/google-maps/2.api/6.marker-clusterer.md create mode 100644 docs/content/scripts/google-maps/2.api/7.circle.md create mode 100644 docs/content/scripts/google-maps/2.api/8.polygon.md create mode 100644 docs/content/scripts/google-maps/2.api/9.polyline.md create mode 100644 docs/content/scripts/google-maps/index.md create mode 100644 src/runtime/components/GoogleMaps/ScriptGoogleMapsOverlayView.vue create mode 100644 src/runtime/components/GoogleMaps/bindGoogleMapsEvents.ts diff --git a/docs/content/scripts/google-maps/.navigation.yml b/docs/content/scripts/google-maps/.navigation.yml new file mode 100644 index 00000000..b7dd8d23 --- /dev/null +++ b/docs/content/scripts/google-maps/.navigation.yml @@ -0,0 +1 @@ +title: Google Maps diff --git a/docs/content/scripts/google-maps/1.guides/.navigation.yml b/docs/content/scripts/google-maps/1.guides/.navigation.yml new file mode 100644 index 00000000..f1ad8d56 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/.navigation.yml @@ -0,0 +1,2 @@ +title: Guides +icon: i-ph-book-duotone diff --git a/docs/content/scripts/google-maps/1.guides/0.getting-started.md b/docs/content/scripts/google-maps/1.guides/0.getting-started.md new file mode 100644 index 00000000..0a61cd1e --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/0.getting-started.md @@ -0,0 +1,164 @@ +--- +title: Getting Started +--- + +A quick walkthrough to get a Google Map with markers running in your Nuxt app. + +## Minimal Map + +```vue + +``` + +This renders a static map placeholder that loads the interactive map on hover. See [Setup](/scripts/google-maps#setup) for API key configuration via environment variables. + +## Adding Markers + +Use `ScriptGoogleMapsAdvancedMarkerElement` to place markers on the map. The `:position` prop sets where the marker appears. + +```vue + +``` + +## Info Windows + +Nest a `ScriptGoogleMapsInfoWindow` inside a marker. It opens automatically when the marker is clicked. + +```vue + +``` + +## Custom Marker Content + +The `#content` slot replaces the default pin with any HTML or Vue template. + +```vue + +``` + +## Custom Popups + +For full control over popup styling (no InfoWindow constraints), use `ScriptGoogleMapsOverlayView` with `v-model:open`. + +```vue + + + +``` + +## Dynamic Markers + +Render markers from data with `v-for`. + +```vue + + + +``` + +## Next Steps + +- [Map Styling](/scripts/google-maps/guides/map-styling) for custom map themes +- [AdvancedMarkerElement API](/scripts/google-maps/api/advanced-marker-element) for all marker options +- [OverlayView API](/scripts/google-maps/api/overlay-view) for custom overlay positioning +- [Map Control](/scripts/google-maps/guides/map-control) for programmatic map access diff --git a/docs/content/scripts/google-maps/1.guides/1.eager-loading.md b/docs/content/scripts/google-maps/1.guides/1.eager-loading.md new file mode 100644 index 00000000..6b13c9d9 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/1.eager-loading.md @@ -0,0 +1,22 @@ +--- +title: Eager Loading Placeholder +--- + +The Google Maps placeholder image is lazy-loaded by default. You should change this behavior if your map is above the fold +or consider using the `#placeholder` slot to customize the placeholder image. + +::code-group + +```vue [Placeholder Attrs] + +``` + +```vue [Placeholder Slot] + + + +``` + +:: diff --git a/docs/content/scripts/google-maps/1.guides/1.performance.md b/docs/content/scripts/google-maps/1.guides/1.performance.md new file mode 100644 index 00000000..192daf79 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/1.performance.md @@ -0,0 +1,127 @@ +--- +title: Performance +--- + +`ScriptGoogleMaps` is optimized by default: the JavaScript API only loads when the user interacts with the map. Before that, a lightweight static image placeholder is shown. + +## Loading Strategies + +### Default: Lazy (Recommended) + +By default, the map loads on `mouseenter`, `mouseover`, or `mousedown`. Most page visitors never interact with the map, so you avoid the Maps JavaScript API charge for those sessions. + +```vue + +``` + +### Immediate Loading + +Forces the JavaScript API to load with the page. Use this only when the map is the primary content and you need it interactive from the start. + +```vue + +``` + +::callout{color="amber"} +Immediate loading charges the Maps JavaScript API ($7/1000) on every page view. Only use this when the map is essential to the page experience. +:: + +### Custom Triggers + +You can control exactly when the map loads using any [Element Event Trigger](/docs/guides/script-triggers#element-event-triggers). + +```vue + +``` + +## Placeholder Optimization + +### Above the Fold + +When the map is visible without scrolling, mark it for priority loading. This sets the placeholder image to `loading="eager"` and adds a `preconnect` hint. + +```vue + +``` + +### Custom Placeholder + +Replace the Google Static Maps image with your own to eliminate Static Maps API charges. Useful when you have a screenshot or illustration of the map area. + +```vue + +``` + +You can also access the generated static map URL if you want to use it with custom styling: + +```vue + +``` + +### Loading State + +Show a custom indicator while the JavaScript API loads: + +```vue + +``` + +## Marker Performance + +When rendering many markers, use `ScriptGoogleMapsMarkerClusterer` to group nearby markers. This significantly reduces DOM elements and improves pan/zoom performance. + +```vue + +``` + +See [Billing & Permissions](/scripts/google-maps/guides/billing) for a full cost breakdown and optimization strategies. diff --git a/docs/content/scripts/google-maps/1.guides/2.marker-control.md b/docs/content/scripts/google-maps/1.guides/2.marker-control.md new file mode 100644 index 00000000..2cb01412 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/2.marker-control.md @@ -0,0 +1,21 @@ +--- +title: Advanced Marker Control +--- + +If you need more control over the markers on the map, you can use the exposed `createAdvancedMapMarker` function which +will return the marker instance. + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/1.guides/2.programmatic-api.md b/docs/content/scripts/google-maps/1.guides/2.programmatic-api.md new file mode 100644 index 00000000..5eea73eb --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/2.programmatic-api.md @@ -0,0 +1,130 @@ +--- +title: Programmatic API +--- + +The `ScriptGoogleMaps` component exposes its internal APIs via template ref, giving you full control beyond what the declarative SFC components provide. + +## Accessing the API + +```vue + + + +``` + +### Exposed Properties + +| Property | Type | Description | +|---|---|---| +| `googleMaps` | `Ref`{lang="html"} | The Google Maps API namespace | +| `map` | `ShallowRef`{lang="html"} | The map instance | +| `createAdvancedMapMarker` | `(options?) => Promise`{lang="html"} | Create a marker programmatically | +| `resolveQueryToLatLng` | `(query: string) => Promise`{lang="html"} | Geocode a location string | +| `importLibrary` | `(key: string) => Promise`{lang="html"} | Load an additional Google Maps library | + +## Creating Markers Programmatically + +For cases where declarative `v-for` markers aren't flexible enough (dynamic data, imperative creation logic), use `createAdvancedMapMarker`: + +```vue + + + +``` + +::callout +For most use cases, prefer the declarative `ScriptGoogleMapsAdvancedMarkerElement` component with `v-for`. Use the programmatic API when you need fine-grained control over marker lifecycle or are integrating with external data sources. +:: + +## Geocoding Queries + +Convert location strings to coordinates using `resolveQueryToLatLng`. When you enable the registry proxy, this resolves server-side (cheaper, API key hidden). Otherwise it falls back to the client-side Places API. + +```vue + + + +``` + +## Importing Libraries + +Google Maps splits functionality into libraries that load on demand. Use `importLibrary` to access geometry, drawing, places, and visualization APIs: + +```vue + +``` + +Available libraries: `marker`, `places`, `geometry`, `drawing`, `visualization` + +## Subscribing to Map Events + +Use the `@ready` event to access the map instance and subscribe to native Google Maps events: + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/1.guides/3.map-control.md b/docs/content/scripts/google-maps/1.guides/3.map-control.md new file mode 100644 index 00000000..3abc1981 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/3.map-control.md @@ -0,0 +1,35 @@ +--- +title: Advanced Map Control +--- + +The component exposes all internal APIs, so you can customize your map as needed. + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/1.guides/3.map-styling.md b/docs/content/scripts/google-maps/1.guides/3.map-styling.md new file mode 100644 index 00000000..3f3c8372 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/3.map-styling.md @@ -0,0 +1,105 @@ +--- +title: Map Styling +--- + +Google Maps supports two styling approaches: legacy JSON styles and cloud-based map IDs. Both work with Nuxt Scripts, including the static map placeholder. + +## JSON Styles + +Use the `mapOptions.styles` prop with a JSON style array. You can find pre-made styles on [Snazzy Maps](https://snazzymaps.com/). + +Styles automatically apply to both the static map placeholder and the interactive map. + +```vue + + + +``` + +## Cloud-Based Map IDs + +Google's [Map Styling](https://developers.google.com/maps/documentation/cloud-customization) lets you create and manage styles in the Google Cloud Console, then apply them with a map ID. + +```vue + +``` + +::callout{color="amber"} +JSON `styles` and `mapId` are mutually exclusive. When you provide both, the component ignores `mapId` and applies `styles`. Cloud-based styling also requires the map to use `AdvancedMarkerElement` (legacy `Marker` is not supported with map IDs). +:: + +## Dark Mode / Color Mode + +Switch map styles automatically based on the user's color mode preference. Provide a `mapIds` object with light and dark map IDs: + +```vue + +``` + +This auto-detects `@nuxtjs/color-mode` if installed. You can also control it manually with the `colorMode` prop: + +```vue + + + +``` + +## Combining Styles with Markers + +Custom styled maps pair well with custom marker content for a cohesive look: + +```vue + +``` diff --git a/docs/content/scripts/google-maps/1.guides/4.billing.md b/docs/content/scripts/google-maps/1.guides/4.billing.md new file mode 100644 index 00000000..8313ce19 --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/4.billing.md @@ -0,0 +1,66 @@ +--- +title: Billing & Permissions +--- + +## Required API Permissions + +Your Google Cloud project needs these APIs enabled: + +| API | Required? | When it's used | +|---|---|---| +| [Maps JavaScript API](https://developers.google.com/maps/documentation/javascript/cloud-setup) | Yes | Interactive map rendering | +| [Static Maps API](https://developers.google.com/maps/documentation/maps-static/cloud-setup) | Recommended | Placeholder image before JS loads (unless you provide a `#placeholder` slot) | +| [Geocoding API](https://developers.google.com/maps/documentation/geocoding/cloud-setup) | Optional | Server-side geocode proxy for query strings (e.g. `center="Brooklyn Bridge, NY"`) | +| [Places API](https://developers.google.com/maps/documentation/places/web-service/cloud-setup) | Optional | Client-side fallback when geocode proxy is unavailable and using query strings | + +## Cost Breakdown + +Google Maps uses a pay-per-use model. Here's what each interaction costs: + +| Action | API Charged | Cost per 1,000 | When it happens | +|---|---|---|---| +| Page loads with map placeholder | Static Maps API | $2 | Every page view (unless `#placeholder` slot overrides) | +| User interacts with map (hover/click) | Maps JavaScript API | $7 | Only when user triggers the map to load | +| Location query resolved server-side | Geocoding API | $5 | Only when using string queries like `"Sydney, Australia"` | +| Location query resolved client-side | Places API | $17 | Fallback when geocode proxy is not enabled | + +### How Nuxt Scripts Minimizes Costs + +Nuxt Scripts is designed to reduce Google Maps billing: + +**Lazy loading**: The JavaScript API ($7/1000) only loads when a user interacts with the map. If they never hover or click, you only pay for the static placeholder ($2/1000). + +**Geocode proxy**: When you enable `googleMaps: true` in the registry, the server resolves location queries via the Geocoding API ($5/1000) instead of the client-side Places API ($17/1000). That's a 70% cost reduction for query resolution. + +**Static maps proxy**: Placeholder images are routed through your server, which enables caching. Repeated visits to the same page serve cached images instead of hitting Google's API again. + +### Cost Optimization Tips + +1. **Use coordinates, not queries**: Pass `center` as `{ lat, lng }` instead of `"Sydney, Australia"` to avoid geocoding charges entirely. + +2. **Provide a `#placeholder` slot**: Replace the static map image with your own placeholder to eliminate Static Maps API charges. + + ```vue + + + + ``` + +3. **Use `trigger="immediate"`** only when needed: The default trigger (mouseover/mousedown) means most page views won't load the JS API. Setting `trigger="immediate"` charges $7/1000 on every page view. + +4. **Consider Iframe Embed for non-interactive maps**: If you don't need full interactivity, the [Google Maps Embed API](https://developers.google.com/maps/documentation/embed/get-started) is free and you can load it with `useScript()`{lang="ts"}. + +## Monthly Cost Estimates + +Assuming 100,000 page views per month: + +| Scenario | Monthly Cost | +|---|---| +| Static placeholder only (no interaction) | ~$200 | +| 20% of visitors interact with map | ~$340 | +| All visitors interact with map | ~$900 | +| Custom `#placeholder` slot, 20% interact | ~$140 | + +These estimates assume you have Google's [$200/month free credit](https://mapsplatform.google.com/pricing/). Most small to mid-size sites fall within the free tier. diff --git a/docs/content/scripts/google-maps/1.guides/4.immediate-loading.md b/docs/content/scripts/google-maps/1.guides/4.immediate-loading.md new file mode 100644 index 00000000..0f26310b --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/4.immediate-loading.md @@ -0,0 +1,11 @@ +--- +title: Loading Immediately +--- + +If you want to load the Google Maps immediately, you can use the `trigger` prop. + +```vue + +``` diff --git a/docs/content/scripts/google-maps/1.guides/5.map-styling.md b/docs/content/scripts/google-maps/1.guides/5.map-styling.md new file mode 100644 index 00000000..ccecbe1c --- /dev/null +++ b/docs/content/scripts/google-maps/1.guides/5.map-styling.md @@ -0,0 +1,31 @@ +--- +title: Map Styling +--- + +You can style the map by using the `mapOptions.styles` prop. You can find pre-made styles on [Snazzy Maps](https://snazzymaps.com/). + +This will automatically work for both the static map placeholder and the interactive map. + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/2.api/.navigation.yml b/docs/content/scripts/google-maps/2.api/.navigation.yml new file mode 100644 index 00000000..e506a40e --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/.navigation.yml @@ -0,0 +1,2 @@ +title: API +icon: i-lucide-code diff --git a/docs/content/scripts/google-maps/2.api/1.script-google-maps.md b/docs/content/scripts/google-maps/2.api/1.script-google-maps.md new file mode 100644 index 00000000..0a0d95b4 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/1.script-google-maps.md @@ -0,0 +1,130 @@ +--- +title: +--- + +The [``{lang="html"}](/scripts/google-maps){lang="html"} component is a wrapper around the [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps/api/use-script-google-maps){lang="ts"} composable. It provides a simple way to embed Google Maps in your Nuxt app. + +It's optimized for performance by using the [Element Event Triggers](/docs/guides/script-triggers#element-event-triggers), only loading the Google Maps when specific elements events happen. + +Before Google Maps loads, it shows a placeholder using [Maps Static API](https://developers.google.com/maps/documentation/maps-static). + +By default, it will load on the `mouseover` and `mouseclick` events. + +### Key Props + +| Prop | Type | Default | Description | +|---|---|---|---| +| `apiKey` | `string` | - | Google Maps API key | +| `center` | `LatLngLiteral \| LatLng \| string` | - | Map center (coordinates or query string) | +| `zoom` | `number` | `15` | Map zoom level (0-21). Reactive, takes precedence over `mapOptions.zoom` | +| `mapOptions` | `google.maps.MapOptions` | - | Full map configuration | +| `markers` | `(string \| AdvancedMarkerElementOptions)[]` | - | Quick markers (use SFC components for full control) | +| `trigger` | `ElementScriptTrigger` | `['mouseenter', 'mouseover', 'mousedown']` | When to load the Google Maps script | +| `aboveTheFold` | `boolean` | `false` | Prioritize placeholder image loading | + +See the [Facade Component API](/docs/guides/facade-components#facade-components-api) for all props, events, and slots. + +### Events + +The [``{lang="html"}](/scripts/google-maps){lang="html"} component emits a single `ready` event when Google Maps loads. + +The `ready` event payload contains: + +| Property | Type | Description | +|---|---|---| +| `googleMaps` | `Ref`{lang="html"} | The Google Maps API namespace | +| `map` | `ShallowRef`{lang="html"} | The map instance | +| `createAdvancedMapMarker` | `(options?) => Promise`{lang="html"} | Create a marker programmatically | +| `resolveQueryToLatLng` | `(query: string) => Promise`{lang="html"} | Geocode a location string | +| `importLibrary` | `(key: string) => Promise`{lang="html"} | Load an additional Google Maps library | + +To subscribe to Google Map events, you can use the `ready` event. + +```vue + + + +``` + +### Slots + +The component provides minimal UI by default, only enough to be functional and accessible. There are a number of slots for you to customize the maps however you like. + +**default** + +The default slot displays content that will always be visible. + +```vue + +``` + +**awaitingLoad** + +This slot displays content while Google Maps is loading. + +```vue + +``` + +**loading** + +This slot displays content while Google Maps is loading. + +Note: This shows a `ScriptLoadingIndicator` by default for accessibility and UX, by providing a slot you will +override this component. Make sure you provide a loading indicator. + +```vue + +``` + +**placeholder** + +This slot displays a placeholder image before Google Maps loads. By default, this will show the Google Maps Static API image for the map. + +By providing your own placeholder slot, you disable the default placeholder image and won't incur charges for the Static Maps API. + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/10.rectangle.md b/docs/content/scripts/google-maps/2.api/10.rectangle.md new file mode 100644 index 00000000..74d4c04e --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/10.rectangle.md @@ -0,0 +1,30 @@ +--- +title: +--- + +Rectangular overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsRectangle"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/11.heatmap-layer.md b/docs/content/scripts/google-maps/2.api/11.heatmap-layer.md new file mode 100644 index 00000000..b9a4de88 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/11.heatmap-layer.md @@ -0,0 +1,32 @@ +--- +title: +--- + +Heatmap visualization layer. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsHeatmapLayer"} +:: + +## Usage + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/2.api/12.overlay-view.md b/docs/content/scripts/google-maps/2.api/12.overlay-view.md new file mode 100644 index 00000000..49f86c89 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/12.overlay-view.md @@ -0,0 +1,99 @@ +--- +title: +--- + +Renders arbitrary Vue slot content at a map lat/lng position. Unlike `InfoWindow`, you have full control over HTML structure and styling. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsOverlayView"} +:: + +## Marker Anchoring + +When nested inside a `ScriptGoogleMapsMarker` or `ScriptGoogleMapsAdvancedMarkerElement`, the overlay automatically inherits the marker's position. This makes it a fully customizable alternative to `InfoWindow`, the overlay follows the marker when dragged. + +```vue + +``` + +## Popup on Marker Click + +Using `v-model:open` keeps the overlay mounted, toggling visibility via CSS. This avoids remount cost and preserves internal state. + +```vue + + + +``` + +For simple cases where remounting is acceptable, `v-if` also works: + +```vue + + + + + +``` + +## Persistent Label + +```vue + +``` + +::callout +The `blockMapInteraction` prop (default `true`) calls `google.maps.OverlayView.preventMapHitsAndGesturesFrom()`{lang="ts"} to stop clicks, taps, and drags from propagating through the overlay to the map. Set it to `false` for non-interactive overlays like labels. +:: diff --git a/docs/content/scripts/google-maps/2.api/13.use-script-google-maps.md b/docs/content/scripts/google-maps/2.api/13.use-script-google-maps.md new file mode 100644 index 00000000..0617a28b --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/13.use-script-google-maps.md @@ -0,0 +1,39 @@ +--- +title: useScriptGoogleMaps() +--- + +The [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps/api/use-script-google-maps){lang="ts"} 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. + +```ts +export function useScriptGoogleMaps(_options?: GoogleMapsInput) {} +``` + +Please follow the [Registry Scripts](/docs/guides/registry-scripts) guide to learn more about advanced usage. + +## Example + +Loading the Google Maps SDK and interacting with it programmatically. + +```vue + + + +``` diff --git a/docs/content/scripts/google-maps/2.api/2.marker.md b/docs/content/scripts/google-maps/2.api/2.marker.md new file mode 100644 index 00000000..eb4d5dd0 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/2.marker.md @@ -0,0 +1,43 @@ +--- +title: +--- + +::callout{color="amber" icon="i-heroicons-exclamation-triangle"} +`google.maps.Marker` is deprecated by Google. Use [``{lang="html"}](/scripts/google-maps/api/advanced-marker-element) instead for new projects. +:: + +Classic map marker with icon support. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsMarker"} +:: + +## Usage + +```vue + +``` + +### With Info Window + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md b/docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md new file mode 100644 index 00000000..c7bcb823 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/3.advanced-marker-element.md @@ -0,0 +1,70 @@ +--- +title: +--- + +Modern advanced marker with HTML content support. This is the recommended marker type. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsAdvancedMarkerElement"} +:: + +## Usage + +```vue + +``` + +### Custom Marker Content + +The `#content` slot replaces the default pin with any HTML or Vue content. The slot content becomes the marker's visual on the map. + +```vue + +``` + +::callout +Use `ScriptGoogleMapsPinElement` if you only need to customize colors or glyph while keeping the pin shape. The `#content` slot replaces the marker entirely. If you use both, `PinElement` takes precedence. +:: + +### With Custom Overlay + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/4.pin-element.md b/docs/content/scripts/google-maps/2.api/4.pin-element.md new file mode 100644 index 00000000..11522e81 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/4.pin-element.md @@ -0,0 +1,29 @@ +--- +title: +--- + +Customizable pin marker. Place as a child of [``{lang="html"}](/scripts/google-maps/api/advanced-marker-element). + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsPinElement"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/5.info-window.md b/docs/content/scripts/google-maps/2.api/5.info-window.md new file mode 100644 index 00000000..b7de4b5a --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/5.info-window.md @@ -0,0 +1,27 @@ +--- +title: +--- + +Information window that automatically opens on parent marker click. Place as a child of [``{lang="html"}](/scripts/google-maps/api/marker) or [``{lang="html"}](/scripts/google-maps/api/advanced-marker-element). + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsInfoWindow"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/6.marker-clusterer.md b/docs/content/scripts/google-maps/2.api/6.marker-clusterer.md new file mode 100644 index 00000000..5a3bcbd1 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/6.marker-clusterer.md @@ -0,0 +1,38 @@ +--- +title: +--- + +Groups nearby markers into clusters for cleaner map visualization at lower zoom levels. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsMarkerClusterer"} +:: + +## Installation + +Requires the `@googlemaps/markerclusterer` peer dependency: + +```bash +pnpm add @googlemaps/markerclusterer +``` + +## Usage + +Child markers register and unregister themselves automatically. + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/7.circle.md b/docs/content/scripts/google-maps/2.api/7.circle.md new file mode 100644 index 00000000..93d93803 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/7.circle.md @@ -0,0 +1,28 @@ +--- +title: +--- + +Circular overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsCircle"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/8.polygon.md b/docs/content/scripts/google-maps/2.api/8.polygon.md new file mode 100644 index 00000000..ca5414f3 --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/8.polygon.md @@ -0,0 +1,29 @@ +--- +title: +--- + +Polygon shape overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsPolygon"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/2.api/9.polyline.md b/docs/content/scripts/google-maps/2.api/9.polyline.md new file mode 100644 index 00000000..499029df --- /dev/null +++ b/docs/content/scripts/google-maps/2.api/9.polyline.md @@ -0,0 +1,29 @@ +--- +title: +--- + +Line path overlay on the map. Place inside a [``{lang="html"}](/scripts/google-maps/api/script-google-maps) component. + +::script-types{script-key="google-maps" filter="ScriptGoogleMapsPolyline"} +:: + +## Usage + +```vue + +``` diff --git a/docs/content/scripts/google-maps/index.md b/docs/content/scripts/google-maps/index.md new file mode 100644 index 00000000..c29acb7b --- /dev/null +++ b/docs/content/scripts/google-maps/index.md @@ -0,0 +1,221 @@ +--- +title: Google Maps +description: Show performance-optimized Google Maps in your Nuxt app. +links: + - label: useScriptGoogleMaps + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/registry/google-maps.ts + size: xs + - label: "" + icon: i-simple-icons-github + to: https://github.com/nuxt/scripts/blob/main/src/runtime/components/ScriptGoogleMaps.vue + size: xs +--- + +[Google Maps](https://maps.google.com/) allows you to embed maps in your website and customize them with your content. + +Nuxt Scripts provides a [`useScriptGoogleMaps()`{lang="ts"}](/scripts/google-maps/api/use-script-google-maps){lang="ts"} composable and a headless [``{lang="html"}](/scripts/google-maps/api/script-google-maps){lang="html"} component to interact with the Google Maps. + +::script-types{exclude-components} +:: + +## Types + +To use Google Maps with full TypeScript support, you will need +to install the `@types/google.maps` dependency. + +```bash +pnpm add -D @types/google.maps +``` + +## Setup + +Enable Google Maps in your `nuxt.config` and provide your API key via environment variable: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + scripts: { + registry: { + googleMaps: true, + }, + }, + runtimeConfig: { + public: { + scripts: { + googleMaps: { + apiKey: '', // NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY + }, + }, + }, + }, +}) +``` + +```text [.env] +NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY= +``` + +You must add this. It registers server proxy routes that keep your API key server-side: +- `/_scripts/proxy/google-static-maps` for placeholder images +- `/_scripts/proxy/google-maps-geocode` for location search + +::callout{color="amber"} +You can pass `api-key` directly on the ``{lang="html"} component, but this approach is not recommended, as it exposes your key in client-side requests. +:: + +See [Billing & Permissions](/scripts/google-maps/guides/billing) for API costs and required permissions. + +## Demo + +::code-group + +:google-maps-demo{label="Output"} + +```vue [Input] + + + +``` + +:: + +## Quick Start + +### Minimal Map + +```vue + +``` + +### Markers with Info Windows + +```vue + +``` + +### Custom Marker Content + +The `#content` slot replaces the default pin with any Vue template. + +```vue + + + +``` + +### Custom Popups + +`ScriptGoogleMapsOverlayView` gives full control over popup styling. Use `v-model:open` to toggle without remounting. + +```vue + + + +``` diff --git a/playground/pages/third-parties/google-maps/sfcs.vue b/playground/pages/third-parties/google-maps/sfcs.vue index 3fd79955..79b6d620 100644 --- a/playground/pages/third-parties/google-maps/sfcs.vue +++ b/playground/pages/third-parties/google-maps/sfcs.vue @@ -58,6 +58,19 @@ const geoJsonData = { ], } +const isOverlayViewShown = ref(false) + +const isOverlayViewOnMarkerShown = ref(false) + +const isOverlayViewOnAdvancedMarkerShown = ref(false) + +const isCustomMarkerContentShown = ref(false) + +const isOverlayPopupShown = ref(false) +const overlayPopupOpen = ref(false) + +const zoom = ref(8) + const googleMapsRef = useTemplateRef('googleMapsRef') whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => { @@ -88,9 +101,9 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => { :width="1280" :height="720" above-the-fold + :zoom="zoom" :map-options="{ center: { lat: -34.397, lng: 150.644 }, - zoom: 8, mapId: 'DEMO_MAP_ID', }" > @@ -224,6 +237,80 @@ whenever(() => googleMapsRef.value?.googleMaps, (googleMaps) => { }" /> + +
+ Custom Overlay +

Vue slot content with full reactivity

+
+
+ + + +
+ Drag me! Custom overlay on Marker +
+
+
+ + + +
+ Custom tooltip +
OverlayView on AdvancedMarker
+
+
+
+ + + + + + + + + +
+ Custom Popup +

v-model:open, no remount

+ +
+
+
+ + {{ `${isOverlayViewShown ? 'Hide' : 'Show'} overlay view` }} + + + + + + + + + + + +
+ + +
diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue index e0d6cd90..03f4d3b6 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue @@ -57,6 +57,11 @@ const props = withDefaults(defineProps<{ * A latitude / longitude of where to focus the map. */ center?: google.maps.LatLng | google.maps.LatLngLiteral | `${string},${string}` + /** + * Zoom level for the map (0-21). Reactive: changing this will update the map. + * Takes precedence over mapOptions.zoom when provided. + */ + zoom?: number /** * Should a marker be displayed on the map where the centre is. */ @@ -152,8 +157,12 @@ const currentMapId = computed(() => { const mapsApi = ref() -if (import.meta.dev && !apiKey) - throw new Error('GoogleMaps requires an API key. Please provide `apiKey` on the or globally via `runtimeConfig.public.scripts.googleMaps.apiKey`.') +if (import.meta.dev) { + if (!apiKey) + throw new Error('GoogleMaps requires an API key. Enable it in your nuxt.config:\n\n scripts: {\n registry: {\n googleMaps: true\n }\n }\n\nThen set NUXT_PUBLIC_SCRIPTS_GOOGLE_MAPS_API_KEY in your .env file.\n\nAlternatively, pass `api-key` directly on the component (note: this exposes the key client-side).') + if (!proxyConfig?.enabled && !props.apiKey) + console.warn('[nuxt-scripts] Google Maps proxy is not enabled. Enable `googleMaps` in your nuxt.config registry to keep your API key server-side. See: https://scripts.nuxt.com/scripts/google-maps#setup') +} // TODO allow a null center may need to be resolved via an API function @@ -175,7 +184,7 @@ const { load, status, onLoaded } = useScriptGoogleMaps({ const options = computed(() => { const mapId = props.mapOptions?.styles ? undefined : (currentMapId.value || 'map') - return defu({ center: centerOverride.value, mapId }, props.mapOptions, { + return defu({ center: centerOverride.value, mapId, zoom: props.zoom }, props.mapOptions, { center: props.center, zoom: 15, }) @@ -236,7 +245,7 @@ async function createAdvancedMapMarker(_options?: google.maps.marker.AdvancedMar const queryToLatLngCache = new Map() -async function resolveQueryToLatLang(query: string) { +async function resolveQueryToLatLng(query: string) { if (query && typeof query === 'object') return Promise.resolve(query) if (queryToLatLngCache.has(query)) { @@ -319,7 +328,7 @@ const googleMaps = { googleMaps: mapsApi, map, createAdvancedMapMarker, - resolveQueryToLatLang, + resolveQueryToLatLng, importLibrary, } as const @@ -383,7 +392,7 @@ onMounted(() => { if (center) { if (isLocationQuery(center) && ready.value) { // need to resolve center from query - center = await resolveQueryToLatLang(center as string) + center = await resolveQueryToLatLng(center as string) } map.value!.setCenter(center as google.maps.LatLng) if (props.centerMarker) { @@ -418,7 +427,7 @@ onMounted(() => { map.value = new mapsApi.value!.Map(mapEl.value!, _options) if (center && isLocationQuery(center)) { // need to resolve center - centerOverride.value = await resolveQueryToLatLang(center) + centerOverride.value = await resolveQueryToLatLng(center) map.value?.setCenter(centerOverride.value) } ready.value = true diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue index b13fd3a4..970804f9 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMapsAdvancedMarkerElement.vue @@ -1,5 +1,6 @@ diff --git a/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue b/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue index 163687d8..712c6fa6 100644 --- a/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue +++ b/src/runtime/components/GoogleMaps/ScriptGoogleMapsCircle.vue @@ -1,5 +1,6 @@