Displaying dynamic API results in a span

I’m getting a list (I’m calling it a collection) from an API, so there’s no telling how many items are in that list. Each list item has sub-items. I’m trying to display the quantity of sub-items next to the quantity of list items.

Something like “3 of 9”

I have the quantity of list items, and am able to get the quantity of sub-items, I just can’t figure out how to display them in the UI.

I was thinking I could create an array of items and use the collection ID as the key, but that doesn’t seem to be working.

collections.page.html

<ion-card *ngFor="let collection of collections">
    <ion-card-header>
        <ion-row>
            <ion-col>
                {{ collection.name }}
            </ion-col>
            <ion-col>
                <!-- DISPLAY THE VALUE FROM GETDATA() HERE -->
               <span name="fieldName" ngDefaultControl [(ngModel)]="collection[collection.id]"></span> 
                of {{ collection.items.length || '0' }}
            </ion-col>
        </ion-row>
    </ion-card-header>
</ion-card>

collections.page.ts

collections: any;
quantity = [];

constructor(public http: HttpClient,private storage: Storage) {
    this.getData();
}

getData() {
    return this.storage.get('token').then(token => {
        if (token) {
            this.http.get('http://api.example.test/collections?api_token=' + token)
                .subscribe((result: any) => {
                    this.collections = result.data.collections;
                    for (const collection of result.data.collections) {
                        const quantity = [];
                        quantity[collection.id] = 0;
                        for (const item of collection.items) {
                            if (item.myitems.length) {
                                for (const myitem of item.myitems) {
                                    quantity[collection.id]++;
                                }
                            }
                        }
                        if (quantity[collection.id] > 0) {

                            // SHOW THIS VALUE IN THE SPAN
                            collection[collection.id] = quantity[collection.id];

                        }
                    }
                }, error => {
                    console.warn(error);
                });
        }
    });
}

Can you explain why we are trying to treat a span as a form control, instead of just saying something like <span [innerText]="whatever"></span>?

As for your question, whenever you are looping with ngFor and want something to be tied to the iteration of the loop, it’s virtually always cleanest to put the thing you want as a property on the loop variable, so that you can write collection.count or something to that effect. In this particular case, though, I would back up and put the entire string that is going to end up in the template (“4 of 9” or whatever) in that slot - that should be the final value of collection.displayCount that goes in the template. The reason for that is that it will make localization to other languages doable; whereas you have locked yourself into English as things stand currently.

1 Like

I was treating it as a form control because I didn’t know about innerText until now, and if I didn’t use the form control it would throw an error that I had to use it as a form control. I’ve removed it and replaced it with innerText and everything is working. Thank you!

I’m trying to do the same thing elsewhere but this time it’s not working. Can you spot what I’m doing wrong?

<ion-note slot="end" *ngFor="let myitem of item.myitems">
    <span [innerText]="item[item.id]"></span>
</ion-note>
.subscribe(
    (response: any) => {
        const item = [];
        item[itemId] = response.data.item_quantity;
        console.log(item[itemId]); // <- shows the correct value
    },
    error => {
        // toast notification
        this.presentToast('Oops!');
    }
);

Failing to give your variables more descriptive names, which means that they tend to all blur together in a confusing soup of item and myitem (those two are mixed up in the template).

You’re right. I’ve taken courses on Ionic and Angular, but none of this was taught, so I’m kind of throwing this stuff up to see what sticks. The span is actually supposed to show myitem.quantity, so I’m actually not sure why it was showing the correct value with item[item.id]. Basically, what I need to do is update the span with the item’s quantity after an update to the API. In the code I showed, response.data.item_quantity is the new quantity returned from the API. so I just need to get that value into the span.

It might sound silly, but I would spend some time away from the Angular bit defining the shape of the domain objects you’re dealing with. No more data, no more item, no more any.

export interface Treat {
  id: string;
  flavor: "sweet" | "savory";
  calories?: number;
  label: string;
}

export interface TreatBag {
  id: string;
  treats: Treat[]
}

Now get all the http junk out of the page and into a service that provides domain objects. If you have to massage what comes over the wire from the API, do it here. If you need other types of slices aside from “all available bags”, such as getBagById(), define it here.

Centralize all the business with API tokens and backend servers into an interceptor. An example that you should be able to adapt:

class AuthService {
  peek(): Promise<string> {...}
}

class ServerService {
  server: Promise<string>;
}

class AuthHttpInterceptor implements HttpInterceptor {
  constructor(private auther: AuthService, private serverer: ServerService) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return from(Promise.all([this.serverer.server, this.auther.peek()])).pipe(
      mergeMap(([server, jwt]) => {
        let tochange: { url?: string, setHeaders?: { Authorization: string } } = {};
        if (server) {
          tochange.url = server + req.url;
        }
        if (jwt) {
          tochange.setHeaders = {Authorization: "Bearer " + jwt};
        }
        let areq = req.clone(tochange);
        return next.handle(areq);
      }));
  }

This uses JWT, but you can use HttpParams to throw things in the query string as you are doing now. Look at how amazingly streamlined this can make the service: everything that is currently being done in getData() collapses into a single simple line:

export class TreatService {
  allTreatBags(): Observable<TreatBag[]> {
    return this.http.get("collections");
  }
}

Now that we can see the forest for the trees, we go back to the page and hopefully the path ahead is crystal clear:

export class TreatPage {
  bags: TreatBag[] = [];
  constructor(private treater: TreatService) {
    this.treater.allTreatBags().subscribe(bags => this.bags = bags);
  }
}
<ion-note *ngFor="let bag of bags">{{bag.treats.length}}</ion-note>

Next up we may have “what do I do when the page’s copy of the data gets stale?”, for which I would recommend this post.

1 Like

Thank you very much, I read this the other day and took a good look at what I was doing. Decided to go back to square one and follow your suggestions; app is now working like a dream, :slight_smile:

1 Like