Reputation: 5497
In my Angular application I have am making an optional HTTP call to check if a user exists. Based on the result of this call, I either want to make another call or stop processing. Without resorting to .toPromise()
, how can I wait for a call that is made in an if
block?
onSomeEvent() {
const contact: IContact = {
...this.userForm.value
};
let cancelProcessing = false;
if (this.isNewUser()) {
// *** This is optional
this.userService.duplicateUserSearch(contact.email, contact.userName)
.subscribe(result => {
if (result.result !== '') {
cancelProcessing = true;
}
});
}
// *** This statement is hit before the observable completes
if (cancelProcessing) {
return;
}
// *** which means that this runs when it shouldn't
this.userService.updateContact(contact)
.subscribe(res => {
this.activeModal.close(res);
});
}
Upvotes: 3
Views: 2279
Reputation: 6070
As you showed with your question, If/Then logic with Observables can be tricky. There is a nice discussion about this here.
You've had a few answers already that could work under certain conditions, and I would have surfed away and left you in the capable hands of Fan or Jeffrey, but I was intrigued by the comment you made to one answer: "What happens if I add more conditions?". That caught my interest, and therefore I wanted to find a pattern that is observable/functional based, clear to read and easily extensible if you decide later to add additional conditions. A good question makes me think hard, and you've certainly done that. :)
I tried applying the pattern that was suggested in the article I linked above to your problem and the code below is what I came up with. As I mentioned in a comment to your original question above, I am not 100% sure you intend for the contact to be updated when the user is NOT a new user, but for now I have assumed that you do. A disclaimer: I have not rigorously tested this such as creating a Stackblitz and creating various test cases, although that is the way I would typically want to answer a question like this, so my apologies for any bugs or unintended consequences in the below code. Alas, the weekend is calling. ;)
In that pattern in the article it is suggested that branches be created for all the possible paths through the if/then logic. I came up with two primary branches that would lead to updateContact() being called. Note that with this pattern it is easy to add a tap()
in any of the branches to do additional work if needed. If I missed a branch you wish to add, that also should be easy to simply add in.
The heart of this pattern is the merge() at the end. This creates a single observable that merges the two that are passed in. If either of them completes then the subscribe will execute and run updateContact()
. In this case they will never both complete because of the filter of isNewUser()
guaranteeing only one will be active, but if you apply this pattern elsewhere you may want to add a take(1)
if you only care about the first asynchronous one that 'wins'.
I also show a subscribe and unsubscribe since I think that is best practice.
onSomeEventSub : Subscription; // component scope variable to add to later unsubscribe with
onSomeEvent() {
const contact: IContact = {
...this.userForm.value
};
// define duplicateUserSearch$ now for easier readability below
const duplicateUserSearch$: Observable<boolean> =
this.userService.duplicateUserSearch(contact.email, contact.userName).pipe(
map(result => (result.result === '')));
// First, create a shareable source for eventual merge of all branches.
// of(0) is completely arbitrary. It would be more interesting to tie
// this to the original event, but that wasn't part of the question. :)
const source$ = of(0).pipe(
share() // same observable will have multiple observers
);
// branch 1 is for when the user is a new user and duplicate logic asserts
const isNewUserAndIsDupe$ = source$.pipe(
filter(() => isNewUser()),
mergeMap(() => duplicateUserSearch$),
filter(res => res)
);
// branch 2 is when the user is NOT a new user
const isNotNewUser$ = source$.pipe(
filter(() => !isNewUser())
);
// and finally the merge that pulls this all together and subscribes
this.onSomeEventSub = merge(isNewUserAndIsDupe$, isNotNewUser$).pipe(
mergeMap(() => this.userService.updateContact(contact))
).subscribe(res => this.activeModal.close(res));
}
ngOnDestroy() {
if (this.onSomeEventSub) this.onSomeEventSub.unsubscribe();
}
Upvotes: 5
Reputation: 7916
The problem lies in that onSomeEvent
is executed synchronously whereas the assignment of cancelProcessing
is executed asynchronously -- later. I think you could do something like the following instead (RxJS 5):
onSomeEvent() {
const contact: IContact = {
...this.userForm.value
};
if (this.isNewUser()) {
this.userService.duplicateUserSearch(contact.email, contact.userName)
.filter((result) => result.result === '')
.switchMap(() => this.userService.updateContact(contact))
.subscribe((res) => this.activeModal.close(res));
} else {
this.userService.updateContact(contact)
.subscribe((res) => this.activeModal.close(res));
}
}
If clause
First the observable stream is filter
-ed by the first condition. This returns an observable that will only forward items from the duplicateUserSearch
observable when result.result
is an empty string.
Next, the passing values are discarded as they are in your example, and replaced by observables returned from updateContact
, which is only called on demand once some value passed the filter used before.
switchMap
flattens the input values, which means that if they are observables, they will be subscribed to and their values will be propagated through the observable returned by switchMap, instead of the observable instances themselves.
Finally we can subscribe to the stream returned by switchMap and observe the values returned by updateContact
directly.
Else clause
If it is not a new user, updateContact
is called unconditionally without first checking with duplicateUserSearch
.
When using RxJS 6.x the pipe
operator is used to specify the sequence of operators in one go:
onSomeEvent() {
const contact: IContact = {
...this.userForm.value
};
if (this.isNewUser()) {
this.userService.duplicateUserSearch(
contact.email,
contact.userName,
).pipe(
filter((result) => result.result === ''),
switchMap(() => this.userService.updateContact(contact)),
).subscribe((res) => this.activeModal.close(res));
} else {
this.userService.updateContact(contact)
.subscribe((res) => this.activeModal.close(res));
}
}
Upvotes: 1
Reputation: 11360
A more functional fashion.
onSomeEvent() {
const contact: IContact = {
...this.userForm.value
};
const updateContact=this.userService.updateContact(contact)
.pipe(tap(res => {
this.activeModal.close(res);
}))
return of(this.isNewUser()).pipe(mergeMap(newUser=>{
if(!newUser)
return updateContact
return this.userService.duplicateUserSearch(contact.email, contact.userName)
.pipe(mergeMap(result=>result.result === ''?updateConcat:empty()))
}))
}
// execute
this.onSomeEvent().subscribe()
Upvotes: 1