Open embpdaniel opened 6 years ago
So make a new sortable list for each section?
Exactly
Are you having issues creating multiple lists then? The only real problem I see is if you want the whole thing to be scrollable, because you'll essentially have multiple scrollables inside one big scrollable, which probably won't work well.
Right, that was exactly my concern. I was thinking of using SectionList
each with a render item being the sortable list so that I can have sortable sections. But SectionList
needs to scroll. I hadn't tried it yet but I was concerned about performance and if it works anyway.
I'd wager the performance won't be great, but you can try it and see
Will try it. thanks
Been setting this up. So far it seems to be working ok and I have been able to sort my sectioned lists using simple views, however when I use the actual views I need, I have one problem. My child row has a TouchableOpacity
component because I need to be able to press on the item. It will not let me sort the row now because I guess it creates a conflict with touches. I have set up the manual row activation as you suggest in the documentation, and I see that the toggleRowActive
method is provided to my child. I call it on the TouchableOpacity
onLongPress and I call it again on the TouchableOpacity
onPressOut, since it is a toggle I figure it will activate after long press and de-activate when I let go. This is not working though. The long press elevates my item, as if it is ready to drag, but when I try to drag, nothing happens. Here is how I have it:
Main.js
<SortableList
style={styles.list}
contentContainerStyle={styles.contentContainer}
renderRow={this._renderRoomsRow}
data={item}
onActivateRow={this._onActivateRow}
onChangeOrder={this._onRoomsChangeOrder}
onReleaseRow={this._onReleaseRow}
scrollEnabled={false}
manuallyActivateRows={true}
/>
_renderRoomsRow = ({ data, active, toggleRowActive }) => {
return (
<SortableRow active={active} toggleRowActive={toggleRowActive}>
<RoomItem item={data} onPress={this._onRoomPressed} />
</SortableRow>
);
};
SortableRow.js
import React, { Component } from 'react';
import { Animated, Platform, Easing } from 'react-native';
import PropTypes from 'prop-types';
/**
* List row for SortableList component.
*/
export default class SortableRow extends Component {
constructor(props) {
super(props);
this._active = new Animated.Value(0);
this._rowStyle = {
flex: 1,
width: window.width,
...Platform.select({
android: {
elevation: 0
}
})
};
this._style = {
...Platform.select({
ios: {
transform: [
{
scale: this._active.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.1]
})
}
],
shadowRadius: this._active.interpolate({
inputRange: [0, 1],
outputRange: [2, 10]
})
},
android: {
transform: [
{
scale: this._active.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.07]
})
}
],
elevation: this._active.interpolate({
inputRange: [0, 1],
outputRange: [2, 6]
})
}
})
};
}
componentWillReceiveProps(nextProps) {
if (this.props.active !== nextProps.active) {
Animated.timing(this._active, {
duration: 300,
easing: Easing.bounce,
toValue: Number(nextProps.active)
}).start();
}
}
render() {
const childrenWithProps = this.props.children
? React.Children.map(this.props.children, child =>
React.cloneElement(child, { onLongPress: this.props.toggleRowActive, onPressOut: this.props.toggleRowActive })
)
: this.props.children;
return <Animated.View style={[this._rowStyle, this._style]}>{childrenWithProps}</Animated.View>;
}
}
SortableRow.propTypes = {
active: PropTypes.bool,
children: PropTypes.any,
toggleRowActive: PropTypes.func
};
RoomItem.js
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import styles from './styles/RoomItem';
import CustomIcon from './CustomIcon';
/**
* Renders a list item for a room.
*/
const RoomItem = props => {
const { item, onPress, onLongPress, onPressOut } = props;
return (
<TouchableOpacity
style={styles.container}
onPress={() => onPress(item)}
onLongPress={onLongPress}
onPressOut={onPressOut}
>
<CustomIcon name={item.private ? 'lock' : 'globe'} size={15} />
<View style={styles.details}>
<Text style={styles.name}>{item.name}</Text>
</View>
</TouchableOpacity>
);
};
RoomItem.defaultProps = {
item: {},
onLongPress: () => {},
onPressOut: () => {}
};
RoomItem.propTypes = {
item: PropTypes.object,
onPress: PropTypes.func.isRequired,
onLongPress: PropTypes.func,
onPressOut: PropTypes.func
};
export default RoomItem;
Hope you can help, thanks
Ah, I fixed this. I'm not supposed to call the toggleRowActive
when I release as that gets handled on its own :)
@i8wu So I have been able to use two lists within a SectionList
component and they have worked fine. I only had two sortable lists to render so it has worked well for me.
The one issue that I am having trouble with is that these lists may be sorted remotely, by the web version of the app. When the changes come in, the sortable list that was updated goes blank for about a second and re-draws, which seems unnecessary because items weren't added or removed, they just changed order. Is there a way to keep it from re-rendering?
In fact, sorting them causes it to blink too because I sort them, and must send the update out to the web app, at the same time the update echos through a real-time state checker which in turn updates my redux store, the component picks up on the change and re-renders because its data was not current. Maybe I'm not understanding how I'm supposed to handle this sort of flow with the sorting component. If you could help direct me in the right direction I would be really grateful
Not sure...maybe you could try not syncing immediately to your app? Possibly have it sync on resume or something? You could also try another list module maybe https://github.com/deanmcpherson/react-native-sortable-listview I'm not actively using this module anymore so I can't really comment on usage/performance.
@i8wu I did try that list too, it doesn't do the blink, but the sorting interaction doesn't play nice with nested lists :( Maybe I will fork your library and try to fix if I can't find another way around it.
I was able to get this working now and haven't encountered issues, by using the order
property and re-using the previous order lists, if the order never changed. I'll explain what I did in case it helps someone:
Overall logic:
SectionList
scrolling disablesSectionList
scrolling re-enablesMyComponent
detects a data change (the new ordered items came in through props)Issues to note: The auto scroll on sort does not work, I had to disable scrolling on each sortable list to get what I needed. But compared to other issues I could have this is no biggie at all.
Here's the code:
import React, { Component } from 'react';
import { Text, TouchableOpacity, View, SectionList, ActivityIndicator } from 'react-native';
import PropTypes from 'prop-types';
import NavigationBar from 'react-native-navbar';
import Icon from 'react-native-vector-icons/FontAwesome';
import SortableList from 'react-native-sortable-list';
import _ from 'lodash';
import MenuButton from './MenuButton';
import SectionHeader from './SectionHeader';
import PersonItem from './PersonItem';
import RoomItem from './RoomItem';
import SortableRow from './SortableRow';
import styles from './styles/MyComponent';
import * as Colors from '../theme/Colors';
import { arrToKeys, arrToObject } from '../utils/data';
import { CHAT_TYPE_DIRECT, CHAT_TYPE_ROOM } from '../constants/app';
import { SCREEN_CHAT } from '../constants/navigationConstants';
const CATEGORY_ROOMS = 'ROOMS';
const CATEGORY_PEOPLE = 'PEOPLE';
export default class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
sections: [],
scrollEnabled: true,
roomsOrder: [],
usersOrder: []
};
}
componentDidMount() {
this._updateSections(this.props);
}
componentWillReceiveProps(nextProps) {
this._updateSections(nextProps);
}
_updateSections = props => {
const { rooms, users } = props;
let roomsOrder = arrToKeys(rooms, 'id');
let usersOrder = arrToKeys(users, 'id');
let sections = [];
sections.push({ title: CATEGORY_ROOMS, data: [rooms] });
sections.push({ title: CATEGORY_PEOPLE, data: [users] });
let roomsCollection = arrToObject(rooms, 'id');
let usersCollection = arrToObject(users, 'id');
this.setState({
sections,
roomsOrder,
usersOrder,
rooms: !_.isEqual(roomsCollection, this.state.rooms) ? roomsCollection : this.state.rooms,
users: !_.isEqual(usersCollection, this.state.users) ? usersCollection : this.state.users
});
};
_title() {
return (
<Text testID={'Title'} style={styles.navBarTitle}>
Connect
</Text>
);
}
_leftButton() {
return <MenuButton onPress={() => this.props.toggleMenu()} />;
}
_rightButton() {
return (
<TouchableOpacity style={styles.touchableOpacity}>
<Icon name="bell-o" size={32} color={'white'} />
</TouchableOpacity>
);
}
_renderLoading = () => {
return (
<View style={styles.loadingContainer}>
<View>
<ActivityIndicator size={'large'} />
<Text style={styles.loadingText}>Loading</Text>
</View>
</View>
);
};
_renderSectionHeader({ section }) {
return <SectionHeader section={section} />;
}
_renderItem = ({ item, section }) => {
let element;
switch (section.title) {
case CATEGORY_PEOPLE:
element = (
<SortableList
style={styles.list}
contentContainerStyle={styles.contentContainer}
renderRow={this._renderUsersRow}
data={this.state.users}
onActivateRow={this._onActivateRow}
onChangeOrder={this._onUsersChangeOrder}
onReleaseRow={this._onReleaseRow}
scrollEnabled={false}
manuallyActivateRows={true}
order={this.state.usersOrder}
/>
);
break;
case CATEGORY_ROOMS:
element = (
<SortableList
style={styles.list}
contentContainerStyle={styles.contentContainer}
renderRow={this._renderRoomsRow}
data={this.state.rooms}
onActivateRow={this._onActivateRow}
onChangeOrder={this._onRoomsChangeOrder}
onReleaseRow={this._onReleaseRow}
scrollEnabled={false}
manuallyActivateRows={true}
order={this.state.roomsOrder}
/>
);
break;
}
return element;
};
_renderUsersRow = ({ data, active, toggleRowActive }) => {
return (
<SortableRow active={active} toggleRowActive={toggleRowActive}>
<PersonItem disabled={active} item={data} term={'me'} onPress={this._onPersonPressed} infoType="bookmark" />
</SortableRow>
);
};
_renderRoomsRow = ({ data, active, toggleRowActive }) => {
return (
<SortableRow active={active} toggleRowActive={toggleRowActive}>
<RoomItem item={data} onPress={this._onRoomPressed} />
</SortableRow>
);
};
_onActivateRow = () => {
this.setState({ scrollEnabled: false });
};
_onPersonPressed = person => {
this.props.navigation.navigate(SCREEN_CHAT, {
type: CHAT_TYPE_DIRECT,
id: person.id,
name: person.full_name
});
};
_onRoomPressed = room => {
this.props.navigation.navigate(SCREEN_CHAT, {
type: CHAT_TYPE_ROOM,
id: room.id,
name: room.name,
isPrivate: room.private
});
};
_onUsersChangeOrder = nextOrder => {
this.setState({ usersOrder: nextOrder });
};
_onRoomsChangeOrder = nextOrder => {
this.setState({ roomsOrder: nextOrder });
};
_onReleaseRow = () => {
this.props.reportLatestBookmarksOrder(this.state.usersOrder, this.state.roomsOrder);
this.setState({ scrollEnabled: true });
};
render() {
return this.props.stateIsLoading ? (
this._renderLoading()
) : this.state.sections.length ? (
<View style={styles.container}>
<SectionList
style={styles.sectionList}
keyExtractor={(item, index) => item.id}
renderItem={this._renderItem}
renderSectionHeader={this._renderSectionHeader}
sections={this.state.sections}
stickySectionHeadersEnabled={true}
scrollEnabled={this.state.scrollEnabled}
/>
</View>
) : null;
}
}
MyComponent.propTypes = {
rooms: PropTypes.array.isRequired,
users: PropTypes.array.isRequired,
reportLatestBookmarksOrder: PropTypes.func.isRequired,
stateIsLoading: PropTypes.bool.isRequired
};
import React, { Component } from 'react';
import { Animated, Platform, Easing } from 'react-native';
import PropTypes from 'prop-types';
/**
* List row for SortableList component.
*/
export default class SortableRow extends Component {
constructor(props) {
super(props);
this._active = new Animated.Value(0);
this._rowStyle = {
flex: 1,
width: window.width,
...Platform.select({
android: {
elevation: 0
}
})
};
this._style = {
...Platform.select({
ios: {
transform: [
{
scale: this._active.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.1]
})
}
],
shadowRadius: this._active.interpolate({
inputRange: [0, 1],
outputRange: [2, 10]
})
},
android: {
transform: [
{
scale: this._active.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.07]
})
}
],
elevation: this._active.interpolate({
inputRange: [0, 1],
outputRange: [2, 6]
})
}
})
};
}
componentWillReceiveProps(nextProps) {
if (this.props.active !== nextProps.active) {
Animated.timing(this._active, {
duration: 300,
easing: Easing.bounce,
toValue: Number(nextProps.active)
}).start();
}
}
render() {
const childrenWithProps = this.props.children
? React.Children.map(this.props.children, child =>
React.cloneElement(child, { onLongPress: this.props.toggleRowActive })
)
: this.props.children;
return <Animated.View style={[this._rowStyle, this._style]}>{childrenWithProps}</Animated.View>;
}
}
SortableRow.propTypes = {
active: PropTypes.bool,
children: PropTypes.any,
toggleRowActive: PropTypes.func
};
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import _ from 'lodash';
import PropTypes from 'prop-types';
import ConnectStatus from './ConnectStatus';
import styles from './styles/PersonItem';
import Avatar from './Avatar';
const PersonItem = props => {
const { item, term, onPress, infoType, onLongPress } = props;
let info;
switch (infoType) {
case 'bookmark':
info = `${item.territory} • ${item.title}`;
break;
default:
info = `${item.title} ${item.department} ${item.territory} ${item.branch}`;
}
// ** The important part here is that I set the onLongPress of this inner touchable opacity, so I can tap the item to go to another screen, and longPress it to sort it **
return (
<TouchableOpacity onLongPress={onLongPress} style={styles.container} onPress={() => onPress(item)}>
<View style={styles.details}>
<Avatar user={item} />
<View style={styles.info}>
<View style={styles.nameContainer}>
<Text style={styles.name}>{item.full_name}</Text>
<ConnectStatus userId={item.id} />
</View>
<Text style={styles.position}>{info}</Text>
</View>
</View>
</TouchableOpacity>
);
};
PersonItem.defaultProps = {
infoType: 'default',
onLongPress: () => {}
};
PersonItem.propTypes = {
item: PropTypes.object,
term: PropTypes.string,
onPress: PropTypes.func.isRequired,
infoType: PropTypes.oneOf(['bookmark', 'default']),
onLongPress: PropTypes.func
};
export default PersonItem;
The RoomItem list item is about the same as PersonItem, so I won't paste that.
@embpdaniel I tried implementing something similar, but didn't have much luck getting rid of the blinking every time Redux updates the data (in your case updating rooms or users). Any ideas what in particular stopped the blinking?
hey @ppetrick sorry just saw your reply. It's been a while since I worked on this, but I believe the key part was basically not re-applying the list data unless an item was added/removed, and making use of the list's order
property to affect list ordering. If an item was simply re-ordered, then the existing list data could still be re-used, and I would simply use the ordering arrays to tell the list component to re-order. The key spot this happens in the code I pasted is in the _updateSections
function:
_updateSections = props => {
const { rooms, users } = props;
let roomsOrder = arrToKeys(rooms, 'id');
let usersOrder = arrToKeys(users, 'id');
let sections = [];
sections.push({ title: CATEGORY_ROOMS, data: [rooms] });
sections.push({ title: CATEGORY_PEOPLE, data: [users] });
let roomsCollection = arrToObject(rooms, 'id');
let usersCollection = arrToObject(users, 'id');
this.setState({
sections,
roomsOrder,
usersOrder,
rooms: !_.isEqual(roomsCollection, this.state.rooms) ? roomsCollection : this.state.rooms,
users: !_.isEqual(usersCollection, this.state.users) ? usersCollection : this.state.users
});
};
In this function you can see that there are arrays that manage the order (roomsOrder
/usersOrder
)separate from the actual list data (rooms
/users
). The rooms order is just basically an array of the ids tied to each item, in the order they came in from my API due to an external order update (in my case, the web version of this app could affect re-ordering). You can see here I only re-apply the actual rooms/users list data if the new list data is not equal to the current data:
rooms: !_.isEqual(roomsCollection, this.state.rooms) ? roomsCollection : this.state.rooms,
users: !_.isEqual(usersCollection, this.state.users) ? usersCollection : this.state.users
HI @embpdaniel
I know this is an old thread but I wanted to know if its possible to do this scenario on this plugin.
I have these
Day 1
Day 2
Hey @jeffreybello I didn't have this need, and in the code I wrote you can see I created two separate sortable lists. Using that set up it wouldn't be possible to transfer one item from one list into the other.
Have you tried maybe just one single sortable list where your categories (Day 1, Day2) are regular items but just simply render differently to look like category headings? This way I think you would be able to transfer items between "categories" since you are dealing with a single sortable list.
@embpdaniel hey that's actually a brilliant and genius idea.
Sure I will try that.
@jeffreybello Hello Brother! Could you share your code if you implemented that.
Hi, I have a sectioned list (each section has it's own header). I need to sort rows within each of these sections, and need to disallow sorting a row outside of its corresponding section. Is it possible with this library?