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:
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!
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>