Last active
September 23, 2023 07:41
-
-
Save ferretwithaberet/e6f57184a6c8320fbca737aadcfdce8b to your computer and use it in GitHub Desktop.
RNUI example Picker wrapper and RemotePicker implementation, integrated with react-query
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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