UNPKG

@cdmbase/react-google-autocomplete

Version:
157 lines (156 loc) 4.72 kB
import React, { useEffect, useRef, useCallback, useState } from "react"; import { debounce as debounceFn } from "lodash-es"; import { loadGoogleMapScript, isBrowser } from "./utils.js"; import { GOOGLE_MAP_SCRIPT_BASE_URL } from "./constants.js"; function useGoogleMapsApi(config) { const [isApiLoaded, setApiLoaded] = useState(false); const loadGoogleMapsApi = (url) => { if (!window.google) { const script = document.createElement("script"); script.src = url; script.async = true; script.defer = true; script.addEventListener("load", () => { setApiLoaded(true); }); document.body.appendChild(script); } else { setApiLoaded(true); } }; const debouncedFunction = useCallback( debounceFn((url) => { loadGoogleMapsApi(url); }, config.debounce), // Adjust the debounce delay as needed [] ); useEffect(() => { if (config.url) { debouncedFunction(config.url); } return () => { debouncedFunction.cancel(); }; }, [config, debouncedFunction]); return isApiLoaded; } function usePlacesWidget(props) { const { ref, onPlaceSelected, apiKey, libraries = "places", inputAutocompleteValue = "new-password", debounce = 500, options: { types = ["(cities)"], componentRestrictions, fields = [ "address_components", "geometry.location", "place_id", "formatted_address" ], bounds, ...options } = {}, googleMapsScriptBaseUrl = GOOGLE_MAP_SCRIPT_BASE_URL, language } = props; const inputRef = useRef(null); const event = useRef(null); const autocompleteRef = useRef(null); const observerHack = useRef(null); const languageQueryParam = language ? `&language=${language}` : ""; const googleMapsScriptUrl = `${googleMapsScriptBaseUrl}?libraries=${libraries}&key=${apiKey}${languageQueryParam}&loading=async`; const isLoaded = useGoogleMapsApi({ url: googleMapsScriptUrl, debounce }); useEffect(() => { const config = { ...options, fields, types, bounds }; if (componentRestrictions) { config.componentRestrictions = componentRestrictions; } if (autocompleteRef.current || !inputRef.current || !isBrowser) return; if (ref && !ref.current) ref.current = inputRef.current; const handleAutoComplete = () => { if (typeof google === "undefined") return console.error( "Google has not been found. Make sure your provide apiKey prop." ); if (!google.maps?.places) return console.error("Google maps places API must be loaded."); if (!(inputRef.current instanceof HTMLInputElement)) return console.error("Input ref must be HTMLInputElement."); autocompleteRef.current = new google.maps.places.Autocomplete( inputRef.current, config ); if (autocompleteRef.current) { event.current = autocompleteRef.current.addListener( "place_changed", () => { if (onPlaceSelected && autocompleteRef && autocompleteRef.current) { onPlaceSelected( autocompleteRef.current.getPlace(), inputRef.current, autocompleteRef.current ); } } ); } }; if (isLoaded) { handleAutoComplete(); } return () => event.current ? event.current.remove() : void 0; }, [isLoaded]); useEffect(() => { if (!React?.version?.startsWith("18") && isBrowser && window.MutationObserver && inputRef.current && inputRef.current instanceof HTMLInputElement) { observerHack.current = new MutationObserver(() => { observerHack.current.disconnect(); if (inputRef.current) { inputRef.current.autocomplete = inputAutocompleteValue; } }); observerHack.current.observe(inputRef.current, { attributes: true, attributeFilter: ["autocomplete"] }); } }, [inputAutocompleteValue]); useEffect(() => { if (autocompleteRef.current) { autocompleteRef.current.setFields(fields); } }, [fields]); useEffect(() => { if (autocompleteRef.current) { autocompleteRef.current.setBounds(bounds); } }, [bounds]); useEffect(() => { if (autocompleteRef.current) { autocompleteRef.current.setComponentRestrictions(componentRestrictions); } }, [componentRestrictions]); useEffect(() => { if (autocompleteRef.current) { autocompleteRef.current.setOptions(options); } }, [options]); return { ref: inputRef, autocompleteRef }; } export { usePlacesWidget as default };