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 2d1b3f0
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 113 deletions.
107 changes: 60 additions & 47 deletions docs/data/material/components/autocomplete/GoogleMaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,100 @@ 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((request, callback) => {
autocompleteService.current.getPlacePredictions(request, callback);
}, 300);

const emptyOptions = [];

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;
useEnhancedEffect(() => {
if (!loaded) {
return undefined;
}

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

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

// Allow to resolve the out of order request resolution.
let active = true;

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

if (value) {
newOptions = [value];
}
let newOptions = [];

if (results) {
newOptions = [...newOptions, ...results];
}
if (value) {
newOptions = [value];
}

setOptions(newOptions);
if (results) {
newOptions = [...newOptions, ...results];
}

setOptions(newOptions);
});

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

return (
<Autocomplete
Expand Down Expand Up @@ -123,25 +132,29 @@ export default function GoogleMaps() {
);
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>
</Grid2>
</Grid2>
</li>
);
}}
Expand Down
125 changes: 68 additions & 57 deletions docs/data/material/components/autocomplete/GoogleMaps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@ 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: string, position: HTMLElement | null, id: string) {
if (!position) {
return;
}
const useEnhancedEffect =
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;

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

const autocompleteService = { current: null };
Expand All @@ -40,78 +39,86 @@ interface PlaceType {
structured_formatting: StructuredFormatting;
}

const fetch = throttle(
(
request: { input: string },
callback: (results?: readonly PlaceType[]) => void,
) => {
(autocompleteService.current as any).getPlacePredictions(request, callback);
},
300,
);

const emptyOptions = [] as any;

export default function GoogleMaps() {
const [value, setValue] = React.useState<PlaceType | null>(null);
const [inputValue, setInputValue] = React.useState('');
const [options, setOptions] = React.useState<readonly PlaceType[]>([]);
const loaded = React.useRef(false);
const [options, setOptions] = React.useState<readonly PlaceType[]>(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`,
document.querySelector('head'),
'google-maps',
const GOOGLE_NAMESPACE = '_google_callback';
const globalContext =
// @ts-ignore
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')!,
);
script.id = 'google-maps';
} else if ((window as any).google && !loaded) {
setLoaded(true);
}

loaded.current = true;
}

const fetch = React.useMemo(
() =>
debounce(
(
request: { input: string },
callback: (results?: readonly PlaceType[]) => void,
) => {
(autocompleteService.current as any).getPlacePredictions(
request,
callback,
);
},
400,
),
[],
);

React.useEffect(() => {
let active = true;
useEnhancedEffect(() => {
if (!loaded) {
return undefined;
}

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

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

// Allow to resolve the out of order request resolution.
let active = true;

fetch({ input: inputValue }, (results?: readonly PlaceType[]) => {
if (active) {
let newOptions: readonly PlaceType[] = [];
if (!active) {
return;
}

if (value) {
newOptions = [value];
}
let newOptions: readonly PlaceType[] = [];

if (results) {
newOptions = [...newOptions, ...results];
}
if (value) {
newOptions = [value];
}

setOptions(newOptions);
if (results) {
newOptions = [...newOptions, ...results];
}

setOptions(newOptions);
});

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

return (
<Autocomplete
Expand Down Expand Up @@ -147,25 +154,29 @@ export default function GoogleMaps() {
);
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>
</Grid2>
</Grid2>
</li>
);
}}
Expand Down
6 changes: 2 additions & 4 deletions docs/data/material/components/autocomplete/autocomplete.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,10 @@ overriding the `filterOptions` prop:
A customized UI for Google Maps Places Autocomplete.
For this demo, we need to load the [Google Maps JavaScript](https://developers.google.com/maps/documentation/javascript/overview) and [Google Places](https://developers.google.com/maps/documentation/places/web-service/overview) API.

:::info
The following demo relies on [autosuggest-highlight](https://github.com/moroshko/autosuggest-highlight), a small (1 kB) utility for highlighting text in autosuggest and autocomplete components.
:::

{{"demo": "GoogleMaps.js"}}

The demo relies on [autosuggest-highlight](https://github.com/moroshko/autosuggest-highlight), a small (1 kB) utility for highlighting text in autosuggest and autocomplete components.

:::error
Before you can start using the Google Maps JavaScript API and Places API, you need to get your own [API key](https://developers.google.com/maps/documentation/javascript/get-api-key).
:::
Expand Down
Loading

0 comments on commit 2d1b3f0

Please sign in to comment.