Search code examples
javascriptangularionic-frameworkgoogle-cloud-firestoreangularfire2

Angular Firestore - Display contents of sub-document array


This question has a long set-up. In the end, I'm asking for syntax coaching, and you'll see that question if you scroll to end of the set-up.

I have a Firestore database that has a fields that contain arrays. Here's an example:

enter image description here

The document itself has other fields beyond the ingredients element. Here's a JSON representation of the Congo Split recipe you see here:

"congosplit": {
  "ingredients": {
    "a": {
      "ingredientAmounts": "2 Pump, 3 Pumps, 4 Pumps",
      "ingredientName": "Banana Syrup",
      "ingredientSizes": "Small, Medium, Large"
    },
    "b": {
      "ingredientAmounts": "2 Pump, 3 Pumps, 4 Pumps",
      "ingredientName": "White Chocolate",
      "ingredientSizes": "Small, Medium, Large"
    },
    "d": {
      "ingredientAmounts": "10 oz, 12 oz, 16 oz",
      "ingredientName": "Steamed Milk and Foam",
      "ingredientSizes": "Small, Medium, Large"
    }
  },
  "instructions": {
    "a": {
      "text": "Free pour milk into cup.  Top with 1/4 inch foam.  Make a circle with chocolate drizzle and use a thermometer to spread the chocolate into a flower pattern.",
      "type": "text"
    }
  },
  "name": "Congo Split",
  "temperature": "hot"
},

As one might infer from the structure above, both the ingredients and the instructions fields may contain more than one element... designed with a, b, and c in the sample below.

I have followed the sample code snippets for using Angular Firestore:

in my recipes.service.ts file, I have the following method that provides a real-time-updating copy of the data in Firestore:

  getRecipesFirestore() {
    this.recipesCollection = this.afs.collection('mission-cafe');
    this.recipesData = this.recipesCollection.valueChanges();
    return this.recipesData;
  }

in the home.ts file I call that method, returning the recipesData:

  ngOnInit() {

   [...]

    this.recipesData = this.recipesService.getRecipesFirestore();
  }

And the good news is I can easily show the content of most of this data set using an *ngFor in the template file:

  <ion-card *ngFor="let item of recipesData | async">
     <ion-title>{{item.name}}</ion-title>
  </ion-card>

This works for all of the keys at the top level of the document field. But when I try to display the sub-elements of ingredients or instructions I get a [object] instead of the content. It is not totally surprising. Here's an example of what doesn't work.

  <ion-card *ngFor="let item of recipesData | async">
     <ion-title>{{item.name}}</ion-title>

     <div *ngFor="let ingred of item>                     < I was hoping to be able
         <ion-text>{{ingred.ingredientName}}</ion-text    < to iterate through elements
     </div>                                               <  of the ingredient array

  </ion-card>

So the question:
I cannot figure out the syntax to get to the content of the maps inside the recipe field.

I can imagine parsing the JSON (I did that when I was using the Real Time Database), but I was hoping to take advantage of the simple syntax made simple with Firestore as implied by the template file above.

Any syntax coaches out there?


Solution

  • So I think that Priyanka was trying to tell me is accurate, but it took me a LONG time to sort through it. A key idea that one needs to understand is that in Firestore, The odd layers are Collections and the even layers are Documents. Always.

    However, within documents, one can have Fields. Those fields can contain Arrays and those arrays can contain maps. And of course, those maps can contain strings.

    If you're keeping track, you have

    Collection
      Document
        Collection
           Document   (As many as you want of the Coll/Doc layers)
             Field
                Map
                   String: String
                Map
                   String: String
                Array
                   Map
                      String: String,
                      String: String
                   Map
                    (et cetera)
    

    So to use my example:

    In my case each document was a recipe that contained a fields of recipe components like the name and temperature. The instructions and ingredients fields are arrays of maps that contain multi-step information, while other fields are simple things like name: "BlendedChaiCream".

    enter image description here

    So the remaining question is how to make all of this data show up in the app. Here's conceptually what you want to have happen.

    enter image description here

    This was the approach:

    In ngOnInit we get the collection, and subscribe to the recipesInfo

    ngOnInit() {

    this.recipesCollection = this.afs.collection('mission-cafe');
    this.recipesInfo = this.recipesCollection.snapshotChanges();
    
    this.recipesInfo.subscribe((actionArray) => {
      this.recipesElements = actionArray.map((item) => ({
        id: item.payload.doc.id,
        ...item.payload.doc.data(),
        expanded: false
      }));
    });
    

    }

    To explain this for newbies... recipesElements is the array over which the template file (the HTML file) will iterate. So we load that up with the IDs of the recipe documents and all of the rest of the recipe documents. That's what the spread operator ... does. It stands in for all of the rest of that data. Then I add one more element expanded that I use later to decide whether the recipe should be expanded (open) or collapsed in an accordion display of ion-cards.

    Then in the template file, I have this:

    <ion-card
      (click)="expandItem($event, false, item)"  <--- use this to toggle open/closed on accordion display
      *ngFor="let item of recipesElements">
      <ion-card-header>
          <ion-card-title>{{item.name}}
      </ion-card-header>
      <ion-card-content>
        <app-expandable expandHeight="6500px" [expanded]="item.expanded">
    
             [[[[insert formatting here for the display]]]]
             [[[[my formatting includes the individual fields]]]]
    
                  item.notes
                  item.image
                  item.status
                  and so on.
    
             [[[[ for instructions, I had an secondary *ngFor like this ]]]]
    
                  <ion-item *ngFor="let ingred of item.ingredients">
             
             [[[[ and then refer to the individual ingredients as ]]]]
    
                  <ion-grid>
                       <ion-row>
                            <ion-col> {{ingred.ingredientName}} </ion-col>
                            <ion-col> {{ingred.ingredientSizes}} </ion-col>
                            <ion-col> {{ingred.ingredientAmounts}} </ion-col>
                       </ion-row>
                  <ion-grid>
              </ion-item>
         <ion-card>
    

    So it is fairly straight-forward. I just needed to understand the structure of a Firestore collection and then create an array from it over which to iterate.