With one Subscriber, an async list showing all items from Observable, what is the most efficient way to temporarily filter results based on user provided search criteria as a string
?
The component imports TokenService
and the template subscribes to tokens
: tokenService.tokens | async
// ./token.service.ts
@Injectable({
providedIn: 'root'
})
export class TokenService {
private tokens$ = new BehaviorSubject<Token[]>([]);
readonly tokens = this.tokens$.asObservable();
constructor() {
this.getTokens();
}
filterTokens(q?: string) { ... }
...
}
The output should display a list of items with names loosely matching the provided search criteria. Provided the letter w
(lower case) as input "Work" and "New" would display from a set of ['Work', 'Test', 'New']
.
Passing the newly filtered values to the BehaviorSubject
obviously makes the values unrecoverable unless cached elsewhere.
filterTokens(q?: string): void {
this.tokens$.next(
this.tokens$.getValue().filter(t => t.issuer.toLowerCase().includes(q.toLowerCase()))
);
}
The following method returns "Work" and "New" when passed tokenService.filterTokens('r') | async
:
filterTokens(q?: string): Observable<Token[]> {
return this.tokens.pipe(
map(tt => tt.filter(t => t.issuer.toLowerCase().includes(q.toLowerCase())))
);
}
This however, is not dynamic based on user input.
3
Answers
You should probably create a subject for storing the query too:
The cleanest way to set up your filterTokens method as you have already done:
And have the consumer (usually in the component), utilize a
switchMap
to call the function whenever the input value changes:If you’re not familiar with
switchMap
, it simply takes the incoming value and maps it to a new observable. Then it will emit the emissions from this new observable. Whenever it receives a new value (search string), it will unsubscribe from the prior "filterTokens observable" and subscribe to the new one.This means it will always emit the results from
service.filterTokens()
using the most recent value ofsearchString$
.Also, you can simplify your service further, by not using a BehaviorSubject at all. Just declare your tokens as an observable to the fetch the data, followed by
shareReplay
:Notice we declare
tokens$
with the call to fetch the data then addswitchMap(1)
, which means any subscribers will get the most recently emitted value upon subscription.Notice here we don’t need to call
getTokens()
in the constructor (which I’m assuming was doing some sort of http call, and explicitly subscribing). In general, you don’t want to subscribe in your services, but rather expose observables that consumers subscribe to when needed. This keeps your data flow completely lazy, because the http call only gets made when there is a subscriber to one of the derived observables, not every time the service is injected.Notice the filter function has been broken out into its own pure function outside the class to improve readability, but of course this is optional.
There isn’t a "Right Way" to do this, there is only the "Way that works for your application". That being said, there are multiple ways to achieve this, one way is to have two
Subjects
, one for holding a list of tokens, the other for doing the search, then merging the two.Working Example
Brief Overview of the Filter
Not a query
This is what happens when
this.tokens
is updated either by initialization or adding/removing a token to the array cache (this.tokens.next([...this.tokens.value, newToken])
). The result will just be aToken[]
.Doing a query
This is what happens when
this.query.next()
is called:from
passes each array item to the pipe (like afor
loop)filter
tests each of the tokens to test the search string (fuzzySearch
returnstrue
/false
if the token matches or not)toArray
converts each observable that passed thefilter
test back to aToken[]
that matched the searchThe
merge()
The merge takes the two
Observables
and runs each when something happens. The result is a singleObservable
from whichever ran. In this case it will be aToken[]
.Implementation
Not sure what your
Token
class is, but here is what I did:Next we have the Service:
And lastly the Component: