Creating a FlatList Grid from props data - react-native

Creating a screen that contains a FlatList grid of photos that I fetch from my rails API. Having trouble displaying the data - https://ibb.co/XkfSvxm. I wish for the images, regardless of how many there are, to be in a square format.
Currently, I can fetch and display the data in square photos except whenever there is 1 or 2 photos on a row. They fill the row rather than take the formatting from the formatItems method (which essentially pushes two empty items into the array forcing the data into a grid).
I've tried a bunch of things including returning the data straight from formatItems and plugging the method straight into the data of the flatlist, but then no data is loading. I also had it working well with a local SQL DB package.
How would I go around arranging so that I can format the data from props and display it correctly in the FlatList?
Here is the code:
class VaultScreen extends React.Component {
state = {
searchQuery: '',
refreshing: false,
setRefreshing: false
};
constructor(props) {
super(props);
this.state = {
items: {},
itemSelected: {}
};
this.props.fetchPosts()
}
componentDidMount() {
let itemData = this.props.posts
this.setState({
items: itemData,
});
// this.formatItems(itemData)
}
formatItems = () => {
const { items } = this.state
const newItems = []
const numberOfFullRows = Math.floor(items.length / 3);
// const numberOfFullRows = Math.floor(itemData.length / 3);
let numberOfElementsLastRow = 1
// items.length - (numberOfFullRows * 3);
while (numberOfElementsLastRow !== 3 && numberOfElementsLastRow !== 0) {
newItems.push({ key: `blank-${numberOfElementsLastRow}`, empty: true });
numberOfElementsLastRow++;
}
return this.setState({ items: newItems })
};
renderItem = ({ item, type }) => {
const { items } = this.state;
if (item.empty === true) {
return <View style={[styles.item, styles.itemInvisible]} />;
} else {
return (
<TouchableOpacity style={styles.item} onPressIn={() => this.setState({ itemSelected: item.id })} onPress={this.viewPhoto} key={item.id}>
<Image source={{ uri: item.image }} style={{ flex: 1, width: '100%', height: undefined }} />
</TouchableOpacity>
);
}
};
render() {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: 'white' }}>
<FlatList
data={this.state.items}
renderItem={this.renderItem}
numColumns={3}
keyExtractor={(item, index) => index.toString()}
refreshControl={<RefreshControl refreshing={this.state.refreshing} onRefresh={() => this.onRefresh()} />}
/>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
posts: state.vault.posts
})
const mapDispatchToProps = {
fetchPosts: () => fetchPosts(),
}
export default connect(mapStateToProps, mapDispatchToProps)(VaultScreen)

hi you can add minWidth and maxWidth styling to the parant View of the render method (in your case give style to TouchableOpacity ) value should be total_width / 3 (+-margin if you want to give) . and then add width:total_width to columnWrapperStyle of FlatList.
NOTE:
-- total_width = 100%
-- keeping minWidth and maxWidth values same will give you perfect view as per your expectation (In your case if total_width : 100% then minWidth and maxWidth values will be 30%)

Related

Get position of individual Items in flatlist

I have flatlist horizontal like below
const DATA = [
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
title: 'First Item',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
title: 'Second Item',
},
{
id: '58694a0f-3da1-471f-bd96-145571e29d72',
title: 'Third Item',
},
{
id: 'bd7acbea-c1b1-46c2-aed5-3ad353abb28ba',
title: 'Fourth Item',
},
{
id: '3ac68afc-c605-48d3-a4f8-fbd291aa97f63',
title: 'Fifth Item',
},
{
id: '58694a0f-3da1-471f-bd961-145571e29d72',
title: 'Sixth Item',
},
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
const renderItem = ({ item }) => (
<Item title={item.title} />
);
return (
<SafeAreaView style={styles.container}>
<FlatList
horizontal
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</SafeAreaView>
);
}
Whenever Item entered the viewport , I want to add animation to that element.I can get X and Y position of scroll with onScroll , now how do i get the positions of items to check if its in view port or if it went away from viewport...
Thank you.
Sorry for the late response. My pc has been super weird lately so when I encounter errors I have to second guess myself, and when nothing appears wrong, I second guess my pc (this time it was entirely me).
Here's my answer. I implemented the basic fade in/out [animation example][1] into the Item component. Whether it fades out or in is decided by the prop isViewable
// Item.js
const Item = (props) => {
const {
item:{title, isViewable}
} = props
/*
I copied and pasted the basic animation example from the react-native dev page
*/
const fadeAnim = useRef(new Animated.Value(1)).current;
const fadeIn = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver:false
}).start();
};
const fadeOut = () => {
Animated.timing(fadeAnim, {
toValue: 0,
duration: 1500,
useNativeDriver:false
}).start();
};
/* end of animation example*/
// fade in/out base on if isViewable
if(isViewable || isViewable == 0)
fadeIn()
else
fadeOut()
const animation = {opacity:fadeAnim}
return (
//add animation to Animated.View
<Animated.View style={[style.itemContainer,animation]}>
<View style={style.item}>
<Text style={style.title}>{title}</Text>
</View>
</Animated.View>
);
}
Create a FlatListWrapper (to avoid the onViewableItemChange on fly error). By doing this, as long as you don't make changes to FlatListWrapper, you wont get the on the fly error
// FlatListWrapper.js
const FlatListWrapper = (props) => {
// useRef to avoid onViewableItemsChange on fly error
const viewabilityConfig = useRef({
// useRef to try to counter the view rerender thing
itemVisiblePercentThreshold:80
}).current;
// wrapped handleViewChange in useCallback to try to handle the onViewableItemsChange on fly error
const onViewChange = useCallback(props.onViewableItemsChanged,[])
return (
<View style={style.flatlistContainer}>
<FlatList
{...props}
horizontal={true}
onViewableItemsChanged={onViewChange}
/>
</View>
);
}
const style = StyleSheet.create({
flatlistContainer:{
borderWidth:1,
borderColor:'red',
width:'50%',
height:40
},
// main FlatList component
const FlatListAnimation = () => {
// store the indices of the viewableItmes
const [ viewableItemsIndices, setViewableItemsIndices ] = useState([]);
return (
<SafeAreaView style={style.container}>
<FlatListWrapper
horizontal={true}
//{/*give each data item an isViewable prop*/}
data={DATA.map((item,i)=>{
item.isViewable=viewableItemsIndices.find(ix=>ix == i)
return item
})}
renderItem={item=><Item {...item}/>}
keyExtractor={item => item.id}
onViewableItemsChanged={({viewableItems, changed})=>{
// set viewableItemIndices to the indices when view change
setViewableItemsIndices(viewableItems.map(item=>item.index))
}}
//{/*config that decides when an item is viewable*/}
viewabilityConfig={{itemVisiblePercentThreshold:80}}
extraData={viewableItemsIndices}
/>
{/* Extra stuff that just tells you what items should be visible*/}
<Text>Items that should be visible:</Text>
{viewableItemsIndices.map(i=><Text> {DATA[i].title}</Text>)}
</SafeAreaView>
);
}
const style = StyleSheet.create({
container:{
padding:10,
alignItems:'center'
},
flatlistContainer:{
borderWidth:1,
borderColor:'red',
width:'50%',
height:40
},
item:{
borderWidth:1,
padding:5,
},
itemContainer:{
padding:5,
}
})
By wrapping your FlatList in a separate file, you wont encounter the "onViewableItemsChange on the fly" error as long as you dont modify FlatListWrapper.js
[1]: https://reactnative.dev/docs/animated
Use onViewableItemsChanged this is called when the items in the flatlist changes.
const handleViewableItemsChanged = (viewableItems, changed) => {}
<Flatlist
...
onViewableItemsChanged={handleViewableItemsChanged}

How to refresh/re-render flatlist on react-native?

im trying to refresh my flatlist from some page without going back to the principal menu, but it doesnt work.
I've already readed about extraData, but it doesnt work either.
Basiclly my program is like that:
I have a page called "passwords" and i add some passwords there from another page called "add passwords". When i click to add a password, i want to refresh the flatlist from the page "passwords" to show me the password that i just added.
This is my code from the page "add passwords"
...
state = {
arr: [],
local: '',
password: '',
obj: {
local: '',
password: ''
},
count: 1,
texto: ''
};
componentDidMount() {
//Here is the Trick
const { navigation } = this.props;
//Adding an event listner om focus
//So whenever the screen will have focus it will set the state to zero
this.focusListener = navigation.addListener('didFocus', () => {
this.setState({ count: 0 });
});
}
storeItem(item) {
try {
//we want to wait for the Promise returned by AsyncStorage.setItem()
//to be resolved to the actual value before returning the value~
console.log(item)
var joined = this.state.arr.concat(item);
console.log(joined)
this.setState({ arr: joined })
AsyncStorage.setItem('array', JSON.stringify(joined));
console.log(this.state.arr)
} catch (error) {
console.log(error.message);
}
}
componentWillMount() {
AsyncStorage.getItem('array').then(array => {
item = JSON.parse(array)
array ? this.setState({ arr: item }) : null;
console.log(item)
})
}
render() {
return (
<View style={styles.container}>
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => this.setState({ local: text })}
value={this.state.local}
/>
<TextInput
secureTextEntry={true}
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => this.setState({ password: text })}
value={this.state.password}
/>
<Button title='Adicionar'
onPress={() => this.storeItem({ local: this.state.local, password: this.state.password }) + alert("Adicionado com sucesso!") + this.props.navigation.navigate('Passwords')}
></Button>
</View>
);
}
}
And this is my page "passwords" where i want to refresh
componentWillMount() {
const { navigation } = this.props;
this.willFocusListener = navigation.addListener(
'willFocus',
() => {
this.setState({ count: 10 })
}
)
AsyncStorage.getItem('array').then(array => {
item = JSON.parse(array)
item ? this.setState({ arr: item }) : null;
console.log(this.state.arr)
})
}
renderItem = ({ item }) => (
<View style={{ flexDirection: 'row' }} style={styles.passwordContainer}>
<Text> {item.local} </Text>
<Text> {item.password} </Text>
</View>
)
render() {
return (
<View style={styles.container}>
<FlatList
data={this.state.arr}
renderItem={this.renderItem}
extraData={this.state} //this is what i tryied
/>
</View>
);
You can use your listener to update the state.
componentWillMount() {
this.willFocusListener = navigation.addListener(
'willFocus',
() => this.updateData()
}
updateData = () => {
this.setState({ count: 10 });
AsyncStorage.getItem('array').then(array => {
item = JSON.parse(array)
item ? this.setState({ arr: item }) : null;
console.log(this.state.arr)
});
}
Any state changes will rerender items.

Can't scroll absolute positioned FlatList when there is nothing behind it

What I'm trying to do:
Make a search bar and show the results in a flat list under the bar.
What I have:
I have a SearchBar and a FlatList, the FlatList needs to but in absolute position so it covers the content on the bottom of the search bar
The Problem:
The FlatList is covering the search bar when it's active and I can't scroll the list or select an item. What I noticed is that if i try to select an item or scroll the list when clicking where the SearchBar should be appearing, I can select and scroll the list.
What I need:
The FlatList to show under the SearchBar and be able to scroll it.
I could use top: 50 to show the FlatList under the SearchBar but it doesn'r seems good
Observations: I'm not that good at styles
import React, { Component } from 'react'
import { Text, View, StyleSheet, TouchableHighlight, FlatList } from 'react-native'
import {
Slider,
SearchBar,
ListItem,
} from 'react-native-elements'
export default class SearchForm extends Component {
state = {
pages: 1,
displayList: false,
itemsPerPage: 5,
}
componentDidMount = async () => {
const {
data = [],
itemsPerPage = 5,
} = this.props
await fetch('https://servicodados.ibge.gov.br/api/v1/localidades/estados', {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
.then(res => res.json())
.then(data => this.setState({
data: data,
displayData: data.slice(0, itemsPerPage)
}))
console.log(this.state.data.length)
}
updateSearch = search => {
const { data, itemsPerPage } = this.state
let s = search ? search.toLowerCase() : ''
this.setState({
displayData: data.filter(res => res.nome.toLowerCase().includes(s)).slice(0, itemsPerPage),
displayList: true,
search: search,
pages: 1,
})
if(this.flatListRef){
this.flatListRef.scrollToOffset({ animated: true, offset: 0 })
}
}
loadMore = () => {
const { pages, displayData, data, search, itemsPerPage } = this.state
const start = pages * itemsPerPage
let end = (pages + 1) * itemsPerPage
let s = search ? search.toLowerCase() : ''
const newData = data.filter(res => res.nome.toLowerCase().includes(s)).slice(start, end)
this.setState({
displayData: [...displayData, ...newData],
pages: pages + 1,
})
console.log(this.state.displayData.length)
}
selectItem = (value) => {
this.setState({
search: value,
displayList: false,
})
}
renderItem = ({ item, index }) => {
return (
<ListItem
style={styles.flatListItem}
containerStyle={styles.flatListItemCointainer}
key={index}
title={item.nome}
onPress={() => this.selectItem(item.nome)}
/>
);
}
render() {
const {
search,
displayData = [],
displayList,
} = this.state
return (
<View style={styles.container}>
<SearchBar
ref={search => { this.search = search }}
placeholder="Type Here..."
leftIcon={false}
noIcon
onChangeText={this.updateSearch}
value={search}
/>
{displayList && <FlatList
style={styles.flatList}
ref={ref => this.flatListRef = ref}
data={displayData}
keyExtractor={(item, index) => index.toString()}
renderItem={this.renderItem}
onEndReached={this.loadMore}
onEndReachedThreshold={0.5}
/>}
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => this.setState({ text })}
value={this.state.text}
/>
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
onChangeText={(text) => this.setState({ text })}
value={this.state.text}
/>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
alignSelf: 'stretch',
backgroundColor: '#fff',
},
flatList: {
height: 200,
width: '100%',
position: 'absolute',
},
flatListItemCointainer: {
backgroundColor: 'rgba(0,0,0,1)'
}
})
Edit: I just change the code a little bit to show what I'm trying to do. Under the SearchBar will have other components (e.g. TextInput) and when the list is active, the list should go on top of that components.
With Shashin Bhayani answer, it's not going on top of things under it, only pushing it down.
This issue in android, to solve it :
import { FlatList } from 'react-native-gesture-handler';
Adding bottom: 0 solved my issue I am using zero because I want the Faltlist to end at the very bottom of the screen it can be any number.
Make sure there is no flex: 1 in flatlist style, contentContainerStyle or on the parent flatlist.
Try this and let me know this solved your issue or not.

How to mak FlatList automatic scroll?

Here is what i try i use setInterval function to set a variable content will be changed every second and i find onMomentumScrollEnd can get the position y when scroll the FlatList
And then i am stuck , i thougt event.nativeEvent.contentOffset.y = this.state.content; can let my FlatList automatic scroll. Obviously it is not.
Any one can give me some suggestion ? Thanks in advance.
My data is from an API
Here is my App.js:
import React from 'react';
import { View, Image, FlatList, Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');
const equalWidth = (width / 2 );
export default class App extends React.Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
this.state = { movies: [], content: 0 };
}
componentWillMount() {
fetch('https://obscure-reaches-65656.herokuapp.com/api?city=Taipei&theater=Centuryasia')
.then(response => response.json())
.then(responseData => {
console.log(responseData);
this.setState({ movies: responseData[0].movie });
})
.catch((error) => console.log(error));
this.timer = setInterval(() => {
this.setState({content: this.state.content+1 })
}, 1000);
}
// get the jsonData key is item and set the value name is movie
renderRow({ item: movie }) {
console.log('renderRow => ');
return (
<View>
<Image source={{ uri: movie.photoHref}} style={{ height: 220, width: equalWidth }} resizeMode="cover"/>
</View>
);
}
render() {
const movies = this.state.movies;
// it well be rendered every second from setInterval function setState
console.log('render');
return (
<View style={{ flex: 1 }}>
<FlatList
data={movies}
renderItem={this.renderRow}
horizontal={false}
keyExtractor={(item, index) => index}
numColumns={2}
onMomentumScrollEnd={(event) => {
console.log(event.nativeEvent.contentOffset.y);
event.nativeEvent.contentOffset.y = this.state.content;
}}
/>
</View>
);
}
}
You need to tell your FlatList that you want it to scroll to a new position using scrollToOffset().
Store a reference to your FlatList in your class by adding the prop
ref={flatList => { this.flatList = flatList }} to it.
Then, call this.flatList.scrollToOffset({ offset: yourNewOffset }) to scroll to the desired offset.
Docs on this method are here.

Highlight a selected item in React-Native FlatList

I put together a simple React-native application to gets data from a remote service, loads it in a FlatList. When a user taps on an item, it should be highlighted and selection should be retained. I am sure such a trivial operation should not be difficult. I am not sure what I am missing.
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
ActivityIndicator,
Image,
TouchableOpacity,
} from 'react-native';
export default class BasicFlatList extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
data: [],
page: 1,
seed: 1,
error: null,
refreshing: false,
selectedItem:'null',
};
}
componentDidMount() {
this.makeRemoteRequest();
}
makeRemoteRequest = () => {
const {page, seed} = this.state;
const url = `https://randomuser.me/api/?seed=${seed}&page=${page}&results=20`;
this.setState({loading: true});
fetch(url)
.then(res => res.json())
.then(res => {
this.setState({
data: page === 1 ? res.results : [...this.state.data, ...res.results],
error: res.error || null,
loading: false,
refreshing: false
});
})
.catch(error => {
this.setState({error, loading: false});
});
};
onPressAction = (rowItem) => {
console.log('ListItem was selected');
console.dir(rowItem);
this.setState({
selectedItem: rowItem.id.value
});
}
renderRow = (item) => {
const isSelectedUser = this.state.selectedItem === item.id.value;
console.log(`Rendered item - ${item.id.value} for ${isSelectedUser}`);
const viewStyle = isSelectedUser ? styles.selectedButton : styles.normalButton;
return(
<TouchableOpacity style={viewStyle} onPress={() => this.onPressAction(item)} underlayColor='#dddddd'>
<View style={styles.listItemContainer}>
<View>
<Image source={{ uri: item.picture.large}} style={styles.photo} />
</View>
<View style={{flexDirection: 'column'}}>
<View style={{flexDirection: 'row', alignItems: 'flex-start',}}>
{isSelectedUser ?
<Text style={styles.selectedText}>{item.name.first} {item.name.last}</Text>
: <Text style={styles.text}>{item.name.first} {item.name.last}</Text>
}
</View>
<View style={{flexDirection: 'row', alignItems: 'flex-start',}}>
<Text style={styles.text}>{item.email}</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
}
render() {
return(
<FlatList style={styles.container}
data={this.state.data}
renderItem={({ item }) => (
this.renderRow(item)
)}
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 50,
},
selectedButton: {
backgroundColor: 'lightgray',
},
normalButton: {
backgroundColor: 'white',
},
listItemContainer: {
flex: 1,
padding: 12,
flexDirection: 'row',
alignItems: 'flex-start',
},
text: {
marginLeft: 12,
fontSize: 16,
},
selectedText: {
marginLeft: 12,
fontSize: 20,
},
photo: {
height: 40,
width: 40,
borderRadius: 20,
},
});
When user taps on an item in the list, "onPress" method is invoked with the information on selected item. But the next step of highlight item in Flatlist does not happen. 'UnderlayColor' is of no help either.
Any help/advice will be much appreciated.
You can do something like:
For the renderItem, use something like a TouchableOpacity with an onPress event passing the index or id of the renderedItem;
Function to add the selected item to a state:
handleSelection = (id) => {
var selectedId = this.state.selectedId
if(selectedId === id)
this.setState({selectedItem: null})
else
this.setState({selectedItem: id})
}
handleSelectionMultiple = (id) => {
var selectedIds = [...this.state.selectedIds] // clone state
if(selectedIds.includes(id))
selectedIds = selectedIds.filter(_id => _id !== id)
else
selectedIds.push(id)
this.setState({selectedIds})
}
FlatList:
<FlatList
data={data}
extraData={
this.state.selectedId // for single item
this.state.selectedIds // for multiple items
}
renderItem={(item) =>
<TouchableOpacity
// for single item
onPress={() => this.handleSelection(item.id)}
style={item.id === this.state.selectedId ? styles.selected : null}
// for multiple items
onPress={() => this.handleSelectionMultiple(item.id)}
style={this.state.selectedIds.includes(item.id) ? styles.selected : null}
>
<Text>{item.name}</Text>
</TouchableOpacity>
}
/>
Make a style for the selected item and that's it!
In place of this.state.selectedItem and setting with/checking for a rowItem.id.value, I would recommend using a Map object with key:value pairs as shown in the RN FlatList docs example: https://facebook.github.io/react-native/docs/flatlist.html. Take a look at the js Map docs as well: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map.
The extraData prop recommended by #j.I-V will ensure re-rendering occurs when this.state.selected changes on selection.
Your onPressAction will obviously change a bit from example below depending on if you want to limit the number of selections at any given time or not allow user to toggle selection, etc.
Additionally, though not necessary by any means, I like to use another class or pure component for the renderItem component; ends up looking something like the following:
export default class BasicFlatList extends Component {
state = {
otherStateStuff: ...,
selected: (new Map(): Map<string, boolean>) //iterable object with string:boolean key:value pairs
}
onPressAction = (key: string) => {
this.setState((state) => {
//create new Map object, maintaining state immutability
const selected = new Map(state.selected);
//remove key if selected, add key if not selected
this.state.selected.has(key) ? selected.delete(key) : selected.set(key, !selected.get(key));
return {selected};
});
}
renderRow = (item) => {
return (
<RowItem
{...otherProps}
item={item}
onPressItem={this.onPressAction}
selected={!!this.state.selected.get(item.key)} />
);
}
render() {
return(
<FlatList style={styles.container}
data={this.state.data}
renderItem={({ item }) => (
this.renderRow(item)
)}
extraData={this.state}
/>
);
}
}
class RowItem extends Component {
render(){
//render styles and components conditionally using this.props.selected ? _ : _
return (
<TouchableOpacity onPress={this.props.onPressItem}>
...
</TouchableOpacity>
)
}
}
You should pass an extraData prop to your FlatList so that it will rerender your items based on your selection
Here :
<FlatList style={styles.container}
data={this.state.data}
extraData={this.state.selectedItem}
renderItem={({ item }) => (
this.renderRow(item)
)}
/>
Source : https://facebook.github.io/react-native/docs/flatlist
Make sure that everything your renderItem function depends on is passed as a prop (e.g. extraData) that is not === after updates, otherwise your UI may not update on changes
First
constructor() {
super();
this.state = {
selectedIds:[]
};
}
Second
handleSelectionMultiple = async (id) => {
var selectedIds = [...this.state.selectedIds] // clone state
if(selectedIds.includes(id))
selectedIds = selectedIds.filter(_id => _id !== id)
else
selectedIds.push(id)
await this.setState({selectedIds})
}
Third
<CheckBox
checked={this.state.selectedIds.includes(item.expense_detail_id) ? true : false}
onPress={()=>this.handleSelectionMultiple(item.expense_detail_id)}
/>
Finally i got the solution to my problem from the answer given by Maicon Gilton