diff --git a/package-lock.json b/package-lock.json index 78daf4356..5c2bba890 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@vis.gl/react-google-maps": "1.8.3", "dotenv": "^17.4.2", "react": "^19.2.8", - "react-dom": "^19.2.7" + "react-dom": "^19.2.8" }, "devDependencies": { "@eslint/css": "^1.4.0", @@ -1387,6 +1387,10 @@ "resolved": "samples/rectangle-event", "link": true }, + "node_modules/@js-api-samples/rgm-autocomplete": { + "resolved": "samples/rgm-autocomplete", + "link": true + }, "node_modules/@js-api-samples/routes-compute-routes": { "resolved": "samples/routes-compute-routes", "link": true @@ -5647,15 +5651,15 @@ } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/readable-stream": { @@ -6931,6 +6935,11 @@ "name": "@js-api-samples/polyline-utility", "version": "1.0.0" }, + "samples/react-circle-simple": { + "name": "@js-api-samples/react-circle-simple", + "version": "1.0.0", + "extraneous": true + }, "samples/react-ui-kit-place-details": { "name": "@js-api-samples/react-ui-kit-place-details", "version": "1.0.0" @@ -6964,10 +6973,19 @@ "@vitejs/plugin-react": "^6.0.2" } }, + "samples/react-web-components-markers": { + "name": "@js-api-samples/react-web-components-markers", + "version": "1.0.0", + "extraneous": true + }, "samples/rectangle-event": { "name": "@js-api-samples/rectangle-event", "version": "1.0.0" }, + "samples/rgm-autocomplete": { + "name": "@js-api-samples/rgm-autocomplete", + "version": "1.0.0" + }, "samples/routes-compute-routes": { "name": "@js-api-samples/routes-compute-routes", "version": "1.0.0" diff --git a/package.json b/package.json index a6af3ad9b..e50e50d4f 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@vis.gl/react-google-maps": "1.8.3", "dotenv": "^17.4.2", "react": "^19.2.8", - "react-dom": "^19.2.7" + "react-dom": "^19.2.8" }, "overrides": { "fast-xml-parser": "5.7.1", diff --git a/samples/new-react-sample.sh b/samples/new-react-sample.sh new file mode 100755 index 000000000..a0bdfa805 --- /dev/null +++ b/samples/new-react-sample.sh @@ -0,0 +1,125 @@ +#!/bin/bash + +# A script to generate boilerplate for a new Google Maps React sample. +# Usage: ./new-react-sample.sh + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +NAME=$1 +# Replace hyphens with underscores for the region tag +REGION_TAG="maps_${NAME//-/_}" +TITLE="React - ${NAME//-/ }" + +# Create the directory +mkdir -p "$NAME/src" + +# Create src/app.tsx +cat > "$NAME/src/app.tsx" << EOF +/** + * @license + * Copyright 2026 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START ${REGION_TAG}_app] +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { APIProvider, Map } from '@vis.gl/react-google-maps'; + +const API_KEY = 'GOOGLE_MAPS_API_KEY'; + +export default function App() { + return ( + + + {/* Add map components here */} + + + ); +} + +export function renderToDom(container: HTMLElement) { + const root = createRoot(container); + root.render( + + + + ); +} +// [END ${REGION_TAG}_app] +EOF + +# Create index.html +cat > "$NAME/index.html" << EOF + + + + + + + + $TITLE + + + + +
+ + + +EOF + +# Create package.json +cat > "$NAME/package.json" << EOF +{ + "name": "@js-api-samples/$NAME", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "bash ../build-single.sh", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --config ../../vite.config.js --base './' && vite --config ../../vite.config.js", + "build:vite": "vite build --config ../../vite.config.js --base './'", + "preview": "vite preview --config ../../vite.config.js" + }, + "author": "Google LLC" +} +EOF + +# Create tsconfig.json +cat > "$NAME/tsconfig.json" << EOF +{ + "extends": "../../tsconfig.react-base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["./src/**/*.ts*"] +} +EOF + +echo "Created React sample '$NAME' successfully!" diff --git a/samples/rgm-autocomplete/README.md b/samples/rgm-autocomplete/README.md new file mode 100644 index 000000000..14648200c --- /dev/null +++ b/samples/rgm-autocomplete/README.md @@ -0,0 +1,70 @@ +# Google Maps JavaScript Sample + +## rgm-autocomplete + +React Google Maps Library - Place Autocomplete + +This sample demonstrates using the Place Autocomplete Element within a React application using the open source vis.gl/react-google-maps library. + +## Setup + +### Before starting run: + +`npm i` + +### Run an example on a local web server + +`cd samples/rgm-autocomplete` +`npm start` + +### Build an individual example + +`cd samples/rgm-autocomplete` +`npm run build` + +From 'samples': + +`npm run build --workspace=rgm-autocomplete/` + +### Build all of the examples. + +From 'samples': + +`npm run build-all` + +### Run lint to check for problems + +`cd samples/rgm-autocomplete` +`npx eslint index.ts` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). + +## Integrating Place Autocomplete & The New Place Class + +When integrating Google Maps Place Autocomplete within a React application, this sample implements several key best practices and addresses common issues developers encounter with the new Places API. + +### 1. Programmatic Instantiation + +Instead of rendering the `` Web Component directly in JSX, this sample programmatically instantiates it using `new placesLibrary.PlaceAutocompleteElement()` and appends it to a React `ref`. + +- **Issue:** React's synthetic event system doesn't always seamlessly handle custom Web Component events (like `gmp-select`). Instantiating the element programmatically and attaching standard DOM event listeners ensures events are captured reliably. + +### 2. Location Restriction & Cross-Context Objects + +When placing the autocomplete Web Component outside the main DOM tree of the map, it may lose automatic context of the map's viewport. To guarantee that search predictions are strictly biased or restricted to the map's current bounds, this sample manually syncs the map's bounds to the autocomplete's `locationRestriction` property. + +- **Issue:** Passing complex Google Maps objects (like `LatLngBounds`) directly across the React boundary can sometimes fail due to cross-context `instanceof` checks. Always use `.toJSON()` (e.g., `map.getBounds().toJSON()`) when assigning bounds to bypass these issues. This ensures users see search predictions relevant to their map view. + +### 3. Handling Selections: `toPlace()` and `fetchFields()` + +When a user selects an item from the autocomplete dropdown, the component fires a `gmp-select` event containing a `placePrediction`. + +- **Issue:** The prediction is _not_ a fully populated Place object. You must convert it using `placePrediction.toPlace()` and then explicitly request the data you need by calling `place.fetchFields({ fields: ['location', 'displayName', 'formattedAddress'] })`. +- If you attempt to access a property on the `Place` object that hasn't been fetched, it will be undefined or throw an error. This is a core design principle of the new Places API to ensure you only request (and pay for) the data you use. + +### 4. Event Cleanup + +Always remove standard DOM event listeners (e.g., `autocomplete.removeEventListener`) and Maps event listeners (`google.maps.event.removeListener`) in your `useEffect` cleanup function to prevent memory leaks when the React component unmounts. diff --git a/samples/rgm-autocomplete/index.html b/samples/rgm-autocomplete/index.html new file mode 100644 index 000000000..577474987 --- /dev/null +++ b/samples/rgm-autocomplete/index.html @@ -0,0 +1,35 @@ + + + + + + + + React - react place autocomplete map + + + + +
+ + + diff --git a/samples/rgm-autocomplete/package.json b/samples/rgm-autocomplete/package.json new file mode 100644 index 000000000..e79e02971 --- /dev/null +++ b/samples/rgm-autocomplete/package.json @@ -0,0 +1,13 @@ +{ + "name": "@js-api-samples/rgm-autocomplete", + "version": "1.0.0", + "type": "module", + "scripts": { + "build": "bash ../build-single.sh", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --config ../../vite.config.js --base './' && vite --config ../../vite.config.js", + "build:vite": "vite build --config ../../vite.config.js --base './'", + "preview": "vite preview --config ../../vite.config.js" + }, + "author": "Google LLC" +} diff --git a/samples/rgm-autocomplete/src/app.tsx b/samples/rgm-autocomplete/src/app.tsx new file mode 100644 index 000000000..a681fd00b --- /dev/null +++ b/samples/rgm-autocomplete/src/app.tsx @@ -0,0 +1,173 @@ +/** + * @license + * Copyright 2026 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +// [START maps_rgm_autocomplete] +import React, { useState, useEffect, useRef } from 'react'; +import { createRoot } from 'react-dom/client'; +import { + APIProvider, + Map, + MapControl, + ControlPosition, + AdvancedMarker, + InfoWindow, + useMap, + useMapsLibrary, + useAdvancedMarkerRef, +} from '@vis.gl/react-google-maps'; + +const API_KEY = 'GOOGLE_MAPS_API_KEY'; + +declare global { + namespace JSX { + interface IntrinsicElements { + 'gmp-place-autocomplete': React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLElement + >; + } + } +} + +const PlaceAutocomplete = ({ + onPlaceSelect, +}: { + onPlaceSelect: (place: google.maps.places.Place | null) => void; +}) => { + const map = useMap(); + const placesLibrary = useMapsLibrary('places'); + const containerRef = useRef(null); + + useEffect(() => { + if (!map || !placesLibrary || !containerRef.current) return; + + // 1. Programmatically instantiate the modern PlaceAutocompleteElement + const autocomplete = new placesLibrary.PlaceAutocompleteElement(); + containerRef.current.appendChild(autocomplete); + + // 2. Manually sync the map's bounds to the autocomplete's locationRestriction. + // We use map.getBounds().toJSON() to pass a plain object literal, which safely + // bypasses any cross-context 'instanceof' wipeout issues in React. + const syncBounds = () => { + const bounds = map.getBounds(); + if (bounds) { + autocomplete.locationRestriction = bounds.toJSON(); + } + }; + + // Sync initially and whenever the map moves. + syncBounds(); + const boundsListener = map.addListener('bounds_changed', syncBounds); + + // 3. Listen for the gmp-select event. + const placeSelectListener = (e: Event) => { + const event = e as google.maps.places.PlacePredictionSelectEvent; + const place = event.placePrediction.toPlace(); + + void place + .fetchFields({ + fields: [ + 'location', + 'viewport', + 'displayName', + 'formattedAddress', + ], + }) + .then(() => { + if (place.viewport) { + map.fitBounds(place.viewport); + } else if (place.location) { + map.setCenter(place.location); + map.setZoom(13); + } + onPlaceSelect(place); + }) + .catch((err: unknown) => { + console.error(err); + }); + }; + + autocomplete.addEventListener('gmp-select', placeSelectListener); + + return () => { + google.maps.event.removeListener(boundsListener); + autocomplete.removeEventListener('gmp-select', placeSelectListener); + + // Clean up the DOM element when unmounting. + if (containerRef.current) { + containerRef.current.innerHTML = ''; + } + }; + }, [map, placesLibrary, onPlaceSelect]); + + return ( +
+
+
+ ); +}; + +export default function App() { + const [selectedPlace, setSelectedPlace] = + useState(null); + const [markerRef, marker] = useAdvancedMarkerRef(); + + return ( + + + + + + + {selectedPlace?.location && ( + + )} + + {selectedPlace?.location && marker && ( + +
+ + {selectedPlace.displayName ?? 'No name'} + +
+ + {selectedPlace.formattedAddress ?? 'No address'} + +
+
+ )} +
+
+ ); +} + +export function renderToDom(container: HTMLElement) { + const root = createRoot(container); + root.render( + + + + ); +} +// [END maps_rgm_autocomplete] diff --git a/samples/rgm-autocomplete/style.css b/samples/rgm-autocomplete/style.css new file mode 100644 index 000000000..cbf89fd6d --- /dev/null +++ b/samples/rgm-autocomplete/style.css @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2024 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_rgm_autocomplete] */ +body { + margin: 0; + font-family: sans-serif; +} + +#app { + width: 100vw; + height: 100vh; +} + +.autocomplete-container input, +.autocomplete-control { + box-sizing: border-box; +} + +.autocomplete-control { + margin: 24px; + background: #fff; +} + +.autocomplete-container { + width: 300px; +} + +.autocomplete-container input { + width: 100%; + height: 40px; + padding: 0 12px; + font-size: 18px; +} + +.autocomplete-container .custom-list { + width: 100%; + list-style: none; + padding: 0; + margin: 0; +} + +.autocomplete-container .custom-list-item { + padding: 8px; +} + +.autocomplete-container .custom-list-item:hover { + background: lightgrey; + cursor: pointer; +} + +/* [END maps_rgm_autocomplete] */ diff --git a/samples/rgm-autocomplete/tsconfig.json b/samples/rgm-autocomplete/tsconfig.json new file mode 100644 index 000000000..b7054a4ed --- /dev/null +++ b/samples/rgm-autocomplete/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.react-base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["./src/**/*.ts*"] +}