Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Obtaining a static component reference within a cdk-virtual-scroller? (References are recycled)

We recently transitioned our scrollable lists to CDK Virtual Scroller. (Angular 7.2.12 with angular/cdk 7.3.7)

In short, it seems that the VirtualScrollViewport is recycling component instances, not just the template as the documentation suggests.

Live MCVE on StackBlitz (updated to reflect EDIT 1).

EDIT 1

A colleague reminded me that we're now using named references instead of ViewChildren(), like so:

HelloComponent (inside the *cdkVirtualFor):

@Component({
  selector: 'hello',
  template: `<h1 [class.active]="active">Data Item {{item}} !</h1>`,
  styles: [`.active {background-color: red; color: white}`]
})
export class HelloComponent  {
  @Input() item: any;
  active: boolean = false;
  toggle = () => this.active = !this.active;
}

And implementing it in the App like:

<cdk-virtual-scroll-viewport itemSize="75">
  <ng-container *cdkVirtualFor="let item of data" templateCacheSize=0>
    <hello #hi [item]="item" (click)="clickByReference(hi)"></hello>
  </ng-container>
</cdk-virtual-scroll-viewport>

// Non-essentials hidden, see StackBlitz
export class AppComponent  {
  data = Array.from(Array(100).keys())
  clickByReference = (element: any): void => element.toggle();
}

It will change the background colour of the clicked element to red, but when scrolling, others (presumably those that match some cached index?) will already be red! Activating one of those will clear the original as well.

The source suggests that templateCacheSize might help, but it doesn't.

Original

The scrollable area contains components which we get a reference to with a @ViewChildren() and QueryList and we track which one we are acting on using an index in the *ngFor (now *cdkVirtualFor), like so:

<cdk-virtual-scroll-viewport itemSize="75">
  <ng-container *cdkVirtualFor="let item of data; let i = index">
    <hello  #hi
            [item]="item"
            (click)="click(i)"></hello>
  </ng-container>
</cdk-virtual-scroll-viewport>

Then, from the page, we communicate with the component in the list:

export class AppComponent  {
  @ViewChildren('hi') hiRefs: QueryList<HelloComponent>;
  data = Array.from(Array(100).keys())

  click = (i: number) => this.hiRefs["_results"][i].say(`Hello as ${i}`);
}

Of course, now that the template is rendered in a virtual scroll container, only the first n are rendered into the DOM. So if you scroll down the list beyond what is initially loaded, hiRefs does not contain a reference to the item with the corresponding index, throwing a ReferenceError for the provided ["_results"][i].

I experimented with trackBy but didn't get anything fruitful.

EDIT: A colleague has also attempted to pass a named reference, which curiously has the same problem.

Updating the HelloComponent to

@Component({
  selector: 'hello',
  template: `<h1 [class.active]="active">Data Item {{item}} !</h1>`,
  styles: [`.active {background-color: red}`]
})
export class HelloComponent  {
  @Input() item: any;
  active: boolean;

  say = (something: any) => this.active = !this.active;
}

And implementing it in the App like:

<hello #hi [item]="item" (click)="clickByReference(hi)"></hello>

It will change the background colour of the clicked element to red, but when scrolling, others (presumably those that match the same index) will already be red, despite not using the @ViewChildren() QueryList at all!

It seems that the CDK is recycling component instance references?

I updated the StackBlitz with the method clickByReference(), and renamed the one above to clickByIndex().

How can I correctly get a reference to the component in the list in order to call methods on it?

like image 766
msanford Avatar asked Apr 15 '19 19:04

msanford


1 Answers

By default, CdkVirtualForOf caches 20 ViewRefs to components that are no longer rendered into the DOM to improve scrolling performance.

While these update to show new bound @Input()s, they do not update their internal state, so previously-cached copies are re-used as a result.

It seems the only solution is to set templateCacheSize: 0:

<ng-container *cdkVirtualFor="let item of data; templateCacheSize: 0">

That way the components are destroyed once they're no longer visible, and state is lost.

Further reading https://github.com/angular/material2/issues/15838 and a doc PR.

like image 50
msanford Avatar answered Nov 16 '22 09:11

msanford