@coocoon/react-awesome-query-builder
Version:
User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder
303 lines (266 loc) • 9.05 kB
JSX
import React from "react";
import debounce from "lodash/debounce";
import { mapListValues, listValuesToArray } from "../utils/stuff";
import { mergeListValues, listValueToOption, getListValue } from "../utils/autocomplete";
const useListValuesAutocomplete = ({
asyncFetch, useLoadMore, useAsyncSearch, forceAsyncSearch,
asyncListValues: selectedAsyncListValues,
listValues: staticListValues, allowCustomValues,
value: selectedValue, setValue, placeholder
}, {
debounceTimeout,
multiple
}) => {
const knownSpecialValues = ["LOAD_MORE", "LOADING_MORE"];
const loadMoreTitle = "Load more...";
const loadingMoreTitle = "Loading more...";
const aPlaceholder = forceAsyncSearch ? "Type to search" : placeholder;
// state
const [open, setOpen] = React.useState(false);
const [asyncFetchMeta, setAsyncFetchMeta] = React.useState(undefined);
const [loadingCnt, setLoadingCnt] = React.useState(0);
const [isLoadingMore, setIsLoadingMore] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
const [asyncListValues, setAsyncListValues] = React.useState(undefined);
// ref
const asyncFectchCnt = React.useRef(0);
const componentIsMounted = React.useRef(true);
const isSelectedLoadMore = React.useRef(false);
// compute
const nSelectedAsyncListValues = listValuesToArray(selectedAsyncListValues);
const listValues = asyncFetch
? (!allowCustomValues ? mergeListValues(asyncListValues, nSelectedAsyncListValues, true) : asyncListValues)
: staticListValues;
//const isDirtyInitialListValues = asyncListValues == undefined && selectedAsyncListValues && selectedAsyncListValues.length && typeof selectedAsyncListValues[0] != "object";
const isLoading = loadingCnt > 0;
const canInitialLoad = open && asyncFetch
&& asyncListValues === undefined
&& (forceAsyncSearch ? inputValue : true);
const isInitialLoading = canInitialLoad && isLoading;
const canLoadMore = !isInitialLoading && listValues && listValues.length > 0
&& asyncFetchMeta && asyncFetchMeta.hasMore && (asyncFetchMeta.filter || "") === inputValue;
const canShowLoadMore = !isLoading && canLoadMore;
const options = mapListValues(listValues, listValueToOption);
const hasValue = selectedValue != null;
// const selectedListValue = hasValue ? getListValue(selectedValue, listValues) : null;
// const selectedOption = listValueToOption(selectedListValue);
// fetch
const fetchListValues = async (filter = null, isLoadMore = false) => {
// clear obsolete meta
if (!isLoadMore && asyncFetchMeta) {
setAsyncFetchMeta(undefined);
}
const offset = isLoadMore && asyncListValues ? asyncListValues.length : 0;
const meta = isLoadMore && asyncFetchMeta || !useLoadMore && { pageSize: 0 };
const newAsyncFetchCnt = ++asyncFectchCnt.current;
const res = await asyncFetch(filter, offset, meta);
const isFetchCancelled = asyncFectchCnt.current != newAsyncFetchCnt;
if (isFetchCancelled || !componentIsMounted.current) {
return null;
}
const { values, hasMore, meta: newMeta } = res && res.values ? res : { values: res };
const nValues = listValuesToArray(values);
let assumeHasMore;
let newValues;
if (isLoadMore) {
newValues = mergeListValues(asyncListValues, nValues, false);
assumeHasMore = newValues.length > asyncListValues.length;
} else {
newValues = nValues;
if (useLoadMore) {
assumeHasMore = newValues.length > 0;
}
}
// save new meta
const realNewMeta = hasMore != null || newMeta != null || assumeHasMore != null ? {
...(assumeHasMore != null ? { hasMore: assumeHasMore } : {}),
...(hasMore != null ? { hasMore } : {}),
...(newMeta != null ? newMeta : {}),
filter
} : undefined;
if (realNewMeta) {
setAsyncFetchMeta(realNewMeta);
}
return newValues;
};
const loadListValues = async (filter = null, isLoadMore = false) => {
setLoadingCnt(x => (x + 1));
setIsLoadingMore(isLoadMore);
const list = await fetchListValues(filter, isLoadMore);
if (!componentIsMounted.current) {
return;
}
if (list != null) {
// tip: null can be used for reject (eg, if user don't want to filter by input)
setAsyncListValues(list);
}
setLoadingCnt(x => (x - 1));
setIsLoadingMore(false);
};
const loadListValuesDebounced = React.useCallback(debounce(loadListValues, debounceTimeout), []);
// Unmount
React.useEffect(() => {
return () => {
componentIsMounted.current = false;
};
}, []);
// Initial loading
React.useEffect(() => {
if (canInitialLoad && loadingCnt == 0 && asyncFectchCnt.current == 0) {
(async () => {
await loadListValues();
})();
}
}, [canInitialLoad]);
// Event handlers
const onOpen = () => {
setOpen(true);
};
const onClose = (_e) => {
if (isSelectedLoadMore.current) {
isSelectedLoadMore.current = false;
if (multiple) {
setOpen(false);
}
} else {
setOpen(false);
}
};
const onDropdownVisibleChange = (open) => {
if (open) {
onOpen();
} else {
onClose();
}
};
const isSpecialValue = (option) => {
const specialValue = option?.specialValue || option?.value;
return knownSpecialValues.includes(specialValue);
};
const onChange = async (_e, option) => {
let specialValue = option?.specialValue || option?.value
|| multiple && option.map(opt => opt?.specialValue || opt?.value).find(v => !!v);
if (specialValue == "LOAD_MORE") {
isSelectedLoadMore.current = true;
await loadListValues(inputValue, true);
} else if (specialValue == "LOADING_MORE") {
isSelectedLoadMore.current = true;
} else {
if (multiple) {
const options = option;
let newSelectedListValues = options.map(o =>
o.value != null ? o : getListValue(o, listValues)
);
let newSelectedValues = newSelectedListValues.map(o => o.value);
if (!newSelectedValues.length)
newSelectedValues = undefined; //not allow []
setValue(newSelectedValues, newSelectedListValues);
} else {
const v = option == null ? undefined : option.value;
setValue(v, [option]);
}
}
};
const onInputChange = async (_e, newInputValue) => {
const val = newInputValue;
//const isTypeToSearch = e.type == 'change';
if (val === loadMoreTitle || val === loadingMoreTitle) {
return;
}
setInputValue(val);
if (allowCustomValues) {
if (multiple) {
//todo
} else {
setValue(val, [val]);
}
}
const canSearchAsync = useAsyncSearch && (forceAsyncSearch ? !!val : true);
if (canSearchAsync) {
await loadListValuesDebounced(val);
} else if (useAsyncSearch && forceAsyncSearch) {
setAsyncListValues([]);
}
};
// to keep compatibility with antD
const onSearch = async (newInputValue) => {
if (newInputValue === "" && !open) {
return;
}
await onInputChange(null, newInputValue);
};
// Options
const extendOptions = (options) => {
const filtered = [...options];
if (useLoadMore) {
if (canShowLoadMore) {
filtered.push({
specialValue: "LOAD_MORE",
title: loadMoreTitle,
});
} else if (isLoadingMore) {
filtered.push({
specialValue: "LOADING_MORE",
title: loadingMoreTitle,
disabled: true
});
}
}
return filtered;
};
const getOptionSelected = (option, valueOrOption) => {
if (valueOrOption == null)
return null;
const selectedValue = valueOrOption.value != undefined ? valueOrOption.value : valueOrOption;
return option.value === selectedValue;
};
const getOptionDisabled = (valueOrOption) => {
return valueOrOption && valueOrOption.disabled;
};
const getOptionLabel = (valueOrOption) => {
if (valueOrOption == null)
return null;
const option = valueOrOption.value != undefined ? valueOrOption
: listValueToOption(getListValue(valueOrOption, listValues));
if (!option && valueOrOption.specialValue) {
// special last 'Load more...' item
return valueOrOption.title;
}
if (!option && allowCustomValues) {
// there is just string value, it's not item from list
return valueOrOption;
}
if (!option) {
// weird
return valueOrOption;
}
return option.title;
};
return {
options,
listValues,
hasValue,
open,
onOpen,
onClose,
onDropdownVisibleChange,
onChange,
inputValue,
onInputChange,
onSearch,
canShowLoadMore,
isInitialLoading,
isLoading,
isLoadingMore,
isSpecialValue,
extendOptions,
getOptionSelected,
getOptionDisabled,
getOptionLabel,
// unused
//selectedListValue,
//selectedOption,
aPlaceholder,
};
};
export default useListValuesAutocomplete;