react-native-sectioned-multi-select
Version:
a multi (or single) select component with support for sub categories, search, chips.
668 lines (629 loc) • 18.2 kB
JavaScript
import React, { Component } from 'react'
import {
Platform,
StyleSheet,
Text,
View,
ScrollView,
Switch,
TouchableWithoutFeedback,
TouchableOpacity,
ActivityIndicator,
Dimensions,
LayoutAnimation,
} from 'react-native'
import SectionedMultiSelect from 'react-native-sectioned-multi-select'
import Icon from 'react-native-vector-icons/MaterialIcons'
const img = require('./z.jpg');
// Sorry for the mess
const items = [
{
title: 'Fruits from various places around the world, if you like',
id: 0,
icon: { uri: 'https://banner2.kisspng.com/20180514/wqq/kisspng-leaf-plant-green-clip-art-5af9b5b7402440.7747356215263144232627.jpg'},
children: [
{
title: 'Apple',
id: 10,
},
{
title: 'Strawberry and Banana and Pineapple and Pawpaw and Peach',
id: 11,
},
{
title: 'Pineapple',
id: 13,
},
{
title: 'Banana',
id: 14,
},
{
title: 'Wátermelon',
id: 15,
},
{
title: 'אבטיח',
id: 17,
},
{
title: 'Raspberry',
id: 18,
},
{
title: 'Orange',
id: 19,
},
{
title: 'Mandarin',
id: 20,
},
{
title: 'Papaya',
id: 21,
},
{
title: 'Lychee',
id: 22,
},
{
title: 'Cherry',
id: 23,
},
{
title: 'Peach',
id: 24,
},
{
title: 'Apricot',
id: 25,
},
],
},
{
title: 'Gèms',
id: 1,
icon: "cake",
children: [
{
title: 'Quartz',
id: 26,
},
{
title: 'Zircon',
id: 27,
},
{
title: 'Sapphirè',
id: 28,
},
{
title: 'Topaz',
id: 29,
},
],
},
{
title: 'Plants',
id: 2,
icon: img,
children: [
{
title: "Mother In Law's Tongue",
id: 30,
},
{
title: 'Yucca',
id: 31,
},
{
title: 'Monsteria',
id: 32,
},
{
title: 'Palm',
id: 33,
},
],
},
{
title: 'No child',
id: 34,
},
]
console.log(items);
// const items2 =
// [{
// title: 'Plants',
// id: 2,
// children: [
// {
// title: "Mother In Law's Tongue",
// id: 30,
// },
// {
// title: 'Yucca',
// id: 31,
// },
// {
// title: 'Monsteria',
// id: 32,
// },
// {
// title: 'Palm',
// id: 33,
// },
// ],
// }]
const items2 = []
for (let i = 0; i < 100; i++) {
items2.push({
id: i,
title: `item ${i}`,
children: [
{
id: `10${i}`, title: `child 10${i}`,
},
{
id: `11${i}`, title: `child 11${i}`,
},
{
id: `12${i}`, title: `child 12${i}`,
},
],
})
}
const styles = StyleSheet.create({
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginTop: 30,
},
container: {
paddingTop: 40,
paddingHorizontal: 20,
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
color: '#333',
},
border: {
borderBottomWidth: 1,
borderBottomColor: '#dadada',
marginBottom: 20,
},
heading: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 5,
marginTop: 20,
},
label: {
fontWeight: 'bold',
},
switch: {
marginBottom: 20,
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'space-between',
},
})
const accentMap = {
'â': 'a',
'Â': 'A',
'à': 'a',
'À': 'A',
'á': 'a',
'Á': 'A',
'ã': 'a',
'Ã': 'A',
'ê': 'e',
'Ê': 'E',
'è': 'e',
'È': 'E',
'é': 'e',
'É': 'E',
'î': 'i',
'Î': 'I',
'ì': 'i',
'Ì': 'I',
'í': 'i',
'Í': 'I',
'õ': 'o',
'Õ': 'O',
'ô': 'o',
'Ô': 'O',
'ò': 'o',
'Ò': 'O',
'ó': 'o',
'Ó': 'O',
'ü': 'u',
'Ü': 'U',
'û': 'u',
'Û': 'U',
'ú': 'u',
'Ú': 'U',
'ù': 'u',
'Ù': 'U',
'ç': 'c',
'Ç': 'C'
};
const tintColor = '#174A87'
const Loading = props => (
props.hasErrored ?
<TouchableWithoutFeedback onPress={props.fetchCategories}>
<View style={styles.center}>
<Text>oops... something went wrong. Tap to reload</Text>
</View>
</TouchableWithoutFeedback>
:
<View style={styles.center}>
<ActivityIndicator size="large" />
</View>
)
const Toggle = props => (
<TouchableWithoutFeedback onPress={() => props.onPress(!props.val)} disabled={props.disabled}>
<View style={styles.switch}>
<Text style={styles.label}>{props.name}</Text>
<Switch onTintColor={tintColor} onValueChange={v => props.onPress(v)} value={props.val} />
</View>
</TouchableWithoutFeedback>
)
export default class App extends Component {
constructor() {
super()
this.state = {
items: null,
loading: false,
selectedItems: [],
selectedItems2: [],
selectedItemObjects: [],
currentItems: [],
showDropDowns: false,
single: false,
readOnlyHeadings: false,
highlightChildren: false,
selectChildren: false,
hasErrored: false,
}
this.termId = 100;
}
componentWillMount() {
// this.fetchCategories()
this.pretendToLoad()
}
componentDidMount() {
// programatically opening the select
// this.SectionedMultiSelect._toggleSelector();
}
getProp = (object, key) => object && this.removerAcentos(object[key])
rejectProp = (items, fn) => items.filter(fn)
pretendToLoad = () => {
this.setState({ loading: true })
setTimeout(() => {
this.setState({ loading: false, items })
}, 4000)
}
// testing a custom filtering function that ignores accents
removerAcentos = (s) => {
return s.replace(/[\W\[\] ]/g, function (a) {
return accentMap[a] || a
})
};
filterItems = (searchTerm, items, { subKey, displayKey, uniqueKey }) => {
let filteredItems = []
let newFilteredItems = []
items.forEach((item) => {
const parts = this.removerAcentos(searchTerm.trim()).split(/[[ \][)(\\/?\-:]+/)
const regex = new RegExp(`(${parts.join('|')})`, 'i')
if (regex.test(this.getProp(item, displayKey))) {
filteredItems.push(item)
}
if (item[subKey]) {
const newItem = Object.assign({}, item)
newItem[subKey] = []
item[subKey].forEach((sub) => {
if (regex.test(this.getProp(sub, displayKey))) {
newItem[subKey] = [...newItem[subKey], sub]
newFilteredItems = this.rejectProp(filteredItems, singleItem =>
item[uniqueKey] !== singleItem[uniqueKey])
newFilteredItems.push(newItem)
filteredItems = newFilteredItems
}
})
}
})
return filteredItems
}
onSelectedItemsChange = (selectedItems) => {
const filteredItems = selectedItems.filter(val => !this.state.selectedItems2.includes(val))
this.setState({ selectedItems: filteredItems })
console.log(selectedItems)
}
onSelectedItemsChange2 = (selectedItems) => {
const filteredItems = selectedItems.filter(val => !this.state.selectedItems.includes(val))
this.setState({ selectedItems2: filteredItems })
}
onConfirm = () => {
this.setState({ currentItems: this.state.selectedItems })
}
onCancel = () => {
this.SectionedMultiSelect._removeAllItems()
this.setState({
selectedItems: this.state.currentItems,
})
console.log(this.state.selectedItems);
}
onSelectedItemObjectsChange = (selectedItemObjects) => {
this.setState({ selectedItemObjects })
console.log(selectedItemObjects)
}
onExpandDropDownsToggle = (expandDropDowns) => {
this.setState({ expandDropDowns })
}
onShowDropDownsToggle = (showDropDowns) => {
this.setState({ showDropDowns })
}
onReadOnlyHeadingsToggle = (readOnlyHeadings) => {
this.setState({ readOnlyHeadings })
}
onSingleToggle = (single) => {
this.setState({ single })
}
onHighlightChildrenToggle = (highlightChildren) => {
this.setState({ highlightChildren })
}
onSelectChildrenToggle = (selectChildren) => {
this.setState({ selectChildren })
}
fetchCategories = () => {
this.setState({ hasErrored: false })
fetch('http://www.mocky.io/v2/5a5573a22f00005c04beea49?mocky-delay=500ms', { headers: 'no-cache' })
.then(response => response.json())
.then((responseJson) => {
this.setState({ cats: responseJson })
})
.catch((error) => {
this.setState({ hasErrored: true })
throw error.message
})
}
filterDuplicates = items => items.sort().reduce((accumulator, current) => {
const length = accumulator.length
if (length === 0 || accumulator[length - 1] !== current) {
accumulator.push(current)
}
return accumulator
}, []);
noResults =
<View key="a" style={styles.center}>
<Text>Sorry! No results...</Text>
</View>;
handleAddSearchTerm = () => {
const searchTerm = this.SectionedMultiSelect._getSearchTerm();
const id = this.termId += 1;
if ( searchTerm.length && !(this.state.items || []).some( item => item.title.includes(searchTerm) ) ) {
const newItem = { id: id, title: searchTerm };
this.setState(prevState => ({items: [...prevState.items || [], newItem]}));
this.onSelectedItemsChange([...this.state.selectedItems, id])
this.SectionedMultiSelect._submitSelection()
}
}
renderSelectText = () => {
const { selectedItemObjects } = this.state;
return selectedItemObjects.length ?
`I like ${selectedItemObjects.map((item, i) => {
let label = `${item.title}, `
if (i === selectedItemObjects.length - 2) label = `${item.title} and `
if (i === selectedItemObjects.length - 1) label = `${item.title}.`
return label
}).join('')}`
:
'Select a fruit'
}
searchAdornment = (searchTerm) => {
return(
searchTerm.length ?
<TouchableOpacity
style={{ alignItems: 'center', justifyContent: 'center'}}
onPress={this.handleAddSearchTerm}
>
<View>
<Icon
size={18}
style={{ marginHorizontal: 15 }}
name="add"
/>
</View>
</TouchableOpacity>
:
null
)
}
onToggleSelector = (toggled) => {
console.log('selector is ', toggled ? 'open' : 'closed');
}
customChipsRenderer = (props) => {
console.log('props', props);
return (
<View style={{backgroundColor: 'yellow', padding: 15,}}>
<Text>Selected:</Text>
{props.selectedItems.map((singleSelectedItem) => {
const item = this.SectionedMultiSelect._findItem(singleSelectedItem)
if (!item || !item[props.displayKey]) return null
return (
<View key={item[props.uniqueKey]} style={{ flex: 0,marginRight: 5, padding: 10, backgroundColor: 'orange' }}>
<TouchableOpacity onPress={() => { this.SectionedMultiSelect._removeItem(item) }}>
<Text>{item[props.displayKey]}</Text>
</TouchableOpacity>
</View>
)
})}
</View>
)
}
render() {
return (
<ScrollView keyboardShouldPersistTaps="always" style={{ backgroundColor: '#f8f8f8' }} contentContainerStyle={styles.container}>
<Text style={styles.welcome}>
React native sectioned multi select example.
</Text>
<SectionedMultiSelect
items={this.state.items}
ref={SectionedMultiSelect => this.SectionedMultiSelect = SectionedMultiSelect}
uniqueKey="id"
subKey="children"
displayKey="title"
iconKey="icon"
autoFocus
// showCancelButton
headerComponent={
<View style={{ padding: 15, position: 'absolute', top: 0, right: 0, zIndex: 99 }}>
<TouchableOpacity onPress={this.SectionedMultiSelect && this.SectionedMultiSelect._cancelSelection}>
<Icon>cancel</Icon>
</TouchableOpacity>
</View>
}
stickyFooterComponent={
<View style={{ padding: 15, }}>
<Text style={{textAlign: 'center'}}>Hi</Text>
</View>
}
// hideConfirm
loading={this.state.loading}
// filterItems={this.filterItems}
// alwaysShowSelectText
customChipsRenderer={this.customChipsRenderer}
chipsPosition="top"
searchAdornment={(searchTerm) => this.searchAdornment(searchTerm)}
renderSelectText={this.renderSelectText}
// noResultsComponent={this.noResults}
loadingComponent={
<Loading
hasErrored={this.state.hasErrored}
fetchCategories={this.fetchCategories}
/>
}
chipRemoveIconComponent={<Icon style={{
fontSize: 18,
marginHorizontal: 6,
}}>cancel</Icon>}
// cancelIconComponent={<Text style={{color:'white'}}>Cancel</Text>}
showDropDowns={this.state.showDropDowns}
expandDropDowns={this.state.expandDropDowns}
animateDropDowns={false}
readOnlyHeadings={this.state.readOnlyHeadings}
single={this.state.single}
showRemoveAll
selectChildren={this.state.selectChildren}
highlightChildren={this.state.highlightChildren}
// hideSearch
// itemFontFamily={fonts.boldCondensed}
onSelectedItemsChange={this.onSelectedItemsChange}
onSelectedItemObjectsChange={this.onSelectedItemObjectsChange}
onCancel={this.onCancel}
onConfirm={this.onConfirm}
selectedItems={this.state.selectedItems}
colors={{ primary: this.state.selectedItems.length ? 'forestgreen' : 'crimson',}}
itemNumberOfLines={3}
selectLabelNumberOfLines={3}
styles={{
// chipText: {
// maxWidth: Dimensions.get('screen').width - 90,
// },
// itemText: {
// color: this.state.selectedItems.length ? 'black' : 'lightgrey'
// },
selectedItemText: {
color: 'blue',
},
// subItemText: {
// color: this.state.selectedItems.length ? 'black' : 'lightgrey'
// },
selectedSubItemText: {
color: 'blue',
},
}}
cancelIconComponent={
<Icon
size={20}
name="close"
style={{ color: 'white' }}
/>
}
/>
<SectionedMultiSelect
items={items2}
ref={SectionedMultiSelect2 => this.SectionedMultiSelect2 = SectionedMultiSelect2}
uniqueKey="id"
subKey="children"
displayKey="title"
// showCancelButton
// hideSelect={true}
selectText={this.state.selectedItems2.length ? 'Select categories' : 'All categories'}
noResultsComponent={this.noResults}
loadingComponent={
<Loading
hasErrored={this.state.hasErrored}
fetchCategories={this.fetchCategories}
/>
}
// cancelIconComponent={<Text style={{color:'white'}}>Cancel</Text>}
showDropDowns={this.state.showDropDowns}
expandDropDowns={this.state.expandDropDowns}
customLayoutAnimation={LayoutAnimation.Presets.spring}
readOnlyHeadings={this.state.readOnlyHeadings}
single={this.state.single}
showRemoveAll
selectChildren={this.state.selectChildren}
highlightChildren={this.state.highlightChildren}
// hideSearch
// itemFontFamily={fonts.boldCondensed}
onSelectedItemsChange={this.onSelectedItemsChange2}
// onSelectedItemObjectsChange={this.onSelectedItemObjectsChange}
onCancel={this.onCancel}
onConfirm={this.onConfirm}
onToggleSelector={this.onToggleSelector}
selectedItems={this.state.selectedItems2}
styles={{
chipText: {
maxWidth: Dimensions.get('screen').width - 90,
},
itemText: {
color: this.state.selectedItems2.length ? 'black' : 'lightgrey'
},
subItemText: {
color: this.state.selectedItems2.length ? 'black' : 'lightgrey'
},
// cancelButton: {
// // flex: 6,
// },
// subItem: {
// paddingVertical: 15,
// },
}}
// numberOfLines={1}
/>
<View>
<View style={styles.border}>
<Text style={styles.heading}>Settings</Text>
</View>
<Toggle name="Single" onPress={this.onSingleToggle} val={this.state.single} />
<Toggle name="Read only headings" onPress={this.onReadOnlyHeadingsToggle} val={this.state.readOnlyHeadings} />
<Toggle name="Expand dropdowns" onPress={this.onExpandDropDownsToggle} val={this.state.expandDropDowns} disabled={!this.state.showDropDowns}/>
<Toggle name="Show dropdown toggles" onPress={this.onShowDropDownsToggle} val={this.state.showDropDowns} />
<Toggle name="Auto-highlight children" onPress={this.onHighlightChildrenToggle} val={this.state.highlightChildren} disabled={this.state.selectChildren}/>
<Toggle name="Auto-select children" onPress={this.onSelectChildrenToggle} val={this.state.selectChildren} disabled={this.state.highlightChildren}/>
<TouchableWithoutFeedback onPress={() => this.SectionedMultiSelect._removeAllItems()}>
<View style={styles.switch}>
<Text style={styles.label}>Remove All</Text>
</View>
</TouchableWithoutFeedback>
</View>
</ScrollView>
)
}
}