List Filtering with Observables (Real Time Search)


#1

Hi everyone!

I’m trying to filter a list with an observable (Subject). I want to reach a real time search. But if I write something in the search box, e.g. the id of a list item as I declacred in my Http-Service (search Method), nothing happen. Does anyone have an idea what I did wrong in my code?

Please note: The “C” in my code stands for “Consumer”

I would be glad about the simplest solution.

Kind regards,
Thomas

Component.ts

import { Component, OnInit } from '@angular/core';
import { NavController, NavParams, PopoverController } from 'ionic-angular';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { debounceTime, distinctUntilChanged, switchMap} from 'rxjs/operators';

import { CVideoAddPage } from '../c-video-add/c-video-add';
import { CVideoDetailPage } from '../c-video-detail/c-video-detail';
import { MoreContentPage } from '../../../more/content/more-content';
import { CVideoModel } from '../c-video.model';
import { CVideoHttp } from '../c-video.http';

@Component({
    selector: 'c-video-list',
    templateUrl: 'c-video-list.html',
})

export class CVideoListPage implements OnInit {

    videos: Observable<CVideoModel[ ]>;
    
    private searchTerms = new Subject<string>();

    constructor(public navCtrl: NavController, public navParams: NavParams, public popoverCtrl: PopoverController, private cVideoHttp: CVideoHttp) {

    }

    ngOnInit(): void {
        this.videos = this.cVideoHttp.getVideos();

        this.searchTerms.pipe(
            // wait 300ms after each keystroke before considering the term
            debounceTime(300),
            // ignore new term if same as previous term
            distinctUntilChanged(),
            // switch to new search observable each time the term changes
            switchMap((term: string) => this.cVideoHttp.searchVideo(term)),
        );

    }

    // Push a search term into the observable stream.
    searchVideo(term: string): void {
        this.searchTerms.next(term);
    }

    addVideo() {
        this.navCtrl.push(CVideoAddPage);
    }

    videoDetail(video) {
        this.navCtrl.push(CVideoDetailPage, video);
    }

    more(more) {
        let popover = this.popoverCtrl.create(MoreContentPage);
        popover.present({ev: more});
    }

}

Component.html

<ion-header #head>
    <c-itoolbar></c-itoolbar>
</ion-header>

<ion-content header-hide fab-hide [header]="head">

    <ion-card>
        <ion-toolbar color="white">
            <ion-searchbar placeholder="Videos (10)" #searchBox (keyup)="searchVideo(searchBox.value)"></ion-searchbar>
            <ion-buttons end>
                <button ion-button icon-only color="dark">
                    <ion-icon name="square"></ion-icon>
                </button>
            </ion-buttons>
            <ion-buttons end>
                <button ion-button icon-only color="dark">
                    <ion-icon name="options"></ion-icon>
                </button>
            </ion-buttons>
        </ion-toolbar>
    </ion-card>

    <ion-card *ngFor="let video of videos | async" (click)="videoDetail(video)">
        <button ion-item>
            <ion-thumbnail item-start>
                <img [src]="video.url">
            </ion-thumbnail>
            <h2 ion-text text-wrap color="black" class="text-bolder">{{video.id}} {{video.title}}</h2>
            <p>{{video.publisher}}</p>
            <p text-wrap>
                <ion-icon name="eye"></ion-icon>
                {{video.views | number}} •
                 <ion-icon name="thumbs-up"></ion-icon>
                 {{video.likes | percent}} •
                 <ion-icon name="time"></ion-icon>
                 {{myDate | amTimeAgo: true}}
             </p>
            <button ion-button small clear icon-only item-end (click) ="more()">
                <ion-icon name="more" color="dark"></ion-icon>
            </button>
        </button>
        <button ion-button full color="red" (click)="delete(video)">Delete</button>
    </ion-card>

    <ion-fab right bottom #fab>
        <button ion-fab color="primary" (click)="addVideo()">
            <ion-icon name="videocam"></ion-icon>
        </button>
    </ion-fab>

</ion-content>

Http-Service

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';

import { CVideoModel } from './c-video.model';

@Injectable()

export class CVideoHttp {

  private videosUrl: any = "http://my-json-server.typicode.com/thomaslohmann/jsonserver/videos";

  constructor(private http: HttpClient) {

  }

  getVideos (): Observable<CVideoModel[]> {
      return this.http.get<CVideoModel[]>(this.videosUrl);
  }

  getVideo (id: number): Observable<CVideoModel> {
      return this.http.get<CVideoModel>(`${this.videosUrl}/${id}`);
  }

  addVideo (VideoModel: CVideoModel): Observable<CVideoModel> {
      return this.http.post<CVideoModel>(this.videosUrl, CVideoModel);
  }

  updateVideo (CVideoModel: CVideoModel ): Observable<CVideoModel> {
      return this.http.put<CVideoModel>(`${this.videosUrl}/${CVideoModel.id}`, CVideoModel);
  }

  deleteVideo(CVideoModel: CVideoModel): Observable<CVideoModel> {
      return this.http.delete<CVideoModel>(`${this.videosUrl}/${CVideoModel.id}`);
}

  searchVideo(term: string): Observable<CVideoModel[]> {
      if (!term.trim()) {
          // if not search term, return empty array.
          return of([]);
      }
      return this.http.get<CVideoModel[]>(`http://my-json-server.typicode.com/thomaslohmann/jsonserver/videos/?id=${term}`);
  }

}

#2
<ion-searchbar [formControl]="searchControl"></ion-searchbar>

searchControl = new FormControl();
searchFilter = this.searchControl.valueChanges.debounceTime(whateverYouWant).startWith('');

#3

Hi Aaron,

thx for your answer.

Where do you put the searchFilter and refers it to what? I also need to use the pipe method for operators and make requests to the server when the user searches for a term. Just filter an already requested list of items is unfortunately not enough. Do you have any other idea?

KInd regards,
Thomas


#4

You don’t need real time search if you’re talking to a server. You also don’t need a pipe, though using lettable operators is probably better style. So it’s hard for me to understand your question.


#5

I explain it in steps:

  1. A user visit your page you show him the first 10 items/videos instantly (first get request)

  2. User scrolls down the page and you show him another 10 videos “Infinite Scroll” (second get request)

  3. User can’t find the video he is interested in. So the user uses the searchbar to find the desired video and the video is displayed “real time” in the list of items/videos (third get request)

I hope this is more understandable.


#6

There’s no such thing as an instant get request. You need to handle what happens if you’re disconnected, if the connection is slow and the user types a search before the first request returns etc. Overall, that sounds like a recipe for pipelining errors. I think you should differentiate between querying the server and searching locally available data.


#7

You are right. I mean in my thought way the user experience would be much better. Think of the Facebook Feed. It’s almost the same. Users see in there feed posts instantly (first request) and scroll down and see more posts (second request). But the feed is not searchable. What I want to achieve is that if a user searches for something, not to show him the search results on a seperate page or page reload, but directly on the current page “real time”. Maybe I will find an efficient solution with not so much round trips.


#8

Hey Thomas, I’m trying to do something very similar to this. Did you ever find a solution?


#9

This is working for me:

     this.childService.getChildList().subscribe(children => {
      this.childrenList = children;
      let val = ev.target.value;
      if (val && val.trim() !== "") {
        this.childrenList = this.childrenList.filter(function(child) {
          return child.name.toLowerCase().includes(val.toLowerCase());
        });
      }
    });
  }

and in the service

 getChildList() {
    return this.childListRef.snapshotChanges().map(changes => {
      return changes.map(c => ({ key: c.payload.key, ...c.payload.val() }));
    });
  }