Just a very flexible component for searching & selecting an item from a (potentially) long list of options.
There is no built in component in React Native, that properly enables selecting an item from a longer list. There are some 3rd-party solutions, but I found them either not really suitable for small mobile devices (for example producing hard to use dropdowns) or not flexible enough, so here is another take, including an (optional) live search.
Usage
import Select from 'components/Select'
<Select
disabled={!isConnected}
labelTextColor='black'
name={'Select Country'}
options={
countries.map((
country: {
id: number,
name: string,
contractorName: string,
flag: string
}) => ({
id: country.id,
name: country.name,
image: country.flag
}))
}
onValueChange={(selectedId: number) => handleLanguageSwitching(selectedId)}
selectedId={activeCountry.id}
mainButtonStyle={{
backgroundColor: 'transparent',
borderColor: '#6c757d'
}}
mainButtonTextStyle={{color: 'black'}}
/>
Reference
Props
Name | Type |
---|---|
disabled? | boolean |
onValueChange | Function |
options | options |
selectedId | number |
showSearch? | boolean |
labelTextColor? | string |
mainButtonTextStyle? | TextStyle |
mainButtonStyle? | ViewStyle |
name | string |
Code
import React, { useState, useEffect } from 'react'
import { FlatList, Image, Input, Modal, Platform, Pressable, StyleSheet, Text, TextStyle, View, ViewStyle } from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { FontAwesome } from '@expo/vector-icons'
type item = {
id: number,
name: string
}
type options = [
item
]
export default function Select(props: {
disabled?: boolean,
onValueChange: Function,
options: options,
selectedId: number,
showSearch?: boolean,
labelTextColor?: 'string',
mainButtonTextStyle?: TextStyle,
mainButtonStyle?: ViewStyle,
name: string
}) {
const [selectedItemId, setSelectedItemId] = useState(props.selectedId)
const [selectedItemText, setSelectedItemText] = useState('')
const [modalVisible, setModalVisible] = useState(false)
const [search, setSearch] = useState('')
const [filteredOptions, setFilteredOptions] = useState(props.options)
const showSearch = (props?.options.length > 15 || props.showSearch) && props.showSearch !== false
const insets = useSafeAreaInsets()
const setItem = (item: {id: number, name: string}) => {
setModalVisible(false)
setSelectedItemId(item.id)
setSelectedItemText(item.name)
setSearch('')
// Return id and name
props.onValueChange(item.id, item.name)
}
// Handle selectedId changes from parent
useEffect(() => {
setSelectedItemId(props.selectedId)
// If item is preselected from parent, e.g. selectedId is defined
if(props.options.length > 0 && props.selectedId > 0) {
const selectedItem = props.options.find(item => item.id === props.selectedId)
if(selectedItem?.name) {
setSelectedItemText(selectedItem.name)
}
}
}, [props.selectedId])
// Handle options changes from parent
// Set up with options data
useEffect(() => {
if(props.options) {
setFilteredOptions(props.options)
if(!props.selectedId || props.selectedId === 0) {
setSelectedItemText('Please select …')
}
}
// setSelectedItemText, if only one option exists
if(props.options.length === 1) {
setItem(props.options?.[0])
}
}, [props.options])
// Handle search
useEffect(() => {
// Check if searched text is not blank
if (search.length > 0) {
// Inserted text is not blank
// Filter the options and update FilteredDataSource
const newData = props.options.filter((item: {name: string}) => {
// Applying filter for the inserted text in search bar
const itemData = item.name
? item.name.toUpperCase()
: ''.toUpperCase()
const textData = search.toUpperCase()
return itemData.indexOf(textData) > -1
})
setFilteredOptions(newData)
} else {
// Inserted text is blank
// Update FilteredDataSource with props.options
setFilteredOptions(props.options)
}
}, [search])
// We need to call this Component as a function, otherwise the search field loses focus on every keystroke
const FlatListHeader = () => {
return (
<>
{ showSearch &&
<>
<SafeAreaView>
<FontAwesome name='search' size={17} style={styles.searchIcon} />
<Input
autoCorrect={false}
containerStyle={{marginTop: 0}}
onChangeText={(text: string) => setSearch(text)}
placeholder='Suchen …'
style={styles.searchInput}
underlineColorAndroid='transparent'
value={search}
/>
{/* clear button */}
{ (search.length > 0) ? (
<Pressable
onPress={() => setSearch('')}
style={styles.searchClearButton}
>
<FontAwesome name='times-circle' size={17} color='#999' />
</Pressable>
) : null
}
</SafeAreaView>
<FlatListSeparator />
</>
}
</>
)
}
type Item = {
id: number
name: string
image?: string
}
// Single list item
const FlatListItem = ({ item }: { item: Item }) => {
const isActive = selectedItemId === item.id
return (
<Pressable
onPress={() => setItem(item)}
style={
({ pressed }) => ({
opacity: pressed ? 0.5 : 1,
...styles.item,
...{backgroundColor: isActive ? '#c9e9e8' : 'transparent'},
...{paddingRight: insets.right},
...{paddingLeft: insets.left}
})
}
>
<Text style={styles.itemText}>
{item.name}
</Text>
{ item.image &&
<Image
style={styles.itemImage}
resizeMode={'contain'}
source={{ uri: item.image }}
/>
}
</Pressable>
)
}
const FlatListSeparator = () => {
return (
<View
style={{
height: StyleSheet.hairlineWidth,
width: '100%',
backgroundColor: '#999',
}}
/>
)
}
const LabelAndButton = () => {
return (
<View style={styles.labelAndButton}>
<Text
style={{fontWeight: 'bold', color: props.labelTextColor}}
>
{props.name}
</Text>
<Pressable
disabled={props.disabled}
onPress={() => { setModalVisible(true) }}
style={{
...styles.mainButton,
...{backgroundColor: props.disabled ? '#dcdde2' : 'white'},
...props.mainButtonStyle
}}
>
<Text
numberOfLines={1}
style={{
flex: 1,
...{color: props.disabled ? '#888' : 'black'},
...props.mainButtonTextStyle
}}
>
{selectedItemText?.replace(/(\r\n|\n|\r)/gm, ' – ')}
</Text>
<FontAwesome
name='angle-down'
size={17}
style={{
...{color: props.disabled ? '#888' : 'black'},
marginTop: 1,
...props.mainButtonTextStyle
}}
/>
</Pressable>
</View>
)
}
return (
<>
<Modal
animationType='slide'
hardwareAccelerated
onRequestClose={() => setModalVisible(false)}
presentationStyle='formSheet'
statusBarTranslucent={true}
supportedOrientations={['portrait', 'landscape']}
transparent={false}
visible={modalVisible}
>
<SafeAreaView style={styles.modalHeader}>
<Pressable
onPress={() => { setModalVisible(!modalVisible) }}
style={{flex: 1}}
>
<Text style={styles.modalCloseText}>
Fertig
</Text>
</Pressable>
<Text style={{fontSize: 18, fontWeight: '800'}}>
Bitte auswählen …
</Text>
{/* empty element needed for flex alignment */}
<View style={{flex: 1}} />
</SafeAreaView>
<FlatList
data={filteredOptions}
keyExtractor={(item, index) => index.toString()}
initialNumToRender={20}
keyboardShouldPersistTaps='handled'
ItemSeparatorComponent={FlatListSeparator}
ListHeaderComponent={FlatListHeader()}
ListFooterComponent={<View style={{height: insets.bottom}} />}
renderItem={FlatListItem}
style={{backgroundColor: '#f5f5f5', height: '100%'}}
/>
</Modal>
<LabelAndButton />
</>
)
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'white',
flex: 1
},
labelAndButton: {
marginTop: 15,
marginLeft: 0,
marginBottom: 7
},
mainButton: {
justifyContent: 'space-between',
flexDirection: 'row',
fontFamily: 'Muli',
fontSize: 15,
textAlignVertical: 'top',
marginTop: 8,
paddingHorizontal: 10,
paddingTop: 11,
paddingBottom: 11,
backgroundColor: 'white',
borderColor: '#979baa',
borderRadius: 8,
borderWidth: 1
},
modalHeader: {
flexDirection: 'row',
paddingRight: 15,
paddingVertical: 15,
paddingLeft: 15,
borderBottomColor: '#999',
borderBottomWidth: StyleSheet.hairlineWidth,
},
modalCloseText: {
color: '#27b6af',
fontSize: 16,
fontWeight: 'bold',
marginTop: 2
},
searchIcon: {
position: 'relative',
top: Platform.OS === 'ios' ? 25 : 33,
left: 25,
zIndex: 1,
width: 20,
color: '#999',
},
searchInput: {
marginTop: -18,
marginBottom: 15,
marginHorizontal: 15,
paddingLeft: 30,
backgroundColor: '#e3e4e8',
borderRadius: 8,
borderColor: 'transparent',
},
searchClearButton: {
position: 'absolute',
right: 15,
top: Platform.OS === 'ios' ? 20 : 28,
zIndex: 1,
alignItems: 'center',
justifyContent: 'center',
width: 30,
height: 30,
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
},
itemImage: {
width: 60,
height: 30,
marginRight: 15
},
itemText: {
fontSize: 16,
flex: 1,
padding: 15,
},
})