GeekyAnts / NativeBase

Mobile-first, accessible components for React Native & Web to build consistent UI across Android, iOS and Web.
https://nativebase.io/
MIT License
20.21k stars 2.39k forks source link

Support for Multi Select component. #5577

Open ThukuWakogi opened 1 year ago

ThukuWakogi commented 1 year ago

Description

Its a little frustrating that native base doesn't have a component like Select but that supports selection of multiple elements.

Problem Statement

i have sort other solution which work as expected but are off since they do not follow the native design system and they are hard to customize.

Proposed Solution or API

For the project I am working on, I implemented a custom MultiSelect component. I would fork the repository and add it as a feature but time doesn't allow. I might add it as a feature once I get time.

Here is the code

import {
  Actionsheet,
  Box,
  Button,
  FlatList,
  FormControl,
  HStack,
  Icon,
  IconButton,
  Text,
  useDisclose,
  useTheme,
} from 'native-base';
import React, { FC, useState } from 'react';
import { Pressable } from 'react-native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { pixelateValue } from '_app/helpers';

interface IMultiSelectProps {
  items?: { label: string; value: string }[];
  values?: string[];
  onValuesChange?: (values: string[]) => void;
  label?: string;
  touched?: boolean;
  isInvalid?: boolean;
  errorMessage?: string;
  helperText?: string;
}

const MultiSelect: FC<IMultiSelectProps> = ({
  items,
  values,
  onValuesChange,
  label,
  touched,
  isInvalid,
  errorMessage,
  helperText,
}) => {
  const { space } = useTheme();
  const { isOpen, onClose, onOpen } = useDisclose();
  const [selectedValues, setSelectedValues] = useState<string[]>([]);

  return (
    <>
      <FormControl isInvalid={touched && isInvalid}>
        <FormControl.Label _invalid={{ _text: { color: 'red.400' } }}>{label}</FormControl.Label>
        <Pressable onPress={() => onOpen()}>
          <Box
            bgColor="white"
            py={pixelateValue(space['2.5'] + 1)}
            px="3"
            borderRadius="sm"
            borderColor={isInvalid ? 'red.600' : 'coolGray.500'}
            borderWidth="1"
          >
            <Text color="text.400">Choose items...</Text>
          </Box>
        </Pressable>
        {touched && isInvalid ? (
          <FormControl.ErrorMessage>{errorMessage}</FormControl.ErrorMessage>
        ) : (
          <FormControl.HelperText>{helperText}</FormControl.HelperText>
        )}
      </FormControl>
      <HStack mt="2" flexWrap="wrap">
        {values?.map((value, index) => (
          <HStack key={index} borderColor="primary.400" borderWidth="1" borderRadius="full" mb="1" mr="1">
            <Text color="primary.400" fontSize="md" pl="2" pr="1">
              {value}
            </Text>
            <IconButton
              icon={<Icon as={MaterialIcons} name="close" />}
              size="xs"
              borderRadius="full"
              onPress={() => {
                setSelectedValues(previousState => previousState.filter(_value => _value !== value));
                onValuesChange && onValuesChange(selectedValues.filter(_value => _value !== value));
              }}
            />
          </HStack>
        ))}
      </HStack>
      <Actionsheet isOpen={isOpen} onClose={onClose}>
        <Actionsheet.Content>
          <FlatList
            data={items}
            keyExtractor={(_item, index) => index.toString()}
            renderItem={({ item: { label, value } }) => (
              <Actionsheet.Item
                bgColor={selectedValues.includes(value) ? 'black:alpha.20' : undefined}
                key={value}
                onPress={() => {
                  if (!selectedValues.includes(value)) {
                    setSelectedValues(previousValues => [...previousValues, value]);
                    onValuesChange && onValuesChange([...selectedValues, value]);
                  } else {
                    setSelectedValues(previousValues => previousValues.filter(_value => _value !== value));
                    onValuesChange && onValuesChange([...selectedValues, value]);
                  }
                }}
              >
                {label}
              </Actionsheet.Item>
            )}
            w="100%"
          />
          <Button variant="outline" my="1" onPress={onClose}>
            Close
          </Button>
        </Actionsheet.Content>
      </Actionsheet>
    </>
  );
};

export default MultiSelect;

This is inspired partially by react-native-sectioned-multi-select

Alternatives

No response

Additional Information

No response

ruchik02 commented 1 year ago

 @ThukuWakogi Thanks for the suggestion. We'll add this feature to our list.

danyhiol commented 1 year ago

@ThukuWakogi great implementation. But what does pixelateValue do?

ThukuWakogi commented 1 year ago

@danyhiol pixelateValue is a custom function which adds px to a number

export const pixelateValue = (value?: number) => `${value || 0}px`;