Split observable into two for segment lists

An observable contains records (each has the fields id, status, name, etc) from a Firestore collection. Some of them have status of ‘active’, and others ‘inactive’. I want to bind this collection to two ion-lists (active ones in one list, inactive in the other) with ion-segments at the top, with the buttons ‘active’ and ‘inactive’ - clicking the ‘active’ button shows the active records only, and the inactive button shows the inactive records.

How should I split the observable into two observables (active and inactive) and bind them to their corresponding lists? Is there a better way of doing this?

Thanks.

Hi obinnae

You have to just replace your array with data array and find active ones inside one array and inactive one inside another array,

var data = { “main”: [{ “id”: “active”, “name”: “name 1” }, { “id”: “inactive”, “name”: “name 2” }] },
activeresult;
inactiveresult;

data.main.some(function (a) {
if (a.id === ‘active’) {
activeresult = a;
}else if(a.id === ‘inactive’){
inactiveresult = a;
}
});

console.log(result);

cheers!!

Use the all observable to create two separate observables using filter

If the stream is an array use map and pipe. Use array.filter to separate active from non active

Thanks, @ishaquemujtaba. Do those arrays respond to changes in the Firestore records?

Thanks, @Tommertom. I’m kinda new to observables and having a hard time grasping map, pipe, filter, etc. Here’s how I was able to get it to work (using AngularFirestore)


this.tagCollection = this.afs.collection<Tag>('tags', ref=> ref.where('userid','==', this.user.email).orderBy("status_date", "desc"));
this.tags = this.tagCollection.snapshotChanges().pipe(
      map(actions => {
        return actions.map(a => {
          const data = a.payload.doc.data();
          const id = a.payload.doc.id;
          return { id, ...data };
        });
      })
    );

this.tags.subscribe((t: Tag[])=>{
      this.activeTags = [];
      this.inactiveTags=[];
      t.forEach(e => {
       if (e.status=='active')
       {
         this.activeTags.push(e);
       }
       else if (e.status=='inactive')
       {
         this.inactiveTags.push(e);
       }
      });
    });

It works as I want, though I don’t know if it’s the most efficient way. Any ideas?

you did it…Have a good day

Also, how can I load the list on page load? As it is now, it is blank until I click on the other segment, then it populates the list.
I already have the arrays declared and populated in ngOnInit(), as well as populated in ionViewDidEnter(), yet it’s blank when the page loads.

Hi

Alternative would be to skip the subscribe in the ts and use async pipe in the ngFor in the list view

Then create two different offsprings of this.tags:

inactiveTags$=this.tags.pipe(map(tags=>tags.filter(tag=>tag.status===‘inactive’)))

And activeTags$=… etc

Code will be much easier

Only thing to check if this will result in two firebase calls which is a waste. Then u need a store in a service (which actually is always a good idea to maintain central state)

And it will be good practive to put that firebase call in a service too.

Well, ngOninit is the place to load stuff (the firebase call - via a service ideally)

Not sure why it does not work. Can’t say from the code

Thanks. I am already using a service for it, and call it within ngOnInit() as follows

ngOnInit() {
    console.log("home ngOnInit");
    this.whichlist = "used";
    this.newTags = [];
    this.usedTags=[];
    
    this.loadTags();
  }

loadTags()
  {
    if (!this.tags) this.tags = this.tagService.getTags();//the service
    this.tags.subscribe((t: Tag[])=>{
      t.forEach(e => {
        (e.status=='new') ? this.newTags.push(e) : this.usedTags.push(e);
      });

      console.log("used tags: ", this.usedTags);
    }); 
    this.checkedTags = [];
  }

The html file…

<ion-segment (ionChange)="showView($event)" color="danger" value="used">
    <ion-segment-button value="used">
      <ion-label>Used ({{usedTags.length}})</ion-label>
    </ion-segment-button>
    <ion-segment-button value="new">
      <ion-label>New ({{newTags.length}})</ion-label>
    </ion-segment-button>
  </ion-segment>

  <div [ngSwitch]="whichlist">
    <ion-list no-padding lines="none" *ngSwitchCase="'used'">
<!-- ... and so on--->

This now works on initial page load (the ion-list is populated correctly). However, when I navigate from the page and return to it, the list is empty. I even tried calling loadTags() from ionViewWillEnter(), but still doesn’t load it. It remains blank unless I update the collection in Firestore.

Am I doing something wrong? What’s the right way to always show the list everytime I enter the page?

Thanks.

Disclaimer: I have never touched Firebase at all, so even by my usual lax standards this is supremely uninformed speculation, but judging solely from the name, general experience with RxJS, and the symptoms you describe, I suspect there might be a chance that snapshotChanges is giving you an Observable that does its thing and completes, ergo is not intended to be reused as you seem to be doing. I would try refactoring so that every Observable containing a snapshotChanges in its birth pipeline is treated as single-use.

Incidentally, if that’s true, then it’s actually saving you from what would otherwise be a memory leak, as you don’t seem to be ever tearing down the subscription created in loadTags.

Hi

snapshotChanges does not complete on its own in AngularFire (= realtime change monitoring). If you subscribe in/through onInit, at least unsubscribe in onDestroy. And don’t resubscribe on ionic hooks. As per rapropros’ recommendations.

If you like ionic page hooks, using observables wouldn’t be the easiest route for you maybe, why not retrieve the data directly from the service at every ionic page hook (e.g. ionDidEnter), where the service one time loads the firebase data? (if you don’t want realtime monitoring). So something like: this.tags=this.myservice.getAllStuff() and the ionsegment change will induce a filter on all stuff, so you don’t need a ngSwitch later: this.tagstoshow=tags.filter(...).

And console.log if the hook really fires… Use console.log a lot to test your assumptions and see what happens.

Afancy rxjs solution with switchMap triggered by the selector and async may be a bit too steep to apply for now.

I find it difficult to say where things go wrong. Could be the segment selector too.

Tom

1 Like

Thanks for explaining. Here’s the service…

    this.tagCollection = this.afs.collection<Tag>('tags', ref=> ref.where('userid','==', this.user.email).orderBy("status_date", "desc"));
    this.tags = this.tagCollection.snapshotChanges().pipe(
      map(actions => {
        return actions.map(a => {
          const data = a.payload.doc.data();
          const id = a.payload.doc.id;
          return { id, ...data };
        });
      })
    );

How would I refactor it to single use? (I’m still learning about observables).
I basically have to resubscribe on each page visit, right?

In that case, is it possible that snapshotChanges emits only when a (previously) unseen “change” (from the POV of the data source) occurs? If so, then might it be that @obinnae is subscribing not to a completed Observable, but instead one that has nothing more to say at the moment. It would seem that such a situation would present with the same symptoms as described here (works on initial interaction, not on navigate-to-other-page-and-come-back-without-changing-anything-to-data).

The simplest way would be to dump the tags property from the page entirely, so the top of loadTags() would just elide to:

this.tagService.getTags().subscribe(...)

Thanks, guys. I finally got it to work (hope I did it right. If not please correct me).
What I did was to remove the ‘tags’ property as you suggested, then assigned this.tagService.getTags().subscribe(...) to a Subscription object, as so…

public tagsSubs: Subscription;
// then in loadTags()
this.tagsSubs = this.tagService.getTags().subscribe(...)

then I unsubscribed in ngOnDestroy…

ngOnDestroy() {
    this.tagsSubs.unsubscribe();
    console.log('home destroyed!');
  }

Hope that’s efficient.

@Tommertom, unfortunately I do need realtime monitoring, else I’d have just loaded the data and filtered as you suggested.

Hi,

that will do fine and looks clean. Always ways to do things different/better.

If your app will use those tags in different components (or this component is recreated many times), then you may end up doing lots of read requests to Firebase delaying the UI possibly and if your app will be very popular cost you money (billing per read request in Firebase) and/or eating mobile data from your user.

Then (or in general even) you are better off putting a service in between with a observable store using ngrx or your own simple version using a BehaviorSubject stream. The service should be triggered to load data based on UI action (login, logout, load-more-records, data update).

Regards,

Tom

1 Like

You can use pipe directly on template. First use synch pipe and then filter pipe.