react-native-multi-selectbox
Version:
Platform independent (Android / iOS) Selectbox | Picker | Multi-select | Multi-picker. The idea is to bring out the common user-interface & user-experience on both platforms.
397 lines (375 loc) • 9.9 kB
JavaScript
import React, { useState, memo, useMemo } from 'react'
import { isEmpty, find } from 'lodash'
import { View, FlatList, Text, TouchableOpacity, TextInput, ScrollView } from 'react-native'
import Colors from './src/constants/Colors'
import Icon from './src/components/Icon'
import Toggle from './src/components/Toggle'
const hitSlop = { top: 14, bottom: 14, left: 14, right: 14 }
const kOptionsHeight = { width: '100%', maxHeight: 180 }
const kOptionListViewStyle = {
width: '100%',
alignItems: 'center',
paddingVertical: 4,
}
const renderItemStyle = { flexShrink: 1 }
function SelectBox({
labelStyle,
containerStyle,
inputFilterContainerStyle,
inputFilterStyle,
optionsLabelStyle,
optionContainerStyle,
multiOptionContainerStyle,
multiOptionsLabelStyle,
multiListEmptyLabelStyle,
listEmptyLabelStyle,
selectedItemStyle,
listEmptyText = 'No results found',
...props
}) {
const [inputValue, setInputValue] = useState('')
const [showOptions, setShowOptions] = useState(false)
function renderLabel(item) {
const kOptionsLabelStyle = {
fontSize: 17,
color: 'rgba(60, 60, 67, 0.6)',
...optionsLabelStyle,
}
return <Text style={kOptionsLabelStyle}>{item}</Text>
}
function renderItem({ item }) {
const { isMulti, onChange, onMultiSelect, selectedValues } = props
const kOptionContainerStyle = {
borderColor: '#dadada',
borderBottomWidth: 1,
width: '100%',
flexDirection: 'row',
alignItems: 'center',
background: '#fff',
paddingVertical: 12,
paddingRight: 10,
justifyContent: 'space-between',
...optionContainerStyle,
}
return (
<View style={kOptionContainerStyle}>
{isMulti ? (
<>
<TouchableOpacity hitSlop={hitSlop} style={renderItemStyle} onPress={onPressMultiItem()}>
{renderLabel(item.item)}
</TouchableOpacity>
<Toggle
iconColor={toggleIconColor}
checked={selectedValues.some(i => item.id === i.id)}
onTouch={onPressMultiItem()}
/>
</>
) : (
<>
<TouchableOpacity hitSlop={hitSlop} style={renderItemStyle} onPress={onPressItem()}>
{renderLabel(item.item)}
<View />
</TouchableOpacity>
</>
)}
</View>
)
function onPressMultiItem() {
return (e) => (onMultiSelect ? onMultiSelect(item) : null)
}
function onPressItem() {
return (e) => {
setShowOptions(false)
return onChange ? onChange(item) : null
}
}
}
function renderGroupItem({ item }) {
const { onTapClose, options } = props
const label = find(options, (o) => o.id === item.id)
const kMultiOptionContainerStyle = {
flexDirection: 'row',
borderRadius: 20,
paddingVertical: 5,
paddingRight: 5,
paddingLeft: 10,
marginRight: 4,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Colors.primary,
flexGrow: 1,
...multiOptionContainerStyle,
}
const kMultiOptionsLabelStyle = {
fontSize: 10,
color: '#fff',
...multiOptionsLabelStyle,
}
return (
<View style={kMultiOptionContainerStyle}>
<Text style={kMultiOptionsLabelStyle}>{label.item}</Text>
<TouchableOpacity style={{ marginLeft: 15 }} hitSlop={hitSlop} onPress={onPressItem()}>
<Icon name="closeCircle" fill="#fff" width={21} height={21} />
</TouchableOpacity>
</View>
)
function onPressItem() {
return (e) => (onTapClose ? onTapClose(item) : null)
}
}
const {
selectIcon,
label,
inputPlaceholder = 'Select',
hideInputFilter,
width = '100%',
isMulti,
options,
value,
selectedValues,
arrowIconColor = Colors.primary,
searchIconColor = Colors.primary,
toggleIconColor = Colors.primary,
searchInputProps,
multiSelectInputFieldProps,
listOptionProps = {},
} = props
const filteredSuggestions = useMemo(
() => options.filter((suggestion) => suggestion.item.toLowerCase().indexOf(inputValue.toLowerCase()) > -1),
[inputValue, options]
)
function multiListEmptyComponent() {
const kMultiListEmptyLabelStyle = {
fontSize: 17,
color: 'rgba(60, 60, 67, 0.3)',
...multiListEmptyLabelStyle,
}
return (
<TouchableOpacity
width="100%"
style={{ flexGrow: 1, width: '100%' }}
hitSlop={hitSlop}
onPress={onPressShowOptions()}>
<Text style={kMultiListEmptyLabelStyle}>{inputPlaceholder}</Text>
</TouchableOpacity>
)
}
function optionListEmpty() {
const kListEmptyLabelStyle = {
fontSize: 17,
color: 'rgba(60, 60, 67, 0.6)',
...listEmptyLabelStyle,
}
return (
<View style={kOptionListViewStyle}>
<Text style={kListEmptyLabelStyle}>{listEmptyText}</Text>
</View>
)
}
const kLabelStyle = {
fontSize: 12,
color: 'rgba(60, 60, 67, 0.6)',
paddingBottom: 4,
...labelStyle,
}
const kContainerStyle = {
flexDirection: 'row',
width: '100%',
borderColor: '#ddd',
borderBottomWidth: 1,
paddingTop: 6,
paddingRight: 20,
paddingBottom: 8,
...containerStyle,
}
return (
<>
<View
style={{
width,
}}>
<Text style={kLabelStyle}>{label}</Text>
<View style={kContainerStyle}>
<View style={{ paddingRight: 20, flexGrow: 1 }}>
{isMulti ? (
<FlatList
data={selectedValues}
extraData={{ inputValue, showOptions }}
keyExtractor={keyExtractor()}
renderItem={renderGroupItem}
horizontal={true}
ListEmptyComponent={multiListEmptyComponent}
{...multiSelectInputFieldProps}
/>
) : (
<TouchableOpacity hitSlop={hitSlop} onPress={onPressShowOptions()}>
<Text style={kSelectedItemStyle()}>{value.item || inputPlaceholder || label}</Text>
</TouchableOpacity>
)}
</View>
<TouchableOpacity onPress={onPressShowOptions()} hitSlop={hitSlop}>
{selectIcon ? selectIcon : <Icon name={showOptions ? 'upArrow' : 'downArrow'} fill={arrowIconColor} />}
</TouchableOpacity>
</View>
{/* Options wrapper */}
{showOptions && (
<FlatList
data={filteredSuggestions || options}
extraData={options}
keyExtractor={keyExtractor()}
renderItem={renderItem}
numColumns={1}
horizontal={false}
initialNumToRender={5}
maxToRenderPerBatch={20}
windowSize={10}
ListEmptyComponent={optionListEmpty}
style={[kOptionsHeight, listOptionProps.style]}
ListHeaderComponent={HeaderComponent()}
{...listOptionProps}
/>
)}
</View>
</>
)
function keyExtractor() {
return (o) => `${o.id}-${Math.random()}`
}
function kSelectedItemStyle() {
return {
fontSize: 17,
color: isEmpty(value.item) ? 'rgba(60, 60, 67, 0.3)' : '#000',
...selectedItemStyle,
}
}
function HeaderComponent() {
const kInputFilterContainerStyle = {
width: '100%',
borderBottomWidth: 1,
borderBottomColor: '#ddd',
flexDirection: 'row',
alignItems: 'center',
paddingRight: 18,
justifyContent: 'space-between',
...inputFilterContainerStyle,
}
const kInputFilterStyle = {
paddingVertical: 14,
paddingRight: 8,
color: '#000',
fontSize: 12,
flexGrow: 1,
...inputFilterStyle,
}
return (
<>
{!hideInputFilter && (
<View style={kInputFilterContainerStyle}>
<TextInput
value={inputValue}
placeholder={inputPlaceholder}
onChangeText={onChangeText()}
style={kInputFilterStyle}
placeholderTextColor="#000"
{...searchInputProps}
/>
<Icon name="searchBoxIcon" fill={searchIconColor} />
</View>
)}
<ScrollView keyboardShouldPersistTaps="always" />
</>
)
function onChangeText() {
return (value) => setInputValue(value)
}
}
function onPressShowOptions() {
return () => setShowOptions(!showOptions)
}
}
SelectBox.defaultProps = {
label: 'Label',
options: [
{
item: 'Aston Villa FC',
id: 'AVL',
},
{
item: 'West Ham United FC',
id: 'WHU',
},
{
item: 'Stoke City FC',
id: 'STK',
},
{
item: 'Sunderland AFC',
id: 'SUN',
},
{
item: 'Everton FC',
id: 'EVE',
},
{
item: 'Tottenham Hotspur FC',
id: 'TOT',
},
{
item: 'Manchester City FC',
id: 'MCI',
},
{
item: 'Chelsea FC',
id: 'CHE',
},
{
item: 'West Bromwich Albion FC',
id: 'WBA',
},
{
item: 'Liverpool FC',
id: 'LIV',
},
{
item: 'Arsenal FC',
id: 'ARS',
},
{
item: 'Manchester United FC',
id: 'MUN',
},
{
item: 'Newcastle United FC',
id: 'NEW',
},
{
item: 'Norwich City FC',
id: 'NOR',
},
{
item: 'Watford FC',
id: 'WAT',
},
{
item: 'Swansea City FC',
id: 'SWA',
},
{
item: 'Crystal Palace FC',
id: 'CRY',
},
{
item: 'Leicester City FC',
id: 'LEI',
},
{
item: 'Southampton FC',
id: 'SOU',
},
{
item: 'AFC Bournemouth',
id: 'BOU',
},
],
}
export default memo(SelectBox)