Components: virtual-scroll: support items of unknown size

Created on 23 Feb 2018  路  77Comments  路  Source: angular/components

Support virtual scrolling over a list of items whose size is not known and needs to be measured

G P2 cdscrolling feature

Most helpful comment

I'm going to be on vacation for most of July, but I've increased the priority as its something I'd like to work on after I get back

All 77 comments

Been testing autosize and seems to work pretty well.

Is this released? Or are any extra steps needed? I'm getting

ERROR Error: StaticInjectorError(AppModule)[CdkVirtualScrollViewport -> InjectionToken VIRTUAL_SCROLL_STRATEGY]: 
  StaticInjectorError(Platform: core)[CdkVirtualScrollViewport -> InjectionToken VIRTUAL_SCROLL_STRATEGY]: 

edit: looks like you need to have both

import { ScrollDispatchModule } from '@angular/cdk/scrolling';
import { ScrollingModule } from '@angular/cdk-experimental/scrolling';

imported for autosize to work

Looking forward to see this feature! :+1:

It seems to be working fine in most of the time, except that there is always an empty space on the bottom that keeps increasing in size each time you scroll down and up a bit.

@fxck could you please share some of your code to show how to use this feature?

Just import import { ScrollingModule } from '@angular/cdk-experimental/scrolling'; and use it with autosize directive.

<cdk-virtual-scroll-viewport [style.height.px]="height" autosize>
  <div *cdkVirtualFor="let item of list">{{ item }}</div>
</cdk-virtual-scroll-viewport>

yes, it works for me :)

But if I use autosize strategy it can not use many functions such as scrollToIndex() ...
Is there any alternative way? For example I am going to scroll to bottom

That's most likely the reason why this is experimental and not in stable. It's just not finished and feature complete yet.

I see. I have tested autosize feature with a list of 121000 + different height items and it works well without any lag. :+1:
Is there any ETA for this feature? (but with normal functions as well :) ) This is really cool. We are going to use this for our project and really want to see this to be released!!!

any update on this feature?

@mmalerba Any updates on this? Thanks

Will this be implemented in production soon?

I've been trying to get autosize strategy to work based on fxck's example, but keep getting this error: "Can't bind to 'cdkVirtualForOf' since it isn't a known property of 'div'. " Any suggestions?

I've been trying to get autosize strategy to work based on fxck's example, but keep getting this error: "Can't bind to 'cdkVirtualForOf' since it isn't a known property of 'div'. " Any suggestions?

Try with the cdk version angular 6.0.2

any updates ? 7.0.4 angular say - Can't bind to 'cdkVirtualForOf'
angular 6.4.7 bug with autosize ( end scroll with space )

any updates ? 7.0.4 angular say - Can't bind to 'cdkVirtualForOf'
angular 6.4.7 bug with autosize ( end scroll with space )

Have you tried importing both the core and experimental scroll modules? I found this alleviated the Can't bind to cdkVirtualForOf issue.

import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ExperimentalScrollingModule} from '@angular/cdk-experimental/scrolling';

@NgModule({
  declarations: [
    AppComponent  
  ],
  imports: [
    BrowserModule,  
    ScrollingModule,
    ExperimentalScrollingModule,    
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

But if I use autosize strategy it can not use many functions such as scrollToIndex() ...
Is there any alternative way? For example I am going to scroll to bottom

I am scrolling to the bottom as follows:

    let top = this.viewport.measureScrollOffset("top");
    let bottom = this.viewport.measureScrollOffset("bottom");
    let offset = top + bottom;    
    this.viewport.scrollToOffset(offset);

However, there are a few layout bugs it appears - but it's to be expected since it is still an experimental feature.

Any updates on this?

any updates ? 7.0.4 angular say - Can't bind to 'cdkVirtualForOf'
angular 6.4.7 bug with autosize ( end scroll with space )

Have you tried importing both the core and experimental scroll modules? I found this alleviated the Can't bind to cdkVirtualForOf issue.

import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ExperimentalScrollingModule} from '@angular/cdk-experimental/scrolling';

@NgModule({
  declarations: [
    AppComponent  
  ],
  imports: [
    BrowserModule,  
    ScrollingModule,
    ExperimentalScrollingModule,    
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

yeah but im get error - itemSize need to be set... if im set 0, ofc scroll load all items... if set any size scroll work not like autosized (((

I have this:

"@angular/cdk": "^7.3.3",
"@angular/cdk-experimental": "7.3.3",
import { ScrollingModule } from '@angular/cdk-experimental/scrolling';
import { ScrollDispatchModule } from '@angular/cdk/scrolling';

imports: [ScrollDispatchModule, ScrollingModule],

And everything works ok with autosize.

@easyproger
What worked for me is itemSize="50" for the cdk-virtual-scroll-viewport and [style.height]="'auto'" on the li without any additional height styles applied.

I am importing both

import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ExperimentalScrollingModule } from '@angular/cdk-experimental/scrolling';

P.S. for some reason adding autosize causing occasional empty space without any list items at the bottom of the viewport.

I tried to integrate mat-expansion-panels as list items. It's causing a few bugs while scrolling.

1) I click on one item to open it
2) I scroll down
3) Suddenly other elements are open as well even if I didn't open them manually. The scrolling behaviour is not smooth anymore.

Does anybody know a workaround for this?

Any update on this? Particularly the re-introduction of the scrolledIndexChange event with variable height items. Unless anyone knows of a work-around to be able to identify when the bottom of the list has been scrolled to, so that I know to fetch more data from the back-end?

@mmalerba in Ionic the item height is optional, but when it is provided, the calculation after scrolling into a new item is skipped, resulting in a better performance:

https://github.com/ionic-team/ionic/blob/master/angular/src/directives/virtual-scroll/virtual-scroll.ts

  /**
   * An optional function that maps each item within their height.
   * When this function is provides, heavy optimizations and fast path can be taked by
   * `ion-virtual-scroll` leading to massive performance improvements.
   *
   * This function allows to skip all DOM reads, which can be Doing so leads
   * to massive performance
   */
  itemHeight?: ItemHeightFn;

Perhaps you can implement it in a similar way for Material?

I tried to integrate mat-expansion-panels as list items. It's causing a few bugs while scrolling.

  1. I click on one item to open it
  2. I scroll down
  3. Suddenly other elements are open as well even if I didn't open them manually. The scrolling behaviour is not smooth anymore.

Does anybody know a workaround for this?

I would expect it have something to do with how caching works - try setting templateCacheSize: 0 on the cdkVirtualFor and test it out.

Any update on this? Particularly the re-introduction of the scrolledIndexChange event with variable height items. Unless anyone knows of a work-around to be able to identify when the bottom of the list has been scrolled to, so that I know to fetch more data from the back-end?

You have found any solution or work-around? Same problem here.

Also has someone else experienced this bug ?
https://angular-virtualdom-bug.stackblitz.io/
With 2 or 3 items inside the virtual scroll (depend on the height of your item) if you scroll fast to the top (once you are at the bottom) one element disappear

Any news here? This is ne of my most wanted features.

I'm going to be on vacation for most of July, but I've increased the priority as its something I'd like to work on after I get back

Will this strategy support dynamic height of the viewport too?

This is great! Are there any plans to integrate this with Material Flat Trees directly? I have documents with large tree structures (with leaves of different sizes) that I would like to display side by side. Virtual scrolling seems to work pretty well when rendering the flattened tree structure manually within the viewport. It would be awesome if I could just activate virtual scrolling with a setting in FlatTreeControl or so...

I check every day about 10 times this issue, cant wait to get this feature 馃憤

Hello, we are also looking forward to this feature.
The experimental version is working almost correctly for us with but 2 flaws:

  1. When initially loading a virtual scroll with many elements, but 2 rows stretching across the entire view area, more than two rows are loaded until you scroll to the last element.
  2. After scrolling to the last element, it is impossible to scroll all the way back up. The view is cut off.

@mmalerba any news?

We need this feature. Still waiting for it. :smile_cat:

It seems to be working fine in most of the time, except that there is always an empty space on the bottom that keeps increasing in size each time you scroll down and up a bit.

Facing the same issue

Any update on this? Particularly the re-introduction of the scrolledIndexChange event with variable height items. Unless anyone knows of a work-around to be able to identify when the bottom of the list has been scrolled to, so that I know to fetch more data from the back-end?

Did you resolve it?

This is such a cool feature, any plans on releasing it to production yet?

This is such a cool feature, any plans on releasing it to production yet?

For that it would need to work properly, I suppose.

Cant release my image board before this feature is done ;P

As of now is there any way to have responsive list design along with virtual scroll?

I'm using Angular Flex Layout along with virtual scroll, so I believe we can be aware of what will be the size of items on any specific screen size, but can we bind itemSize property to populate it dynamically,

it won't be essentially updated once assigned apart from probably when orientation changes on Tabs or cellular devices.

someone got a good working service to calculate (without to much lost of performance) the hight of items?

Scrolling down with autosize directive is working fine but while scrolling up its jumping to the start of the list.

Anyone in the world other than me worrying about this? Is this issue a solvable or can we solve it by proving it can't be solved?

@SvenBudak @tibinthomas what I did to solve that issue instead of scrolling directly to the selected element I scroll element by it's height height to the top or bottom, check if element I look for is visible, if not scroll again until element is found. It's not perfect but it works in most situations.

@mmalerba said they would get back to this after vacation, but seems like they are working on other things now :/

Yes, supporting Ivy and creating new Test Harnesses has taken priority over this work. This is still a high priority issue that the team intends to address.

any news when this is going to be released?

cdk-virtual-scroll-viewport makes no sense to me with a fixed height.

the most important thing is that those inner items must be of fixed height

Is this thread not virtual scroll, for items of unknown size?

any update on this? My user case needs various size of rows. Using the fixed size virtual-scroll has a lot of jumping issues, but the auto-sized one is still not production ready?

It's not. I wouldn't hold my breath. This library supports varying row height with a cache:
https://github.com/rintoj/ngx-virtual-scroller

in autosize, scroll down event working fine while scrolling up facing jump issue when scrolling gets to stop. any update for the same?

Here's a brute approach to a very annoying missing functionality that nobody from the angular team seems to care about.

interface RowInfo {
    height: number
    id: string
}

export class CustomVirtualScrollStrategy implements VirtualScrollStrategy {

    private viewport:CdkVirtualScrollViewport;
    private rowSizeCache = new Map<string, RowInfo>();
    private orderedItemIds:string[] = [];
    scrolledIndexChange = new Subject<number>();
    attachedSubject = new Subject<CdkVirtualScrollViewport>();


    constructor(private minItemHeight:number=48, private renderItemsBefore=2, private renderItemsAfter=2) {
    }

    onContentRendered(): void {
    }

    attach(viewport: CdkVirtualScrollViewport): void {
        if(viewport == this.viewport){
            return
        }
        this.attachedSubject.next(viewport);
        this.viewport = viewport;

        const changeObserver = new Observable<HTMLElement>(subscriber => {

            const watchedEl = viewport.elementRef.nativeElement.firstChild as HTMLElement;

            const mutationObserver = new MutationObserver(() => {
                subscriber.next(watchedEl)
            });

            mutationObserver.observe(viewport.elementRef.nativeElement.firstChild, {
                attributes: true,
                //characterData: true,
                childList: true,
                subtree: true,
                attributeOldValue: true,
                //characterDataOldValue: true
            });

           return () => {
               mutationObserver.disconnect();
           }
        });

        changeObserver.pipe(
            debounceTime(100),
            takeUntil(this.attachedSubject)
        ).subscribe(rootEl => {
            let changed = false;
            // each child of cdk-virtual-scroll-viewport should have data-row-id and data-row-index attributes defined
            // <div *cdkVirtualFor="let row of dataSource.onData() | async; let index = index;" [attr.data-row-id]="row.getId()" [attr.data-row-index]="index">
            const list = Array.from(rootEl.querySelectorAll("[data-row-id][data-row-index]")) as HTMLElement[];



            for(const el of list){
                const id = el.getAttribute('data-row-id');
                const idx = parseInt(el.getAttribute('data-row-index'), 10);
                let rowInfo = this.rowSizeCache.get(id);
                if(!rowInfo){
                    rowInfo = {height:0, id: id};
                    this.rowSizeCache.set(id, rowInfo)
                }

                const clientHeight = el.clientHeight;

                if(rowInfo.height != clientHeight){
                    rowInfo.height = clientHeight;
                    changed = true
                }

                if(!isNaN(idx)){
                    this.orderedItemIds[idx] = id;
                }
            }

            if(changed){
                this.updateTotalContentSize();
                this.updateRenderedRange()
            }

        })

    }

    private updateRenderedRange() {
        if (!this.viewport) {
            return;
        }

        // TODO handle item removal
        // TODO handle reordering items
        // TODO various other optimizations can be performed as we don't always need to check each row height

        const scrollOffset = this.viewport.measureScrollOffset();
        const viewportSize = this.viewport.getViewportSize();
        const maxOffset = scrollOffset+viewportSize;
        const maxLastVisibleIndex = Math.ceil(maxOffset / this.minItemHeight);

        const dataLength = this.viewport.getDataLength();

        let offset = 0;
        const visibleRange = {startIndex:NaN, endIndex:NaN, startOffset:0, endOffset:0}; // contains rows which are partially or fully visible
        for(let i=0;i < Math.min(dataLength, maxLastVisibleIndex); i++){

            let itemHeight = this.minItemHeight;
            const itemId = this.orderedItemIds[i];
            if(itemId){
                itemHeight = this.rowSizeCache.get(itemId).height
            }

            if(offset > maxOffset){
                break;
            }

            if(offset+itemHeight >= scrollOffset){
                if(isNaN(visibleRange.startIndex)){
                    visibleRange.startIndex = i;
                    visibleRange.startOffset = offset;
                }

                visibleRange.endIndex = i;
                visibleRange.endOffset = offset + itemHeight;
            }

            offset += itemHeight;

        }

        const firstVisibleIndex = visibleRange.startIndex;

        for(let i=this.renderItemsBefore; i>0 && visibleRange.startIndex > 0; i-=1){
            visibleRange.startIndex -= 1;
            visibleRange.startOffset -= this.getItemHeight(visibleRange.startIndex)
        }

        for(let i=this.renderItemsAfter; i>0 && visibleRange.endIndex < dataLength-1; i-=1){
            visibleRange.endIndex += 1;
            visibleRange.endOffset += this.getItemHeight(visibleRange.endIndex)
        }

        this.viewport.setRenderedRange({start:visibleRange.startIndex, end: visibleRange.endIndex+1});
        this.viewport.setRenderedContentOffset(visibleRange.startOffset);
        this.scrolledIndexChange.next(firstVisibleIndex);
    }

    private updateTotalContentSize() {
        if (!this.viewport) {
            return;
        }

        let totalHeight = 0;

        for(let i=0; i< this.viewport.getDataLength(); i++){
            totalHeight += this.getItemHeight(i);
        }

        this.viewport.setTotalContentSize(totalHeight);
    }

    private getItemHeight(index: number): number{
        const itemId = this.orderedItemIds[index];
        if(itemId){
            return this.rowSizeCache.get(itemId).height
        }
        return this.minItemHeight;
    }


    detach(): void {
        if(this.viewport){
            this.viewport = null;
            this.attachedSubject.next(null)
        }
    }

    onContentScrolled(): void {
        this.updateRenderedRange();
    }

    onDataLengthChanged(): void {
        this.updateTotalContentSize();
        this.updateRenderedRange();
    }

    onRenderedOffsetChanged(): void {
    }

    scrollToIndex(index: number, behavior: "auto" | "smooth"): void {
        //TODO
    }


}

Working Example

feed.component.html

 <cdk-virtual-scroll-viewport
        #viewport class="example-viewport" 
[style.height.px]="getHeight"  <!-- calc(100vh)  -->
 autosize fxFlex>
                    <div  #listComponent  fxFlex fxLayout="column" *ngIf="messages && messages.length > 0">
                        <div
                             *cdkVirtualFor="let item of messages;let index = index; templateCacheSize: 0;
                                trackBy: indexTrackFn; let i = index"
                                  class="feed-messages"  [style.height.px]="item">
                            <app-message *ngIf="item?.senderId"
                                         [messageInput]="item"
                                         [messageUser]="chatUsers.users[item.senderId]"
                            ></app-message>
                        </div>
                    </div>
                </cdk-virtual-scroll-viewport>

feed.component.ts

export class FeedComponent implements OnInit, AfterViewInit , OnDestroy {
  @ViewChild(CdkVirtualScrollViewport, {static: true}) viewport: CdkVirtualScrollViewport;
  @Input('isPageLoaded') set setIsPageLoaded(isPageLoaded) {
    if (isPageLoaded) {
      (<any>this.viewport).checkViewportSize();
      console.log('isPageLoaded', isPageLoaded);
    }
  };
ngOnInit(): void {
    fromEvent(window, 'resize')
      .pipe(
        distinctUntilChanged(),
        debounceTime(10),
        takeUntil(this.deactivatedSubject)
      ).subscribe(() => {
           (<any>this.viewport).checkViewportSize();

    });
    this.viewport.elementScrolled().subscribe(() => {
      console.log(this.viewport.getRenderedRange()) //{start: 4, end: 19}
    })
  }

}

@dudipsh Can you share online example?

I'm trying with 9.2.0 version and receiving error message:

ERROR Error: "Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set."

@arkadiusz-wieczorek

did you import the ScrollingModule from cdk-experimental?

import { ScrollingModule } from '@angular/cdk/scrolling';
import { ScrollingModule as ExperimentalScrollingModule} from '@angular/cdk-experimental/scrolling';

@NgModule({
  declarations: [
  ],
  imports: [
    ScrollingModule,
    ExperimentalScrollingModule,
  ]
})
export class FeedModule { }

thanks @dudipsh, my fault!

Looking at using an expander in virtual scroll ie a click a button and an item height expands to contain a sub-menu, which can vary per item.

Tried using the the autosize, but didn't seem to work quite right.

Is the plan for this this be able to also support this?

Autosize works but there are other features missing then. For example scrolledIndexChange is not working. But I really need to know which element is at the top.

Autosize works but there are other features missing then. For example scrolledIndexChange is not working. But I really need to know which element is at the top.

Have you tried to use the viewport to get that information?
using the this.viewport.getRenderedRange()

see the example : https://github.com/angular/components/issues/10113#issuecomment-607102447

@buenjybar

    ngOnInit(): void {
        this.route.paramMap.subscribe((res) => {
            this.activeChannel = res.get('id');
            this.chatService.readMessagesInChannel(this.activeChannel, 0, 40).subscribe((channelData: any) => {
                this.messages = channelData.data.reverse().map((item, index) => {
                    this.scrollTo(index * 299, 'auto');
                    return item;
                });
                this.startIndex = channelData.totalCount;
            });
        });

        this.viewport.elementScrolled()
            .pipe(debounceTime(150))
            .subscribe((res) => {
                const {start, end} = this.viewport.getRenderedRange();
                if (start === 0) {
                    this.fetchMore();
                }
            });

    }

    scrollTo(size, behavior: 'auto' | 'smooth' = 'auto') {
        setTimeout(() => {
            this.viewport.elementRef.nativeElement.scrollTo({top: size, behavior, left: 0});
        }, 300);
    }

Autosize works but there are other features missing then. For example scrolledIndexChange is not working. But I really need to know which element is at the top.

Have you tried to use the viewport to get that information?
using the this.viewport.getRenderedRange()

see the example : #10113 (comment)

Yes, sure with some hack you can make it work. It still would be nice if the API that the components provide would work no matter if item size is fixed or dynamic. I mean the (scrolledIndexChange) output.

@ewalddieser

I usually agree with this approach
But this issue opened from 2018, it looks like it's time for "hacks"

By the way
Denis Hilt
works on a solution for a chat experience
virtual scroll that starts from the bottom with auto height
its in progress but its looks like a very good solution
https://github.com/dhilt/ngx-ui-scroll

working example
https://stackblitz.com/edit/ngx-ui-scroll-chat-app

When it will be released?

@dudipsh Can you share online example?

I'm trying with 9.2.0 version and receiving error message:

ERROR Error: "Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set."

@arkadiusz-wieczorek
I moved to this package https://github.com/dhilt/ngx-ui-scroll no issues... work perfectly

I am looking forward to this feature too

'cdk-virtual-scroll-spacer' is not taking height beyond 22339600px, that means user can not scroll down further if list showing in virtual table is beyond 22339600px height.
example: i have log viewer with millions of records, cdk-virtual-scroll-spacer height is calculated based on itemSize * numberOfItems.
to show 2 millions of records with 20px row height need 40million px cdk-virtual-scroll-spacer height, but chrome is limiting cdk-virtual-scroll-spacer height to 22339600px.
how do we overcome this limit ?

@vraddy this has nothing to do with this ticket. Also if you think scrolling through 2 million entries is feasible, reconsider your design choices (i.e. truncate your data).

any news about this ticket?

Any progress for this issue?

ngx-virtual-scroller lib is best for unknown size just used it till any next update.
https://github.com/rintoj/ngx-virtual-scroller/

@thackerronak its not really working. the guy who created this one is not available for accepting merge requests and he also cant update the npm package. My team was testing community updates from ngx-virtual-scroller but its not working well. alot bugs and problems.

We have to wait for cdk solution. The Ionic Team wrote anything about that ion-virtual-scroll will switch to cdk. maybe ionic team is working together with the angular team on this problem. we wait already 4 years. I am not sure that this fix will come any day... i have already 4 apps they are ready to release if this cdk fix is out. i worked last 5 years on them... i just hope it will come...

Does https://github.com/rintoj/ngx-virtual-scroller/ work with tables that have sticky headers?

@Mmatiasn
i used "ngx-virtual-scroller" ( for NPM package )
you can try to fork and implement sticky headers
https://github.com/dudipsh/ngx-ui-table

If anyone has stumbled across the requirement for items of different, but known size (a mixture between the fixed and autosize scrolling strategies) - I've elaborated more in this SO question.

Sad to say, but the performance is much worse comparing to the fixed scrolling strategy.

Was this page helpful?
0 / 5 - 0 ratings