Skip to content

Commit

Permalink
[autocomplete][docs] Improve Google Maps search example
Browse files Browse the repository at this point in the history
  • Loading branch information
oliviertassinari committed Dec 9, 2024
1 parent b3ab4cf commit 5ce77c0
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 135 deletions.
150 changes: 94 additions & 56 deletions docs/data/material/components/autocomplete/GoogleMaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,124 @@ import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import LocationOnIcon from '@mui/icons-material/LocationOn';
import Grid from '@mui/material/Grid';
import Grid2 from '@mui/material/Grid2';
import Typography from '@mui/material/Typography';
import parse from 'autosuggest-highlight/parse';
import { debounce } from '@mui/material/utils';
import throttle from 'lodash/throttle';

// This key was created specifically for the demo in mui.com.
// You need to create a new one for your application.
const GOOGLE_MAPS_API_KEY = 'AIzaSyC3aviU6KHXAjoSnxcw6qbOhjnFctbxPkE';

function loadScript(src, position, id) {
if (!position) {
return;
}
const useEnhancedEffect =
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;

function loadScript(src, position) {
const script = document.createElement('script');
script.setAttribute('async', '');
script.setAttribute('id', id);
script.src = src;
position.appendChild(script);
return script;
}

const autocompleteService = { current: null };
const fetch = throttle(async (request, callback) => {
const { suggestions } =
await window.google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(
request,
);

callback(
suggestions.map((suggestion) => {
const place = suggestion.placePrediction;
// Map to the old AutocompleteService.getPlacePredictions format
// https://developers.google.com/maps/documentation/javascript/places-migration-autocomplete
return {
description: place.text.text,
structured_formatting: {
main_text: place.mainText.text,
main_text_matched_substrings: place.mainText.matches.map((match) => ({
offset: match.startOffset,
length: match.endOffset - match.startOffset,
})),
secondary_text: place.secondaryText?.text,
},
};
}),
);
}, 300);

const emptyOptions = [];
let sessionToken;

export default function GoogleMaps() {
const [value, setValue] = React.useState(null);
const [inputValue, setInputValue] = React.useState('');
const [options, setOptions] = React.useState([]);
const loaded = React.useRef(false);
const [options, setOptions] = React.useState(emptyOptions);
const callbackId = React.useId().replace(/:/g, '');
const [loaded, setLoaded] = React.useState(false);

if (typeof window !== 'undefined' && !loaded.current) {
if (typeof window !== 'undefined') {
if (!document.querySelector('#google-maps')) {
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`,
const GOOGLE_NAMESPACE = '_google_callback';
const globalContext =
window[GOOGLE_NAMESPACE] || (window[GOOGLE_NAMESPACE] = {});
globalContext[callbackId] = () => {
setLoaded(true);
};

const script = loadScript(
`https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places&loading=async&callback=${GOOGLE_NAMESPACE}.${callbackId}`,
document.querySelector('head'),
'google-maps',
);
script.id = 'google-maps';
} else if (window.google && !loaded) {
setLoaded(true);
}

loaded.current = true;
}

const fetch = React.useMemo(
() =>
debounce((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 400),
[],
);

React.useEffect(() => {
let active = true;

if (!autocompleteService.current && window.google) {
autocompleteService.current =
new window.google.maps.places.AutocompleteService();
}
if (!autocompleteService.current) {
useEnhancedEffect(() => {
if (!loaded) {
return undefined;
}

if (inputValue === '') {
setOptions(value ? [value] : []);
setOptions(value ? [value] : emptyOptions);
return undefined;
}

fetch({ input: inputValue }, (results) => {
if (active) {
let newOptions = [];
// Allow to resolve the out of order request resolution.
let active = true;

if (value) {
newOptions = [value];
}
if (!sessionToken) {
sessionToken = new window.google.maps.places.AutocompleteSessionToken();
}

if (results) {
newOptions = [...newOptions, ...results];
}
fetch({ input: inputValue, sessionToken }, (results) => {
if (!active) {
return;
}

setOptions(newOptions);
let newOptions = [];

if (results) {
newOptions = results;

if (value) {
newOptions = [
value,
...results.filter((result) => result.description !== value.description),
];
}
} else if (value) {
newOptions = [value];
}
setOptions(newOptions);
});

return () => {
active = false;
};
}, [value, inputValue, fetch]);
}, [value, inputValue, loaded]);

return (
<Autocomplete
Expand All @@ -114,34 +147,39 @@ export default function GoogleMaps() {
)}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
const matches =
option.structured_formatting.main_text_matched_substrings || [];
const matches = option.structured_formatting.main_text_matched_substrings;

const parts = parse(
option.structured_formatting.main_text,
matches.map((match) => [match.offset, match.offset + match.length]),
);
return (
<li key={key} {...optionProps}>
<Grid container sx={{ alignItems: 'center' }}>
<Grid item sx={{ display: 'flex', width: 44 }}>
<Grid2 container sx={{ alignItems: 'center' }}>
<Grid2 sx={{ display: 'flex', width: 44 }}>
<LocationOnIcon sx={{ color: 'text.secondary' }} />
</Grid>
<Grid item sx={{ width: 'calc(100% - 44px)', wordWrap: 'break-word' }}>
</Grid2>
<Grid2 sx={{ width: 'calc(100% - 44px)', wordWrap: 'break-word' }}>
{parts.map((part, index) => (
<Box
key={index}
component="span"
sx={{ fontWeight: part.highlight ? 'bold' : 'regular' }}
sx={{
fontWeight: part.highlight
? 'fontWeightBold'
: 'fontWeightRegular',
}}
>
{part.text}
</Box>
))}
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{option.structured_formatting.secondary_text}
</Typography>
</Grid>
</Grid>
{option.structured_formatting.secondary_text ? (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{option.structured_formatting.secondary_text}
</Typography>
) : null}
</Grid2>
</Grid2>
</li>
);
}}
Expand Down
Loading

0 comments on commit 5ce77c0

Please sign in to comment.