Skip to content

Instantly share code, notes, and snippets.

@ferretwithaberet
Last active September 23, 2023 07:41
Show Gist options
  • Save ferretwithaberet/e6f57184a6c8320fbca737aadcfdce8b to your computer and use it in GitHub Desktop.
Save ferretwithaberet/e6f57184a6c8320fbca737aadcfdce8b to your computer and use it in GitHub Desktop.
RNUI example Picker wrapper and RemotePicker implementation, integrated with react-query
import { useMemo, forwardRef, Children, memo } from "react";
import { useTranslation } from "react-i18next";
import {
View,
TextField,
Picker as _Picker,
Colors,
} from "react-native-ui-lib";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
import {
faChevronDown as falChevronDown,
faSearch as falSearch,
faCheck as falCheck,
} from "@fortawesome/pro-light-svg-icons";
export const PickerInput = memo((props) => {
const { selected, ...restProps } = props;
return (
<TextField
{...restProps}
trailingAccessory={
<FontAwesomeIcon icon={falChevronDown} color={Colors.$iconNeutral} />
}
/>
);
});
export const PickerSearch = memo((props) => {
const { onSearchChange, onCustomSearchChange, ...restProps } = props;
return (
<View
style={{ borderBottomWidth: 1, borderColor: Colors.$outlineDefault }}
paddingH-s4
>
<TextField
leadingAccessory={
<View marginR-s2>
<FontAwesomeIcon icon={falSearch} color={Colors.$iconPrimary} />
</View>
}
onChangeText={onCustomSearchChange ?? onSearchChange}
{...restProps}
/>
</View>
);
});
export const PickerItemSelectedComponent = memo(() => (
<FontAwesomeIcon icon={falCheck} color={Colors.$iconPrimary} />
));
const PickerItem = (props) => {
return (
<_Picker.Item
selectedIcon={() => <PickerItemSelectedComponent />}
{...props}
/>
);
};
const sortPickerChildren = (selectedValues, child1, child2) => {
if (selectedValues.includes(child1.props.value)) return -1;
if (selectedValues.includes(child2.props.value)) return 1;
return 0;
};
const defaultRenderPicker = (props) => <PickerInput {...props} />;
const defaultRenderCustomSearch = (props) => <PickerSearch {...props} />;
const Picker = forwardRef((props, ref) => {
const { t } = useTranslation();
const {
preset,
value,
label,
placeholder,
floatingPlaceholder,
mode,
topBarProps,
showSelectedAbove = false,
readonly = false,
renderPicker = defaultRenderPicker,
renderCustomSearch = defaultRenderCustomSearch,
onCustomSearchChange,
children,
...restProps
} = props;
const sortedChildren = useMemo(() => {
if (!showSelectedAbove) return children;
const valueArray =
value != null ? (Array.isArray(value) ? value : [value]) : [];
return Children.toArray(children).sort((a, b) =>
sortPickerChildren(valueArray, a, b)
);
}, [value, children]);
const renderPickerComponent = (selected, pickerLabel) => {
const value =
mode === _Picker.modes.MULTI && selected?.length
? t("general.selection", { count: selected.length })
: pickerLabel;
return renderPicker({
selected,
value,
preset,
label,
placeholder,
floatingPlaceholder,
readonly,
});
};
const renderSearchComponent = (props) => {
const {
searchStyle,
searchPlaceholder = t("general.search"),
onSearchChange,
} = props;
return renderCustomSearch({
preset,
style: searchStyle,
placeholder: searchPlaceholder,
onSearchChange,
onCustomSearchChange,
});
};
return (
<View pointerEvents={readonly ? "none" : undefined}>
<_Picker
{...restProps}
ref={ref}
preset={preset}
value={value}
label={label}
placeholder={placeholder}
floatingPlaceholder={floatingPlaceholder}
mode={mode}
topBarProps={{
title: label ?? placeholder,
doneLabel: t("actions.done"),
...topBarProps,
}}
renderPicker={renderPickerComponent}
renderCustomSearch={renderSearchComponent}
>
{sortedChildren}
</_Picker>
</View>
);
});
Picker.Item = PickerItem;
Picker.modes = _Picker.modes;
export default Picker;
import { useState, useEffect } from "react";
import { useDebounce } from "@uidotdev/usehooks";
import { SEARCH_DEBOUNCE_MS } from "@/utils/constants";
import { combineOnProps } from "@/utils/components";
import { useFlatListProps } from "@/utils/lists";
import Picker, { PickerInput } from "@/components/Picker";
import LoaderView from "@/components/LoaderView";
const defaultLabelExtractor = (item) => item.name;
const defaultValueExtractor = (item) => item.id;
const defaultOptionsExtractor = (query) => query.combinedData;
const RemotePicker = (props) => {
const {
value,
useOptions,
useSelected = () => ({ data: null }),
valueParamKey = "id",
searchParamKey = "q",
listProps,
pickerModalProps,
renderItem,
labelExtractor = defaultLabelExtractor,
valueExtractor = defaultValueExtractor,
optionsExtractor = defaultOptionsExtractor,
mode = Picker.modes.SINGLE,
onPress,
...restProps
} = props;
const [visible, setVisible] = useState(false);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, SEARCH_DEBOUNCE_MS);
const selectedQuery = useSelected({
enabled: mode === Picker.modes.SINGLE && value != null,
params: {
[valueParamKey]: value,
},
});
const optionsQuery = useOptions({
enabled: visible,
params: {
[searchParamKey]: debouncedSearch,
},
});
const options = optionsExtractor(optionsQuery);
const [newListProps, restListProps] = useFlatListProps({
...listProps,
isLoading: optionsQuery.hasNextPage ?? false,
ListEmptyComponent: optionsQuery.isLoading ? <LoaderView /> : undefined,
onEndReached: () =>
optionsQuery.isEnabled &&
optionsQuery.hasNextPage &&
optionsQuery.fetchNextPage(),
});
useEffect(() => {
if (visible) return;
setSearch("");
}, [visible]);
const toggleVisible = (force) => {
if (typeof force === "boolean") setVisible(force);
else setVisible((visible) => !visible);
};
const renderPickerItem = (item) => {
const label = labelExtractor(item);
const value = valueExtractor(item);
return (
<Picker.Item
key={value}
label={label}
value={value}
item={item}
renderItem={renderItem}
/>
);
};
const pickerValue = selectedQuery.data
? labelExtractor(selectedQuery.data)
: null;
return (
<Picker
renderPicker={(props) => (
<PickerInput {...props} value={pickerValue ?? props.value} />
)}
{...restProps}
value={value}
mode={mode}
listProps={{
...restListProps,
...newListProps,
}}
pickerModalProps={{
...pickerModalProps,
onDismiss: combineOnProps(pickerModalProps?.onDismiss, () =>
toggleVisible(false)
),
}}
onPress={combineOnProps(onPress, () => toggleVisible(true))}
onCustomSearchChange={(value) => setSearch(value)}
>
{options.map(renderPickerItem)}
</Picker>
);
};
RemotePicker.Item = Picker.Item;
RemotePicker.modes = Picker.modes;
export default RemotePicker;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment