humblots
humblots

Reputation: 93

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?

Upvotes: 2

Views: 2737

Answers (3)

Saptarsi
Saptarsi

Reputation: 888

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

Upvotes: 2

humblots
humblots

Reputation: 93

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

Upvotes: 0

Julien
Julien

Reputation: 2756

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().

Upvotes: 0

Related Questions