Skip to content

Instantly share code, notes, and snippets.

@erickvieira
Last active May 18, 2021 22:48
Show Gist options
  • Save erickvieira/3f14742fbd2fcb4b7858989bbfc02a71 to your computer and use it in GitHub Desktop.
Save erickvieira/3f14742fbd2fcb4b7858989bbfc02a71 to your computer and use it in GitHub Desktop.
THE ULTIMATE ANTD SEARCH INPUT (autocomplete, react, antd, antdesign, ant design, search, dynamic)
import React, { useCallback, useMemo, useState } from "react";
import { AutoComplete, Empty } from "antd";
import { debounce, get } from "lodash";
import { MaterialIcon } from "icons";
import { mdiLoading } from "@mdi/js";
import Text from "components/Text";
import useMountEffect from "hooks/Lifecycle/useMountEffect";
export interface SearchInputProps<T extends Dict> {
value?: string;
onChange?: (value?: string) => void;
onSelect?: (value?: T) => void;
placeholder?: string;
request: (params: Dict) => Promise<Utils.Page<T>>;
loadOnMount?: boolean;
debounce?: number;
valuePath: string;
optionPath?: string;
getOptions?: (page: Utils.Page<T>) => T[];
queryParamName?: string;
staticQueryParams?: Dict;
renderOption?: (result: T) => React.ReactNode;
style?: React.CSSProperties;
}
const SearchInput = <T extends Dict>({
value: formControlValue,
onChange = () => {},
onSelect = () => {},
placeholder,
request,
loadOnMount = true,
debounce: debounceFactor = 700,
valuePath,
optionPath = valuePath,
getOptions = (page) => page.items,
queryParamName = "query",
staticQueryParams = {},
renderOption = (result) => get(result, optionPath),
style = {},
}: SearchInputProps<T>) => {
const adapted: Record<"value" | "selectedOption", string> = useMemo(() => {
const valueIsString = typeof formControlValue === "string";
return {
value: valueIsString
? formControlValue
: get(formControlValue ?? {}, valuePath),
selectedOption: valueIsString
? formControlValue
: get(formControlValue ?? {}, optionPath || valuePath),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [loading, setLoading] = useState(false);
const [typingSearch, setTypingSearch] = useState(false);
const [selectedOption, setSelectedOption] = useState(adapted.selectedOption);
const [lastSelectedOption, setLastSelectedOption] = useState(
adapted.selectedOption
);
const [results, setResults] = useState([] as T[]);
useMountEffect(() => {
if (typeof formControlValue === "object" && adapted.value) {
onChange(adapted.value);
}
if (loadOnMount) fetchData("");
});
const fetchData = useCallback(
async (query: string) => {
setLoading(true);
setResults([]);
try {
const page = await request({
[queryParamName]: query,
...staticQueryParams,
});
setResults(getOptions(page));
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const fetchThrottle = debounce((inputValue: string) => {
if (!inputValue && !loadOnMount) return;
fetchData(inputValue);
}, debounceFactor);
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleSearch = useCallback(fetchThrottle, []);
const handleSelect = (value: string) => {
let selectedOptionValue: string;
try {
const index = results.findIndex(
(result) => get(result, valuePath) === value
);
if (index === -1) throw new Error();
selectedOptionValue = get(results[index], optionPath);
onSelect(results[index]);
} catch {
selectedOptionValue = value;
}
setSelectedOption(selectedOptionValue);
setLastSelectedOption(selectedOptionValue);
onChange(value);
setTypingSearch(false);
};
const handleChange = useCallback((value: string) => {
setSelectedOption(value);
setTypingSearch(true);
}, []);
const handleFocus = useCallback(() => {
if (!typingSearch) setSelectedOption("");
}, [typingSearch]);
const handleBlur = useCallback(() => {
setSelectedOption((currentSelectedOption) => {
if (!currentSelectedOption) {
setTypingSearch(false);
return lastSelectedOption;
}
return currentSelectedOption;
});
}, [lastSelectedOption]);
return (
<AutoComplete
placeholder={placeholder}
value={selectedOption}
searchValue={adapted.value}
onSearch={handleSearch}
onSelect={handleSelect}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
notFoundContent={<Empty />}
style={{ ...style, position: "relative" }}
>
{loading && (
<AutoComplete.Option disabled value="" key="loading">
<Text disabled italic>
<MaterialIcon style={{ marginRight: 8 }} path={mdiLoading} spin />
Loading...
</Text>
</AutoComplete.Option>
)}
{results.map((result, index) => (
<AutoComplete.Option key={index} value={get(result, valuePath)}>
{renderOption(result)}
</AutoComplete.Option>
))}
</AutoComplete>
);
};
export default React.memo(SearchInput);
// outside the component (an util maybe)
const renderSchoolOption = (option: any) => {
if (option instanceof Object) {
const school = option as Models.School;
return (
<Flex>
<Avatar size={16} src={school.logo} />
{school.name}
</Flex>
);
}
return option;
};
// inside the component
<SearchInput
placeholder="Type school's name here"
request={schoolApi.list}
valuePath="id"
optionPath="name"
renderOption={renderSchoolOption}
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment