angular autocomplete suggestion list - javascript

I have an autocomplete function like this
chooseArtist: OperatorFunction<string, readonly string[]> = (text$: Observable<string>) =>
text$.pipe(
debounceTime(200),
distinctUntilChanged(),
map((term: any) => term.length < 2 ? []
: this.artistlookuplist.filter((v: any) => v.name.toLowerCase().indexOf(term.toLowerCase()) > -1).slice(0, 10))
)
And I have a service that populates the artistlookuplist like this:
getArtists(): void {
this.artistService.getSearchArtist(this.searchstring).subscribe((data: any[]) => {
this.artistlookuplist = data;
});
I would like to combine these two. So that the list of autocomplete suggestion is only fetched when the chooseArtist function is called from the autocomplete field.
Any ideas how?

I understand that you want to combine the two observables. When you need combine two observables, one depending from another you use switchMap rxjs operator
import {of} from 'rxjs
import {debounceTime,distictUntilChahne,switchMap} from 'rxjs/operators'
text$.pipe(
debounceTime(200),
distinctUntilChanged(),
switchMap((term:any)=>{
return term.length<2? of([]):
this.artistService.getSearchArtist(term)
})
)
See that you have a first observable, the text$, and based in the value of "term" you return an observable (if term.length<2 use the of rxjs operator to return an objervable of an empty array, else the observable that return the service.
You can see switchMap as some like map. When we use map, we transform the response (but return an array or a string or...), when we use switchMap we "transform" the response return a new observable.

Related

How to get the observable value inside a .map() function

I have a function _populateData that creates a new list of properties from another list of properties.
There is an observable getOwnerForProperty that returns the owner's value.
//Get single owner observable
public getOwnerForProperty(prop: any){
return this._manageOwnerService._getOwnersOfProperty(prop).pipe(map(o => o[0]))
How can I call the observable from within the .map() function to obtain the observable's value and attach it to the new object as seen below?
In my opinion, it would not be a good idea to subscribe getOwnerForProperty function in the .map(). Please advise on the best way to approach this following best practices.
/**
* Returns the active properties data.
*
* #param props - The property list.
* #returns An array of properties
*/
private _populateData(props: Property[]) {
return
const populated = props
.filter(prop => !prop.isArchived)
.map((p: Property) => {
// refactoring here
this.getOwnerForProperty(p).pipe(
map((owner: Owner) => {
const obj = {
propertyName: p.info.name.toUpperCase(),
owner: owner.name,
createdOn: p.createdOn ? __FormatDateFromStorage(p.createdOn) : ''
}
return obj;
})
)
}
)
return populated;
}
}
It's not entirely clear from your question what exactly you are trying to achieve, but here is my solution, so you will hopefully get the idea:
filter for the properties you want to "enrich".
use forkJoin to create an array of observables and wait for all of them to complete.
map each property to the observable you want to wait for.
map the result of the observable to the initial property and enrich it with the owner object.
forkJoin returns an observable which will basically emit a single array of enriched objects and complete. If you wish to await this, you can wrap this in lastValueFrom operator, like await lastValueFrom(forkJoin(...))
function _populateData(props: Property[]) {
const propertiesToPopulate = props.filter((prop) => !prop.isArchived);
forkJoin(
propertiesToPopulate.map((p: Property) => {
return getOwnerForProperty(p).pipe(
map((owner) => ({
...p,
owner,
}))
);
})
);
}

Why Rxjs combineLatest depends on both subscription and not call when one of them changed?

I have two subscriptions item as you see in the image:
one of them is search input another one is a selection filter.
I want to change the result when one or both of them change.
I use Rxjs combineLatest so when both of them or search input change, everything is ok, but when changing the Partner type at the first, the response does not change.
ngOnInit() {
super.ngOnInit();
this.getAllPartners();
combineLatest([this.searchPartnerFc.valueChanges, this.filterFc.valueChanges])
.pipe(
switchMap(([search, filter]) => {
let partnersBySearch = this.getSearchResult(search);
let partnersByFilter = this.getFilterResult(filter);
this.partners = partnersBySearch.filter(item => partnersByFilter.indexOf(item) > 0);
return this.partners;
}),
)
.subscribe();
}
getFilterResult(filterKey: string) {
if (filterKey == 'All') return this.allPartners;
else return this.allPartners.filter(partner => partner.partnerType == filterKey);
}
getSearchResult(searchString: string) {
return this.allPartners.filter(x => x.partnerName.toLowerCase().includes(searchString));
}
You can achieve your desired result by providing a default emission for each of your source observables using startWith:
combineLatest([
this.searchPartnerFc.valueChanges.pipe(startWith('')),
this.filterFc.valueChanges.pipe(startWith('All'))
]).pipe(
...
);
I think it's not working because of the nature of the combineLatest operator. It will only emit when both of the observables have emitted an initial value first. So when only the first observable emits, and the second doesn't, it will not do anything:
https://www.learnrxjs.io/learn-rxjs/operators/combination/combinelatest
I think you need a merge operator here:
https://www.learnrxjs.io/learn-rxjs/operators/combination/merge
Also please take a look here on this thread:
How to 'wait' for two observables in RxJS

How to create a waterfall observable in rxjs?

I am sure a waterfall observable isn't a thing, but it best describes what I want to do.
Code
Given is a map of paths and behavior objects that represent their content.
const pathmap = new Map<string, BehaviorSubject<string>>();
pathmap.set("foo.txt", new BehaviorSubject<string>("file is empty"));
pathmap.set("bar.txt", new BehaviorSubject<string>("file is empty"));
Also given is a BehaviourSubject that contains the active path.
const activePath = new BehaviorSubject<string | null>(null);
...
activePath.next('bar.txt');
What I need:
I want to create a single chained Observable that triggers an event when:
A) The active file path got changed.
B) The content of a file got changed.
What I have so far:
https://codesandbox.io/s/bold-dream-3hfjdy?file=/src/index.ts
function getFileContentObservable(): Observable<string> {
return currentFile.asObservable()
.pipe(map((path: string) => pathmap.get(path).asObservable()))
.pipe(map((content: string) => `<h1>${content}</h1>`));
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Type 'string' is not assignable to type 'Observable<string>'
}
How would I chain this?
Try switchMap Operator.
function getFileContentObservable(): Observable<string> {
return currentFile.asObservable().pipe(
filter(Boolean),
switchMap((path: string) => pathmap.get(path).asObservable()),
map((content: string) => `<h1>${content}</h1>`)
);
}

How to serialize execution of array of observables

I have a validation process which validates data in the table row by row. Because each row validation uses a shared resource, access to it must be serialized.
public validate():Observable<boolean>{
const rowValidations:Observable<boolean>[] = dataRows.map(row=>this.validateSingleRow(row);
return forkJoin(...rowValidations).pipe(
map(results=>results.every(r=>r))
)
}
If I understand correctly, forkJoin will not wait for each observable to finish before subscribing to the next one like concat would so that will probably fail. concat on the other hand serializes all the observables into a single stream.
How can I get a subscription order like with concat but have an array of results of each observable like with forkJoin effectively synchronizing execution of each inner observable (like Javas synchronzied validateSingleRow)?
Actually, if you know that each this.validateSingleRow(row) will always emit only once you can use toArray():
concat(...rowValidations).pipe(
toArray(),
);
concat will guarantee correct order and toArray() will collect all emissions into a single array and reemit it after the source Observable completes.
Otherwise, if validateSingleRow might emit multiple times and you always want only its last value you could use scan:
const indexedRowValidations = rowValidations.map((o, index) => o.pipe(
map(result => [index, result]),
));
concat(...indexedRowValidations ).pipe(
scan((acc, [index, result]) => {
acc[index] = result;
return acc;
}, {}),
takeLast(1),
);
(I didn't test it but I believe you get the idea :)).
Would something like this do the trick for you?
class SomeClass {
dataRows = [1, 2, 3];
public validate(): Observable<boolean[]> {
return this.resolveSequentially(this.dataRows);
}
private validateSequentially<T>([cur, ...obs]: T[]): Observable<boolean[]> {
return cur
? this.validateSingleRow(cur).pipe(
switchMap((x) =>
this.validateSequentially(obs).pipe(map((arr) => [x, ...arr]))
)
)
: of([]);
}
// Mock
private validateSingleRow(cur: any) {
console.log(`Validating ${cur}...`);
return of(Math.floor(Math.random() * 2) === 1).pipe(
delay(1000),
tap((x) => console.log(`Result: ${x}`))
);
}
}
const obj = new SomeClass();
obj.validate().subscribe(console.log);
StackBlitz demo
Solution that meets my requirement is simpler than one might think. I have used concat with toArray() like this
const rowValidations:Observable<boolean>[] = dataRows.map(row=>defer(()=>this.validateSingleRow(row));
return concat(...rowValidations).pipe(
toArray(),
map(results=>results.every(r=>r))
)
so validateSingleRow is executed one by one and toArray transforms boolean stream into array of boolean.

Angular Debounce within ValueChanges of form

I have a list called filteredUserNames which contains a lot of users. Everytime I change a value on my form it starts a new filter of the data. I know that to delay the time so that not every character triggers a new filter I need to use debounce, but I'm not sure where to add it. Should it be inside the value changes subscription? Also what is the correct way to implement it?
I have
searchString = new BehaviorSubject("");
searchString$ = this.searchString.asObservable();
In Constructor
this.myForm = this.fb.group({
searchString: [""],
});
this.myForm.controls.searchString.valueChanges.subscribe((val) => {
// SHOULD THE DEBOUNCE GO HERE ?? //
this.searchString.next(val);
}
In ngOnInit
this.searchString$.subscribe((searchTerm) => {
console.log(this.userNames);
if (this.userNames !== undefined) {
this.filteredUserNames = this.userNames.filter(
(userName) =>
userName.searchTerms
.toLowerCase()
.indexOf(searchTerm.toLowerCase()) !== -1
);
};
});
try this and you can add distinctUntilChanged to ignore similar values and tap operator for your side effects which is in your case emitting a new value to your behaviorSubject
import { tap, distinctUntilChanged, debounceTime} from 'rxjs/operators';
...
this.myForm.controls.searchString.valueChanges.pipe(
debounceTime(400),
distinctUntilChanged(),
tap((val) => this.searchString.next(val))
).subscribe()

Categories