Search code examples
angularrxjsangular-ng-ifbehaviorsubject

Weird behavior of BehaviorSubject, async pipe, and *ngIf combination: ngOnChanges receives the change that theoretically cannot be possible


I have two components - Parent component which fetches the data from the server, and child component which is displaying fetched entries as a list - but only if the list is not null or empty.

My problem is (short): Even if there is *ngIf checking that the list has some values, emission is done and in child component I get first emission with null items: ![First emission] (https://i.sstatic.net/OHRB5.png).

Second emission is though okay... ![Secod emission] (https://i.sstatic.net/vK8Op.png).

Does somebody have an idea?


The parent component also contains search field where you can search entries. Here is the code:

<lookup-area [debounceTime]="100"
             (searchValue)="searchChanged($event)"></lookup-area>

<app-virtual-list *ngIf="(channels$ | async)?.length > 0"
                  [items]="channels$ | async"
                  [itemHeightPx]="32"
                  [maxContainerHeightPx]="190 - ((channels$ | async)?.length ? 29 : 0)"
                  [trackByFn]="trackByFn">
    <ng-template let-item>Some template</ng-template>
</app-virtual-list>
@Component({
             selector: 'app-filter',
             templateUrl: './filter.component.html',
             changeDetection: ChangeDetectionStrategy.OnPush,
             encapsulation: ViewEncapsulation.None
           })
export class FilterComponent implements OnInit, OnDestroy {

    public search$ = new BehaviorSubject('');

    constructor(private service: YoutubeSearchService) {
    }

    public ngOnInit(): void {
        this.channels$ = combineLatest([this.service.getYoutubeChannels(), this.search$]).pipe(
            map(([channels, search]) => search ? channels.filter(channel => (channel.name || channel.id).startsWith(search)) : channels),
            map(channels => channels?.sort((a, b) => (a.name || a.id).localeCompare((b.name || b.id), undefined, { sensitivity: 'base' })))
        );
    }

    public ngOnDestroy(): void {
        this.search$.complete();
    }

    public searchChanged(search: string): void {
        this.search$.next(search);
    }

    public trackByFn(index: number, item: YoutubeChannel): any {
        return item.id;
    }

}

And the child component contains the logic for displaying. Here is the code:

@Component({
             selector: 'app-virtual-list',
             templateUrl: './virtual-list.html',
             styleUrls: ['./virtual-list.less'],
             changeDetection: ChangeDetectionStrategy.OnPush,
             encapsulation: ViewEncapsulation.None
           })
export class VirtualList<T> implements OnChanges {

  // Mandatory
  @Input() public items: T[];
  @Input() public itemHeightPx: number;

  // Optional
  @Input() public maxContainerHeightPx: number | undefined; // If undefined, scroll container's height is 100%

  
  public ngOnChanges(changes: SimpleChanges): void {
      const { items, itemHeightPx, maxContainerHeightPx } = changes;

      if (this.maxContainerHeightPx && (items || itemHeightPx || maxContainerHeightPx)) {
         this.setContainerScrollHeight();
      }
  }

}

I tried:

  1. wrapping everything in <ng-container *ngIf="(channels$ | async)?.length > 0">, but no success
  2. wrapping in another div, still did not work

I have no clue what else to try because it seems that *ngIf does not work which is really weird.

I would expect that the first change comes with the items inside since that is what *ngIf is telling.


Solution

  • Not sure I understand your issue, but I already see a bad thing in your code, so try to resolve it and see if the issue persists.

    You don't need to create 3 subscriptions, a single one is enough.

    <ng-container *ngIf="channels$ | async as channels">
      <app-virtual-list 
        *ngIf="channels?.length"
        [items]="channels"
        [itemHeightPx]="32"
        [maxContainerHeightPx]="190 - (channels?.length ? 29 : 0)"
        [trackByFn]="trackByFn"
      >
        <ng-template let-item>Some template</ng-template>
      </app-virtual-list>
    </ng-container>