Skip to content

Instantly share code, notes, and snippets.

@jeneg
Last active June 7, 2025 10:34
Show Gist options
  • Save jeneg/587b444f455f8fdec3bd1566ea40eb09 to your computer and use it in GitHub Desktop.
Save jeneg/587b444f455f8fdec3bd1566ea40eb09 to your computer and use it in GitHub Desktop.
Google Places Autocomplete Directive (SSR ready)
import {
Directive,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
OnDestroy,
OnInit,
Output,
PLATFORM_ID,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
// 1. First, ensure you have the Google Maps types installed:
// npm install --save-dev @types/google.maps
// 2. Add the Google Maps script to your index.html.
// Remember to replace YOUR_API_KEY with your actual Google Cloud API key.
// Using importLibrary (as done in the directive) is the modern approach.
// <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"></script>
declare var google: any;
@Directive({
// The selector makes the directive usable as an attribute on any element.
selector: '[appGooglePlacesAutocomplete]',
exportAs: 'googlePlacesAutocomplete', // Allows you to get a reference to the directive in your template
})
export class GooglePlacesAutocompleteDirective implements OnInit, OnDestroy {
/**
* Input to pass autocomplete options to the Google Maps API.
* See: https://developers.google.com/maps/documentation/javascript/reference/places-widget#AutocompleteOptions
*/
@Input() options?: google.maps.places.AutocompleteOptions;
/**
* Event emitter that fires when a place is selected from the autocomplete dropdown.
* The emitted value is the full PlaceResult object.
*/
@Output() placeChanged = new EventEmitter<google.maps.places.PlaceResult>();
private autocomplete: google.maps.places.Autocomplete | undefined;
private eventListener: google.maps.MapsEventListener | undefined;
constructor(
private elementRef: ElementRef<HTMLInputElement>,
private ngZone: NgZone, // Inject NgZone to run event listener outside of Angular's zone
@Inject(PLATFORM_ID) private platformId: Object, // Inject PLATFORM_ID to check if running in a browser
) {}
async ngOnInit(): Promise<void> {
// Check if we are running in a browser environment before executing any browser-specific code.
if (isPlatformBrowser(this.platformId)) {
// Defensive check to ensure the Google Maps script has been loaded.
if (typeof google === 'undefined' || !google.maps) {
console.error('Google Maps JavaScript API is not loaded.');
return;
}
try {
// Asynchronously load the 'places' library
const placesLibrary: google.maps.PlacesLibrary = (await google.maps.importLibrary(
'places',
)) as google.maps.PlacesLibrary;
if (!placesLibrary) {
console.error('Google Maps "places" library failed to load.');
return;
}
// The Autocomplete class is on the returned library object
const Autocomplete = placesLibrary.Autocomplete;
// Instantiate the Autocomplete service on the host input element.
this.autocomplete = new Autocomplete(this.elementRef.nativeElement, this.options);
// Listen for the 'place_changed' event.
this.eventListener = this.autocomplete.addListener('place_changed', () => {
const place = this.autocomplete?.getPlace();
// When a place is selected, emit the event.
// We run this inside NgZone to ensure that change detection runs properly,
// as the Google Maps event runs outside of Angular's zone.
if (place) {
this.ngZone.run(() => {
this.placeChanged.emit(place);
});
}
});
} catch (error) {
console.error('Error loading Google Maps places library:', error);
}
}
}
/**
* Lifecycle hook that cleans up the component when it's destroyed.
*/
ngOnDestroy(): void {
// Also check for browser environment here before touching the DOM or window-specific objects.
if (isPlatformBrowser(this.platformId)) {
// Clean up the event listener to prevent memory leaks.
if (this.eventListener) {
this.eventListener.remove();
}
// Google Maps creates a separate container for predictions that needs to be cleaned up.
// We do this by finding the container associated with our input element and removing it.
if (this.autocomplete) {
// The pac-container is the class name Google gives to the dropdown.
const pacContainers = document.querySelectorAll('.pac-container');
pacContainers.forEach((container) => {
container.remove();
});
// Clear all listeners from the autocomplete instance
if (google && google.maps && google.maps.event) {
google.maps.event.clearInstanceListeners(this.autocomplete);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment