Reputation: 18371
Using observable, I want to filter and display a list. The input event
is fired only when the user starts typing. Therefore the list is not displayed at the first place. How can I assign a default value to the observable this.filterLocation$
until the inputEvent
starts to be triggered?
template
<ng-template ngFor let-location [ngForOf]="filterLocation$ | async">
<a mat-list-item href="#">{{location}}</a>
</ng-template>
component
ngAfterViewInit() {
const searchBox = document.querySelector('#search-input');
this.filterLocation$ = fromEvent(searchBox, 'input')
.pipe(
map((e: any) => {
const value = e.target.value;
return value ? this.locations
.filter(l => l.toLowerCase().includes(value.toLowerCase()))
: this.locations;
}),
startWith(this.locations)
)
}
}
Using startWith
makes the list to be displayed initially. But the following error is thrown:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngForOf: null'. Current value: 'ngForOf: name1,name2'.
Upvotes: 17
Views: 34833
Reputation: 222309
Initial value can be provided to an observable with startWith
operator, as it was already mentioned in now-deleted answer.
The problem is that filterLocation$
is assigned too late, after filterLocation$ | async
was evaluated to null
. Since the change occurs on same tick, this results in change detection error (though ExpressionChangedAfterItHasBeenCheckedError
can be considered a warning if its occurrence is expected).
The solution is to move the code from ngAfterViewInit
to ngOnInit
, before change detection was triggered.
This is not always possible. An alternative is to provide a value asynchronously, so it doesn't interfere with initial change detection.
By delaying the whole observable with delay
operator (acceptable solution for user input because it's not time critical):
this.filterLocation$ = fromEvent(searchBox, 'input')
.pipe(
map((e: any) => {
const value = e.target.value;
return value ? this.locations
.filter(l => l.toLowerCase().includes(value.toLowerCase()))
: this.locations;
}),
startWith(this.locations),
delay(0)
)
Or by making initial value asynchronous with a scheduler:
import { asyncScheduler } from 'rxjs'
...
this.filterLocation$ = fromEvent(searchBox, 'input')
.pipe(
map((e: any) => {
const value = e.target.value;
return value ? this.locations
.filter(l => l.toLowerCase().includes(value.toLowerCase()))
: this.locations;
}),
startWith(this.locations, asyncScheduler)
)
Upvotes: 18