Marek Kotowski
Marek Kotowski

Reputation: 43

RXJS AngularFire - get Observables from Observable

I have been trying to get data from Firebase (using Angular).

Program summary

It's a small app to store meal recipes, their ingredients to generate cumulated shopping list based on meal planning - daily, weekly, etc.

What I have:

1. Firebase database structure (fragment of it)

    Ingredients (collection)
      Id: string
      Name: string
      Tags: array of string
    Recipes (collection)
      CreatedOn: Timestamp
      MealTitle: string
      MealType: string
      Method: string
      RecipeIngredients (subcollection)
        Id: string
        IngredientId: string
        ExtendedDescription: string
        QTY: number
        UOM: string

As you can see Recipes collection contains RecipeIngredients sub-collection which has IngredientId from Ingredients collection and some additional data.

2. Method for getting list of ingredients from RecipeIngredients sub-collection for specific recipe

  getRecipeIngredients(recipeId: string): Observable<RecipeIngredient[]> {
    this.recipesCollection = this.afs.collection('Recipes', ref => ref.orderBy('CreatedOn', 'desc'));

    this.recipeIngredients = this.recipesCollection.doc(recipeId).collection('RecipeIngredients').snapshotChanges().pipe(
      map(changes => {
        return changes.map(action => {
          const data = action.payload.doc.data() as RecipeIngredient;
          data.Id = action.payload.doc.id;

          return data;
        })
      })
    )

    return this.recipeIngredients;
  }

3. Method for getting specific ingredient based on its Id

getIngredientById(ingredientId: string): Observable<Ingredient> {
    this.ingredientDoc = this.afs.doc<Ingredient>(`Ingredients/${ingredientId}`);

    this.ingredient = this.ingredientDoc.snapshotChanges().pipe(
      map(action => {
        if (action.payload.exists === false) {
          return null;
        } else {
          const data = action.payload.data() as Ingredient;
          data.Id = action.payload.id;
          return data;
        }
      })
    )

    return this.ingredient;
  }

4. What I want to achieve

I want to display a list of ingredients for specific recipe in a table containing columns: Ingredient Name, Extended Description, QTY, UOM. The thing is I cannot figure out how to get Ingredient name. Reasonable solution for me is to do it inside getRecipeIngredients an extend its returning result by that field or by whole Ingredients collection. I tried RxJS operators like mergeFlat or combineLatest but I got stuck.

I could of course change the database structure but that is also a learning project for me. I believe I'll have this kind of challenge sooner or later.

Upvotes: 4

Views: 440

Answers (1)

Frederick
Frederick

Reputation: 872

This was an interesting one, but I think I have it worked out. Please check out my StackBlitz. This decision tree is usually good enough to provide you a function for most Observable scenarios, but I believe this one required a combination of functions.

Relevant code:

const recipeId = 'recipe1';
this.recipe$ = this.getRecipeIngredients(recipeId).pipe(concatMap(recipe => {
  console.log('recipe: ', recipe);
  const ingredients$ = recipe.recipeIngredients.map(recipeIngredient => this.getIngredientById(recipeIngredient.id));
  return forkJoin(ingredients$)
    .pipe(map((fullIngredients: any[]) => {
      console.log('fullIngredients: ', fullIngredients);
      fullIngredients.forEach(fullIngredient => {
        recipe.recipeIngredients.forEach(recipeIngredient => {
          if (recipeIngredient.id === fullIngredient.id) {
            recipeIngredient.name = fullIngredient.name;
          }
        });
      });
      return recipe;
  }));
})).pipe(tap(finalResult => console.log('finalResult: ', finalResult)));

I included the console logs to help illustrate the state of the data as it's piped around. The main idea here was to use concatMap to tell the recipe Observable that I need to map it with another observable once this one is complete, then to use a forkJoin to get the full representation of each recipe ingredient, and finally mapping those results back to the original recipe.

Upvotes: 1

Related Questions