How do I render/animate out items that leave array - reactjs

Currently, I have a scraper that scrapes slack messages and stores them in a db somewhere.
On the frontend, I am pulling every second to see if new messages pop up. And then I render those messages on screen.
If anyone on slack replies or emotes on a message, the message gets removed from the backend thus getting removed from the frontend.
What I am trying to do now is when an item gets removed, I would like to animate it somehow.
Here is some of my current code:
async componentDidMount() {
await this.grab_channels()
await this.grab_slack_user_data()
await this.grab_items()
setInterval(() => {
this.grab_items()
}, this.state.settings.seconds_per_slack_messages_pull * 1000 )
}
grab_items() {
let url = this.state.settings.api_url + 'channel/' + this.state.selected_channel + '/now'
return new Promise( resolve => {
axios.get( url )
.then( res => {
this.setState( { items: res.data } )
resolve()
} )
})
}
And finally, items get rendered:
this.props.items.map( t => {
return (
<Item
key={ t.usr + '_' + t.ts }
task={ t }
user={ this.props.slack_users[ t.usr ] }
settings={ this.props.settings }
now={ this.state.now }
/>
)
} )
I was thinking of doing some sort of check within grab_items() but I wouldn't know how to continue after that. It would be easy to determine which ones should be rendered out but the problem is actually doing it.
Anyone have experience building something like this out?
Thanks!

Using Transition Groups is one way to do this:
https://github.com/reactjs/react-transition-group
Take a look at this example:
https://reactcommunity.org/react-transition-group/transition-group

For the check part in your function grab_items
/* include "clone" so that we don't modify state directly */
import clone from 'clone'
grab_items() {
let url = this.state.settings.api_url + 'channel/' + this.state.selected_channel + '/now'
return new Promise(resolve => {
axios.get(url).then(res => {
/* figure out what items to remove before you set the state */
let itemsToShow = []
for (let i = 0; i < this.state.items.length; i++) {
let ifFound = false
let t = clone(this.state.items[i])
for (let j = 0; j < res.data.length; j++) {
if (t.key === res.data[j].key) {
ifFound = true
}
}
/* if ifFound is false, it means it is not in the messages any more. */
if(!ifFound){
t.haveAnimation = true
itemsToShow.push(t)
}
itemsToShow = itemsToShow.concat(res.data)
this.setState(itemsToShow)
}
})
})
}
Then every second when it re-pull the data, you will have a list of items to show. The list has the items need to have the "disappear" animation and also it has the new messages.
To make the animation work, in the render part:
this.props.items.map(t => {
return (
<Item
key={t.usr + '_' + t.ts}
className={t.haveAnimation ? 'animationCSS' : ''}
task={t}
user={this.props.slack_users[t.usr]}
settings={this.props.settings}
now={this.state.now}
/>
)
}
Above code should attach the css class to the Item. You can put whatever css animation in the class

Related

Arraylist doesn't get refilled and/or filtered

I have a list in angular, an array. OnInit it gets filled from the right corresponding database items. I created a form above it. When you enter something in the form, it acts like a filter. This works, the first time. When you erase something from the form and enter something else, the list should be refreshed and afterwards filtered based on the new input. This doesn't happen. I put the formula that happens on onInit in my function to refill the list.
Below you can find my function (I left the console logs in) and a screenshot of the problem. First I look for a user (joeri.boons#hi10.be) which returns three results. Than I erase the user and look based on a month 7. The screen returns a new unfilterd list while in the console it still holds the list of 3 x user joeri.boons#hi10.be. So there is an inconsistency to. If you look at screen result you would think of a filter problem, the console points at a refreshproblem.
if more code is required let me know.
updateWithFilter(): void {
console.log("function update filter reached")
console.log(this.listadapted);
if(this.listadapted == true){
// this.timesheetsHandled = {} as TimeSheet[];
this.getHandledSheet();
console.log("getHandledSheet executed")
}
if(this.filterUsername.trim() && !this.filterYear && !this.filterMonth){
console.log("option 1 reached")
console.log(this.filterUsername.trim());
console.log(this.filterYear);
console.log(this.filterMonth);
this.timesheetsHandled = this.timesheetsHandled.filter(sheet => sheet.username == this.filterUsername);
this.listadapted = true;
} else if(!this.filterUsername.trim() && !this.filterYear && this.filterMonth){
console.log("option 2 reached");
console.log(this.filterUsername.trim());
console.log(this.filterYear);
console.log(this.filterMonth);
console.log("before filter");
this.timesheetsHandled.forEach(sheet => console.log(sheet.username));
this.timesheetsHandled = this.timesheetsHandled.filter(sheet => sheet.month == this.filterMonth);
console.log("after filter");
this.timesheetsHandled.forEach(sheet => console.log(sheet.username));
// console.log(this.timesheetsHandled.filter(sheet => sheet.month == this.filterMonth));
this.listadapted = true;
} else if .. more options
}
ngOnInit(): void {
this.getHandledSheet();
}
getHandledSheet(): void {
this.timesheetService.getAllTimesheets().subscribe({next: (response: TimeSheet[]) => {this.timesheetsHandled = response.filter(sheet => sheet.status == 'HANDLED') }}) ;
}
My guess would be that this is caused by loading data in ngOnInit. As the documentation (https://angular.io/api/core/OnInit) states : [...] It is invoked only once when the directive is instantiated.
I suspect that you create one instance and re-use it and the ngOnInit method does not get called again.
UPDATE:
The issue is that the call to this.getHandledSheet(); does a call to .. .subscribe({next: .. which is delayed and the rest of the function is executed first.
So the actual code after next: is only executed after the timeSheetService is done loading the data.
So either you apply the filter in the
{next: (response: TimeSheet[]) => {this.timesheetsHandled = response.filter(sheet => sheet.status == 'HANDLED') }}
block after filtering for 'HANDLED' or you'll try to await in the update function.
Create two variables, one that will always remain unfiltered, then another that will be filtered.
The problem will be that the original list is filtered, hence you are losing the original data after filtering!
timesheetHandled: TimeSheet[];
timesheetHandledOriginal: TimeSheet[];
updateWithFilter(): void {
console.log('function update filter reached');
console.log(this.listadapted);
if (this.listadapted == true) {
// this.timesheetsHandled = {} as TimeSheet[];
this.getHandledSheet();
console.log('getHandledSheet executed');
}
if (this.filterUsername.trim() && !this.filterYear && !this.filterMonth) {
console.log('option 1 reached');
console.log(this.filterUsername.trim());
console.log(this.filterYear);
console.log(this.filterMonth);
this.timesheetsHandled = this.timesheetHandledOriginal.filter(
sheet => sheet.username == this.filterUsername
);
this.listadapted = true;
} else if (!this.filterUsername.trim() && !this.filterYear && this.filterMonth) {
console.log('option 2 reached');
console.log(this.filterUsername.trim());
console.log(this.filterYear);
console.log(this.filterMonth);
console.log('before filter');
this.timesheetsHandled.forEach(sheet => console.log(sheet.username));
this.timesheetsHandled = this.timesheetHandledOriginal.filter(
sheet => sheet.month == this.filterMonth
);
console.log('after filter');
this.timesheetsHandled.forEach(sheet => console.log(sheet.username));
// console.log(this.timesheetsHandled.filter(sheet => sheet.month == this.filterMonth));
this.listadapted = true;
}
// else if .. more options
}
ngOnInit(): void {
this.getHandledSheet();
}
getHandledSheet(): void {
this.timesheetService.getAllTimesheets().subscribe({
next: (response: TimeSheet[]) => {
this.timesheetsHandled = response.filter(sheet => sheet.status == 'HANDLED');
this.timesheetHandledOriginal = JSON.parse(JSON.stringify(this.timesheetsHandled));
},
});
}

drag to reorder and save state with reactjs

I'm writing a small app that uses the react-sortable-hoc
everything is great but im having issues displaying the list ordered by order
I have
user 0
user 1
user 2
when I drag user 2 above user 0
instead of getting
user 2
user 0
user 1
I get
user 2
user 1
user 0
I think It has to do with the way I'm setting the order in the state. but I can't figure it out.
this is how I set the order on sort end
const onSortEnd = ({ oldIndex, newIndex }) => {
setUsers(prevState => {
const newItems = [...prevState];
newItems[newIndex].order = oldIndex;
newItems[oldIndex].order = newIndex;
return newItems.sort((a, b) => a.order - b.order);
})
};
here's the app running so you can play with it.
https://codesandbox.io/s/winter-https-xelrd?fontsize=14&hidenavigation=1&theme=dark
I have fixed it,
here is the working url to play with https://codesandbox.io/s/quizzical-colden-rm62y
You were correct in guessing that the problem was with the onSortEnd function. Instead of swapping the newIndex and oldIndex position we just need to either bubble them up or down.
Here is a working code, it can be cleaned up a bit, but you got the idea :)
const onSortEnd = ({ oldIndex, newIndex }) => {
setUsers(prevState => {
const newItems = [...prevState];
if (oldIndex > newIndex) {
for (let i = oldIndex - 1; i >= newIndex; i--) {
newItems[i].order++;
newItems[oldIndex].order = newIndex;
}
} else if (oldIndex < newIndex) {
for (let i = oldIndex + 1; i <= newIndex; i++) {
newItems[i].order--;
newItems[oldIndex].order = newIndex;
}
}
return newItems.sort((a, b) => a.order - b.order);
});
};
Hope it helps. Happy coding :)
What you do is swapping.
If you want to just "insert" the element in the new position you will have to update all the items between the two positions.
In your case, one approach would be to just move the element and re-create the order for all items
setUsers(prevState => {
const newItems = [...prevState];
newItems.splice(newIndex, 0, newItems.splice(oldIndex, 1)[0]).forEach((item,index)=>{
item.order = index;
});
return newItems
});
Demo at https://codesandbox.io/s/confident-river-mrh3p
So looks like your code is simply swapping the elements. This does not seem like what you really want to do. In fact you really want to remove the element and insert it at a given position. I think since you already have the oldIndex and newIndex, you can approach the sort function as follows:
const onSortEnd = ({ oldIndex, newIndex }) => {
setUsers(prevState => {
var newItems = [...prevState];
let elem = newItems[oldIndex]
newItems.splice(oldIndex, 1)
newItems.splice(newIndex, 0, elem)
return newItems
});
};
There isn't really a need for order and is capturing more than the minimum state required (unless you use it elsewhere).

Nested callback function (React native JS )

I have a couple of different functions that helps set different states of the component and i wish to run the functions in order of one another. I know that there are multiple posts on these but they seem to be mostly catered for running one function after the other but i have more than two functions that i need to execute in order
Desired order
1) Set state of starting and final destination
2) Run this.getDirections() (This function sets the state of arrOfPolylines which i desire to reset through resetRouteSelectionStatus())
3) Run resetRouteSelectionStatus()
4) After running these functions i wish to have an empty this.state.arrOfPolylines
Actual results
There is no error in the code but it isnt entering the resetRouteSelectionStatus() as none of the console log are printed. Can someone please guide me on the right path?
<Button
onPress={() => { //on button press set final destination and starting location
{
(this.state.tempDestination.longitude != null && this.state.tempStarting.longitude != null) &&
this.setState({
finalDestination: {
latitude: this.state.tempDestination.latitude,
longitude: this.state.tempDestination.longitude,
},
startingLocation: {
latitude: this.state.tempStarting.latitude,
longitude: this.state.tempStarting.longitude,
}
}, () => {
this.getDirections((this.state.startingLocation.latitude + "," + this.state.startingLocation.longitude), (this.state.finalDestination.latitude + "," + this.state.finalDestination.longitude),
() => {this.resetRouteSelectionStatus()});
}
);
}
}}
title="Determine Directions"
color="#00B0FF"
resetRouteSelectionStatus() {
console.log('entered reset route selection status function')
this.setState({arrOfPolyline: null }, () => {console.log("i should be null nd come first" + this.state.arrOfPolyline)}) ;
this.setState({ selectChallengeStatus: null });
this.setState({ userRouteSelectionStatus: null }); //when user click on button set seleection status to 0 so route options will be displayed again after generation new route
//this.setState({arrOfDirectionDetails: []}); // clear past direction details when user select generate route with new starting/ending location
// clear past poly lines when user has selected new routes
//console.log("everything has been reset");
}
async getDirections(startLoc, destinationLoc) {
let resp = await fetch(`https://maps.googleapis.com/maps/api/directions/json?origin=${startLoc}&destination=${destinationLoc}&key="KEY"&mode=driving&alternatives=true`)
let respJson = await resp.json();
let routeDetails = respJson.routes;
let tempPolyLineArray = [];
let tempDirArrayRoute = [];
//console.log(startLoc);
//console.log(destinationLoc);
for (i = 0; i < routeDetails.length; i++) // respJson.routes = number of alternative routes available
{
let tempDirArray = []; // at every new route, initalize a temp direction array
let points = Polyline.decode(respJson.routes[i].overview_polyline.points);
let coords = points.map((point, index) => {
return {
latitude: point[0],
longitude: point[1]
}
})
tempPolyLineArray.push(coords);
for (k = 0; k < routeDetails[i].legs[0].steps.length; k++) // for each route go to the correct route, enter legs ( always 0), get the number of instruction for this route
{
//console.log (routeDetails[i].legs[0].steps[k])
tempDirArray.push(routeDetails[i].legs[0].steps[k]) // push all instructions into temp direction array
//this.state.arrOfDirectionDetails.push(routeDetails[i].legs[0].steps[k]); // for each instruction save it to array
}
tempDirArrayRoute.push(tempDirArray); // at the end of each route, push all instructions stored in temp array as an array into state
}
this.setState({ arrOfDirectionDetails: tempDirArrayRoute });
this.setState({ arrOfPolyline: tempPolyLineArray });
//creating my html tags
let data = [];
let temptitle = "Route ";
for (let j = 0; j < routeDetails.length; j++) {
data.push(
<View key={j}>
<Button
title={temptitle + j}
onPress={() => this.updateUser(j)}
/>
</View>
)
}
this.setState({ routebox: data })
}
So a few things to note here:
I would extract the inline function for the onButtonPress function into an async function
Something along the lines of:
const onButtonPressHandler = async () => {
... put your code here
}
Then you'll modify your onButtonPress to something like:
<Button
onPress={this.onButtonPressHandler}
... rest of your props here
/>
You can then properly use async / await in the buttonPress handler.
The setState function is not a synchronous function. If you rely on the results right away you'll be disappointed.
Each time you call setState you could trigger a rerender.
I would instead merge all of your setState calls into a single at the end.
The getDirections function does not include the callback, while it should be:
async getDirections(startLoc, destinationLoc, callback) {
.....
this.setState({ routebox: data },callback());
}
or but not sure if it would be in order:
async () => {
await this.getDirections((this.state.startingLocation.latitude + "," + this.state.startingLocation.longitude), (this.state.finalDestination.latitude + "," + this.state.finalDestination.longitude) );
this.resetRouteSelectionStatus();
}
You might need edit the resetRouteSelectionStatus to be:
resetRouteSelectionStatus = async ()=>{

Best way to remove a specific item in AsyncStorage

I'm storing an array of objects in AsyncStorage and would like to know the best way to remove a specific object. Right now I´m passing an id to my function and then I loop through the array to match the id and remove the object and then the array in AsyncStorage is updated. This seems to work ok, but I wonder if this is optimal or if there is a better way to do this?
My function right now:
export function removeData(id) {
AsyncStorage.getItem('#books')
.then((books) => {
const updatedBooks = (JSON.parse(books))
for (let i = 0; i < updatedBooks.length; i++) {
if(updatedBooks[i].id == id) {
updatedBooks.splice(i, 1);
}
}
AsyncStorage.setItem('#books', JSON.stringify(updatedBooks));
})
}
My function for adding data to AsyncStorage:
export function addData(book) {
AsyncStorage.getItem('#books')
.then((books) => {
const b = books ? JSON.parse(books) : [];
b.push(book);
AsyncStorage.setItem('#books', JSON.stringify(b));
});
}
Button to add data with sample data to show structure:
<Button
title = "Add book"
onPress={() => addData({
id: 1,
title: 'Harry Potter',
author:'J.K. Rowling',
thumbnail:'https://covers.openlibrary.org/w/id/7984916-M.jpg',
})
To remove single item
AsyncStorage.removeItem('key', (err) => {
// key 'key' will be removed, if they existed
// callback to do some action after removal of item
});
To remove multiple items
let keys = ['k1', 'k2'];
AsyncStorage.multiRemove(keys, (err) => {
// keys k1 & k2 removed, if they existed
// callback to do some action after removal of item
});
Reference:
RemoveItem method
MultiRemove method

How to store data from firebaselistobservable to an array?

I'm trying to copy the data from firebase to an array using angular 2. But i'm unable to push the data into the array.
Here's the code:
Variables:
uid: string = '';
agencyItems: FirebaseListObservable<any[]>;
trackerItems: FirebaseListObservable<any[]>;
agencyID: any[] = [];
getData()
this.af.auth.subscribe(auth => {
if (auth) {
this.uid = auth.auth.uid;
}
});
this.getAgencyData();
console.log("AgentID: ",this.agencyID);
console.log("Array Length = ",this.agencyID.length); //PROBLEM HERE: Array agencyID is still 0.
this.getTrackerData();
getAgencyData():
console.log("Fetching agency data");
this.agencyItems = this.af.database.list('/agencies/',{preserveSnapshot:true});
this.agencyItems.subscribe(snapshots => {
snapshots.forEach(snapshot => {
console.log(snapshot.val()._id);
this.agencyID.push(snapshot.val()._id);
});
});
getTrackerData():
for (let i = 0; i < this.agencyID.length; i++)
{
console.log("Fetching Tracker data");
this.trackerItems = this.af.database.list('/tracker/' + this.agencyID[i]);
this.trackerItems.subscribe(trackerItems => trackerItems.forEach(Titem =>
console.log("Tracker name: " + Titem.name),
));
}
Here is the debug console screenshot:
Since i'm a newbie to web programming some code may seem completely unnecessary.
What am I doing wrong in this code? How can I implement the same.
The problem is the location where, or better WHEN, you are checking the length of the array. You make an asynchronous call when you fetch the data, but you are checking the length of the array before the data has been returned. Therefore the array is still empty.
Try the following in getAgencyData():
console.log("Fetching agency data");
this.agencyItems = this.af.database.list('/agencies/',{preserveSnapshot:true});
this.agencyItems.subscribe(snapshots => {
snapshots.forEach(snapshot => {
console.log(snapshot.val()._id);
this.agencyID.push(snapshot.val()._id);
console.log("Array Length = ",this.agencyID.length); // See the length of the array growing ;)
});
// EDIT
this.getTrackerData();
});

Resources