I have two Subjects in an Angular component that leverage the same set of pipeable operators to provide typeahead search lookups for two different form fields. Example:
this.codeSearchResults$ = this.codeInput$
.pipe(
untilDestroyed(this),
distinctUntilChanged(),
debounceTime(250),
filter(value => value !== null),
switchMap((value: string) => {
const params: IUMLSConceptSearchParams = {
...TERMINOLOGY_SEARCH_PARAMS,
sabs: this.sabs,
term: value
};
return this.terminologyService.umlsConceptSearch(params);
}),
);
The definition for pipe appears that it will accept an arbitrary number of functions, however providing the functions via spread
this.codeSearchResults$ = this.codeInput$.pipe(...operators);
isn't working as expected. How can I provide a single input source of functions for both Subjects to keep my code DRY?
Following option #2 from Dan Kreiger's answer, my final code is as follows:
const operations = (context) => pipe(
untilDestroyed(context),
distinctUntilChanged(),
debounceTime(250),
filter(value => value !== null),
switchMap(value => {
const term: string = value as unknown as string;
const params: IUMLSConceptSearchParams = {
...TERMINOLOGY_SEARCH_PARAMS,
sabs: context.sabs,
term,
};
return context.terminologyService.umlsConceptSearch(params) as IUMLSResult[];
}),
);
this.codeSearchResults$ = this.codeInput$
.pipe(
tap(() => this.codeLookupLoading = true),
operations(this),
tap(() => this.codeLookupLoading = false),
) as Observable<IUMLSResult[]>;
this.displaySearchResults$ = this.displayInput$
.pipe(
tap(() => this.displayLookupLoading = true),
operations(this),
tap(() => this.displayLookupLoading = false),
) as Observable<IUMLSResult[]>;
I needed to compose a couple of tap()
functions that were unique per Subject and it works as expected.
Here are 4 possible ways.
1. Utility function that returns a list of operators
If you want to reuse it between contexts, you can try making a function that accepts the thisArg
and returns an array of operators.
Then you can spread the invoked function in the argument passed to pipe
.
/**
* @param {object} thisArg - context using the typeahead
* @returns {OperatorFunction[]}
*
* list of pipepable operators
* that can have a dynamic `this` context
*/
const typeAhead = thisArg => [
untilDestroyed(thisArg),
distinctUntilChanged(),
debounceTime(250),
filter(value => value !== null),
switchMap((value: string) => {
const params: IUMLSConceptSearchParams = {
...TERMINOLOGY_SEARCH_PARAMS,
sabs: thisArg.sabs,
term: value
};
return thisArg.terminologyService.umlsConceptSearch(params);
})
]
// subject A
this.codeSearchResults$ = this.codeInput$
.pipe(...typeAhead(this));
// subject B
this.articleSearchResults$ = this.articleInput$
.pipe(...typeAhead(this));
Note: terminologyService
and sabs
would need to be present on the this
context you pass to this function.
It looks like you are using these for components, so as long as terminologyService
is being injected as a dependency and sabs
is a static member of the component, it should work.
2. Utility function that returns composed operators
Alternatively, you could accomplish the same this by importing pipe
from rxjs
to chain the operators together.
import { pipe } from "rxjs";
// ...
const typeAhead = (thisArg) =>
pipe(
untilDestroyed(thisArg),
distinctUntilChanged(),
debounceTime(250),
filter((value) => value !== null),
switchMap((value: string) => {
const params: IUMLSConceptSearchParams = {
...TERMINOLOGY_SEARCH_PARAMS,
sabs: thisArg.sabs,
term: value
};
return thisArg.terminologyService.umlsConceptSearch(params);
})
);
In this case, you do not need to use the spread operator since the operators are composed together using pipe
's left-to-right function composition.
// subject A
this.codeSearchResults$ = this.codeInput$
.pipe(typeAhead(this));
// subject B
this.articleSearchResults$ = this.articleInput$
.pipe(typeAhead(this));
Note: terminologyService
and sabs
would need to be present on the this
context you pass to this function.
3. Service for custom operators
You could make a reusable service for your custom piped operators. This would allow you to grab the terminologyService
singleton from the reusable service itself.
However, it looks like sabs
will still need to be available from whereever it is coming from.
If you choose to do this, make sure to declare TerminologyService
in the top level providers - see example here
Then you could inject it into your components.
import { TerminologyService } from "./terminologyService.service";
import { Injectable } from "@angular/core";
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
@UntilDestroy()
@Injectable({
providedIn: 'root',
})
export class PipedOperatorsService {
constructor(private terminologyService: TerminologyService) {}
typeAhead(thisComponent) {
return pipe(
untilDestroyed(thisComponent),
distinctUntilChanged(),
debounceTime(250),
filter((value) => value !== null),
switchMap((value: string) => {
const params: IUMLSConceptSearchParams = {
...TERMINOLOGY_SEARCH_PARAMS,
sabs: thisComponent.sabs,
term: value
};
return this.terminologyService.umlsConceptSearch(params);
})
);
}
}
Then you can use it in a component:
import { Component, OnInit } from "@angular/core";
import { PipedOperatorsService } from "./pipedOperators.service";
import { Observable } from 'rxjs';
@Component({
selector: "some-root",
templateUrl: "./some.component.html",
})
export class SomeComponent implements OnInit {
sabs = ['What', 'is', 'a', 'sab', '?', '🐶'];
codeSearchResults$: Observable<string[]>;
constructor(private pipedOperatorsService: PipedOperatorsService){}
ngOnInit() {
this.codeSearchResults$ = this.codeInput$.pipe(
this.pipedOperatorsService.typeAhead(this)
);
}
}
Note: sabs
would need to be present on the this
context you pass to this function. I made a dummy one here.
4. Service for custom operators (no this
context)
If you want to make this truly reusable and not have to worry about this
, you can just compose the operators you need that do not require a this
context.
import { Injectable } from "@angular/core";
import { pipe } from "rxjs";
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
@Injectable({
providedIn: 'root',
})
export class PipedOperatorsService {
get typeAhead() {
return pipe(
distinctUntilChanged(),
debounceTime(250),
filter((value) => value !== null),
);
}
}
Then be sure to add the specific operators you need in the component:
import { Component, OnInit } from "@angular/core";
import { TerminologyService } from "./terminologyService.service";
import { PipedOperatorsService } from "./pipedOperators.service";
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
@UntilDestroy()
@Component({
selector: "some-root",
templateUrl: "./some.component.html",
})
export class SomeComponent implements OnInit {
sabs = ['What', 'is', 'a', 'sab', '?', '🐶'];
codeSearchResults$: Observable<string[]>;
constructor(private pipedOperatorsService: PipedOperatorsService, private terminologyService: TerminologyService){}
ngOnInit() {
this.codeSearchResults$ = this.codeInput$.pipe(
untilDestroyed(this),
this.pipedOperatorsService.typeAhead,
switchMap((value: string) => {
const params: IUMLSConceptSearchParams = {
...TERMINOLOGY_SEARCH_PARAMS,
sabs: this.sabs,
term: value
};
return this.terminologyService.umlsConceptSearch(params);
})
);
}
}
Note: sabs
would need to be present on the this
context you pass to this function. I made a dummy one here.
It's been a while since I've used angular. I hope some of these examples are useful.