ngx-virtual-scroller icon indicating copy to clipboard operation
ngx-virtual-scroller copied to clipboard

Loading in chunks, vsEnd keeps triggering with endIndex === buffer.length-1

Open SpeedoPasanen opened this issue 6 years ago • 13 comments

What am I doing wrong? Without scrolling at all, vsEnd triggers everytime pics$ is updated, and with an endIndex value equaling the length of items-1. Should I not use AsyncPipe? Does virtual-scroller need to have a fixed pixel height instead of 100% (.h-100)?

@Component({
  selector: 'app-pic-list',
  templateUrl: './pic-list.component.html',
  styleUrls: ['./pic-list.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PicListComponent implements OnInit, OnDestroy {
  private take = 100;
  destroy$ = new Subject();
  pics$: Observable<IPic[]>;
  private buffer: IPic[] = [];
  private loading = false;
  private gotAll$ = new Subject();
  @ViewChild('scroll', { static: true }) scroll: VirtualScrollerComponent;
  constructor(private api: ApiService) {}

  ngOnInit() {
    this.pics$ = this.scroll.vsEnd.pipe(
      takeUntil(this.destroy$),
      takeUntil(this.gotAll$),
      startWith(null),
      filter(
        event =>
          !this.loading && (!event || event.endIndex >= this.buffer.length - 1)
      ),
      tap(e => console.log(e, this.buffer.length)),
      tap(() => (this.loading = true)),
      switchMap(() =>
        this.api
          .get<IPic[]>('pics', {
            take: this.take,
            skip: this.buffer.length
          })
          .pipe(
            tap(pics => pics.length < this.take && this.gotAll$.next()),
            tap(pics => (this.buffer = this.buffer.concat(pics))),
            map(() => this.buffer),
            tap(() => (this.loading = false))
          )
      )
    );
  }
  ngOnDestroy() {
    this.destroy$.next();
  }
}
<virtual-scroller #scroll [items]="pics$ | async" class="h-100">
  <app-pic [pic]="pic" *ngFor="let pic of scroll.viewPortItems"></app-pic>
</virtual-scroller>
// app-pic.scss
:host { display:inline-block; height:200px; width:200px; } 
// Real scroll position should be close to 0
// tap(e => console.log(e, this.buffer.length))
$event: { 
  endIndex: 699
  endIndexWithBuffer: 699
  maxScrollPosition: 0
  scrollEndPosition: 28000
  scrollStartPosition: 0
  startIndex: 0
  startIndexWithBuffer: 0
}
this.buffer.length: 700

SpeedoPasanen avatar Jun 01 '19 07:06 SpeedoPasanen

Tried also :host { display:block; height:200px } for app-pic, produced the same eternal vsEnd-loop so it's not due to being multi column.

SpeedoPasanen avatar Jun 01 '19 07:06 SpeedoPasanen

Ok so indeed this seems to be caused by using any percentage height instead of pixels (height:50% etc, still same). <virtual-scroller #scroll [items]="pics$ | async" style="height:500px"> fixes it.

So how to fill the container height?

SpeedoPasanen avatar Jun 01 '19 08:06 SpeedoPasanen

Ugly ass work-around below. Any better way?

hostHeight$: Observable<number>;
constructor(private api: ApiService, private host: ElementRef) {}

ngOnInit() {
   this.hostHeight$ = fromEvent(window, 'resize').pipe(
      startWith(null),
      debounceTime(300),
      map(() => this.host.nativeElement.offsetHeight),
      tap(console.log)
   );
<virtual-scroller
  #scroll
  [items]="pics$ | async"
  [style.height]="(hostHeight$ | async) + 'px'"
>

SpeedoPasanen avatar Jun 01 '19 08:06 SpeedoPasanen

Please convert your code to a StackBlitz if you'd like help with troubleshooting.

speige avatar Jun 01 '19 10:06 speige

Weird, I can't reproduce it in Stackblitz which uses Angular 7, I'm on 8 with Ivy. Maybe it's got something to do with Ivy (and it not being prod rdy yet). :D Every vsEnd event just has the endIndex set to the last item and scrollEndPosition set to the highest possible value.

Sorry. I'll keep an eye on it later when Ivy becomes standard. For now, doesn't seem like anything to waste more time on.

Blitz anyway in case you wanna test with Ivy. All should be identical to the way I'm doing it in my actual app: https://stackblitz.com/edit/angular-1sw1l2

SpeedoPasanen avatar Jun 01 '19 20:06 SpeedoPasanen

No wait, it was the Grid layout that was missing, that broke it. Updated Blitz. https://stackblitz.com/edit/angular-1sw1l2

Conclusion is if virtual-scroller is inside an 1fr grid row it doesn't matter if the component itself is display:block or flex, if it has a 100% height instead of a fixed pixel height the vsEnd event gets the wrong scroll position ie. thinks it's always scrolled to the bottom.

Setting the grid row height to a pixel amount also does fix this, eg. if you do this in the Blitz: grid-template-rows: auto 500px; instead of grid-template-rows: auto 1fr;

SpeedoPasanen avatar Jun 01 '19 20:06 SpeedoPasanen

Interesting. Thanks for creating the stackblitz. I can reproduce the issue now.

I'm not sure off hand why it's broken, My first guess is that the grid layout is somehow causing the code to get incorrect measurements, but I'll need to do some debugging/troubleshooting.

I'm glad you found a workaround for now.

speige avatar Jun 03 '19 08:06 speige

I too have the same issue. I put cards in the tab container. It is rendering properly. but it auto loads the remaining items as well without scroll interaction. Please any one suggest a workaround for this.

arunkumarsubburaj avatar Sep 23 '19 13:09 arunkumarsubburaj

I too have the same issue. I put cards in the tab container. It is rendering properly. but it auto loads the remaining items as well without scroll interaction. Please any one suggest a workaround for this.

This works fine assuming your host component is taking up all the available viewport height (eg. flex): https://github.com/rintoj/ngx-virtual-scroller/issues/336#issuecomment-497924042

SpeedoPasanen avatar Sep 23 '19 18:09 SpeedoPasanen

I'm using css grid as it works very well for masonry layout. The workaround to this problem for me was to use distinctUntilKeyChanged('scrollEndPosition') when subscribing to the vsEnd event.

spenoir avatar Oct 14 '20 11:10 spenoir

I'm using css grid as it works very well for masonry layout. The workaround to this problem for me was to use distinctUntilKeyChanged('scrollEndPosition') when subscribing to the vsEnd event.

But, the vsEnd event still fires in an infinite loop correct? You are just ignoring it?

raysuelzer avatar Oct 04 '21 23:10 raysuelzer

Not infinite but a hell of a lot more than it should. It just fires a whole load of duplicate events. Last time I checked it fired around 7 or 8 times I think.

On Tue, Oct 5, 2021 at 12:56 AM Ray Suelzer @.***> wrote:

I'm using css grid as it works very well for masonry layout. The workaround to this problem for me was to use distinctUntilKeyChanged('scrollEndPosition') when subscribing to the vsEnd event.

But, the vsEnd event still fires in an infinite loop correct? You are just ignoring it?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rintoj/ngx-virtual-scroller/issues/336#issuecomment-933944152, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABP4IEDAYMCJHYZCGW3EL3UFI5LVANCNFSM4HR76EKA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.

spenoir avatar Oct 05 '21 17:10 spenoir

Yeah, it fires infinitely for me when you reach the end. It seems to have to do with an improper or failure to round fractional values. Forgetting the exact issue, but I'm pretty sure that someplace it is comparing the current scroll position (rounded) with the scroll position not rounded. So if you end up having a difference there then it will loop infinitely.

raysuelzer avatar Oct 07 '21 21:10 raysuelzer