Search code examples
angularsearchangular-servicesbehaviorsubject

Search bar as a service in angular 7


Before asking this question, I made some research on here and only found people asking about passing search values to a rest API and getting the result back.
My problem is quite different. I have two components and a service:

  • A searchbar component (responsible for getting the user input on each keyup)
  • An applist component (displays all the apps / only filtered apps)
  • An app service responsible for getting all the user apps from the backend once, and then taking the search input from the searchbar component and updating the behaviorSubject.

At all moments, I need to keep a copy of the original app list to filter it.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map, distinctUntilChanged, filter } from 'rxjs/operators';
import { Observable, BehaviorSubject } from 'rxjs';
import { Application } from '../models/apps.model';

@Injectable({
  providedIn: 'root'
})
export class AppsService {
  private appListSubject: BehaviorSubject<Application[]>;
  private filteredAppListSubject: BehaviorSubject<Application[]>;
  private appList: Observable<Application[]>;

  constructor(private httpClient: HttpClient) {
    this.appListSubject = new BehaviorSubject<Application[]>({} as Application[]);
    this.filteredAppListSubject = new BehaviorSubject<Application[]>({} as Application[]);
    this.appList = this.filteredAppListSubject.asObservable().pipe(distinctUntilChanged());
  }

  getApps(): Observable<Application[]> {
    return this.httpClient.get('http://localhost:3000/api/user/apps').pipe(map(
      (res: any) => {
        this.setAppList = res;
        return res;
      }
    ));
  }

  public get getCurrentAppList() {
    return this.appList;
  }

  public set setAppList(apps: Application[]) {
    this.appListSubject.next(apps);
    this.filteredAppListSubject.next(apps);
  }

  public searchApp(searchTerm: string) {
    const filteredApps = this.appListSubject.pipe(filter(
      (app: any) => {
        if (app.name.includes(searchTerm)) {
          return app;
        }
      }
    ));
    this.filteredAppListSubject.next(filteredApps);
  }
}

This my current implementation of the app service.

My objective is to have the applist component subscribe to appList, and whenever the searchbar component asks the service to look for a value, the applist component gets a new array of apps from the observable.

Right now, since the pipe function returns an observable, I can't add the result as the next value of the behavior subject.

I would also like to have your opinion on this particular implementation. Thanks!


Solution

  • I think your design has made the problem look more complex than it is.

    The flow of data is one way:

    HeaderComponent -- searchTerm --> AppService -- apps --> AppListComponent

    I think you just need a Subject in the AppService that serves as the proxy. The HeaderComponent will pass search terms into it, and the AppListComponent will receive the search results.

    app.service.ts

    @Injectable({ providedIn: 'root' })
    export class AppService {  
    
      private apps: Application[];
      private filteredApps$: Subject<Application[]> = 
        new ReplaySubject<Application[]>(1);
    
      getSearchResults(): Observable<Application[]> {
        return this.filteredApps$.asObservable();
      }
    
      search(searchTerm: string): Observable<void> {
        return this.fetchApps().pipe(
          tap((apps: Application[]) => {
            apps = apps.filter(app => app.name.toLowerCase().includes(searchTerm));
            this.filteredApps$.next(apps);
          }),
          map(() => void 0)
        );
      }
    
      private fetchApps(): Observable<Application[]> {
        // return cached apps
        if (this.apps) {
          return of(this.apps);
        }
    
        // fetch and cache apps
        return this.httpClient.get('http://localhost:3000/api/user/apps').pipe(
          tap((apps: Application[]) => this.apps = apps)
        );
      }
    }
    

    The app service will cache the http response the first time a search is made. When a search term comes into the service it will fetch the apps, filter them, and emit them through the subject. The component that lists the search results will subscribe to the subject.

    header.component.ts

    constructor(private appService: AppService) {
    }
    
    searchTerm = '';
    
    ngOnInit() {
      this.appService.search(this.searchTerm).subscribe();
    }
    
    onSearchTermChange(): void {
      this.appService.search(this.searchTerm).subscribe();
    }
    

    header.component.html

    <input [(ngModel)]="searchTerm" (ngModelChange)="onSearchTermChange()" />
    

    app-list.component.ts

    constructor(private appService: AppService) {
    }
    
    apps$: Observable<Application[]> = this.appService.getSearchResults();
    

    app-list.component.html

    <div *ngFor="let app of apps$ | async">
       {{app.name}}
    </div>
    

    DEMO: https://stackblitz.com/edit/angular-nbdts8