edkeveked
edkeveked

Reputation: 18371

Observable with initial value

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'.

live code

Upvotes: 17

Views: 34833

Answers (1)

Estus Flask
Estus Flask

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

Related Questions