Last active
June 7, 2025 10:34
-
-
Save jeneg/587b444f455f8fdec3bd1566ea40eb09 to your computer and use it in GitHub Desktop.
Google Places Autocomplete Directive (SSR ready)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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