@cdmbase/react-google-autocomplete
Version:
React component for google autocomplete.
157 lines (156 loc) • 4.72 kB
JavaScript
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
};