angular rxjs pipe remove element from array - javascript

I have a service with a delete function. The delete function will call an api and it will return true or false. When true, I will lookup the index in my array, splice it and return the new array. So for example
private items = [];
onItemDeleted = new Subject<any>();
delete(id:number): Observable<any> {
return this.http.delete('http://my.api.com/item/' + id)
.pipe(
switchMap(checkServerSuccessResponse),
map(data => {
const index1 = this.items.findIndex((element) => {
return element.id === id;
});
if (index1 >= 0 ) {
this.items.splice(index1,1);
}
this.onItemDeleted.next(this.items);
return this.items;
}
),
catchError(returnFalse),
);
}
I have a helper for the switchmap :
export function checkServerSuccessResponse(data: Response): Observable<any> {
return (data && data['success'] === true) ? of(data) : throwError("server responded false");
}
Although this works, I have a feeling the map section can reformatted. I first thought of filter (after the switchmap) to exclude the element with the id I've supplied, then emit the new array, but then I realised, the filter is not subscribed to the this.items array.
What would be the best approach to do this?

I don't know you other code, for example where this.items coming from, why do you publish updated items to onItemDeleted. But I would probably: a) pass this.items to delete method also like delete(id, items) because on the time when response will arrive, you don't know what will happen with this.items; b) that thing within the map, move to separate function, that will be removeById(items, id); c) simplify pipe. Like this:
private items = [];
onItemDeleted = new Subject<any>();
removeById(fromItems, id) {
const index1 = fromItems.findIndex((element) => {
return element.id === id;
});
if (index1 >= 0 ) {
fromItems.splice(index1,1);
}
return fromItems;
}
// who ever calls this, should provide copy of items also
// then you will be kinda protected from concurrent
// modification, when http response complete, but this.items
// is completely different, from when http request started
delete(fromItems, id:number): Observable<any> {
return this.http.delete('http://my.api.com/item/' + id)
.pipe(
switchMap(checkServerSuccessResponse),
map(data => this.removeById(fromItems, id)),
tap(items => this.onItemDeleted.next(items)),
catchError(returnFalse),
);
}

Related

How to avoid multiple subscribe which is used in other function in Angular?

Consider this code:
// search for a location and get its geo-coordinates
searchLocation(location: string): void {
this.gs
.getLocationInfo(location)
.pipe(
tap((data: GoogleResponse) => {
if (data.status === 'OK') {
this.lng = data.results[0].geometry.location.lng;
this.lat = data.results[0].geometry.location.lat;
this.name = data.results[0].name;
}
}),
map((response) => response)
)
.subscribe((data: GoogleResponse) => {
if (this.mode === 'tours') {
this.getTours();
} else if (this.mode === 'guides') {
this.getLocalGuides();
}
});
}
getTours(): void {
this.isLoading = true;
this.ds.setLoadingStatus(this.isLoading);
this.router.navigate(['/tours-view']);
this.ts.getTourByPoiLocation(this.lng, this.lat).subscribe((tours: Tour[]) => {
this.tours = tours;
this.ds.setTours(this.tours);
this.gs.setLocationName(this.name);
});
}
First I make an HTTP Request to Google using the Geocoordinate API to get the Geometry and save the data within the tap operator and then I'm using subscribe as this is an observable. (I also don't even use data: GoogleResponse in the subscribe.)
Within the subscribe I call the method getTours() to get some places saved in my DB with the latitude and longitude. I make another request to my server to retrieve the data. As this is an observable as well, I used subscribe again.
Everything works but I want to ask if there is any optimization to this code.
I think I did some bad practices especially the subscribe in the subscribe.
Can I solve this with mergeMap or something?
Yes, you can use mergeMap like this:
// search for a location and get its geo-coordinates
searchLocation(location: string): void {
this.gs
.getLocationInfo(location)
.pipe(
tap((data: GoogleResponse) => {
if (data.status === 'OK') {
this.lng = data.results[0].geometry.location.lng;
this.lat = data.results[0].geometry.location.lat;
this.name = data.results[0].name;
}
}),
mergeMap((response: GoogleResponse) => this.ts.getTourByPoiLocation(this.lng, this.lat))
)
.subscribe((tours: Tour[]) => {
if (this.mode === 'tours') {
this.getTours(tours);
} else if (this.mode === 'guides') {
this.getLocalGuides();
}
});
}
getTours(tours: Tour[]): void {
this.isLoading = true;
this.ds.setLoadingStatus(this.isLoading);
this.router.navigate(['/tours-view']);
this.tours = tours;
this.ds.setTours(this.tours);
this.gs.setLocationName(this.name);
}
Now you just have one subscribe
Reading your question and your code it seems to me that you want to do the following:
get location info invoking getLocationInfo
once the response from getLocationInfo is received (more precisely, when the Observable returned by getLocationInfo emits), set some state attributes based on the values of the response received (if the condition data.status === 'OK' is true)
after this, decide (based on the mode value) whether to trigger the execution of the getTours or getLocalGuides method
in the case you trigger the execution of the getTours (i.e. if mode === 'tours'), you set some state, navigate to a new page and you trigger the execution of another Observable, the one returned by getTourByPoiLocation, and use the value emitted by this Observable to set some more state attributes with the response received
the function getTourByPoiLocation actually receives as input parameters properties of the value emitted by the Observable returned by getLocationInfo (in your code it uses the this.lng, this.lat which are set based on the value emitted by the Observable returned by getLocationInfo)
it is not clear what happens in case the method getLocalGuides is executed: this method can either execute another Observable or can run some standard synchronous logic; I assume it executes another Observable
If this understanding is right, I would try something along these lines
searchLocation(location: string): void {
this.gs
.getLocationInfo(location)
.pipe(
tap((data: GoogleResponse) => {
if (data.status === 'OK') {
this.lng = data.results[0].geometry.location.lng;
this.lat = data.results[0].geometry.location.lat;
this.name = data.results[0].name;
}
}),
// use concatMap operator to execute the next Observable
// after the upstream Observable has notified something
// We use the value emitted (of type GoogleResponse) to pass
// the input parameters to the getTourByPoiLocation function
concatMap((data: GoogleResponse) => {
if (this.mode === 'tours') {
this.isLoading = true;
this.ds.setLoadingStatus(this.isLoading);
this.router.navigate(['/tours-view']);
// here we return the next Observable - concatMap requires, as
// input, a function that returns an Observable
return this.ts.getTourByPoiLocation(data.lng, data.lat)
.pipe(
tap((tours: Tour[]) => {
this.tours = tours;
this.ds.setTours(this.tours);
this.gs.setLocationName(this.name);
})
);
}
if (this.mode === 'guides') {
// return the Observable that executes the logic for this case
return functionThatReturnsTheObservableForGuides()
}
})
)
.subscribe();
}
You can factorize the code for the this.mode === 'tours' case in its own method, like this
getTours(lng, lat) {
this.isLoading = true;
this.ds.setLoadingStatus(this.isLoading);
this.router.navigate(['/tours-view']);
return this.ts.getTourByPoiLocation(data.lng, data.lat)
.pipe(
tap((tours: Tour[]) => {
this.tours = tours;
this.ds.setTours(this.tours);
this.gs.setLocationName(this.name);
})
);
}
and end up with this version
searchLocation(location: string): void {
this.gs
.getLocationInfo(location)
.pipe(
tap((data: GoogleResponse) => {
if (data.status === 'OK') {
this.lng = data.results[0].geometry.location.lng;
this.lat = data.results[0].geometry.location.lat;
this.name = data.results[0].name;
}
}),
// use concatMap operator to execute the next Observable
// after the upstream Observable has notified something
// We use the value emitted (of type GoogleResponse) to pass
// the input parameters to the getTourByPoiLocation function
concatMap((data: GoogleResponse) => {
if (this.mode === 'tours') {
return getTours(data.lng, data.lat);
}
if (this.mode === 'guides') {
// return the Observable that executes the logic for this case
return functionThatReturnsTheObservableForGuides()
}
})
)
.subscribe();
}
I could not set up an equal example, so there may be syntactical errors, but I hope this give an idea about how to avoid nested subscriptions in this case.

Why is it that My Array is Undefined after Pushing an Element from an Observable

So basically, I have a web application that retrieves data from Firebase using rxjs observables.
here's my code,
initializeItems(){
this.travelList$ = this.plsdala.getTravelList()
.snapshotChanges()
.map(
changes => {
return changes.map(c=>({
key: c.payload.key, ...c.payload.val()
})).slice().reverse();//to reverse order
})
this.travelList$.subscribe(res => {
for(let i=0;i<res.length;i++){
this.ListOfitems.push (res[i].toAddress);
}
})
}
this is called from the constructor. problem here is that i cannot check if it is push successfully and if try to print in console , it wont print. why?
the element pushed is needed for filtering. heres is the code for filtter. but when i print the this.ListOfitems in console it is undefined and im wondering unto why? when the elements are initialized first
getItems(ev: any) {
console.log("awdaw");
console.log(this.ListOfitems);
if (this.ListOfitems.length>1){
console.log("otin");
let val = ev.target.value;
if (val && val.trim() != '') {
this.ListOfitems = this.ListOfitems.filter((ListOfitems) => {
return (ListOfitems.toLowerCase().indexOf(val.toLowerCase()) > -1);
})
}
}
}
when you are declaring list of items if you want to push into the array you need to declare it empty first like this:
ListOfitems: string[] = [];

Filtering Json with .map

I'm trying to filter this .json in order not to show private videos.
It's for a app of a youtube channel.
There are some videos that are private and I don't want to show them in the app.
I need help filtering the .json
Currently it's returning an empty json. if I remove the .map it returns the complete list of videos with the privete ones.
getPlayListVideos(listId: string) {
return this.http.get('https://www.googleapis.com/youtube/v3/playlistItems?key=' + this.apiKey + '&fields=items/snippet/resourceId/videoId,items/snippet/publishedAt,items/snippet/title,items/snippet/thumbnails/high/url&playlistId=' + listId +'&part=snippet,id&maxResults=25')
.map((res) => {
return res.json()['items'].filter(item => {
if(item.snippet.title === 'Private video'){
return false;
}
});
})
}
You need to return true if you don't want to filter the current item. Currently you're returning either false or undefined implicitly since you don't return anything in that case. So you need to do this:
if(item.snippet.title === 'Private video'){
return false;
} else {
return true;
}
or better yet return item.snippet.title !== 'Private video'.
Bonus formatting - I'd write the whole thing like this personally:
getPlayListVideos(listId: string): any[] {
return this.http.get('https://www.googleapis.com/youtube/v3/playlistItems?key=' + this.apiKey + '&fields=items/snippet/resourceId/videoId,items/snippet/publishedAt,items/snippet/title,items/snippet/thumbnails/high/url&playlistId=' + listId + '&part=snippet,id&maxResults=25')
.map(res => res.json()['Items'])
.map((items: any[]) => items.filter(item => item.snippet.title !== 'Private video'))
}

Return from inside an observable

I'm think fundamentally i'm doing something wrong. I'm trying to find a single entity by pulling it from filtered list, if there isn't an entity there I need to create one.
I feel like this function is wrong because I should be returning an observable but instead returning nothing
getOrCreateNew(receiverId: number) : Observable<Conversation> {
var userId = this.identity.userInfo.id;
//TODO: you should be using an expression here, and having a builder for generating your
//filters in case you want to switch them in the future
var employerFilter = new PropertyFilterNode("EmployerUserId", FilterCondition.Equal, receiverId.toString());
var employeeFilter = new PropertyFilterNode("EmployeeUserId", FilterCondition.Equal, userId.toString());
let conversationFilter = new BinaryFilterNode(employerFilter, employeeFilter, Combiner.Or);
this.getList(conversationFilter).subscribe(entities => {
if (entities == null || entities.length == 0) {
let conversation: Conversation;
conversation.employerUserId = receiverId;
conversation.employeeUserId = userId;
return this.create(conversation);
}
else {
let entity = entities[0];
return Observable.of(entity); //.Return(entity)
}
});
return null;
}
How can I return an observable which is return from inside subscribe?
The way getList subscription works suggests that it is supposed to be mergeMap or switchMap (considering that create returns an observable as well):
return this.getList(conversationFilter).mergeMap(entities => {
if (entities == null || entities.length == 0) {
let conversation: Conversation;
conversation.employerUserId = receiverId;
conversation.employeeUserId = userId;
return this.create(conversation);
}
else {
let entity = entities[0];
return Observable.of(entity); //.Return(entity)
}
});
In this case an observable that is returned from getOrCreateNew should be subscribed in order to emit values because it isn't subscribed internally.
Place the return before this.getList also :
return this.getList(conversationFilter).subscribe(entities => {

Angular 2 How to send a parameter in Observable interval - Continuous upload of data feed

I want to change the get messages to get only the delta of the messages I haven't got yet. So if I will have multiple users it will each time get me other peoples messages only. I want to retrieve the delta every 5 seconds and append it to the results i have already retrieved.
getMessages(){
let maxMessageId = 0;
console.log(this.messages);
if(this.messages.length > 0 )
{
maxMessageId = Math.max.apply(Math, this.messages.map(function(message){return message.messageAutoIncreamentId;}));
}
return Observable.interval(5000)
.switchMap(() =>this.http.get(this._domainUrl + 'message?maxMessageId='+ maxMessageId))
.map((response: Response)=>{
const messages = response.json().obj;
let transformedMessages : Message[] = [];
for( let message of messages)
{
transformedMessages.push(
new Message(
message.content,
message.user.firstName ,
message._id,
message.user._id,
message.messageAutoIncreamentId)
);
}
this.messages = transformedMessages;
return transformedMessages;
})
.catch((error: Response) => {
this.errorService.handleError(error.json());
return Observable.throw(error.json());
});
}
The problem here is that the maxMessageId is always 0, because it’s initialised outside the interval probably. How can I pass a parameter maxMessageId to the switchMap ?
Is there a better alternative then Rx / Observables for pulling data continuously?
So your first function that sets up the listener shouldn't hold the messages, I moved them outside the function so they can be better accessed.
When you call getMessages() now it will make the request every 5 seconds, process the results, and push back onto the messages array.
What you needed to do is when you run your maxId calculation you need to RERUN it inside the observable. You change the messages array so your max is going to change. You could run it on your limited array (transformedMessages) but I just re-ran it over the main one. This should work, let me know if it gives you issues.
-D
let maxMessageId = 0;
let messages = [];
getMessages(){
// initial set of max
if(messages.length > 0 )
{
maxMessageId = getMaxId(this.messages);
}
return Observable.interval(5000)
.switchMap(() =>this.http.get(this._domainUrl + 'message?maxMessageId='+ maxMessageId))
.map((response: Response)=>{
const messages = response.json().obj;
let transformedMessages : Message[] = [];
for( let message of messages)
{
transformedMessages.push(
new Message(
message.content,
message.user.firstName ,
message._id,
message.user._id,
message.messageAutoIncreamentId)
);
}
messages = transformedMessages;
// this changes the messages value, so we should also change the maxID
maxMessageId = getMaxId(messages);
return transformedMessages;
})
.catch((error: Response) => {
this.errorService.handleError(error.json());
return Observable.throw(error.json());
});
}
getMaxId(messageList) {
return Math.max.apply(Math, messageList.map(function(message){return message.messageAutoIncreamentId;}));
}
Probably what you want is
return Observable.interval(5000)
.map(() => {
if(this.messages.length > 0 ) {
return Math.max.apply(Math, this.messages.map(function(message){return message.messageAutoIncreamentId;}));
} else {
return 0;
}
})
.switchMap((maxMessageId) => this.http.get(this._domainUrl + 'message?maxMessageId='+ maxMessageId))
.map((response: Response)=>{
...
this.messages = this.messages.concat(transformedMessages);
return transformedMessages;
})
.catch((error: Response) => {
this.errorService.handleError(error.json());
return Observable.throw(error.json());
});

Categories