places-autocomplete-svelte
Version:
A flexible, accessible, and secure Svelte component leveraging the Google Maps Places Autocomplete API (New) to provide a user-friendly way to search for and retrieve detailed address information.
341 lines (257 loc) • 15.4 kB
Markdown
# Places (New) Autocomplete Svelte
[](https://badge.fury.io/js/places-autocomplete-svelte)
[](https://opensource.org/licenses/MIT)
A flexible, accessible, and secure [Svelte](https://kit.svelte.dev) component leveraging the [Google Maps Places Autocomplete API (New)](https://developers.google.com/maps/documentation/javascript/place-autocomplete-overview).
The component handles API loading, session tokens, debounced fetching, and accessibility, allowing you to focus on building your application. It intelligently manages the Google Maps API loader, creating a shared instance via Svelte's context that prevents conflicts with other map components on the same page.
**Two initialisation patterns:**
- **Simple/Automatic**: Pass your API key directly to the component for basic use cases
- **Advanced/Manual**: Initialise the loader once in a parent component when using multiple Google Maps libraries or components
## Available: Standalone JavaScript Library
Need this functionality for a non-Svelte project? Check out our companion vanilla JavaScript library, `places-autocomplete-js`, which offers the same core Google Places (New) Autocomplete features.
[View `places-autocomplete-js` on GitHub](https://github.com/alexpechkarev/places-autocomplete-js)
## Features
* Integrates with the modern **Google Maps Places Autocomplete API (New)**.
* **Automatic Shared Loader:** Intelligently creates a single Google Maps loader instance and shares it via Svelte's context.
* **Highly Accessible:** Follows WAI-ARIA patterns for comboboxes, with full keyboard navigation and screen reader support.
* **Secure:** Safely renders suggestions to protect against XSS attacks.
* Automatically handles **session tokens** for cost management.
* **Debounced Input:** Limits API calls while the user is typing (configurable).
* **Suggestion Highlighting:** Automatically highlights the portion of text matching the user's input.
* **Imperative API:** Exposes `clear()`, `focus()`, and `getRequestParams()` methods for direct control.
* **Customisable Styling:** Easily override default styles using the `options.classes` prop.
* **TypeScript Support:** Fully written in TypeScript with included type definitions.
* **Event Handling:** Provides `onResponse` and `onError` callbacks.
## Demo
See a live demo of the component in action: [Basic Example](https://places-autocomplete-demo.pages.dev/)
[Reactive parameters](https://places-autocomplete-demo.pages.dev/examples/reactive-parameters) - change the search criteria based on user input, like filtering by country or change results language.
[Customise request parameters](https://places-autocomplete-demo.pages.dev/examples/customise-request-parameters) - construct a `requestParams` object and control various aspects of the search, including language, region, and more.
[Retain Input Value After Selection](https://places-autocomplete-demo.pages.dev/examples/retain-input-value) -
This example demonstrates how to configure the component to keep the selected address visible in the input field after a suggestion is chosen.
<img src="places-autocomplete-svelte.gif" alt="A video demonstrating the Places Autocomplete Svelte component in action, showing address suggestions and selection.">
## Requirements
* **Google Maps API Key** with the "Places API" enabled. Refer to [Use API Keys](https://developers.google.com/maps/documentation/javascript/get-api-key) for detailed instructions.
## Installation
```bash
npm install places-autocomplete-svelte
# or
yarn add places-autocomplete-svelte
```
## Usage
### Basic Usage (Automatic Initialisation)
For simple use cases, just pass your API key to the component. It will automatically handle the Google Maps loader initialisation:
```javascript
<script lang="ts">
import { PlaceAutocomplete } from 'places-autocomplete-svelte';
import type { PlaceResult } from 'places-autocomplete-svelte/interfaces';
// Get API Key securely (e.g., from environment variables)
const PUBLIC_GOOGLE_MAPS_API_KEY = import.meta.env.VITE_PUBLIC_GOOGLE_MAPS_API_KEY;
const handleResponse = (response: PlaceResult) => {
console.log('Selected:', response.formattedAddress);
};
const handleError = (error: string) => {
console.error('Error:', error);
};
</script>
<PlaceAutocomplete
{PUBLIC_GOOGLE_MAPS_API_KEY}
onResponse={handleResponse}
onError={handleError}
/>
```
### Advanced Usage (Manual Initialisation)
For applications that need multiple Google Maps libraries (e.g., `places`, `maps`, `marker`) or multiple map components, initialise the loader once in a parent component. This approach:
- Loads all required libraries in a single API call (more efficient)
- Prevents "Loader must not be called again" errors
- Shares the loader instance across all child components via Svelte context
- Works seamlessly with SvelteKit's SSR (only initialises in the browser)
**When to use manual initialisation:**
- Using multiple Google Maps components on the same page
- Need to load multiple libraries (`maps`, `marker`, `geometry`, etc.)
- Building a layout that shares map functionality across routes
- Want centralised error handling for the loader
```javascript
// In +layout.svelte or +page.svelte
<script lang="ts">
import { browser } from '$app/environment';
import { PlaceAutocomplete } from 'places-autocomplete-svelte';
import { setGMapsContext, initialiseGMaps, importLibrary } from 'places-autocomplete-svelte/gmaps';
import { onMount } from 'svelte';
// 1. Set the context at the top level (must be synchronous)
setGMapsContext();
// 2. Initialise the loader in the browser
if (browser) {
initialiseGMaps({
key: import.meta.env.VITE_PUBLIC_GOOGLE_MAPS_API_KEY,
v: 'weekly'
}).catch((error) => {
console.error('Failed to initialise Google Maps:', error);
});
}
// 3. Load additional libraries as needed
let map: google.maps.Map;
onMount(async () => {
const { Map } = await importLibrary('maps');
const { AdvancedMarkerElement } = await importLibrary('marker');
const mapElement = document.getElementById('map');
if (mapElement) {
map = new Map(mapElement, {
center: { lat: 51.5072, lng: -0.1276 },
zoom: 10,
mapId: 'YOUR_MAP_ID'
});
}
});
// 4. Handle autocomplete responses
const handleResponse = (response: PlaceResult) => {
console.log('Selected:', response.formattedAddress);
// Update map with selected location
if (response.location && map) {
map.setCenter(response.location);
map.setZoom(15);
}
};
const handleError = (error: string) => {
console.error('Error:', error);
};
</script>
<!-- The component automatically uses the shared context -->
<!-- No need to pass PUBLIC_GOOGLE_MAPS_API_KEY when using manual initialisation -->
<PlaceAutocomplete
onResponse={handleResponse}
onError={handleError}
/>
<div id="map" class="h-96 w-full"></div>
```
**Available helper functions from `places-autocomplete-svelte/gmaps`:**
- `setGMapsContext()` - Creates the shared context (call once at the top level)
- `getGMapsContext()` - Retrieves the context (returns stores for initialisation state and errors)
- `hasGMapsContext()` - Checks if context exists (useful for conditional logic)
- `initialiseGMaps(options)` - Initialises the loader with your API key and options
- `initialiseGMapsNoContext(options)` - Initialises without context (for edge cases)
- `importLibrary(library)` - Dynamically imports Google Maps libraries
## Security
### API Key Security
Your Google Maps API Key is a sensitive credential. To prevent unauthorised use and unexpected charges, you **must** restrict it.
1. Go to the [Google Cloud Console](https://console.cloud.google.com/google/maps-apis/credentials).
2. Select your API key.
3. Under **Application restrictions**, select **HTTP referrers (web sites)** and add your application's domain(s) (e.g., `your-domain.com/*`).
4. Under **API restrictions**, select **Restrict key** and choose the APIs you are using (e.g., **Places API**, **Maps JavaScript API**).
### XSS Protection
This component is designed to be secure out-of-the-box. It safely renders user-input and API responses to prevent Cross-Site Scripting (XSS) vulnerabilities.
## Accessibility
This component is built to be accessible and follows the [WAI-ARIA Authoring Practices for a Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
* **Keyboard Navigation:** Users can navigate suggestions using `ArrowUp`, `ArrowDown`, select with `Enter`, and close the list with `Escape`.
* **Screen Reader Support:** Uses `role="combobox"`, `aria-autocomplete`, `aria-expanded`, and `aria-activedescendant` to provide a clear experience for screen reader users.
* **Focus Management:** Focus remains on the input field while navigating the suggestion list.
## Props
| Prop | Type | Required | Default | Description |
| :--- | :--- | :--- | :--- | :--- |
| `PUBLIC_GOOGLE_MAPS_API_KEY` | `string` | No* | - | Your Google Maps API Key. **Required for automatic initialisation.** Optional if you've initialised the loader in a parent component using `initialiseGMaps()`. |
| `onResponse` | `(response: PlaceResult) => void` | Yes | - | Callback triggered when a user selects a place. Receives the full place details object. |
| `onError` | `(error: string) => void` | Yes | - | Callback triggered when an error occurs (API loading, network issues, etc.). |
| `fetchFields` | `string[]` | No | `['formattedAddress', 'addressComponents']` | Place Data Fields to request from the API. See [Place Data Fields](https://developers.google.com/maps/documentation/javascript/place-data-fields). **Affects API billing.** |
| `requestParams` | `Partial<RequestParams>` | No | `{ inputOffset: 3 }` | Parameters for the Autocomplete API request (language, region, location bias, etc.). See RequestParams interface. |
| `options` | `Partial<ComponentOptions>` | No | `{ debounce: 100 }` | UI and behavior options (placeholder, debounce delay, distance display, custom classes, etc.). See ComponentOptions interface. |
*Either `PUBLIC_GOOGLE_MAPS_API_KEY` prop OR manual initialisation with `initialiseGMaps()` is required.
## Component Methods (Imperative API)
Get a reference to the component instance using `bind:this` to call its methods directly.
**Example:**
```javascript
<script lang="ts">
import PlaceAutocomplete from 'places-autocomplete-svelte';
let autocompleteComponent: PlaceAutocomplete | undefined = $state(undefined);
</script>
<PlaceAutocomplete bind:this={autocompleteComponent} ... />
<button onclick={() => autocompleteComponent?.clear()}>Clear</button>
<button onclick={() => autocompleteComponent?.focus()}>Focus</button>
```
| Method | Signature | Description |
| :--- | :--- | :--- |
| `clear()` | `() => void` | Clears the input, removes suggestions, and resets the session token. |
| `focus()` | `() => void` | Sets focus on the text input field. |
| `getRequestParams()` | `() => RequestParams` | Returns the current internal `requestParams` object. |
## Options
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `placeholder` | `string` | `''` | Placeholder text for the input field. |
| `debounce` | `number` | `100` | Delay in ms before firing API request. Set to `0` to disable. |
| `distance` | `boolean` | `true` | Show distance from `requestParams.origin` (if provided). |
| `distance_units` | `'km' \| 'miles'` | `'km'` | Units for displaying distance. |
| `label` | `string` | `''` | Optional label text displayed above the input. |
| `autofocus` | `boolean` | `false` | Automatically focus the input on mount. |
| `autocomplete` | `string` | `'off'` | The `autocomplete` attribute for the input field. |
| `classes` | `Partial<ComponentClasses>` | `{}` | Object to override default CSS classes. See Styling section. |
| `clear_input` | `boolean` | `true` | If `false`, retains the `formattedAddress` in the input after selection. |
## Styling (`options.classes`)
Customise the component by providing your own CSS classes via `options.classes`.
**Available Class Keys:**
* `section`: The main container `section`.
* `container`: The `div` containing the input and suggestions list.
* `label`: The `label` element.
* `input`: The main text `input` element.
* `icon_container`: Container for the optional icon.
* `icon`: SVG string for the icon.
* `ul`: The `<ul>` element for the suggestions list.
* `li`: Each `<li>` suggestion item.
* `li_current`: Class added to the currently highlighted `<li>`.
* `li_div_container`: Container `div` within each list item.
* `li_div_one`: First inner `div` (contains the main text).
* `li_div_one_p`: The `<p>` tag containing the main suggestion text.
* `li_div_two`: Second inner `div` (contains the distance).
* `li_div_two_p`: The `<p>` tag containing the distance text.
* `kbd_container`: Container for the keyboard hint keys.
* `kbd_escape`: The `<kbd>` tag for the 'Esc' hint.
* `kbd_up`: The `<kbd>` tag for the 'Up Arrow' hint.
* `kbd_down`: The `<kbd>` tag for the 'Down Arrow' hint.
* `highlight`: The class applied to the `<span>` wrapping the matched text. Defaults to `'font-bold'`.
**Example:**
```javascript
const options = {
classes: {
input: 'form-input w-full rounded-md shadow-sm',
ul: 'absolute bg-white shadow-lg rounded-md mt-1 w-full z-10',
li_current: 'bg-blue-500 text-white',
highlight: 'text-blue-700 font-semibold'
}
};
```
## Events
* **`onResponse`**: `(response: PlaceResult) => void`
* Fired after a user selects a suggestion and `fetchFields` are retrieved.
* **`onError`**: `(error: string) => void`
* Fired on any error (API loading, fetching suggestions, etc.).
## TypeScript
This component is written in TypeScript with full type definitions included.
**Available imports:**
```typescript
// Component
import { PlaceAutocomplete } from 'places-autocomplete-svelte';
// Types and interfaces
import type {
PlaceResult,
ComponentOptions,
RequestParams,
FormattedAddress,
ComponentClasses,
Props
} from 'places-autocomplete-svelte/interfaces';
// Google Maps loader helpers
import {
setGMapsContext,
getGMapsContext,
hasGMapsContext,
initialiseGMaps,
initialiseGMapsNoContext,
importLibrary,
type GMapsContext,
type APIOptions
} from 'places-autocomplete-svelte/gmaps';
```
## Google Places API & Billing
* This component uses the Google Maps JavaScript API (Places library). Usage is subject to Google's terms and pricing.
* It uses **Session Tokens** automatically to group Autocomplete requests, which can reduce costs.
* Place Details requests (via `fetchFields`) are billed separately. Only request the fields you need to manage costs.
## Contributing
Contributions are welcome! Please feel free to open an issue or submit a pull request.
## License
[MIT](LICENSE)