0

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!

Japhet
  • 161
  • 1
  • 2
  • 12

3 Answers3

1

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 }))
            )
          )
        )
      )
    );
}
Amer
  • 6,162
  • 2
  • 8
  • 34
  • If I do a simple subscribe + console.log of the result in my desired component, it still doesn't emit anything. I attached the getPublicUserById() method in the original post if that is the problem – Japhet Mar 04 '22 at 15:48
  • 1
    I updated my answer. The problem is that `getPublicUserById` observable is not completed after emitting the value, so the `forkJoin` won't emit any value. – Amer Mar 04 '22 at 16:04
  • Thats what I was missing all day long, thank you! – Japhet Mar 04 '22 at 16:06
  • Do you have an alternate solution for when you don't have ` .valueChanges()` method available in the first observable. When I do this, it only returns one object. Does not iterate. – priya vyas Dec 15 '22 at 01:14
0
  1. Check if you need switchMap or mergeMap. IMO I'd cancel the existing observable when the valueChanges() emits. See here for the difference b/n them.
  2. Use Array#map with RxJS forkJoin function to trigger the requests in parallel.
  3. Use RxJS map operator with destructuring syntax to add additional properties to existing objects.
  4. 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
              }))
            )
          )
        )
      )
    );
}
ruth
  • 29,535
  • 4
  • 30
  • 57
  • I tried your solution but it still doesn't emit any data even when im doing a basic .subscribe(res => console.log(res)) in my desired component. – Japhet Mar 04 '22 at 15:34
  • If it helps, I attached the getPublicUserById() method in the original question – Japhet Mar 04 '22 at 15:47
0

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.