I try to do a custom autocomplete component in my angular 16 project. That component should be able to lazy fetch the data from my component store.
I achieve to do something like that :
// MySearchComponent
@Input({ required: true }) searchValues$?: Observable<SearchInputObject[]>;
@Input({ required: true }) searchValuesSubscription!: any;// the trigger
@Input({ required: true }) fieldCtrl!: FormControl<SearchInputObject | null>; // form control to return the selected value
With searchValuesSubscription
a trigger of my effect, and searchValues$
the select associated in the store.
I find that solution not very developper friendly, as if I use the component 10 times, I'll have to create those 2 properties in the parent component to use MySearchComponent
ngOnInit() {
this.searchValues$ = this._myStore.companyCategories$;
this.searchValuesSubscription$ = this._myStore.loadCompanyCategories;
}
And to call the component :
<app-async-search-input
[fieldCtrl]="docCategoryCtrl"
[searchValues$]="searchValues$"
[searchValuesSubscription]="searchValuesSubscription$">
</app-async-search-input>
An extract of the Effect, just in case it helps :)
readonly loadCompanyCategories = this.effect<void>(trigger$ =>
trigger$.pipe(
exhaustMap(() =>
combineLatest([this.userInstance$, this.selectedCompanyId$]).pipe(
switchMap(([instance, selectedCompanyId]) => {
if (instance && selectedCompanyId) {
return this._apiService.getCompanyCategories(instance, selectedCompanyId).pipe(
tapResponse(
companiesCategories => {
this.patchState({
nonPersistent: {
...this.get().nonPersistent,
companyCategories: companiesCategories,
},
});
},
(error: HttpErrorResponse) => console.error(error)
)
);
}
return [];
})
)
)
)
);
Here a stackblitz that show a simplified version of what I did : https://stackblitz.com/edit/stackblitz-starters-frdx5b?file=src%2Fapp%2Fasync-autocomplete%2Fasync-autocomplete.component.ts You need to click on "Load books" so the AsyncAutocompleteComponent trigger the store, and then display the list of book in a select.
Is there a way to have a trigger that return on observable of the values ? Or is it something that shouldn't be done with an Effect ? Is there another way to do that ? I'm open to all advice ! Thanks !
Ok, here is a forked stackblitz that shows the correct way to handle event and data flow.
As I mentioned in my comment, passing observables and subscriptions into components is an anti-pattern. Doing this should be avoided unless absolutely necessary.
I am going to just post the pieces of code that are different than what you show above...
When you need to trigger something in a parent component, use an @Output
, rather than passing a subscription.
@Input({ required: true }) searchValues: Book[] | null = [];
@Input({ required: true }) fieldCtrl!: FormControl<any | null>; // form control to return the selected value
@Output() loadBooks = new EventEmitter();
Then this component should handle the async binding for the data, so that change detection is run for the async-autocomplete component whenever the data changes. Also, the there is a new event handler for when the button is pressed, which calls the load in the store.
<p>Select a book :</p>
<app-async-autocomplete
[searchValues]="searchValues$ | async"
(loadBooks)="onLoadBooks()"
[fieldCtrl]="fieldCtrl"
></app-async-autocomplete>
The load function effect you wrote also needed some adjustments. There really is no need to use an exhaustMap
and then a switchMap
. I recommend using either combineLastestWith
from rxjs
or the concatLatestFrom
found in the @ngrx/operators
library when you need to unwrap observables while processing an effect.
readonly loadBooks = this.effect<void>((trigger$) =>
trigger$.pipe(
tap((_) => console.log('fetch books')),
combineLatestWith(this.userInstance$, this.selectedCompanyId$),
switchMap(([_, instance, selectedCompanyId]) => {
if (instance && selectedCompanyId) {
return this._booksHttpService.getBooks().pipe(
tapResponse(
(newBooks) => {
console.log('newBooks=');
console.table(newBooks);
this.patchState({
books: newBooks,
});
},
(error: HttpErrorResponse) => console.error(error)
)
);
}
return [];
})
)
);
Please see the stackblitz to see how it all works together.