Different Pipe Based on Filter RxJS - javascript

I have the following problem. I want to subscribe to an observable using the async pipe in Angular. But the data in the pipe can originate from different data sources and the pipe can look different depending on the data source.
The way it is implemented now does not work, because I override the first value of this.data$ and thus source A never gets subscribed to.
Is it possible to 'split' the pipe depending on a filter?
problem.component.ts:
// Data source A and logic A
this.data$ = this.service.context.pipe(
filter(context => context.flag === true),
switchMap(context => this.dataSourceA(context.id))
map(data => this.processDataA(data))
);
// Data source B and logic B
this.data$ = this.service.context.pipe(
filter(context => context.flag === false),
switchMap(context => this.dataSourceB(context.id))
map(data => this.processDataB(data))
);
problem.component.html
<pro-table [data]="data$ | async"></pro-table>

Move the logic to your switchMap()
this.data$ = this.service.context.pipe(
switchMap(context => context.flag
? this.dataSourceA(context.id).pipe(map(d => this.processDataA(d)))
: this.dataSourceB(context.id).pipe(map(d => this.processDataB(d)))
)
);

That's just wrong way to achieve the result you desire. You have to take over to something called "higher order observable": Observable<Observable<T>>. Like a "higher order function" (that able to produce another function based on it's arguments), this guy is able to produce another Observable based on whatever parametrization you rely on.
this.service.context.pipe(
switchMap(context => context.flag ? this.dataSourceA(context.id).pipe(...) : this.dataSourceB(context.id).pipe(...))
);
...it might seem a littble bit esoteric at the beginging, but actually it isn't. \
P.S. Important point to remember: higher order observables let you parametrize your streams if a fancy way: by dealing with streams of streams.

You can add a Subject which will catch many data sources and subscribe to that:
this.data$ = new Subject();
observer = {
next(data) { this.data$.next(data) },
error(msg) { /*handle error*/ }
};
// Data source A and logic A
this.service.context.pipe(
filter(context => context.flag === true),
switchMap(context => this.dataSourceA(context.id))
map(data => this.processDataA(data))
).subscribe(observer);
this.service.context.pipe(
filter(context => context.flag === false),
switchMap(context => this.dataSourceB(context.id))
map(data => this.processDataB(data))
).subscribe(observer);
In this situation though it seems like you can use the flag to keep it in one chunk:
this.service.context.pipe(
switchMap(context => context.flag ?
this.dataSourceA(context.id) : this.dataSourceB(context.id)
)
map(data => this.processData(data)) // handle both cases
).subscribe(observer);

Related

RXJS how to use OR condition

I have two Observables
For example, I am waiting HTTP POST call to come back OR WebSocket call come back so I can continue.
Either call come back I need to verify the information and until certain condition met. Follow example that either one come back I can continue. But if I use filter, it will stop and wait wont proceed.
switchMap(() =>
webSocket.access.pipe(
filter((data) => data.access.role === "ADMIN"),
filter((data) => data.access.status === "ACTIVE")
)
),
switchMap(() =>
httpService.access.pipe(filter((res) => res.access === true))
);
Use the race operator:
race(
switchMap(() =>
webSocket.access.pipe(
filter((data) => data.access.role === "ADMIN"),
filter((data) => data.access.status === "ACTIVE")
)
),
switchMap(() =>
httpService.access.pipe(filter((res) => res.access === true))
)
).pipe(/* do subsequent operations here */);
https://www.learnrxjs.io/learn-rxjs/operators/combination/race
I Imagine
obsPost$:Observable<any>=...
socket$:Observable<any>=
We use merge to get any of the observables. But transform each
observable in an object with property "id" and "data"
Then filter (get the "access" value according the value of "id")
Finally return only the "data"
myObs$=merge(obsPost$.pipe(map((res:any)=>({id:'post',data:res}))
socket$.pipe(map((res:any)=>({id:'socket',data:res}))).pipe(
filter((res:any)=>{
const access:any=res.id=="post"? data.data.access.role:
data.access
return access==true || access=='ADMIN' || access=='ACTIVE')
}),
map(res=>res.data))

Firestore: Array of references within a document into an object with array of objects (AngularFire)

I have a collection, where each single document have an array of references.
The structure in Firestore is like this:
collection1
doc1
doc2
doc3
name: string
...
refs: Array
collection2/x786123nd...
collection2/z1237683s...
...
I want to convert that into:
[
{<doc1 as object>},
{<doc2 as object},
{ name: "Document 3", refsResult:
[
{<object here mapped from reference>},
{<object here mapped from reference>},
]
}
]
Long story short, from firebase I need to get an output of a list of object where each object has a list of objects instead of references.
THE FOLLOWING SNIPPET IS NOT WORKING
I am trying to do it at service level in Angular using RxJS to transform the output, something like this:
return this.afs.collection('collection1')
.valueChanges()
.pipe(
switchMap(objects => from(objects).pipe(
switchMap(obj => from(obj.refs).pipe(
switchMap(ref => ref.get()),
map(r => ({ ...obj, refsResult: r.data() })),
tap(obj => console.log('OBJ', obj)),
)),
)),
tap(objects => console.log(objects))
);
But it seems that I only receive one object instead of a list of objects with 2. Also, it seems that the refsResult is also a single object instead of an array. I am sure I am using the switchMaps wrong, probably they are cancelling the others results or similar.
I would like to keep the solution within the chain of rxjs operators.
EDIT 1
I got it working in a hacky way, I am just sharing it here in order to give more context, and see if we can find a way to do it with RxJS.
return this.afs.collection('collection1')
.valueChanges()
.pipe(
tap(objects => {
objects.forEach(object => {
object.refsResult = [];
object.refs.forEach(ref => {
ref.get().then(d => object.refsResult.push(d.data()));
})
})
}),
tap(programs => console.log(programs))
);
The code above works but not very well, since first return the collection and then, mutates the objects to inject the items within the array. Hopefully it helps.
Help is very appreciated! :)
You can use combineLatest to create a single observable that emits an array of data.
In your case, we can use it twice in a nested way:
to handle each object in collection
to handle each ref in refs array for each object
The result is a single observable that emits the full collection of objects, each with their full collection of ref result data. You then only need a single
switchMap to handle subscribing to this single observable:
return this.afs.collection('collection1').valueChanges().pipe(
switchMap(objects => combineLatest(
objects.map(obj => combineLatest(
obj.refs.map(ref => from(ref.get()).pipe(map(r => r.data())))
).pipe(
map(refsResult => ({ ...obj, refsResult }))
)
))
))
);
For readability, I'd probably create a separate function:
function appendRefData(obj) {
return combineLatest(
obj.refs.map(ref => ref.get().pipe(map(r => r.data())))
).pipe(
map(refsResult => ({ ...obj, refsResult }))
);
}
return this.afs.collection('collection1').valueChanges().pipe(
switchMap(objects => combineLatest(objects.map(appendRefData)))
);

How to make recursive HTTP calls using RxJs operators?

I use the following method in odder to retrieve data by passing pageIndex (1) and pageSize (500) for each HTTP call.
this.demoService.geList(1, 500).subscribe(data => {
this.data = data.items;
});
The response has a property called isMore and I want to modify my method in odder to continue HTTP calls if isMore is true. I also need to merge the returned values and finally return the accumulated values.
For example, assuming that there are 5000 records and until 10th HTTP call, the service returns true for isMore value. After 10th HTTP call, it returns false and then this method sets this.data value with the merged 5000 records. For this problem, should I use mergeMap or expand or another RxJs operator? What is the proper way to solve this problem?
Update: I use the following approach, but it does not merge the returned values and not increase the pageIndex. For this reason it does not work (I tried to make some changes, but could not make it work).
let pageIndex = 0;
this.demoService.geList(pageIndex+1, 500).pipe(
expand((data) => {
if(data.isComplete) {
return of(EMPTY);
} else {
return this.demoService.geList(pageIndex+1, 500);
}
})
).subscribe((data) => {
//your logic here
});
Update II:
of({
isMore : true,
pageIndex: 0,
items: []
}).pipe(
expand(data => demoService.geList(data.pageIndex+1, 100)
.pipe(
map(newData => ({...newData, pageIndex: data.pageIndex+1}))
)),
// takeWhile(data => data.isMore), //when using this, it does not work if the total record is less than 100
takeWhile(data => (data.isMore || data.pageIndex === 1)), // when using this, it causing +1 extra HTTP call unnecessarily
map(data => data.items),
reduce((acc, items) => ([...acc, ...items]))
)
.subscribe(data => {
this.data = data;
});
Update III:
Finally I made it work by modifying Elisseo's approach as shown below. Howeveri **I need to make it void and set this.data parameter in this getData() method. How can I do this?
getData(pageIndex, pageSize) {
return this.demoService.geList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (data.isMore) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ items: [...data.items, ...res.items] }))
);
}
return of(data);
})
);
}
I want to merge the following subscribe part to this approach but I cannot due to some errors e.g. "Property 'pipe' does not exist on type 'void'."
.subscribe((res: any) => {
this.data = res;
});
getData(pageIndex, pageSize) {
return this.demoService.getList(pageIndex, pageSize).pipe(
switchMap((data: any) => {
if (!data.isCompleted) {
return this.getData(pageIndex+1, pageSize).pipe(
map((res: any) => ({ data: [...data.data, ...res.data] }))
);
}
return of(data);
})
);
}
stackblitz
NOTE: I updated pasing as argument pageIndex+1 as #mbojko suggest -before I wrote pageIndex++
UPDATE 2
Using expand operator we need take account that we need feed the "recursive function" with an object with pageIndex -it's necesarry in our call- for this, when we make this.demoService.getList(data.pageIndex+1,10) we need "transform the result" adding a new property "pageIndex". for this we use "map"
getData() {
//see that initial we create "on fly" an object with properties: pageIndex,data and isCompleted
return of({
pageIndex:1,
data:[],
isCompleted:false
}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
//here we use map to create "on fly" and object
map((x:any)=>({
pageIndex:data.pageIndex+1, //<--pageIndex the pageIndex +1
data:[...data.data,...x.data], //<--we concatenate the data using spread operator
isCompleted:x.isCompleted})) //<--isCompleted the value
)
}),
takeWhile((data: any) => !data.isCompleted,true), //<--a take while
//IMPORTANT, use "true" to take account the last call also
map(res=>res.data) //finally is we only want the "data"
//we use map to return only this property
)
}
Well we can do a function like this:
getData() {
of({pageIndex:1,data:[],isCompleted:false}).pipe(
expand((data: any) => {
return this.demoService.getList(data.pageIndex,10).pipe(
tap(x=>{console.log(x)}),
map((x:any)=>({
pageIndex:data.pageIndex+1,
data:[...data.data,...x.data],
isComplete:x.isComplete}))
)
}),
takeWhile((data: any) => !data.isComplete,true), //<--don't forget the ",true"
).subscribe(res=>{
this.data=res.data
})
}
See that in this case we don't return else simple subscribe to the function and equal a variable this.data to res.data -it's the reason we don't need the last map
Update 3 by Mrk Sef
Finally, if you don't want your stream to emit intermittent values and you just want the final concatenated data, you can remove the data concatenation from expand, and use reduce afterward instead.
getData() {
of({
pageIndex: 1,
data: [],
isCompleted: false
})
.pipe(
expand((prevResponse: any) => this.demoService.getList(prevResponse.pageIndex, 10).pipe(
map((nextResponse: any) => ({
...nextResponse,
pageIndex: prevResponse.pageIndex + 1
}))
)
),
takeWhile((response: any) => !response.isCompleted, true),
// Keep concatenting each new array (data.items) until the stream
// completes, then emit them all at once
reduce((acc: any, data: any) => {
return [...acc, ...data.data];
}, [])
)
.subscribe(items => {
this.data=items;
});
}
It doesn't matter if you're total record change as long as api response give you the isMore flag.
I'm skipping the part how to implement reducer action event i'm assuming you've already done that part. So i will just try to explain with pseudo codes.
You have a table or something like that with pagination data. on intial state you can just create an loadModule effect or using this fn:
getPaginationDataWithPageIndex(pageIndex = 1){
this.store.dispatch(new GetPaginationData({ pageIndex: pageIndex, dataSize: 500}));
}
in your GetPaginationData effect
... map(action => {
return apicall.pipe(map((response)=> {
if(response.isMore){
return new updateState({data:response.data, isMore: responseisMore})
} else {
return new updateState({isMore: response.isMore}),
}
}})
`
all you have to left is subscribing store in your .ts if isMore is false you will not display the next page button. and on your nextButton or prevButton's click method you should have to just dispatch the action with pageIndex
I do not think recursion is the correct approach here:
interval(0).pipe(
map(count => this.demoService.getList(count + 1, 500)),
takeWhile(reponse => response.isMore, true),
reduce((acc, curr) => //reduce any way you like),
).subscribe();
This should make calls to your endpoint until the endpoint returns isMore === false. The beautiful thing about interval is that we get the count variable for free.
But if you are set on using recrsion, here is the rxjs-way to do that using the expand-operator (see the docs). I find it slightly less readable, as it requires an if-else-construct which increases code complexity. Also the outer 'counter' variable just isn't optimal.
let index = 1;
this.demoService.geList(index, 500).pipe(
expand(response => response.isMore ? this.demoService.geList(++index, 500) : empty()),
reduce((acc, curr) => //reduce here)
).subscribe();

double combineLatest doesn't emit update

In my project there are activities that people have created, joined, bookmarked or organized. I've read a lot of these question already. But most of the code was less complex or people forgot to subscribe...
I would like to get all the activities in a certain time period and then add creator information (name, image, etc) and add booleans if the user retrieving these activities has joined/bookmarked/organized this activity. The code I used before would provide live updates (ex. I join an activity, by adding my userId to the participants array and the activity.joined would update to true).
Previous code:
public getActivities(user: UserProfileModel): Observable<Array<ActivityModel>> {
const now: number = moment().startOf('day').unix();
const later: number = moment().startOf('day').add(30, 'day').unix();
return this.afs.collection<ActivityModel>(`cities/${user.city.id}/activities`, ref => ref
.where('datetime', '>=', now)
.where('datetime', '<=', later))
.valueChanges({ idField: 'id' })
.pipe(
map(activities => activities.map(activity => {
const bookmarked = activity.bookmarkers ? activity.bookmarkers.includes(user.uid) : false;
const joined = activity.participants ? activity.participants.includes(user.uid) : false;
const organized = activity.organizers ? activity.organizers.includes(user.uid) : false;
return { bookmarked, joined, organized, ...activity } as ActivityModel;
}))
);
}
The I wanted to add the creator as an observable object, so their latest changes in name or profile picture would be shown. But with this code change, my getActivities doesn't emit any updates anymore...
My new code:
public getActivities(user: UserProfileModel): Observable<Array<CombinedActivityCreatorModel>> {
const now: number = moment().startOf('day').unix();
const later: number = moment().startOf('day').add(30, 'day').unix();
return this.afs.collection<ActivityModel>(`cities/${user.city.id}/activities`, ref => ref
.where('datetime', '>=', now)
.where('datetime', '<=', later))
.valueChanges({ idField: 'id' })
.pipe(
concatMap(activities => {
const completeActivityData = activities.map(activity => {
const activityCreator: Observable<UserProfileModel> = this.getCreator(activity.creator);
const bookmarked = activity.bookmarkers ? activity.bookmarkers.includes(user.uid) : false;
const joined = activity.participants ? activity.participants.includes(user.uid) : false;
const organized = activity.organizers ? activity.organizers.includes(user.uid) : false;
return combineLatest([
of({ bookmarked, joined, organized, ...activity }),
activityCreator
]).pipe(
map(([activityData, creatorObject]: [ActivityModel, UserProfileModel]) => {
return {
...activityData,
creatorObject: creatorObject
} as CombinedActivityCreatorModel;
})
);
});
return combineLatest(completeActivityData);
})
);
}
The code has become a bit complex, that I don't see the solution myself. Anybody that can offer some assistance?
Looks like one of activityCreator doesn't emit a value, combineLatest requires all observables to emit at least once.
I would recommend you to debug how activityCreator behaves.
If it's fine that it doesn't emit you have 2 options: startWith to set a value for an initial emit, or defaultIfEmpty, it emits in case if stream is going to be closed without any emit.
activityCreator = this.getCreator(activity.creator).pipe(
// startWith(null), // for example if you want to trigger combineLatest.
// defaultIfEmpty(null), // in case of an empty stream.
);
another thing is concatMap it requires an observable to complete, only then it switches to the next one, parhaps mergeMap or switchMap fits here better.
Try the code below and add its output to the comments. Thanks.
const activityCreator: Observable<UserProfileModel> = this.getCreator(activity.creator).pipe(
tap(
() => console.log('getCreator:emit'),
() => console.log('getCreator:error'),
() => console.log('getCreator:completed'),
),
);

How to return paginated data from the store OR new data from an api service using ngrx-effects in an angular2 app?

My question is a continuation of this excellent question and answer concerning the shape of paginated data in a redux store. I am using ngrx/store in an angular 2 app.
{
entities: {
users: {
1: { id: 1, name: 'Dan' },
42: { id: 42, name: 'Mary' }
}
},
visibleUsers: {
ids: [1, 42],
isFetching: false,
offset: 0
}
}
Based on the above shape I believe if the offset (or page, sort, etc.) from an incoming request payload changed then the visible users would change as well as the user entities by calling the DB. I have some actions and reducer functions to handle this and it works as expected. If the offset remains the same and the user is returning to the page the way they left it then the user entities should be returned by the store not the DB.
Where I am struggling is where to put that logic and which rxjs operators to use (still learning this).
I think the correct place is an effect. Here is what I have now in my angular2 app (I am injecting Actions, Store, and my UserService) that pulls new data every time the page is loaded.
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.switchMap(() => this.userService.query()
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
My best idea is something like this:
#Effect loadUsers$ = this.actions$
.ofType('LOAD_USERS')
.withLatestFrom(this.store.select(state => state.visibleUsers.offset))
.switchMap(([action, state]) => {
//something that looks like this??
//this syntax is wrong and I can't figure out how to access the action payload
state.offset === payload.offset
? this.store.select(state => state.entities.users)
: this.userService.query()
}
.map((results) => {
return new LoadUsersSuccessAction(results);
}))
.catch(() => Observable.of(new LoadUsersFailureAction()));
Not sure how to make this work. Thanks ahead.
I don't like answering my own questions but it took me quite a while to find the answer to this. I won't accept this answer as I am not sure this is the best way to go about things (still learning the ins/outs). It does however, work perfectly.
I found the correct syntax in a github gist. This code does not match my example but it clearly demonstrates "2 options for conditional ngrx effects" by either returning a store or api observable using some sort of condition.
Hope this helps someone.
Here it is:
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
return Observable.if(
() => existsInStore,
Observable.of(new storeActions.SetSelectedStore(storeName)),
this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store))
);
});
#Effect()
selectAndLoadStore$: Observable<Action> = this.actions$
.ofType(storeActions.SELECT_AND_LOAD_STORE)
.withLatestFrom(this.store.select(ngrx.storeState))
.map(([action, storeState]) => [action.payload, storeState])
.switchMap(([storeName, storeState]) => {
const existsInStore = Boolean(storeState.urlNameMap[storeName]);
let obs;
if (existsInStore) {
obs = Observable.of(new storeActions.SetSelectedStore(storeName));
} else {
obs = this.storeService.getByUrlName(storeName)
.map(store => new storeActions.LoadSelectedStoreSuccess(store));
}
return obs;
});

Categories

Resources