Dynamically populating nested react-native-collapsible - react-native

I need to populate a menu with items from an api request.
I made some sample items; const items. Some of the menu items have children, and the children can also have children, so I need to nest several levels of menu items.
I made an Accordion() with Collapsible from react-native-collapsible and an AccordionItem() for items that have no children.
function App() renders the menu items twice. Once by manually adding Accordions and AccordionItems and once by mapping items[]. RootMenuItem() is called for each top level item and renders that item and its sub-items by recursion.
When manually adding each item it works the way I want it to. I need to populate the menu programatically, but nested accordions rendered by RootMenuItem() are misbehaving on android and iOS. When testing in Web on snack.io it seems to be working fine.
Here is a snack with my complete App.js:
https://snack.expo.dev/#dissar/nested-collapsibles
Am I doing something wrong?
Does anybody have any tips for doing this in a better way?
PS: The dynamically rendered items have weird widths when testing on snack.io, but don't worry about that.

I seem to have fixed it myself by removing the View on line 46 and 56;
function RootMenuItem({item}){
if(item.children.length > 0) {
return(
<View style={{flex: 1}} key={item.id}> // <---- I removed this
<Accordion item={item} style={styles.menuItemView}>
{
item.children.map(child => (
<View style={{paddingLeft: 18}} key={child.id}>
<RootMenuItem item={child} style={{paddingLeft: 10}}/>
</View>
))
}
</Accordion>
</View> // <---- Also removed this
)
}
else return (
<AccordionItem item={item}/>
)
}
Not really sure though why that View made the nested accordions not work as they should. Please let me know if you have the answer.

I have a better solution without using any 3rd party library. This is completely customised and easy to understand. I used the same format of data as you used.
first of all, we have a component
const [active, setActive] = useState(null);
return (
<ScrollView
style={{ marginTop: 50 }}
contentContainerStyle={styles.container}>
{arr.map((x, i) => (
<Item
key={x.name}
active={active}
i={i}
setActive={setActive}
child={x.child}
/>
))}
</ScrollView>
);
then for the list items and their child
function Item({ i, active, setActive, child }) {
const onPress = () => {
LayoutAnimation.easeInEaseOut();
setActive(i == active ? null : I);
};
const [childActive, setChildActive] = useState(null);
const open = active == I;
return (
<TouchableOpacity style={styles.item} onPress={onPress} activeOpacity={1}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text>Header - {i + 1}</Text>
{child.length ? <Text>{open ? 'close' : 'open'}</Text> : null}
</View>
{open &&
child.map((x, i) => {
if (x.child.length) {
return (
<Item
key={x}
active={childActive}
i={i}
setActive={setChildActive}
child={x.child}
/>
);
}
return (
<Text key={x} style={styles.subItem}>
- SOME DATA
</Text>
);
})}
</TouchableOpacity>
);
}
It's a completely dynamic process, you can extend the chain as long as you want. You can also check out the expo to look at its works.
https://snack.expo.dev/#akash0208/forlorn-popsicle

Related

Pushing a flatlist item up or down

I have a flatlist. The flatlist contains todos that I can add, edit and delete.
I have two buttons on each todo-item, one for up and one for down.
If I want to push them up 1 step (until it reaches 0) or down (until it reaches the last item) how would I do this? Help is much appreciated, this feels basic but I can't seem to find an answer that works.
Here is my code:
<FlatList
removeClippedSubviews={false}
data = {todos.sort(function(a,b){return b.date - a.date})}
keyExtractor={(item)=>item.key}
renderItem={({item})=>{
return(
<TouchableOpacity>
<View>
<TextInput
style={styles.toDoItem}
value={item.title}
onChangeText={(text)=>handleUpdateText(text, item)}
multiline={true}
maxLength={55}
blurOnSubmit={true}
/>
<TouchableOpacity onPress={()=>handleMoveUp(item.key, item)}>
<FontAwesome name="plus-circle" size={30} color={black}/>
</TouchableOpacity>
<TouchableOpacity onPress={()=>handleMoveDown(item.key, item)}>
<FontAwesome name="minus-circle" size={50} color={black}/>
</TouchableOpacity>
The FlatList's data must first be stored into a state.
const [todos, setTodos] = useState([])
Since you are sorting the initial states in your FlatList according to their dates, it might be useful to do this exactly once in the following useEffect.
React.useEffect(() => {
let temp = [...todos]
temp.sort(function(a,b){return b.date - a.date})
setTodos(temp)
}, [setTodos])
We could also do this directly in the initial state.
Use todos of your state in your FlatList.
<FlatList
data = {todos}
...
</FlatList
...
Then, implement a function that alters the state of your items as follows.
const handleMoveUp = React.useCallback(
(index) => {
if (index !== 0) {
let prevState = [...todos]
let temp = prevState[index - 1]
prevState[index - 1] = prevState[index]
prevState[index] = temp
setItems(prevState)
}
},
[setTodos, todos]
)
add this function as the onPress function of your TouchableOpacity that moves items up, that is
<TouchableOpacity onPress={()=>handleMoveUp(item.key)}>
<FontAwesome name="plus-circle" size={30} color={black}/>
</TouchableOpacity>
The process for moving down is implemented in a very similar fashion. Just use the same code as in handleMoveUp but increase the index instead of decreasing it.

algolia instant search display few itmes and use pagination

I am currently working with algolia and I am trying to show the data on my app, there are over 37 products in my DB and I would like to display them in my app. I want to display 15 items on a single page and then pressing the next button it will show the next 15,
I have currectly manage to connect my react app to algolia, and see the data, however I can not manage to display 15 items and then by pressing next it will just show the next 15.
My Hit component looks like this:
function InfiniteHits({ hits, hasMore, refine }) {
return (
<View>
<View>
<FlatList
style={{
width: Platform.OS === "ios" ? 380 : 400,
}}
data={hits}
keyExtractor={(item) => item.objectID}
ItemSeparatorComponent={() => <View style={styles.separator} />}
onEndReached={() => hasMore && refine()}
renderItem={({ item }) => (
<View style={styles.item}>
// here i render the data so when I clik on any of them it opens it in the modal
</View>
)}
/>
</View>
</View>
);
}
InfiniteHits.propTypes = {
hits: PropTypes.arrayOf(PropTypes.object).isRequired,
hasMore: PropTypes.bool.isRequired,
refine: PropTypes.func.isRequired,
};
export default connectInfiniteHits(InfiniteHits);
then in my app I have:
<InstantSearch searchClient={searchClient} indexName="name of db">
<SearchBox />
<ConnectedPagination padding={2} />
<Hits />
</InstantSearch>
and for my Pagination I have:
const range = (start, end) =>
Array.from({ length: end - start + 1 }, (_, i) => start + i);
const Pagination = ({ padding = 3, refine, currentRefinement, nbPages }) => (
<View
style={{
flexDirection: "row",
justifyContent: "space-around",
}}
>
<TouchableOpacity onPress={() => refine(1)}>
<Text>first</Text>
</TouchableOpacity>
{range(
Math.max(1, currentRefinement - padding),
Math.min(nbPages, currentRefinement + padding)
).map((page) => (
<TouchableOpacity
key={page}
onPress={() => refine(page)}
style={{
color: currentRefinement === page ? "red" : "unset",
}}
>
<Text>{page}</Text>
</TouchableOpacity>
))}
<TouchableOpacity onPress={() => refine(nbPages)}>
<Text>last</Text>
</TouchableOpacity>
</View>
);
const ConnectedPagination = connectPagination(Pagination);
it correctly calculates the number of pages, and display them (in another db of mine there will more than 20 pages) however app still shows all the data below, and when I search for something I can see that it changes the number of pages, but when I click on it it wont do anything, so the question is how can I display few items per page and using the pagination just move through them?
the documentation comes with hitperpage, pagination and scrollto, they have a component and also a widget as well, I am not quit sure how to use them, I am a bit confused when looking at the tutorial.
Oki I figured it out in some ways, I am using InfiniteHits here, which displays all the data from the DB and if you want to use Pagination and go through pages in your app instead of InfiniteHits you have to use Hits. This is what fixed it for me.

DoneButton is not rendering in 'react-native-app-intro-slider-rerender-on-prop-change'

I am trying to make a multiple choice test with a few questions using React Native package 'react-native-app-intro-slider'. The problem was that this package was not rerendering the items when something changes, state for example. I found a similar or refactored package called 'react-native-app-intro-slider-rerender-on-prop-change' that did the same thing but rerendered items on a state change. But the problem is that it does not render the Done button at last slide.
The last package i used was this :
import AppIntroSlider from 'react-native-app-intro-slider-rerender-on-prop-change';
The render method for the 'MCTContainer' (Multiple Choice Container) class is :
render() {
if (this.state.showRealApp) {
this.props.navigation.navigate('TopicGroupDetails', { topicGroup: this.state.topicGroup });
return null;
} else {
const questions = this.state.questions;
return (
<AppIntroSlider
renderItem={this._renderItem}
slides={questions}
onDone={this._onDone}
onSkip={this._onDone}
activeDotStyle={{ backgroundColor: '#039BE5' }}
showPrevButton
showNextButton
showSkipButton
showDoneButton
renderPrevButton={this._renderPrevButton}
renderNextButton={this._renderNextButton}
renderSkipButton={this._renderSkipButton}
renderDoneButton={this._renderDoneButton}
/>
)
}
}
and its renderDoneButton method is like following :
_renderDoneButton = () => {
return (
<View style={styles.buttonCircle}>
<Icon
name="check"
color="#039BE5"
size={24}
style={{ backgroundColor: 'transparent' }}
/>
</View>
);
};
This is actually the same, exactly the same as I used for the component 'SliderComponent' where I show tutorial introductory text in multiple slides. It renders the 'done' button fine. But the only differenence is that I used package 'react-native-app-intro-slider' there, not 'react-native-app-intro-slider-rerender-on-prop-change'.
Please help me if someone has encountered such a problem. Thanx in advance ...
I finally solved it.
I just added a condition on "renderItem" function of the "AppIntroSlider", that checks whether it is the last slide. If it is it renders a button that calls "AppIntroSlider"-s "_onDone" method that completes the slide show.
_renderItem = ({item}) => {
return(
{
(this.state.isLastSlide) ?
(
<TouchableHighlight
onPress={this._onDone}
>
<View>
<Text>
COMPLETE SLIDE SHOW
</Text>
<Icon
name="check"
color="white"
size={24}
/>
</View>
</TouchableHighlight>
) :
(<View></View>)
}
)
}

How do I flex horizontally within a column layout? Currently only half the width of the screen

How do I flex horizontally within a column layout? Currently only half the width of the screen
Here's the current render code:
render() {
const {handle_data} = this.state
if (handle_data.length) {
return (
<ScrollView style={styles.container}>
{
handle_data.map(pack => {
console.log('this.cleanDataForFlatlist(pack): ', this.cleanDataForFlatlist(pack))
return (
<View style={styles.pack}>
<Text style={{fontWeight: 'bold'},{fontSize: 24}}>{pack[0].pack_name}</Text>
<Text style={{fontWeight: 'bold'},{fontSize: 16}}>{pack[0].pack_description}</Text>
<View style={styles.pack}>
<FlatList
data={this.cleanDataForFlatlist(pack)}
keyExtractor={this._keyExtractor}
renderItem={this._renderHandle}
/>
</View>
</View>
)
})
}
</ScrollView>
);
} else {
return null
}
}
Here's what it looks like now
I do not see your code about an item, so I show an example code. your problem is mainly the item render.
// the soruceData is the data souce,in other words, your handle_data
<View style={{flex:1, backgroundColor:'transparent'}} >
<FlatList
data={this.state.sourceData}
renderItem={this._renderItem}
/>
</View>
//the render item method, I suggest you put the item into a pure component
_renderItem = ({ item }) => {
return (
<View style={{flexDirection:'column',width:'92%'}>
id={item.id} // the every item should hava a unique id or key
<ImageView source={{uri:"image url"}} style={{width:,height:}}/>
<Text numberOfLines={1} style={{fontSize: 18,
color: '#353535',}}>"content"</Text>
/>
);
};
you set layout width or height should use flex or percent. in this case, your UI is flexible.
At the same time, I suggest you read felx layout guide
In the end, I suggest you remove the scroll view, move views which algin above the flatList into the flatList ListHeaderComponent. it can avoid the scroll conflict.

Make VirtualizedList show as Grid

I'm trying to make something like this:
The problem: The project was built with immutablejs and according to React Native Docs, I can't use FlatList thus I can't use numColumns props feature of that component.
AFAIK, my only choice is to use VirtualizedList as the docs points out, but I can't figure out how to display the cells as a grid as shown above.
I've already tried to add style props in both cell and view wrapper, but none of the code used to align the cells, like the picture I posted, is ignored. In fact it was showing perfect when I was using ScrollView, but due the HUGE lag I'm moving the code to VirtualizedList.
Any help? Anything would be welcome, I already digged a lot on Google but I can't find anything about this.
Some sample code:
<View>
<VirtualizedList
data={props.schedules}
getItem={(data, index) => data.get(index)}
getItemCount={(data) => data.size}
keyExtractor={(item, index) => index.toString()}
CellRendererComponent={({children, item}) => {
return (
<View style={{any flexbox code gets ignored here}}>
{children}
</View>
)}}
renderItem={({ item, index }) => (
<Text style={{also here}} key={index}>{item.get('schedule')}</Text>
)}
/>
</View>
Answering my own question:
I got it working by copying the FlatList.js source code from react-native repo.
Here's an example code:
<VirtualizedList
data={props.schedules}
getItem={(data, index) => {
let items = []
for (let i = 0; i < 4; i++) {
const item = data.get(index * 4 + i)
item && items.push(item)
}
return items
}}
getItemCount={(data) => data.size}
keyExtractor={(item, index) => index.toString()}
renderItem={({item, index}) => {
return (
<View key={index} style={{flexDirection: 'row'}}>
{item.map((elem, i) => (
<View key={i}>
<Text key={i}>{elem.get('horario')}</Text>
</View>
))}
</View>
)
}}
/>
The number 4 is for the number of columns. The key parts are in the getItem and adding flexDirection: 'row' at renderItem in the View component.