Home > Enterprise >  Iterate through Observable object-array and add additional data from another Observable to every obj
Iterate through Observable object-array and add additional data from another Observable to every obj

Time:03-05

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:

  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
              }))
            )
          )
        )
      )
    );
}

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.

  • Related