Angular 11 how to make one of the http request in higher order mapping conditionally - rxjs

I want to use a better solution if exists so it will make the following simpler.
If this.projectguid is empty then want to use the switchMap call otherwise the other one.
Can anyone suggest me a better solution for this?
getWorksheet(): Observable<Worksheet | number> {
if(this.projectsGuid === '') {
return this.apiService.get('projects/latest')
.pipe(
switchMap((res: {id: string, name: string}) => {
this.projectsGuid = res.id
let getUrl = `projects/${this.projectsGuid}/questionnaires/${this.questionnaireId}/worksheets/latest`;
return this.apiService.get<Worksheet>(getUrl).pipe(
catchError((err) => {
return of(err.status);
})
);
})
)
} else {
let getUrl = `projects/${this.projectsGuid}/questionnaires/${this.questionnaireId}/worksheets/latest`;
return this.apiService.get<Worksheet>(getUrl).pipe(
catchError((err) => {
return of(err.status);
})
);
}
}

You could define a projectGuid$ observable that emits the this.projectsGuid value if not empty, and otherwise emits the result of the http call (mapped to just the id):
const projectGuid$ = this.projectsGuid !== ''
? of(this.projectsGuid)
: this.apiService.get('projects/latest').pipe(
map(({id}) => id),
tap(guid => this.projectsGuid = guid)
);
Then you can pipe the projectGuid to the call to fetch the worksheet:
return projectGuid$.pipe(
map(guid => `projects/${guid}/questionnaires/${this.questionnaireId}/worksheets/latest`),
switchMap(url => this.apiService.get<Worksheet>(url).pipe(
catchError(err => of(err.status))
))
);

Related

Angular 11 switchmap not working after catch error

I have two dropdowns in my angular app. Second one is populated based on first dropdown value. I am using switchmap. It works fine as long as there is no error. As soon there is no values to populate second dropdown and there is an error, subsequent call is not happening when i change values in first dropdown. Am i doing anything wrong here?
Here is my code:
private customListItems$ = this.auditFilterService.subjectType$ // this is first option value
.pipe(
takeUntil(this.destroy$),
filter(x => x && x !== ''),
switchMap((selectedSubjectType) => {
const result = this.customListsService.getCustomListItemsByTypeName({
typeName: selectedSubjectType,
onlyActive: true
} as CustomListItemsByLocationParams);
return result;
}),
catchError(err => {
console.log('error', err);
return of(undefined);
})
);
Following fix suggested in the comment by munleashed solves my issue:
private customListItems$ = this.auditFilterService.subjectType$.pipe(
takeUntil(this.destroy$),
filter((x) => x && x !== ''),
switchMap((selectedSubjectType) => {
const result = this.customListsService
.getCustomListItemsByTypeName({
typeName: selectedSubjectType,
onlyActive: true,
} as CustomListItemsByLocationParams)
.pipe(
catchError((err) => {
console.log('error', err);
return of(null);
})
);
return result;
})
);

Combining observable outputs in to one array

I need to make two calls to Firebase (as it doesn't support OR queries) and merge the output into one array at the end to return to the calling service.
I have something that gets pretty close but it outputs a 2D array of arrays (one for each call to Firebase). I've tried a few things and this is the best I can get to. Any help on tidying up the below would be great.
getAllFriends(): Observable<[Friendship[], Friendship[]]> {
const invitesSent = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('inviter', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
const invitesReceived = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('invitee', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
return combineLatest([invitesSent, invitesReceived]);
}
Friendship is just an object with property: value pairs, nothing special.
I have tried then putting a .pipe() after this returned observable but that just stops the subscription firing in the calling service.
What about returning, at the end, something like this
return combineLatest([invitesSent, invitesReceived]).pipe(
map(([frienships_1, friendships_2]) => ([...friedships_1, ...friendships_2]))
)

RxJS: Use shareReplay to POST HTTP request once, then use ID returned to queue updates

Essentially I need:
const saveObservable = new Subject().asObservable();
const create$ = of("ID").pipe(tap(() => console.log("executed")), shareReplay());
const subscription = saveObservable.pipe(
concatMap(({ files = [], ...attributes }) =>
create$.pipe(
tap(id => console.log("queue updates to", id))
)
)
).subscribe();
saveObservable.next({})
This will make it so my initial save operation: of("ID") only executes once. Then, all further executions of this save will use the ID returned and queue up.
What I'm struggling with is that I can't put create$ inside my concatMap because it creates a new instance of the observable and shareReplay is effectively useless.
But I basically need it within the concatMap so that I can use attributes.
How can I do that?
saveObservable.pipe(
concatMap(({ files = [], ...attributes }) => {
const create$ = fromFetch("https://www.google.com", { attributes }).pipe(tap(() => console.log("executed")), shareReplay());
return create$.pipe(
tap(a => console.log(a))
)
})
);
vs.
const create$ = fromFetch("https://www.google.com", { attributes?? }).pipe(tap(() => console.log("executed")), shareReplay());
saveObservable.pipe(
concatMap(({ files = [], ...attributes }) => {
return create$.pipe(
tap(a => console.log(a))
)
})
)
Not sure if this is the best approach, but here's what comes to mind.
You can use a ReplaySubject as a subscriber. When used this way, it will cache the emitted values that come from the source.
So you could have something like this:
const replSubj = new ReplaySubject(/* ... */);
saveObservable.pipe(
concatMap(({ files = [], ...attributes }) => {
// We first subscribe to `replSubj` so we can get the stored values
// If none of the stored ones match the condition imposed in `first`,
// simply emit `of({ key: null })`, which means that a request will be made
return merge(replSubj, of({ key: null }))
.pipe(
// Check if we've had a request with such attributes before
// If yes: just return the stored value received from the subject
// If not: make a request and store the value
// By using `first` we also make sure the subject won't have redundant subscribers
first(v => v.key === attributes.identifier || v.key === null),
switchMap(
v => v.key === null
? fromFetch("https://www.google.com", { attributes }).pipe(tap(() => console.log("executed")))
.pipe(
map(response => ({ key: attributes.identifer, response })), // Add the key so we can distinguish it later
tap(({ response }) => replSubj.next(response)) // Store the value in the subject
)
: of(v.response) // Emit & complete immediately
),
)
}),
);
Note that ReplaySubject can have a second parameter, windowTime, which specifies how long the values should be cached.
I solved it with:
let create$: Observable<EnvelopeSummary>;
const [, , done] = useObservable(
updateObservable$.pipe(
concatMap(attributes => {
if (create$) {
return create$.pipe(
concatMap(({ envelopeId }) =>
updateEnvelope({ ...userInfo, envelopeId, attributes }).pipe(
mapTo(attributes.status)
)
)
)
} else {
create$ = createEnvelope({ ...userInfo, attributes }).pipe(
shareReplay()
);
return create$.pipe(mapTo(attributes.status))
}
}),
takeWhile(status => status !== "sent")
)
);

Store dispatch recalls the http get of the effect several times

During dispatch, my effect is called repeatedly until my backend responds and the data is loaded. I need help in understanding how to load the data with just one GET REQUEST and then load from the store if the data is actually already present.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
filter((cases) => {
return cases.length > 0;
}),
tap(() => {
this.store.dispatch(new ImportTaskLoadCasesLoadedFromStoreAction());
}),
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
map((action: ImportTaskLoadCasesAction) => action),
switchMap((payload) => {
return this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({ total: response['count'], cases: response['results'] });
}),
catchError((error) => {
this.logger.error(error);
return of(new ImportTaskLoadCasesLoadErrorAction(error));
})
);
})
);
Yes i have a reducer for handeling my Success Action like this :
case ImportCasesActionTypes.ImportTaskLoadCasesSuccessAction:
return {
...state,
loading: false,
cases: action.payload.cases,
total: action.payload.total
};
It's called in my effects.
Does the below work? This is assuming you have a reducer that handles the ImportTaskLoadCasesSuccessAction; Maybe supplying a working example will help, as there is a bit of guessing as how state is being managed.
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
tap(
(cases) => {
if (cases.length <= 0) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
// personally, I would have the component/obj that is consuming this.cases$ null check the cases$, removed for brevity
shareReplay()
);
export const selectCasesData = createSelector(
selectImportTaskCasesState,
state => state ? state.cases : []
);
export const selectImportTaskCasesData = createSelector(
selectCasesData,
cases => {
return cases.slice(0);
}
);
#Effect()
ImportCasesLoad$: Observable<any> = this.actions$
.pipe(
ofType<ImportTaskLoadCasesAction>(ImportCasesActionTypes.ImportTaskLoadCasesAction),
mergeMap(() => this.importCases.get()
.pipe(
map(response => {
return new ImportTaskLoadCasesSuccessAction({
total: response['count'],
cases: response['results']
});
}),
// catch error code removed for brevity
);
)
);
If you only want the call this.importCases.get() to fire one time, I suggest moving the action dispatch out of the .pipe(tap(...)). As this will fire every time a subscription happens.
Instead, set up this.cases$ to always return the result of select(selectImportTaskCasesData),. Functionally, you probably want it to always return an array. But that is up to your designed desire.
Foe example ...
this.cases$ = this.store
.pipe(
takeWhileAlive(this),
select(selectImportTaskCasesData),
);
Separately, like in a constructor, you can dispatch the this.store.dispatch(new ImportTaskLoadCasesAction());. If you want it to only get called when cases$ is empty, you can always wrap it in a method.
e.g.
export class exampleService() {
ensureCases(): void {
this.store.pipe(
select(selectImportTaskCasesData),
take(1)
).subscribe(_cases => {
if (_cases && _cases.length < 1 ) {
this.store.dispatch(new ImportTaskLoadCasesAction());
}
}),
}
}

RxJS Filter Array of Arrays

I'm trying to perform a filter on a array of arrays in rxjs. Consider the following:
function guard1(): boolean | Observable<boolean> {}
function guard2(): boolean | Observable<boolean> {}
function guard3(): boolean | Observable<boolean> {}
const routes = [
{ name: 'Foo', canActivate: [guard1, guard2] },
{ name: 'Bar', canActivate: [guard3] },
{ name: 'Moo' }
];
I want to filter the routes array to only routes that return true
from the combination of results of inner array canActivate or if it doesn't have canActivate, I want it to be NOT filtered out.
Lets say guard1 returned true and guard2 returned false, I'd expect route Foo to not be in the filtered list.
I took a stab at this but its not quite doing what I expect:
this.filteredRoutes = forkJoin(of(routes).pipe(
flatMap((route) => route),
filter((route) => route.canActivate !== undefined),
mergeMap((route) =>
of(route).pipe(
mergeMap((r) => r.canActivate),
mergeMap((r) => r()),
map((result) => {
console.log('here', result, route);
return route;
})
)
)));
If I were writing this outside of RXJS, the code might look something like this:
this.filteredRoutes = [];
for (const route of this.routes) {
if (route.canActivate) {
let can = true;
for (const act of route.canActivate) {
let res = inst.canActivate();
if (res.subscribe) {
res = await res.toPromise();
}
can = res;
if (!can) {
break;
}
}
if (can) {
this.filteredRoutes.push(route);
}
} else {
this.filteredRoutes.push(route);
}
}
Thanks!
I'm sure there's other (and likely better ways to handle this, but it works...
from(routes).pipe(
concatMap((route) => {
// handle if nothing is in canActivate
if (!route.canActivate || route.canActivate.length === 0) {
// create an object that has the route and result for filtering
return of({route, result: true})
};
const results = from(route.canActivate).pipe(
// execute the guard
switchMap(guard => {
const result: boolean | Observable<boolean> = guard();
if (result instanceof Observable) {
return result;
} else {
return of(result);
}
}),
// aggregate the guard results for the route
toArray(),
// ensure all results are true
map(results => results.every(r => r)),
// create an object that has the route and result for filtering
map(result => ({route, result})),
);
return results;
}),
// filter out the invalid guards
filter(routeCanActivateResult => routeCanActivateResult.result),
// return just the route
map(routeCanActivateResult => routeCanActivateResult.route),
// turn it back into an array
toArray()
)
// verify it works
.subscribe(routes => routes.forEach(r => console.log(r.name)));
Also, here is a working example in stackblitz.

Resources