'How can I return an Observable with data of other Observables using his return value in the others

I have a project in which I want to return an Observable of Hero object. My heroes have multiple id properties to fetch data from other Observables as a Skill or a Rarity. My Observables come from the AngularFire library. Here is my Hero class what I have right now:

export class Hero extends Serializable {
  id?: string;
  name?: string;
  description?: string;
  idRarity?: string;
  rarity?: Rarity;
  stats: Stats;
  idSkill1?: string;
  idSkill2?: string;
  skill1?: Skill;
  skill2?: Skill;
  visual?: string;
  dateAdded?: Date;

  constructor() {
    super();
    this.stats = {};
    this.visual = "assets/images/placeholder.jpg";
  }
  ...

}

  // functions from the HeroService
  getHeroes(): Observable<Hero[]> {
    return this.db.collection<JsonArray>(HeroService.url)
      .valueChanges()
      .pipe(
        map(documents => {
          return documents.map(data=> {
            return this.getHeroFromData(data);
          });
        })
      );
  }

  getHero(id: string): Observable<Hero | undefined> {
    // Returns hero|undefined observable
    return this.getHeroDocument(id).valueChanges()
      .pipe(
        map(data => {
          return data ? this.getHeroFromData(data) : undefined
        })
      );
  }

  getHeroFromData(data:JsonArray) {
    let hero = new Hero().fromJSON(data);
    if (hero.idSkill1 && hero.idSkill2 && hero.idRarity) {
      this.skillService.getSkill(hero.idSkill1).subscribe(skill => hero.skill1 = skill);
      this.skillService.getSkill(hero.idSkill2).subscribe(skill => hero.skill2 = skill);
      this.rarityService.getRarity(hero.idRarity).subscribe(rarity => hero.rarity = rarity);
    }
    return hero;
  }

The issue I'm facing is that when my hero is returned, the data for my properties of rarity and skills are not set yet.

Is there a way to wait for all values to be received from the other Observables before returning the hero object?



Solution 1:[1]

RxJs 6.5+

forkJoin will full fill your requirement here.

know about mergeMap also

  // functions from the HeroService
  getHero(id: string): Observable<Hero | undefined> {
    // Returns hero|undefined observable
    return this.getHeroDocument(id)
      .valueChanges()
      .pipe(
        mergeMap((data) => {
          return data ? this.getHeroFromData(data) : of(undefined);
        })
      );
  }

  getHeroFromData(data: JsonArray): Observable<Hero> {
    let hero = new Hero().fromJSON(data);
    if (hero.idSkill1 && hero.idSkill2 && hero.idRarity) {
      forkJoin({
        skill1: this.skillService.getSkill(hero.idSkill1),
        skill2: this.skillService.getSkil1(hero.idSkill2),
        rarity: this.rarityService.getRarity(hero.idRarity),
      }).pipe(
        map((res) => {
          hero.skill1 = res.skill1;
          hero.skill2 = res.skill2;
          hero.rarity = res.rarity;

          return hero;
        })
      );
    }
  }

Solution 2:[2]

If your hero and skills are loaded once, you should consider using promises instead of observables. If you cannot change it to the core, you can convert an observable to a promise with .toPromise(). Then you can use Promise.all().

getHeroFromData(data:JsonArray): Promise<Hero> {
    let hero = new Hero().fromJSON(data);
    if (hero.idSkill1 && hero.idSkill2 && hero.idRarity) {
      const promise 1 = this.skillService.getSkill(hero.idSkill1).toPromise().then(skill => hero.skill1 = skill);
      const promise2 = this.skillService.getSkill(hero.idSkill2).subscribe(skill => hero.skill2 = skill);
      const promise3 = this.rarityService.getRarity(hero.idRarity).subscribe(rarity => hero.rarity = rarity);
      return Promise.all([promise1, promise2, promise3]).then(() => hero);
    } else {
      return Promise.resolve(hero);
    }        
  }

Then you must use then when calling this method. Because hero is build in a async way, you must treat the return in a async way. Use then().

Solution 3:[3]

After a few attempts, I manage to achieve it using zip operator

Here is what I have in the end.

  getHeroes(): Observable<Hero[]> {
    return this.db.collection<JsonArray>(HeroService.url)
      .valueChanges()
      .pipe(
        mergeMap((docs) => {
          return zip(docs.map((doc) => { return this.getHeroFromData(doc) }))
        })
      );
  }


  getHero(id: string): Observable<Hero | undefined> {
    return this.getHeroDocument(id).valueChanges()
      .pipe(
        mergeMap(data => { return data ? this.getHeroFromData(data) : of(undefined) })
      );
  }

  private getHeroFromData(data: JsonArray): Observable<Hero> {
    const hero = new Hero().fromJSON(data);
    if (hero.idSkill1 && hero.idSkill2 && hero.idRarity) {
      return zip(
        this.skillService.getSkill(hero.idSkill1),
        this.skillService.getSkill(hero.idSkill2),
        this.rarityService.getRarity(hero.idRarity)
      ).pipe(
        map(res => {
          console.log(res);
          hero.skill1 = res[0];
          hero.skill2 = res[1];
          hero.rarity = res[2]
          return hero;
        })
      )
    }
    return of(hero);
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Julien
Solution 3 humblots