3

A view lists entries using a *ngFor based on an Observable:

view.html

<ion-searchbar [(ngModel)]="filter" (ionInput)="filterMeds()"></ion-searchbar>
<ion-list>
  <ion-item *ngFor="let medicine of getMeds$() | async">
      {{medicine.name}}
  </ion-item>
</ion-list>

The component just relays an observable via the getMeds$() function:

component.ts

private _refreshOnInput$: Subject<any> = new Subject();


getMeds$(){
  return this.medsService.medsArray$;
}

The service itself just returns the array as an observable: Observable<Medicine[]>

Medicine is a plain object made of a few strings an numbers.

Now, my question: view.html adds a searchbar. Whenever the users modifies the search criteria, the filterMeds() function is called:

filterMeds(){
  this._refreshOnInput$.next(true);
}

This following a blog post (cmd-f the text "refresh button" to see what I'm trying to implement).

I understand my *ngFor must iterate on an observable that merges both the original this.medsService.medsArray$ and the _refreshOnInput() after applying some filtering.

** component.ts attempt **

filteredMeds$: Observable<Medicine[]>

// Constructor

// An event is fired whenever the search input's updated
filteredMeds$ = this._refreshOnInput$.map( () =>
  this.medsService.medsArray$
    .map(meds =>
      meds.filter(med =>
        med.name.toLowerCase().indexOf(this.filter.toLowerCase()) > -1
      )
    )
);

this.meds$ = Observable.merge(this.medsService.medsArray$, filteredMeds$);

// Updating the getMeds$() method:
getMeds$(){
  return this.medsService.medsArray$;
}

I can't figure out how to create a valid filtered stream. The same error happens when I directly serve the filteredMeds$ observable to *ngFor:

ERROR Error: Error trying to diff '[object Object]'. Only arrays and iterables are allowed
Jem
  • 6,226
  • 14
  • 56
  • 74

2 Answers2

1

I guess that you should iterate by (getMeds$() | async) in this way:

<ion-item *ngFor="let medicine of (getMeds$() | async)">

because in your example, you are using async pipe on medicine object, no result of getMeds$ method.


But better way is use public reference to medsService (in component) and your ngFor loop should looks like that:

<ion-item *ngFor="let medicine of (medsService.medsArray$ | async)">

because you shouldn't use method in html (as a argument of ngFor loop or ngIf condition). Every change in view will be called getMeds$() method in your case, which is completely unnecessary for Observables.

Jaroslaw K.
  • 5,224
  • 2
  • 39
  • 65
  • Hi Jaroslaw, thanks for the input but I forgot to mention that the basic unfiltered observable displays properly. The issue comes from my filtering. But thanks for the best practice tip! – Jem Sep 27 '17 at 09:15
0

Maybe this is not the best solution BUT it works. So, please comment on this approach:

  private _refreshMeds: Subject<any> = new Subject();
  meds$: Observable<any>;
  filter: string = '';

  constructor(@Inject(MedsService) private medsService: MedsService) {

    let filtered$ = new Observable( obs => {
      this._refreshMeds.subscribe( () =>
        this.medsService.medsArray$.first().subscribe( medsArray => {

          const filteredMeds = medsArray.filter(med =>
            (this.filter.length === 0 ) ||
            (med.name.toLowerCase().indexOf(this.filter.toLowerCase()) > -1));
          obs.next(filteredMeds);
        })
      )
    });

    // Merging both the unfiltered (ie. at page load) or the filtered version
    this.meds$ = Observable.merge(this.medsService.medsArray$, filtered$);
  }

  // Called by the input's changed listener (ionic: ionInput)
  filterMeds(){
    this._refreshMeds.next(true);
  }
Jem
  • 6,226
  • 14
  • 56
  • 74