Reputation: 1664
I'm trying to create an in-memory singleton that holds the current vendor a person is browsing on.
A guard is used on all specific routes to catch the parameter:
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean | UrlTree> {
let currentUrl = this._router.url;
const param = route.params['vendname'];
return this._vendorService.getByName(param).pipe(map(a => {
if (a == null) {
this._snackBarService.open('Vendor not found', 'x', { duration: 5000 });
return this._router.parseUrl(currentUrl);
}
return true;
}));
}
A service is used to get the vendor by name. If it exists in-memory return it. If it doesn't, get it from the server first.
set vendor(value: IUser) {
this._vendor.next(value);
}
get vendor$(): Observable<IUser> {
return this._vendor.asObservable();
}
getByName(name: string): Observable<IUser> {
const result = this.vendor$.pipe(map(v => {
if (v != null && v.displayName == name) {
return v;
}
else {
return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
this.vendor = v;
return of(v)
// ...
}));
}
}))
return result;
}
The problem is I need to check vendor$
for its value which returns an Obervable<IUser>
but the switchMap also returns an Obervable<IUser>
, causing the result to be Observable<Observable<IUser>>
. How can I make the result
return a single User Observable?
Upvotes: 0
Views: 2432
Reputation: 1664
I'm adding another solution that worked.
Seeing as though the canActivate
returns a Promise
as well, you can also use the trusty old async
await
method.
Note: the toPromise
is now lastValueFrom
in rxjs 7.
async getByVendorName(name: string): Promise<IUser> {
const v:IUser = await new Promise(resolve => {
this.vendor$.subscribe(a => {
if (a != null && a.displayName.toLocaleLowerCase() == name) {
resolve(a);
}
});
resolve(null);
});
if (v == null) {
return this.Get<IUser>(`api/vendor/${name}`).pipe(switchMap(v => {
this.vendor = v;
return of(v);
})).toPromise();
}
return Promise.resolve(v);
}
Upvotes: 0
Reputation: 1852
I believe you are misunderstanding the use case of switchMap()
becuase it's not the cause of your Observable<Observable<IUser>>
. Your first map()
operator inside getByName()
is either returning a value of type IUser
(when true) or an observable of IUser
(when false). So getByName()
returns either Observable<IUser>
or Observable<Observable<IUser>>
.
However, if you want to try to utilize replaying in-memory values from an observable, then I would recommend shareReplay()
. Also, here's a recommended pattern for such a case.
private vendorName = new Subject<string>();
public vendor$ = this.vendorName.pipe(
distinctUntilChanged(),
switchMap(name=>this.Get<IUser>(`api/vendor/${name}`)),
shareReplay(1)
)
public getByName(name:string){
this.vendorName.next(name);
}
Then in the guard file.
canActivate(
// ...
){
let currentUrl = this._router.url;
const param = route.params['vendname'];
this._vendorService.getByName(param);
return this._vendorService.vendor$.pipe(
map(vendor=>{
if(!vendor){
this._snackBarService.open('Vendor not found', 'x', { duration: 5000 });
return this._router.parseUrl(currentUrl);
}
return true;
})
);
With this setup, your component(s) & guard can subscribe to vendor$
to get the IUser
data it needs. When you have the name to get the vendor, you can invoke getByName()
. This will trigger the following steps:
userName
subject will emit the new name.vendor$
observable (subscribed to userName
) will take that name, then switchMap to get the value from the inner observable (the Get
method)shareReplay(1)
ensures any subscription to vendor$
(current or future) will get the last (1) emitted value from memory.If you need to update the vendor name, just invoke getByName()
with your new name and everything subscribed to vendor$
will automatically get the updated value.
Upvotes: 1
Reputation: 21367
related to what I see vendor$ is a stream of IUser, here is a refactoring of your code using iif rxjs operator to subscribe to the first or the second observable based on a your condition
this.vendor$.pipe(
mergeMap(v => iif(
() => v && v.displayName == name, of(v), this.Get<IUser>(`url`).pipe(tap(v => this.vendor = v))
)
)
)
Upvotes: 0