I have a Angular service returning an array of "cleaningDuty" objects. Inside a duty object, there is a nested object called "currentCleaner" with an id.
[
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
},
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
},
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
}
]
with the help of the currentCleaner.id
I want to fetch the user data of the currentCleaner from a UserService dynamically in the same pipe()
method and add the returned user data to the cleaningDuty object. Then the object should look like this:
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
},
cleanerData: {
name: string;
photoUrl: string;
// other user data
}
},
Unfortunately I just cant get this to work even after investing days into it. I tried almost every combination from forkJoin()
, mergeMap()
and so on. I know a nested subscribe()
method inside the target component would get the job done, but I want to write the best code possible quality-wise. This is my current state of the service method (it adds the user observable instead of the value to the cleaningDuty object):
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) => {
let currentCleaners = duties.map((duty) =>
this.userService.getPublicUserById(duty.currentCleaner.id),
);
return forkJoin([currentCleaners]).pipe(
map((users) => {
console.log(users);
duties.forEach((duty, i) => {
console.log(duty);
duty.cleanerInfos = users[i];
});
return duties;
}),
);
}),
);
}
The getPublicUserById()
method:
getPublicUserById(id: string) {
return this.firestore.collection('publicUsers').doc(id).valueChanges();
}
Any help would be greatly appreciated!
CodePudding user response:
The forkJoin function will:
Wait for Observables to complete and then combine last values they emitted; complete immediately if an empty array is passed.
So, you have to make sure that all the inner Observables have been completed, then the forkJoin
will emit the combined values, and you can achieve that using take(1) operator, like the following:
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) =>
// The `forkJoin` will emit its value only after all inner observables have been completed.
forkJoin(
duties.map((duty) =>
// For each duty, fetch the cleanerData, and append the result to the duty itself:
this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
take(1), // to complete the sub observable after getting the value from it.
map((cleanerData) => ({ ...duty, cleanerData }))
)
)
)
)
);
}
CodePudding user response:
- Check if you need
switchMap
ormergeMap
. IMO I'd cancel the existing observable when thevalueChanges()
emits. See here for the difference b/n them. - Use
Array#map
with RxJSforkJoin
function to trigger the requests in parallel. - Use RxJS
map
operator with destructuring syntax to add additional properties to existing objects. - Ideally defined types should be used instead of
any
type.
Try the following
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
switchMap((duties: any) =>
forkJoin(
duties.map((duty: any) => // <-- `Array#map` method
this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
map((data: any) => ({ // <-- RxJS `map` operator
...duty, // <-- the `currentCleaner` property
cleanerData: data // <-- new `cleanerData` property
}))
)
)
)
)
);
}
CodePudding user response:
Firstly I would suggest you to improve the typings on your application, that will help you to see the issue more clear. For my answer I will assume you have 3 interfaces defined CurrentCleaner, CleanerData and RoomData wich is just:
interface RoomData {
currentCleaner: CurrentCleaner
cleanerData: CleanerData
}
Then you can write a helper function to retrieve the room data from a current cleaner:
getRoomData$(currentCleaner: CurrentCleaner): Observable<RoomData> {
return this.userService.getPublicUserById(currentCleaner.id).pipe(
map(cleanerData => ({ currentCleaner, cleanerData }))
)
}
As you can see, this functions take a current cleaner, gets the cleaner data for it and maps it into a RoomData object.
Having this next step is to define your main function
getAllForRoommates(flatId: string, userId: string): Observable<RoomData[]> {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) => {
// here we map all duties to observables of roomData
let roomsData: Observable<RoomData>[] = duties.map(duty => this.getRoomData$(duty.currentCleaner));
// and use the forkJoin to convert Observable<RoomData>[] to Observable<RoomData[]>
return forkJoin(roomsData)
}),
);
}
I think this way is easy to understand what the code is doing.