components icon indicating copy to clipboard operation
components copied to clipboard

Scrolling SDK (virtual-scroll) - View isn't updated when list is updated (items added/removed)

Open Gil-Epshtain opened this issue 6 years ago • 21 comments

What is the expected behavior?

If a data-list that binded to <cdk-virtual-scroll-viewport>, is updated (items are added/ removed from this list), the <cdk-virtual-scroll-viewport> updated as well.

What is the current behavior?

Changing the list will not updated the view (<cdk-virtual-scroll-viewport>)

What are the steps to reproduce?

StackBlitz starter: https://stackblitz.com/edit/angular-hsr4tz?file=app%2Fcdk-virtual-scroll-overview-example.html
In this demo you can see that after adding items to the list, the view isn't updated.

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Angular 7.1.4 Material 7.2.0 Browser: Chrome

Is there anything else we should know?

The documentation doesn't discuss about adding/removing items from data-list. See docs at: https://material.angular.io/cdk/scrolling/overview

Gil-Epshtain avatar Dec 25 '18 13:12 Gil-Epshtain

when will this fix be released?

klew582 avatar Mar 15 '19 23:03 klew582

Try this:

add() {
    this.items.push('NEW - ' + this.items.length);
    this.items = [...this.items];
}

https://stackblitz.com/edit/angular-hsr4tz-mz26wq

elgs avatar Mar 23 '19 15:03 elgs

@elgs - I like your answer, you can even make it one-liner with: this.items = [...this.items, ('NEW - ' + this.items.length)];

However, you need to remember that "virtual-scroll" directive purpose is to make an extremely long (thousands items or even more) list to be more smooth. But if each time you need to "Add" an item, you regenerate the whole list... then you lose the purpose of this directive, since re-creating the whole list each time you add an item, is very inefficient.

Gil-Epshtain avatar Mar 24 '19 14:03 Gil-Epshtain

@Gil-Epshtain agreed!

@crisbeto any chance to get any update about this fix? Thanks.

elgs avatar Mar 25 '19 08:03 elgs

@Gil-Epshtain from my experience, this workaround is actually not bad, I mean, performance wise. The selling point of the virtual scroll is to swap in/swap out rendered items from the backed full item list. With this workaround, the backed full item list got completely redone, but the rendered window (of 50, let's say) of items remains 50. It may not be ideal, but this actually does not affect the selling point of the scroll view. My $.02.

elgs avatar Mar 27 '19 17:03 elgs

However, you need to remember that "virtual-scroll" directive purpose is to make an extremely long (thousands items or even more) list to be more smooth.

This statement is true.

But if each time you need to "Add" an item, you regenerate the whole list... then you lose the purpose of this directive, since re-creating the whole list each time you add an item, is very inefficient.

No, you missed the point how the scroll view works. The scroll view has two parts: 1) The backing full list of items (could be thousands of), 2) the small amount of rendered items in DOM (like 50 items). The point of the scroll view is to prevent the thousands of items from being poured in the DOM. Copying the references of each items from the old backed list to a new list is actually cheap (though not ideal), the expensive part is rendering DOMs in the browser. In this case, you only redo the backed list, but the number of rendered DOM remain unchanged, so you don't lose the purpose of this directive.

elgs avatar Mar 27 '19 18:03 elgs

@elgs - I think the best way to test this is with a real world example - once I've implement this - I'll return with conclusions

Gil-Epshtain avatar Mar 28 '19 14:03 Gil-Epshtain

@Gil-Epshtain you can force a detect changes to update the virtual scroll from ChangeDetectionRef.detectChange(), this method force the view to detect the changes. That can update the virtual-scroll list;

 @Component({
      selector: 'app-component',
      templateUrl: './app-component.html',
      styleUrls: ['./app-component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush /* This is a strategy to detect changes by 
      demand,  it isn't necessary to your case;*/
 })

 items: AppComponentItems[] = [];

 constructor(changeDetectorRef: ChangeDetectorRef){}

 addItem(){
      items.push(newItem);
      changeDetectorRef.detectChange();
 }

renaoliveira avatar Apr 12 '19 14:04 renaoliveira

@Gil-Epshtain you can force a detect changes to update the virtual scroll from ChangeDetectionRef.detectChange(), this method force the view to detect the changes. That can update the virtual-scroll list;

 @Component({
      selector: 'app-component',
      templateUrl: './app-component.html',
      styleUrls: ['./app-component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush /* This is a strategy to detect changes by 
      demand,  it isn't necessary to your case;*/
 })

 items: AppComponentItems[] = [];

 constructor(changeDetectorRef: ChangeDetectorRef){}

 addItem(){
      items.push(newItem);
      changeDetectorRef.detectChange();
 }

Did not work for me.

murtuzamacdev avatar Apr 28 '19 06:04 murtuzamacdev

Did not work for me.

Same for me changeDetectorRef is not working

alexdabast avatar Jun 27 '19 08:06 alexdabast

I don't think this is a bug. Change detection is not intended to be "deep" checking each item in the array. It just checks the reference. Because you are using push, the reference does not change. You should use deconstructing like @elgs mentioned. You can use trackBy to prevent re-rendering the whole list

literalpie avatar Jul 01 '19 15:07 literalpie

I don't think this is a bug. Change detection is not intended to be "deep" checking each item in the array. It just checks the reference. Because you are using push, the reference does not change. You should use deconstructing like @elgs mentioned. You can use trackBy to prevent re-rendering the whole list

I'm wondering why a regular *ngFor can detect changes on a push ? I mean pushing an item in a array then showing the element with a *ngFor is working fine Shouldn't we expect the same behaviour for the *cdkVirtualFor

alexdabast avatar Jul 02 '19 07:07 alexdabast

Help:: After assigning list as new object, it moves the scrolling position to the top and not retaining the scrolling position.

moneychaudhary avatar Sep 05 '19 10:09 moneychaudhary

Is the issue fixed? when will this be available for us?

MayuriRathod avatar Nov 12 '19 06:11 MayuriRathod

this.items = [...this.items, ('NEW - ' + this.items.length)]; worked with me, but how to replace existing object with new one

medTL avatar Dec 10 '19 09:12 medTL

now it is 8.2021 and this issue is still not be fixed ? and i found no mention about this unexpected behavior in the docs. I for sure have not the intention to make a copy of all the data on every update this makes the whole virtual scroll quite irreal. Please what is the reason for this strategy, do i miss something ?

wernert avatar Aug 11 '21 00:08 wernert

@wernert there is a trade off. Change detection is expensive, even so for a list. I think your best bet would be to use the [...someArray].

elgs avatar Aug 11 '21 00:08 elgs

I just had a use case where the immutable array approach ([...someArray]) was not feasible due to frequent updates to the large array. Luckily *cdkForOf also accepts the array as an Observable. That means you can mutate the array and the Observable successfully notifies *cdkForOf when it needs to update:

<cdk-virtual-scroll-viewport>
  <div *cdkVirtualFor="let item of items$">{{ item }}</div>
</cdk-virtual-scroll-viewport>

<button (click)="add()">Add item</button>
  items$ = new BehaviorSubject(
    Array.from({ length: 100 }).map((_, i) => this.item(i))
  );

  add() {
    const items = this.items$.getValue();
    items.push(this.item(items.length));
    this.items$.next(items);
  }

  private item(index: number): string {
    return `Item #${index}`;
  }

StackBlitz Example

samherrmann avatar Jul 15 '22 20:07 samherrmann

Approach suggested by @samherrmann worked for me. Saved my hours of debugging.

ChaitanyaBabar avatar Oct 07 '22 12:10 ChaitanyaBabar

I just had a use case where the immutable array approach ([...someArray]) was not feasible due to frequent updates to the large array. Luckily *cdkForOf also accepts the array as an Observable. That means you can mutate the array and the Observable successfully notifies *cdkForOf when it needs to update:

<cdk-virtual-scroll-viewport>
  <div *cdkVirtualFor="let item of items$">{{ item }}</div>
</cdk-virtual-scroll-viewport>

<button (click)="add()">Add item</button>
  items$ = new BehaviorSubject(
    Array.from({ length: 100 }).map((_, i) => this.item(i))
  );

  add() {
    const items = this.items$.getValue();
    items.push(this.item(items.length));
    this.items$.next(items);
  }

  private item(index: number): string {
    return `Item #${index}`;
  }

StackBlitz Example

u are a legend

sabindev avatar Feb 21 '23 14:02 sabindev

If none of the solutions here helped you, you might try reducing the templateCacheSize. I couldn't see updates but playing with the cache finally helped for me.

View Recycling

RonStrauss avatar Apr 16 '24 08:04 RonStrauss