TextInput lost focus after typing one symbol when searching - javascript

I have a FlatList
<View style={styles.container}>
<FlatList data={this.state.restaurants}
renderItem={({ item }) => this.renderItem(item.restaurant)}
keyExtractor={restaurant => restaurant.key}
ListHeaderComponent={() => this.renderHeaderComponent()}
ItemSeparatorComponent={this.renderSeparator}/>
</View>
And have TextInput in header it. I am using TextInput as search bar.
renderHeaderComponent() {
return(
<View style={{ flexDirection: 'row', marginTop: 10, borderBottomColor: '#CED0CE', borderWidth: 1, borderColor: 'transparent' }}>
<Icon name='search' size={30} style={{ marginLeft: 10, marginRight: 10 }}/>
<TextInput
style={{height: 40, flex: 1}}
onChangeText={(text) => this.onChangeText(text)}
placeholder='Type text for search'
clearButtonMode='while-editing'
value={this.state.searchText}
/>
</View>
);
};
In onChangeMethod i filter my data.
onChangeText(text) {
const filteredRestaurants = _.filter(this.props.list, (restaurantObject) => {
const restaurant = restaurantObject.restaurant;
const result = restaurant.name.trim().toLowerCase().includes(text.trim().toLowerCase());
return result;
})
this.setState({
searchText: text,
restaurants: filteredRestaurants
});
}
The problem is following. When I type one symbol in TextInput then focus is lost immediately from TextInput? How can I keep focus in TextInput while typing?

You need to use an auto-bound method for this, as ListHeaderComponent is of type ReactClass, and your current method basically re-creates and re-binds its render every time the data updates, which is not what you want. This concept is further explained in this comment
Anyway, for your example, to fix your issues you should
1) Change your ListHeaderComponent prop to
ListHeaderComponent={this.renderListHeader}
2) Now you want to change your renderHeaderComponent method to be an auto-bound method, and by doing this a new render will not be instantiated every time you change data ( Or enter text into the `TextInput)
renderListHeader = () => (
<View style={{ flexDirection: 'row', marginTop: 10, borderBottomColor: '#CED0CE', borderWidth: 1, borderColor: 'transparent' }}>
<Icon name='search' size={30} style={{ marginLeft: 10, marginRight: 10 }}/>
<TextInput
style={{height: 40, flex: 1}}
onChangeText={(text) => this.onChangeText(text)}
placeholder='Type text for search'
clearButtonMode='while-editing'
value={this.state.searchText}
/>
</View>
)

I ran into this, and to solve it I wrapped the renderListHeader in a React.useMemo hook and passed the state hook as an item to the dependency array.
renderListHeader = useMemo(() => (
<View style={{ flexDirection: 'row', marginTop: 10, borderBottomColor: '#CED0CE', borderWidth: 1, borderColor: 'transparent' }}>
<Icon name='search' size={30} style={{ marginLeft: 10, marginRight: 10 }}/>
<TextInput
style={{height: 40, flex: 1}}
onChangeText={(text) => this.onChangeText(text)}
placeholder='Type text for search'
clearButtonMode='while-editing'
value={this.state.searchText}
/>
</View>
), [this.onChangeText])

This is still an issue for SectionList as of react-native 0.61.5. The auto-bound method doesn't work since the ListHeaderComponent re-renders when data is becomes an empty array.
I used the following work-around :
Move the text input code at the same level than the section list
Using position absolute, position it at the place you want.
Wrap it in an Animated.View
Leverage Animated.event to translate Y the Animated.View
Code sample
const animatedScrollYValue = useRef(new Animated.Value(0)).current;
...
<View>
<Animated.View style={{
position: 'absolute',
top: 142,
left: 30,
right: 30,
zIndex: 1,
transform: [{ translateY: Animated.multiply(animatedScrollYValue, new Animated.Value(-1)) }] }}>
// Your text input
</Animated.View>
<Animated.SectionList
scrollEventThrottle={1}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: animatedScrollYValue } } }], { useNativeDriver: true })}
keyExtractor={(item) => item.id}
ListHeaderComponent={// Whatever you want but make you include space for the absolute TextInput}
sections={data}
renderItem={renderItem}
renderSectionHeader={renderHeader}
/>
</View>

I found another workaround for SectionList that seems to work so far, and I'll update this answer if I find it stops working. Rather than rendering my component in ListHeaderComponent I add a dummy section at the start of my data and then use a conditional in renderSectionHeader to render it out.
<SectionList
sections={[{ title: 'header', data: [] }, ...sections]}
renderSectionHeader={({ section }) =>
section.title === 'header' ? (
<MyListHeaderComponent />
) : (
<DefaultSectionHeaderComponent />
)
}
/>
Having worked with some pretty hairy CollectionView screens in Swift/UIKit it's not that different from how we would handle a similar need in that environment so hopefully that means under the hood perf won't be an issue, but again I'll update this answer if that becomes the case.
Another option may be to just add a dummy item to your sections array so that it never becomes empty but I haven't tried that.

Related

ScrollView not working in react-native app, content disappears when attempting to add flex and flexGrow

for some reason a ScrollView in one of my components isn't working despite working in every other component I've implemented it in. Attempting to implement solutions to similar problems seems to just make the content I want displayed disappear.
I'm expecting to have a scrollable list of sample restaurant dishes. I created some dummy data to pass in for now but noticed it didn't scroll after reaching the end of the phone screen.
const testFoods = [
{
title: "test",
description: "Lorem Ipsum",
price: "$7.77",
image: "dummyLink",
},
// Same as above but 4 more times, didn't want to clog up the description
];
export default function MenuItems() {
return (
<ScrollView showsVerticalScrollIndicator={false}>
{testFoods.map((food, index) => (
<View key={index}>
<View style={styles.menuItemStyle}>
<FoodDetails food={food} />
<FoodImage food={food} />
</View>
</View>
))}
</ScrollView>
);
}
const FoodDetails = (props) => (
<View style={{ width: 240, justifyContent: "space-evenly" }}>
<Text style={styles.titleStyle}>{props.food.title}</Text>
<Text>{props.food.description}</Text>
<Text>{props.food.price}</Text>
</View>
);
const FoodImage = (props) => (
<View>
<Image
source={{ uri: props.food.image }}
style={{
width: 100,
height: 100,
borderRadius: 8,
}}
/>
</View>
);
const styles = StyleSheet.create({
menuItemStyle: {
flexDirection: "row",
justifyContent: "space-between",
margin: 20,
},
titleStyle: {
fontSize: 19,
fontWeight: "600",
},
});
The result is like so
Result with code above
The Header component with the sample restaurant image is a separate component by the way.
I have more data that can't be seen as for whatever reason the screen refuses to scroll. I'm using my actual phone for the tests but the result is the same when I use an emulator, scrolling doesn't work. After looking online I thought I would try adding a parent View with flex: 1 and a flexGrow: 1 to contentContainerStyle inside the ScrollView like so.
export default function MenuItems() {
return (
<View style={{ flex: 1 }}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ flexGrow: 1 }}
>
{testFoods.map((food, index) => (
<View key={index} style={styles.menuItemStyle}>
<FoodDetails food={food} />
<FoodImage food={food} />
</View>
))}
</ScrollView>
</View>
);
}
But that only resulted in the content disappearing. Reloading the app didn't change anything either Result after trying above code
Attempting to use a FlatList had the same result. I've also read that having percentage based styling values on height for any of the children components can make the scrolling stop working but all my components don't utilize such. Oddly enough when I change the styling on the outer view to height: 400, I'm able to get the scrolling to work but this is very sloppy and will likely only work on phone screens similar to mine. I know the ScrollView component is working fine, as when I add "horizontal" to it the scrolling works just fine and I'm able to scroll to the last item in the dataset. Obviously all the content is horizontal now though. After adding horizontal too ScrollView, scrolling works fine horizontally
Any ideas? Could it be some part of my styling I'm not noticing? I'm unable to test this on IOS so I'm not sure if it's an Android specific problem, would be strange though as scrolling worked fine in my other components.
Here's also the Header component code just in case it could be anything in there, although It shouldn't be.
const testImage =
"https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Restaurant_N%C3%A4sinneula.jpg/800px-Restaurant_N%C3%A4sinneula.jpg";
const testTitle = "Sample Restaurant";
const testDescription = "Thai · Comfort · $$ · 🎫 · 4 ⭐ (217+)";
export default function About() {
return (
<View>
<RestaurantImage image={testImage} />
<RestaurantTitle title={testTitle} />
<RestaurantDescription description={testDescription} />
</View>
);
}
const RestaurantImage = (props) => (
<Image source={{ uri: props.image }} style={{ width: "100%", height: 180 }} />
);
const RestaurantTitle = (props) => (
<Text
style={{
fontSize: 29,
fontWeight: "600",
marginTop: 10,
marginHorizontal: 15,
}}
>
{props.title}
</Text>
);
const RestaurantDescription = (props) => (
<Text
style={{
marginTop: 10,
marginHorizontal: 15,
fontWeight: "400",
fontSize: 15.5,
}}
>
{props.description}
</Text>
);
Wrap your card with TouchableOpacity.
<TouchableOpacity activeOpacity={0} key={index} style={styles.menuItemStyle}>
<FoodDetails food={food} />
<FoodImage food={food} />
</TouchableOpacity>
I hope this thing will work.

React Native: How to conditionally style an element based on the rest of the attributes in the same object?

I have a list of icons in a modal in my React Native app. Since I need to show the availability of these icons based on the colour, I have put them into an array with a boolean attribute. But what I want to know is how to add a conditional style to the icon element based on the boolean attribute of the same object.
Here is the structure of my array:
const icons = [
{
name: 'ATM',
bool: false,
return:
<FontAwesome
name='euro'
size={25}
color={bool ? 'green' : 'grey'}
/>
}
]
I then display the icons in my modal like this:
<View style={{flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-evenly'}}>
{icons.map((obj, index) => {
return (
<View style={{justifyContent: 'center', alignItems: 'center', padding: wp('2.0%')}}>
<TouchableOpacity>
{obj.return}
</TouchableOpacity>
<Text>{obj.name}</Text>
</View>
)
})}
</View>
I've also tried using
this.bool ? 'green' : 'grey'
which does not work.
I don't think there's a way to access the value of bool from within.
You can try setting the right style when initializing the array but I'd recommend not to store react Elements directly in large arrays for performance reasons and instead display icons in your modal like this:
<View style={{flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-evenly'}}>
{icons.map((obj, index) => {
return (
<View style={{justifyContent: 'center', alignItems: 'center', padding: wp('2.0%')}}>
<TouchableOpacity>
<FontAwesome
name='euro'
size={25}
color={obj.bool ? 'green' : 'grey'}
/>
</TouchableOpacity>
<Text>{obj.name}</Text>
</View>
)
})}
</View>
Hope so this will help you
const icons = [
{
name: 'ATM',
bool: false,
return:
<FontAwesome
name='euro'
size={25}
color={this.bool == true ? 'green' : 'grey'}
/>
}
]

Align header beside items in flatlist?

I have 2 columns in flatList and I'm trying to align the header component besides the items itself,
Like this
But I got the "add image" above then the items below it,
I'm trying to solve it by using flexWrap in content container style but since I was using numColumns i got a warning that tells flexWrap not supported and use numColumns instead.
so i don't know how can i solve it, so if anybody can help in this case!
here's a snack
Code snippet
const renderItems = ({ item, index }) => {
return (
<View style={{ flex: 0.5, margin: 4 }}>
<View style={[styles.imgContainer, { borderWidth: 0 }]}>
<Image
style={styles.imgStyle}
source={{
uri:
'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcR1K8ypPsfNVQU8lVxl1i2_ajismMS_w6FA4Q&usqp=CAU',
}}
/>
</View>
</View>
);
};
const renderHeader = () => (
<TouchableOpacity
// onPress={appeandImgs}
style={styles.imgContainer}>
<Image
style={styles.imgStyle}
source={{
uri: 'https://static.thenounproject.com/png/3322766-200.png',
}}
/>
</TouchableOpacity>
);
const keyExtractor = (item, index) => String(index);
<FlatList
data={[1,2,3]}
style={styles.flatList}
numColumns={2}
renderItem={renderItems}
ListHeaderComponentStyle={{
backgroundColor: '#ff0',
width: ScreenWidht / 2 - 20,
}}
keyExtractor={keyExtractor}
ListHeaderComponent={renderHeader}
columnWrapperStyle={{
backgroundColor: '#f07',
}}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: 12,
paddingTop: 15,
}}
/>

scroll when the keypad is open

On my screen, I type in the input field and get search results accordingly. The list is rendered within a ScrollView but it still doesn't let me scroll when the keypad is open (in Android at least).
How can I fix this?
This is the component where the scroll view is rendered.
export const LocationsFound: React.FunctionComponent<LocationsFoundProps> = ({
addressesFound,
}) => {
return (
<>
{addressesFound.length > 0 ? (
<KeyboardAwareScrollView
style={styles.searchResultsContainer}
keyboardShouldPersistTaps={'always'}
keyboardDismissMode={'on-drag'}
>
{addressesFound.map((addressDetails: addressDetailsType) => {
return (
<View
key={addressDetails.placeName}
style={styles.resultContainer}>
<Text
style={styles.text}
onPress={() => handleLocationSelection(addressDetails)}>
{addressDetails.placeName}
</Text>
</View>
);
})}
</KeyboardAwareScrollView>
) : null}
</>
);
};
const styles = StyleSheet.create({
searchResultsContainer: {
width: moderateScale(400),
paddingHorizontal: moderateScale(50),
paddingRight: moderateScale(65),
marginTop: moderateScale(10),
},
resultContainer: {
marginTop: moderateScale(10),
borderBottomWidth: 1,
borderBottomColor: 'grey',
},
text: {
fontSize: moderateScale(15),
},
});
This is the component where the LocationsFound component is called.
return (
<SafeAreaView style={styles.safeAreaViewContainer}>
<View style={styles.container}>
<View style={styles.searchFieldContainer}>
<AddressSearchInput
addressType="favouritePoint"
placeholder="Ort eingeben"
/>
</View>
<View style={styles.dropdown}>
<LocationsFound
addressesFound={locations.addressesFoundList}
/>
</View>
</View>
</SafeAreaView>
);
};
export const styles = StyleSheet.create({
safeAreaViewContainer: {
flex: 1,
},
container: {
height: '100%',
backgroundColor: 'white',
width: '100%',
display:"flex",
flexDirection:"column",
flex: 1
},
dropdown: {
position: 'absolute',
top: moderateScale(215),
zIndex: moderateScale(10),
backgroundColor: '#fff',
flex: 1
},
});
I also tried adding
onScrollBeginDrag={Keyboard.dismiss}
but it doesn't make a difference.
sounds like a height, issue, without all the code, no one is going to give your clear answers without speculation. the keyboard doesn't shrink the view, check out this package, it may help - https://github.com/APSL/react-native-keyboard-aware-scroll-view

React-native how to move screen up on textinput

I have a login screen created using react-native.
How can I shift my screen up when the user is typing in the textInput?
Do I listen to the onFocus() event and use css styling to change the style of the view?
In 2017 (RN 0.43) there is special component for this: KeyboardAvoidingView
You can use ScrollView to control screen up and down movements. As long as user hasn't focused any TextInput, you can disable scroll. On focus, just shift up the scrollview using Content Offset prop.
<TextInput
onFocus={this.textInputFocused.bind(this)}
/>
textInputFocused() {
//do your stuff here. scroll screen up
}
Hope it helps!
import {KeyboardAvoidingView} from 'react-native';
<KeyboardAvoidingView style={styles.container} behavior="padding" enabled>
<Text style={{height: 100, marginTop: 30}}> test text before input</Text>
<Text style={{height: 100, marginTop: 30}}> test text before input</Text>
<Text style={{height: 100, marginTop: 30}}> test text before input</Text>
<Text style={{height: 100, marginTop: 30}}> test text before input</Text>
<Text style={{height: 100, marginTop: 30}}> test text before input</Text>
<TextInput
style={{height: 40, borderColor: 'gray', borderWidth: 1}}
onChangeText={(text) => this.setState({text})}
value={this.state.text}
/>
<Text style={{height: 100, marginTop: 20}}>1 test text after input</Text>
<Text style={{height: 100, marginTop: 20}}>2 test text after input</Text>
<Text style={{height: 100, marginTop: 20}}>3 test text after input</Text>
<Text style={{height: 100, marginTop: 20}}>4 test text after input</Text>
<Text style={{height: 100, marginTop: 20}}>5 test text after input</Text>
</KeyboardAvoidingView>
Run in snack :
https://snack.expo.io/H1BE5ZoXV
Night Fury's answer is pretty good, though wouldn't fuss with the ScrollView's contentOffset, I'd use the ScrollResponder:
render() {
return (
<ScrollView ref="myScrollView">
<TextInput
ref="myInput"
onFocus={this._scrollToInput.bind(this)}
/>
</ScrollView>
);
}
_scrollToInput {
const scrollResponder = this.refs.myScrollView.getScrollResponder();
const inputHandle = React.findNodeHandle(this.refs.myInput)
scrollResponder.scrollResponderScrollNativeHandleToKeyboard(
inputHandle, // The TextInput node handle
0, // The scroll view's bottom "contentInset" (default 0)
true // Prevent negative scrolling
);
}
See the method definition: scrollResponderScrollNativeHandleToKeyboard
This package does a greate job, introduces a KeyboardAwareScrollView component that scrolls the view up matching the input with the keyboard, then scrolling back down.
And another solution, working with RN 0.2, this time instead of squashing the content it scrolls.
inputFocused: function(ref) {
this._scroll(ref, 75);
},
inputBlurred: function(ref) {
this._scroll(ref, 0);
},
_scroll: function(ref, offset) {
setTimeout(() => {
var scrollResponder = this.refs.myScrollView.getScrollResponder();
scrollResponder.scrollResponderScrollNativeHandleToKeyboard(
React.findNodeHandle(this.refs[ref]),
offset,
true
);
});
},
...
render: function() {
return <View style={{flex: 1}}>
<ScrollView ref="myScrollView" keyboardDismissMode='interactive' contentContainerStyle={{flex: 1}}>
<TextInput
ref="myInput"
onFocus={this.inputFocused.bind(this, 'myInput')}
onBlur={this.inputBlurred.bind(this, 'myInput')} />
</ScrollView>
</View>
}
It's a crap shoot to get the native keyboard awareness functionality of ScrollView working. For my Android app, it works perfectly in one screen that is nearly identical as the other for which doesn't work. And on iOS, it just doesn't work. This is what's working for me:
import { Keyboard, ScrollView, StyleSheet, View } from 'react-native';
this.state = {
filler: false,
}
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide.bind(this));
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
_keyboardDidShow() {
this.setState({filler: true})
setTimeout(() => this.vertical && this.vertical.scrollToEnd({animated: true}), 0);
}
_keyboardDidHide() {
this.setState({filler: false})
}
...
return (
<ScrollView ref={ref => this.vertical = ref}>
<TextInput/>
{ this.state.filler ? <View style={styles.filler}/> : null }
</ScrollView>
)
styles.filler = {
height: 'Keyboard Height'
}
Note: This might only work if your <TextInput/> is at the bottom of the screen which it was in my case.

Categories

Resources