Search code examples
javascriptfirebase-realtime-databaseionic4angularfire2

Ionic 5 Firebase Realtime - Favorites in a list


I have an Ionic 5 app with a list of motorcycles. In that, the user can favorite them, using a toggle star icon. Then, I create a register in the user's node containing the motorcycle data, like this:

enter image description here

The problem is that I am not able to display the "star" icon in the list (always displays "star-outline") when the motorcycle was previously marked as "favorite" by the current user, although I can obtain this data from Firebase.

So far I have tried this way, unsuccessfully (the star icon keeps "star-outline" in all items):

list.page.html

<ion-col size="6" *ngFor="let item of motos; let i = index;">
  <ion-card>
    <ion-card-header>
      <ion-fab vertical="top" horizontal="start">
        <ion-fab-button (click)="addToFavorites(item, i)" icon-only color="clear">
          <ion-icon [name]="item.favourited ? 'star' : 'star-outline'"></ion-icon>
        </ion-fab-button>
      </ion-fab>
    </ion-card-header>
    <ion-card-content>
        <div>{{item.fabricante}} {{item.nome}}</div>
        <div>{{item.ano_model}}</div>
    </ion-card-content>
  </ion-card>
</ion-col>

list.page.ts

import { CrudMethodsService } from './services/crud-methods.service';

basePath = 'motos';
basePathU = 'users';

constructor(private crudServices: CrudMethodsService, public afAuth: AngularFireAuth) {      
 this.crudServices.getMotos(this.basePath).subscribe(res => {
    this.motos = res;
    this.afAuth.authState.subscribe(user => {
      if (user){
        this.userId = user.uid;
        const favoritas = 'favoritas';
        this.crudServices.getFavorites(this.basePathU, this.userId, favoritas).subscribe(us => {
          this.isFavorite = us;
          this.isFavorite.forEach(resp => {
            const item = this.motos;
            const p = this.favorites.indexOf(item);
            item.favourited = item.favourited ? false : true;
            for (let j = 0; j < this.motos.length; j++) {
              if (resp.item.key === this.motos[j].key && p === -1) {
                this.favorites.push(item);
                item.favourited = true;
              }
            }
          });
        });
      }
    });
  });
}

crud-methods.service.ts

getMotos(basePath: string) {
  return this.db.list(basePath).snapshotChanges().pipe(map(changes => {
    return changes.map(c => {
      const data = c.payload.val();
      const id = c.payload.key;
      return { key: id, ...(data as object) };
    });
  }));
}

getFavorites(basePath: string, key: string, path: string) {
  return this.db.list(basePath + '/' + key + '/' + path).snapshotChanges().pipe(map(changes => {
    return changes.map(c => {
      const data = c.payload.val();
      const id = c.payload.key;
      return { key: id, ...(data as object) };
    });
  }));
}

Solution

  • You could use the rxjs operator switchmap to achieve your merging behavior. switchmap lets you use the result of an Observable but respond with another Observable. So you can chain observables without nested subscriptions like you did (which often causes issues). This would look something like this in your constructor:

    this.motosWithFavoritesObservable = this.afAuth.authState.pipe(
      map(user => user.uid),
      switchMap(userid => {
        return this.crudServices.getFavorites(this.basePathU, userid, 'favoritas');
      }),
      switchMap((favorites: { key: string }[]) => {
        return this.crudServices.getMotos(this.basePath).pipe(
          map((motos: { key: string, favourited: boolean }[]) => {
            favorites.forEach(fav => {
              const index = motos.findIndex(moto => moto.key === fav.key)
              if (index !== -1) { // if the moto corresponding to the fav is found in the list
                motos[index].favourited = true;
              }
            })
          })
        )
      })
    );
    
    motosWithFavoritesObservable.subscribe(motos => this.motos = motos);