Emmit old value if SwitchMap has error in Rxjs - javascript

I am trying to get a member value from other web api in rxjs.
I did this in a pipe method with switchMap. But if there is a problem with getting the member value then I want to skip old model with values to next method.
So I dont want to return null after switchMap worked.
Here is my code:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`),(empCard, card) => ({ empCard, card }) ),
//second subscribe
switchMap((emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode),
(emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
//I use this subscribe only set to balance
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
return of([]); //I want to retun emp value
}),
reduce((acc, value) => acc.concat(value), [])
)
How can I solve this problem?
Thanks

I think you can achieve that by creating a closure:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`), (empCard, card) => ({ empCard, card })),
switchMap(
(emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode)
.pipe(
map((emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
// `emp` available because of closure
return of(emp);
}),
)
),
reduce((acc, value) => acc.concat(value), [])
)
Also notice that I gave up on switchMap's custom resultSelector, as it can easily be replaced with a map operator.

Related

RxJS - Conditionally add observable in a pipe

So i have a function like below
showLoader = () => <T>(source: Observable<T>) => {
LoaderService.loadPage.next(true);
return source.pipe(
finalize(() => {
LoaderService.loadPage.next(false);
})
);
};
And then i use it while making HTTP calls like below
return this.http.get(url).pipe(showLoader())
But let's say i encounter a scenario where i need the loader or any observable for that matter based on a condition; something like below
const loader : boolean = false
return this.http.get(url).pipe(concat(...), loader ? showLoader() : of(values))
I tried using the iif operator like below
const loader : boolean = false
return this.http.get(url).pipe(concat(...), mergeMap(v => iif(() => loader, showLoader(), of(v))))
and got the following error
TS2345: Argument of type '(source: Observable) => Observable'
is not assignable to parameter of type 'SubscribableOrPromise<{}>'.
Can someone hep me understand where i am going wrong and how to rectify the same
you could do it like this:
showLoader = (show: boolean = true) => <T>(source: Observable<T>) => {
if (!show) { // just go straight to source
return source;
}
return defer(() => { // defer makes sure you don't show the loader till actually subscribed
LoaderService.loadPage.next(true);
return source.pipe(
finalize(() => {
LoaderService.loadPage.next(false);
})
)
})
};
use:
return this.http.get(url).pipe(showLoader(false))
but the way you seem to be statically accessing LoaderService reeks of design issues and bugs in your future. FYI.
I would suggest something like following:
const startWithTap = (callback: () => void) =>
<T>(source: Observable<T>) => of({}).pipe(
startWith(callback),
switchMap(() => source)
);
const showLoader = () => <T>(source: Observable<T>) => concat(
iif(
() => loader,
source.pipe(
startWithTap(() => LoaderService.loadPage.next(true)),
finalize(() => {
LoaderService.loadPage.next(false);
})
),
EMPTY
), source);
return this.http.get(url).pipe(
showLoader()
);

Angular wait until subscribe is done and give values to other function

i have this following function
file: subcategory.service.ts
getSubCategoriesById(inp_subCatid: String): Observable<any>{
this.getSubCategoriesList().snapshotChanges().pipe(
map(changes =>
changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
)
)
).subscribe(subCategories => {
subCategories.filter(function (subCat) {
return subCat.id == inp_subCatid;
});
});
and i´m calling the top function in the following file
file: subcategory.page.ts
this.SubCategoryService.getSubCategoriesById(subCatid).subscribe((subCategories: any) => {
this.subCat = subCategories ;
})
the problem what i got is i´m getting following error message:
ERROR TypeError: "this.SubCategoryService.getSubCategorysById(...) is undefined"
i want to get the data when there are loaded from the file "subcategory.service.ts"
hope someone can help me.
Your method should be like this:
getSubCategories(inp_subCatid: string): Observable<any> {
return this.getSubCategoriesList().snapshotChanges().pipe(
map(changes => changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
).filter((subCat) => subCat.id === inp_subCatid)
));
}
Then you will be able to use like this:
this.subCategoryService.getSubCategories(subCatid)
.subscribe(subCategories => this.subCat = subCategories);
If I'm interpreting correclty your methods, it seems to me that you're using firebase... if so, after you call this.yourService.getSubCategories(subCatid) for the first time, your subscription will remain active so that your subcategories will be updated for every change on the database, even if you change subCatid, the previous database query will be alive. To avoid it, I suggest that you take just one emission of snapshotChanges():
getSubCategories(inp_subCatid: string): Observable<any> {
return this.getSubCategoriesList().snapshotChanges().pipe(
// finish the subscription after receiving the first value
take(1),
map(changes => changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
).filter((subCat) => subCat.id === inp_subCatid)
));
}
Thanks a lot
what if i want to filter for a specific data ?? like for "id"
getSubCategoriesbyId(inp_subCatid): Observable<any>{
this.getSubCategoriesList().snapshotChanges().pipe(
map(changes =>
changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
)
)
).subscribe(subCategories => {
subCategories.filter(function (subCat) {
return subCat.id == inp_subCatid;
});
});
}
and then to get the filtered data back
this.yourService.getSubCategoriesbyId(subCatid)
.subscribe(subCategories => console.log(subCategories));

implement infinite scroll with rxjs

I'm having trouble when implement infinite scroll with rxjs
I have tried following snippet
var lastid = null
fromEvent(window, 'scroll')
.pipe(
filter(() => isAtPageBottom()),
exhaustMap(() => from(this.getList(lastid))),
takeWhile(list => list.length !== 0),
scan((cur, list) => [...cur, ...list], [])
)
.subscribe(list => {
this.setState({list: list})
})
async function getList (lastid) {
const list = await request('/api/list?lastid=' + lastid)
listid = list[list.length-1].id
return list
}
How to pass the lastid to each request without the global variable 'lastid'?
Thanks
haven't test the code but you can try the below code snippet. The technique here is to start off with a state that contains your list and lastId then use expand to keep the stream going and listen for scroll
of({ list: [], lastId: null }).pipe(
expand((obj) => {
return fromEvent(window, 'scroll')
.pipe(
filter(() => isAtPageBottom()),
exhaustMap(() => from(this.getList(obj.lastid))),
map(res => ({ list: obj.list.concat(res), lastId: res[res.length - 1].id }))
)
}),
takeWhile(({ list }) => list.length !== 0))

Is there a way to use a observable returning function for each element of another observable array?

I get an Observable<Group[]> from my Firebase collection.
In this Group class is an id which I wanna use to retrieve another dataset array from Firebase, which would be messages for each unique group Observable<Message[]>.(each group has its own chat: Message[])
And it want to return an observable which hold an array of a new Type:
return { ...group, messages: Message[] } as GroupWithMessages
the final goal should be Observable<GroupWithMessages[]>
getGroupWithChat(): Observable<GroupWithMessages[]> {
const groupColl = this.getGroups(); // Observable<Group[]>
const messages = groupColl.pipe(
map(groups => {
return groups.map(meet => {
const messages = this.getMessagesFor(group.uid);
return { messages:messages, ...group} as GroupWithMessages
});
})
);
return messages;
}
}
and here the Message function
getMessagesFor(id: string): Observable<Message[]> {
return this.afs.collection<Message>(`meets/${id} /messages`).valueChanges();
}
sadly that doesnt work because when i create the new Obj I cannot bind messages:messages because messages ist vom typ Observable<Message[]>
I hope that cleares things
UPDATE:
my main problem now comes down to this:
getGroupsWithMessages() {
this.getJoinedGroups()
.pipe(
mergeMap(groups =>
from(groups).pipe(
mergeMap(group => {
return this.getMessagesFor(group.uid).pipe(
map(messages => {
return { ...group, messages } as GroupIdMess;
})
);
}),
tap(x => console.log('reaching here: ', x)),
toArray(),
tap(x => console.log('not reaching here = completed: ', x))
)
),
tap(x => console.log('not reaching here: ', x))
)
.subscribe(x => console.log('not reaching here: ', x));
}
when i call that function my console.log is as follows:
Not sure if I follow what you're doing here but the logic look like you'd want:
getGroupWithChat() {
return this.getGroups.pipe(map(groups=> {
return groups.map(group => this.getMessagesFor(group.uid));
})).subscribe(); // trigger "hot" observable
}
Let me know if I can help further after you clarify.
UPDATE:
So it looks like you need to get the UID of the group before making the call to get the GroupMessages[]?
get Group: Observable
call getMessagesFor(Group.uid)
this example gets groups result$ then
concatMap uses groups result$ to make the messages query
this.getGroups().pipe(
concatMap((group: Group) => this.getMessagesFor(group.uid))
).subscribe((messages: GroupWithMessages[]) => {
console.log(messages);
});
You may still want to map them together but it seems like you know how to do that. concatMap waits for the first to finish, then makes the second call which you need.
Is this closer?
Use forkJoin to wait for messages to be received for all groups. Then map the result of forkJoin to an array of GroupWithMessages like this -
getGroupWithChat(): Observable<GroupWithMessages[]> {
return this.getGroups()
.pipe(
switchMap(groups => {
const messagesForAllGroups$ = groups.map(group => this.getMessagesFor(group.uid));
return forkJoin(messagesForAllGroups$)
.pipe(
map(joined => {
//joined has response like -
//[messagesArrayForGroup0, messagesArrayForGroup1, messagesArrayForGroup2....];
const messagesByGroup = Array<GroupWithMessages>();
groups.forEach((group, index) => {
//assuming that GroupWithMessages has group and messages properties.
const gm = new GroupWithMessages();
gm.group = group;
gm.messages = joined[index];
messagesByGroup.push(gm);
});
return messagesByGroup;
})
)
})
)
}
I usually do that by splitting Observable<any[]> to Observable<any> and then mergeMap the results to inner Observable.
Something like this should work:
getMessagesFor(id: string): Observable<number> {
return of(1);
}
getGroups(): Observable<string[]> {
return of(["1", "2"]);
}
getGroupWithChat() {
this.getGroups().pipe(
mergeMap(groups => from(groups)), // Split the stream into individual group elements instead of an array
mergeMap(group => {
return this.getMessagesFor(group).pipe(
map(messages => {
return Object.assign(group, messages);
})
);
})
);
}
Edit:
Consider BehaviorSubject. It doesn't complete at all:
const behSub: BehaviorSubject<number[]> = new BehaviorSubject([1, 2, 3]);
setTimeout(() => {
behSub.next([4, 5, 6]);
}, 5000);
behSub
.pipe(
mergeMap(arr =>
from(arr).pipe(
tap(), // Do something with individual items, like mergeMap to messages
toArray() // Go back to array
)
)
)
.subscribe(console.log, null, () => {
console.log('Complete');
});

how to create only one subscribe() of this code

I've made this code that do its work, using RxJS:
from(dateQuery.first())
.subscribe((result) => {
query.greaterThanOrEqualTo('createdAt', result.createdAt);
const dateFormat = moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss');
from(query.find())
.map(el => el.map((e) => {
return {
id: e.id,
username: e.get('username')
}
}))
.mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
}))
.subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed'));
},
(error) => console.log(error)
);
My question: is it possible to create only one subscription instead of this two? Because I need to use result of the first subscription also in the other mergeMap before and I don't know how to store it if I try to do only one subscription.
Thank you for help.
You can do something like that
from(dateQuery.first()).pipe(
tap(() => query.greaterThanOrEqualTo('createdAt', result.createdAt)),
map(result => moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss')),
mergeMap(dateFormat => from(query.find()).pipe(
map(el => el.map((e) => ({
id: e.id,
username: e.get('username')
}))),
mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
})),
tap(() => res.send(serialize(next))),
catchError(error => of('Find error'))
))
).subscribe();
Here's my first idea from what I understood.
I have exported the first subscription in an observable and I reuse it to construct the second observable (find$) where I do the second subscription.
I don't know what query.greaterThanOrEqualTo('createdAt', result.createdAt) does, so I put it in a .do() operator as a side effect. If you need to execute this method before executing the remaining code, return an observable or a promise from it and use a mergeMap as well.
const dataQueryFirst$ = dateQuery.first();
const find$ = dataQueryFirst$
.do((result) => query.greaterThanOrEqualTo('createdAt', result.createdAt))
.mergeMap(result => {
const dateFormat = moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss');
return from(query.find())
.map(el => el.map((e) => {
return {
id: e.id,
username: e.get('username')
}}))
.mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
}))
});
find$.subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed')
);
I would implement it with a "switchMap" .
Plus i would extract some logic in functions to make it more readable. And i prefere the new "pipe" way of rxjs since v5.5.
from(dateQuery.first()).pipe(
// TAP because it seems to be a sideeffect. If this could stop your stream, "filter" would be appropriate
tap(result =>query.greaterThanOrEqualTo('createdAt', result.createdAt)),
// MAP because we can discard "result" and only need the Moment-Object in the rest of the stream
map(result => getMoment(result.createdAt),
// switchMap to switch to the second stream, because it is only one command (in a long pipe) you don´t need a explicit return-command
switchMap( createdAt =>
from(query.find()).pipe(
// MAP because we now want a UserObjects-Array in our stream
map(el => createUserObjects(el)),
// MAP because we want our NamesUpdateList. Depending on the usage you may not need "Observable.of()" here
map(userObjects => createNamesUpdateList(userObjects, createdAt ))
)
)
).subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed')
)
getMoment(date){
return moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss')
}
createUserObjects(el){
el.map((e) => {
return {
id: e.id,
username: e.get('username')
}
}
}
createNamesUpdateList(array, dateFormat){
return {
names: array,
lastUpdate: dateFormat
}
}
warm regards

Categories