Ionic and auto content refresh

Hi,
i’m developing an Ionic 6 APP with e-commerce functionality.

For summary i have 2 page: ListPage and WishListPage

ListPage
I do an http client request to get list of items (on ngOnInit.) and show data this way:
<ion-item inset="true" *ngFor="let item of results | async" (click)="itemTapped($event, item)" lines="none" shape="round" class="ion-margin-bottom ion-margin-start ion-margin-end">

WishlistPage
Show a list of items readed from a WishlistService service where i have:

  private items = []; /*this is where i push items tapped/added clicking on items list*/
  private itemsCount = new BehaviorSubject(0);
  private itemsTotal = new BehaviorSubject(0);

To show wishlist items i use this code where wishlistItems is WishlistService.items:
<ion-item inset="true" *ngFor="let item of wishlistItems" (click)="itemTapped($event, item)" lines="none" shape="round" class="ion-margin-bottom ion-margin-start ion-margin-end">

For every item i show a badge with quantity and i can update quantity or delete item
When add, subtract or remove quantity from items, i don’t know how, template show automatically items not removed or updated and this is OK.

My problems are:

  1. In ListPage how can i merge information from http response and wishlist items to show badge quantity, without making http request again?

  2. In ListPage tapping item i show a modal to set quantity and add item to wishlist and this work but: How i can show/update badge quantity on that item?

  3. In WishListPage i can manipulate wishlist items (remove or update quantity), but when go back to previous ListPage how can update items badge with updated quantity?

At this moment to add to ListPage items quantity from wishlist items i do this:

  __mixItemsWithWishlist(){

        //TODO: trasofrmazione in funzione e..
        this.results = this.response.pipe(
          map(res => {
             const els = res; 
             els.forEach(item => {
               let x = 0;
               let arr = this.wishlistItems./*value.*/filter(x => {
                 return x.id == item.id;
               });
               if (arr.length > 0)
                 x = arr[0].quantity;
               item.wishlist_quantity = x;
             });
             return els;
           })
         );
         this.spinner = false;
     
  }

but calling this function on every itemtapped do an HTTP request and do a refresh of entire view (visually bad)

Thanks for all

While you wait for better answers, here is a blast of opinions.

This breaks a couple of my private rules:

  • all HTTP requests must come from services, never pages or components
  • do not use lifecycle events to manage data freshness

You had WishlistService.items as private, which made me inordinately happy, because it would have been impossible for you to access it from WishlistPage and shoot yourself in the foot thusly. Alas, either it wasn’t really private or you made a bypass.

Another rule:

  • never expose raw data from services - only do so as Observables

The only exception to this rule is if you are 100% certain that the data will never change during the lifetime of the app - for example, if you have whiteboxing constants that are burned into the app at build time, those could safely be exposed as ordinary strings.

I’m not certain I understand the exact questions you’re asking in the “my problems are” bit, so generally speaking, one last rule:

  • design pages first, allowing them to assume the presence of a method in a service that delivers an Observable of exactly the data desired by the page in a format that is as easy as possible for the page to present. then work backwards to make that happen in the service.

Thanks rapropos for your reply.

Here are some clarifications and questions:

Clarifications:

  1. All HTTP requests come from a API services. In my APP actually I have 4 services:
  • web-service (for all HTTP API requests)
  • wishlist-service (to manage manage/add/update/delete item to/from wishlist)
  • storage-service (to save wishlist items on storage to avoid lost of data on app closing)
  • data-service (to save general info about use of app. For example current language choise from user, etc…)
  1. On WishListPage component I declare this:
 constructor(
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private modalCtrl: ModalController,
    private wishlistService: WishlistService)

and get items inside wishlist in this way:
this.wishlistService.getItems()
that return items. No bypass

Questions :

  1. I don’t understand this: “do not use lifecycle events to manage data freshness”. Should I make the API request on the component constructor and not on ngOnInit?

CURRENT STATUS
I have a page that show items coming from an observable returned by the web-service service by an HTTP request. This items are displayed via the async pipes on the html template:
<ion-item inset="true" *ngFor="let item of results | async" (click)="itemTapped($event, item)" lines="none" shape="round" class="ion-margin-bottom ion-margin-start ion-margin-end">
where results is the Observable. Items have title, image, price.

ItemTapped open a modal sheet (via Component) to choose quantity to add to wishlist. On modal confirm button i do this:

  1. Add item to WishList via service
  2. Close modal

After close modal i want to do this, but i don’t know how:

  1. Show a ion-badge on item, which was clicked, with the quantity selected.

and in general this:

  1. When i have observable from web-service how show for every item a ion-badge if the item is in my wishlist (wishlist.service)?
  2. On page that show my dish items, i can go to wishlist page via a header button. IN wishlist page i can manage quantity for each item on wishlist (or i can remove some) but when i go back via how i can update ion-badge quantity of the item with values changed in WishListPage?

I hope my problems are clear and I apologize if the terms I use are not correct but my last approach with Ionic dates back to 4/5 years ago. And sorry for mi english :slightly_smiling_face:

Thanks a lot

Best regards

getItems is a bypass, if it returns that private field. The point I’m trying to make is that when you give anything that isn’t an Observable out of a service, you have no way to communicate to whoever called it “hey, that data changed”. This is the fundamental wall you’re banging into.

You should design your services so that question becomes irrelevant, as described above. The great thing about diligently only returning Observables out of services is that you don’t care when you get the Observable. It will always deliver the latest data whenever it changes. That is what I mean about not using lifecycle events to manage data freshness. Data freshness should be managed by an Observable, and then you don’t have to rely on lifecycle events to call service methods multiple times. You call them once, whenever convenient (ngInit is fine), grab the Observable, subscribe to it, and tear down the subscription when the component goes away (I use the @ngneat/until-destroy library to manage this for me).

Thanks,
but how i can do these:

After close modal i want to do this, but i don’t know how:

Show a ion-badge on item, which was clicked, with the quantity selected.

and in general this:

When i have observable from web-service how show for every item a ion-badge if the item is in my wishlist (wishlist.service)?
On page that show my dish items, i can go to wishlist page via a header button. IN wishlist page i can manage quantity for each item on wishlist (or i can remove some) but when i go back via how i can update ion-badge quantity of the item with values changed in WishListPage?

I don’t know how mix observable response items with items inside wishlist service. Any suggests?

This is the WebService.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { timeout, catchError, retry, map } from 'rxjs/operators';
import { DataService } from './../services/data.service';

@Injectable({
  providedIn: 'root'
})
export class WebServiceService {

  url = 'https://***************'
  bearer_token = '*************';
  timeout: number = 5000;

  headers = new HttpHeaders({
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${this.bearer_token}`
  })

  constructor(
    public dataApp : DataService,
    private http: HttpClient) { }

  /**
  * Get the plates list of a section of a menu for an ID using the "menu_id" parameter and the "section_id" parameter
  * 
  * @param {int} menu_id Menu id to retrieve information
  * @param {int} section_id Section id to retrieve information
  * @param {string} app_area Menu id to retrieve information

  * @returns Observable with detailed information
  */
   __getMenusSectionsDishes(menu_id : number, section_id : number = null, app_area : string = 'restaurant'): Observable<any> {

    const json_body = {
                        function : 'menus_sections_dishes:read',
                        request : {
                          menu_id : menu_id,
                          lang_code : this.dataApp.currentLanguageCode,
                          section_id : section_id,
                          app_area : app_area
                        }    
                      };

    let response = this.http.post(this.url, json_body, { headers: this.headers })
    .pipe(
      timeout(this.timeout)
    );
    return response;

  }

}

This is WishlistService.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { StorageService } from './storage.service'; 

@Injectable({
  providedIn: 'root'
})

export class WishlistService {

  private items = [];
//  private items : BehaviorSubject<Array<any>> = new BehaviorSubject([]);
  private itemsKey = new BehaviorSubject("");
  private itemsCount = new BehaviorSubject(0);
  private itemsTotal = new BehaviorSubject(0);

  constructor(
    private storage: StorageService) {
  }

  setItemsKey(key){

    let clear = false;
    if (this.itemsKey.value != key)
      clear = true;

    this.itemsKey.next(key);

    if (clear)
      this.resetItems();

    this.getItemsFromStorage(key);

  }

  getItems() {
    return this.items;
  }

  getItemsKey() {
    return this.itemsKey;
  }

  getItemsCount() {
    return this.itemsCount;
  }

  getItemsTotal() {
    return this.itemsTotal;
  }

  addItem(item, quantity : number = 1)
  {
    let added = false;
    let price_to_add : number = 0;
    let item_list = this.items/*.value*/;

    for (let p of item_list) {
      if (p.id === item.id) {
        p.quantity += quantity;
        price_to_add = p.price * quantity;
        added = true;
        break;
      }
    }
    if (!added) {
      item.quantity = quantity;
      price_to_add = item.price * quantity;
      item_list.push(item);
    }

    this.items = item_list;
    //this.items.next(item_list);
    this.itemsCount.next(this.itemsCount.value + quantity);
    this.itemsTotal.next(this.itemsTotal.value + price_to_add);

    this.storage.set(this.itemsKey.value, item_list).then((result) => {
    }).catch((error) => {
    });

  }

  updateItem(item, quantity : number = 1)
  {
    let old_quantity : number = 1;
    let old_price : number = 0;
    let new_price : number = 0;
    let item_list = this.items/*.value*/;

    for (let [index, p] of item_list.entries()) {
      if (p.id === item.id)
      {
        old_quantity = p.quantity;
        old_price = p.price * p.quantity;
        p.quantity = quantity;
        new_price = p.price * quantity;

      }
    }

    this.items = item_list;
    //this.items.next(item_list);
    this.itemsCount.next(this.itemsCount.value + (quantity - old_quantity));
    this.itemsTotal.next(this.itemsTotal.value + new_price - old_price);

    this.storage.set(this.itemsKey.value, item_list).then((result) => {
    }).catch((error) => {
    });

    return this.items;
  }

  deleteItem(item) {

    let price : number = 0;
    let item_list = this.items/*.value*/;

    for (let [index, p] of item_list.entries()) {
      if (p.id === item.id) {
        price = p.price * p.quantity;
        this.itemsCount.next(this.itemsCount.value - p.quantity);
        this.itemsTotal.next(this.itemsTotal.value - price);
        item_list.splice(index, 1);
      }
    }

    this.items = item_list;
    //this.items.next(item_list);

    this.storage.set(this.itemsKey.value, item_list).then((result) => {
    }).catch((error) => {
    });

    return this.items;
  }

  resetItems() {
    this.items = [];
    //this.items.next([]);
    this.itemsCount.next(0);
    this.itemsTotal.next(0);
  }

  clearItems() {
    this.resetItems();
    this.storage.remove(this.itemsKey.value);
  }



  getItemsFromStorage(key){

    this.storage.get(key).then((data: any) => {

      if (data) {
        this.items = data;

        let counter = 0;
        let totalPrice = 0;

        data.forEach((currentValue, index) => {
          
          counter += currentValue.quantity;
          totalPrice += (currentValue.price *  currentValue.quantity);
        });

        this.itemsCount.next(counter);
        this.itemsTotal.next(totalPrice);
    
      }
      else
      {
        this.resetItems();
      }
    })

  }

}

This is the page that show items:

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { WebServiceService } from './../../api/web-service.service';
import { WishlistService } from './../../services/wishlist.service';
import { Observable, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';

import { ModalController } from '@ionic/angular';
import { PlateModalPage } from '../../modals/plate/plate-modal.page';
//import { StorageService } from './../../services/storage.service'; 

@Component({
  selector: 'app-plates',
  templateUrl: './plates.page.html',
  styleUrls: ['./plates.page.scss'],
})
export class PlatesPage implements OnInit {

  response: Observable<any>;
  results : Observable<any> = null;

  pageItem: any;
  pageTitle: string = '';
  restaurantItem: any;
  menuItem: any;
  sectionItem: any;

  listTitle: string = '';
  spinner: boolean = true;

  wishlistItems = [];
//  wishlistItems : BehaviorSubject<Array<any>> = new BehaviorSubject([]);
  wishlistItemsCount: BehaviorSubject<number>;
  wishlistItemsTotal: BehaviorSubject<number>;

  constructor(    
    private activatedRoute: ActivatedRoute,
    private router: Router,
    private modalCtrl: ModalController,
    //private storage: StorageService,
    private wishlistService: WishlistService, 
    private webService: WebServiceService
  ) { 

    let nav = this.router.getCurrentNavigation();
    if (nav.extras && nav.extras.state && nav.extras.state.restaurant) {
      this.restaurantItem = nav.extras.state.restaurant;
      this.pageItem = nav.extras.state.restaurant;
      //this.pageItem.title = this.pageItem.title + ' ' + this.restaurantItem.app_area;
    }

    if (nav.extras && nav.extras.state && nav.extras.state.menu) {
      this.menuItem = nav.extras.state.menu;
      this.listTitle = this.menuItem.title;
    }

    if (nav.extras && nav.extras.state && nav.extras.state.section) {
      this.sectionItem = nav.extras.state.section;
      this.listTitle = this.sectionItem.title;
    }

//  this.pageTitle = this.pageItem.title + ' ' + this.restaurantItem.app_area;
    this.pageTitle = this.pageItem.title;


  }

  ngOnInit() {

    this.wishlistItems = this.wishlistService.getItems();
    this.wishlistItemsCount = this.wishlistService.getItemsCount();
    this.wishlistItemsTotal = this.wishlistService.getItemsTotal();

    
    // Get the ID that was passed with the URL
    let menu_id = null;
    if (this.activatedRoute.snapshot.paramMap.get('menu_id'))    
      menu_id = Number(this.activatedRoute.snapshot.paramMap.get('menu_id'));
    else
      menu_id = this.menuItem.id;

    let section_id = null;
    if (this.activatedRoute.snapshot.paramMap.get('section_id'))
      section_id = Number(this.activatedRoute.snapshot.paramMap.get('section_id'));

    this.__getMenusSectionsDishes(menu_id, section_id);
  }
  
  //TODO: enable or disable for auto update view on back arrow from wishlist. Await response from https://forum.ionicframework.com/t/ionic-and-auto-content-refresh/219242
  ionViewWillEnter(){
    //this.wishlistItems = this.wishlistService.getItems();
    //this.wishlistItemsCount = this.wishlistService.getItemsCount();
    //this.wishlistItemsTotal = this.wishlistService.getItemsTotal();
    //this.__mixItemsWithWishlist();
  }

  __getMenusSectionsDishes(menu_id, section_id) {
    let app_area = this.restaurantItem.app_area;
    this.response = this.webService.__getMenusSectionsDishes(menu_id, section_id, app_area);
    this.__mixItemsWithWishlist();
  }  

  __mixItemsWithWishlist(){

      this.results = this.response.pipe(
        map(res => {
            const els = res; 
            els.forEach(item => {
              let x = 0;
              let arr = this.wishlistItems./*value.*/filter(x => {
                return x.id == item.id;
              });
              if (arr.length > 0)
                x = arr[0].quantity;
              item.wishlist_quantity = x;
            });
            return els;
          })
        );
     //    this.results = this.response; //così posso usare il pipe async sul template
     
      this.spinner = false;
  }

  /*Open plate detail*/
  itemTapped(event, item){  
    this.presentModal(event, item);
  }

  /*Go to wishlist*/
  goToWishList(){
    this.router.navigate(['/', 'wishlist'], { state: { restaurant: this.restaurantItem } });
  }
  
  async presentModal(event, item){

    item.total = item.price;
    const modal = await this.modalCtrl.create({
      component: PlateModalPage,
      breakpoints: [0, 0.7],
      initialBreakpoint: 0.7,
      handle: false,
      componentProps: {
        plateItem: item,
        restaurantItem : this.restaurantItem
      }
    });
    await modal.present();
  /*  const { data } = await modal.onDidDismiss();
    if (data) {
      this.__mixItemsWithWishlist();
    }*/
  }
}

This is items html:

      <ion-item inset="true" *ngFor="let item of results | async" (click)="itemTapped($event, item)" lines="none" shape="round" class="ion-margin-bottom ion-margin-start ion-margin-end">
        <ion-thumbnail slot="start">
          <img [src]="item.image">
        </ion-thumbnail>
        <ion-label class="ion-text-wrap">
            <ion-grid>
              <ion-row class="plate-row-1">
                <ion-col size="12">
                  <h5><strong>{{item.title}}</strong></h5>
                </ion-col>
              </ion-row>
              <ion-row  class="plate-row-2">
                <ion-col size="10">
                  <strong>{{item.price | currency:'EUR':'symbol':'1.2-2':'it-IT'}} <span *ngIf="item.price_type != 'UNIT'">/ {{item.price_type}}</span></strong>
                </ion-col>
                <ion-col size="2">
                  <span *ngIf="item.wishlist_quantity > 0">
                    <ion-badge color="primary">{{item.wishlist_quantity}}</ion-badge>
                  </span>  
                  </ion-col>  
              </ion-row>  
            </ion-grid>
        </ion-label>
      </ion-item>