adrisons
adrisons

Reputation: 3723

Scroll to bottom with cdk-virtual-scroll (Angular 8)

Goal

Display a list of messages and scroll to the bottom when a new message is received, even when I am at the top. I would like to scroll fully bottom even with elements of different heights.

Problem

With virtual scroll, I have to set the [itemSize] property, but for me it is not a static value:

Also, I am using ng-content to insert a button from the parent to load previous messages. What I see is that, when _scrollToBottom is invoked, instead of taking me to the bottom, it takes me to a bit higher. I suspect this is because of the different heights of elements inside virtual-scroll.

I have read this autosize scroll strategy issue from Angular: https://github.com/angular/components/issues/10113; but I am not sure this will solve my problem.

Any idea of what I could do will be welcome.

Test

Codesandbox: https://codesandbox.io/s/angular-virtual-scroll-biwn6

Video with the error: https://gofile.io/d/8NG9HD


Solution

The solution given by Gourav Garg works. Simply by executing twice the scroll function.

I am doing this now:


  private _scrollToBottom() {
    setTimeout(() => {
      this.virtualScrollViewport.scrollTo({
        bottom: 0,
        behavior: 'auto',
      });
    }, 0);
    setTimeout(() => {
      this.virtualScrollViewport.scrollTo({
        bottom: 0,
        behavior: 'auto',
      });
    }, 50);
  }

I think it is not very elegant but works fine.

Upvotes: 10

Views: 15719

Answers (5)

Bullsized
Bullsized

Reputation: 577

Angular Material v17 here. Although the main solution proposes two timeouts, it is achievable with a callable function:

  // Access the CDK Virtual Scroll Viewport
  @ViewChild(CdkVirtualScrollViewport) viewport: CdkVirtualScrollViewport;

  viewportScrollToBottom(): void {
    this.viewport.scrollTo({
      bottom: 0,
      behavior: 'smooth',
    });
  }

Upvotes: 0

Joseph Zabinski
Joseph Zabinski

Reputation: 850

My particular use case was different from the original post, but similar enough to say something. If you are trying to scroll to the bottom of the CDK's virtual scrolling viewport using scrollToIndex, it isn't working, and you end up here, it might help you to know a few things.

  1. The viewport finishes initializing itself asynchronously in its ngOnInit. (See this code.) So if you try to interact with it before it is finished initializing, it silently fails. (This feature request would at least provide an error when an empty viewport is scrolled.)
  2. It has nothing declaring that initialization is finished. (See this feature request.)

If you call scrollToIndex directly in ngAfterViewInit on cdk-virtual-scroll-viewport's parent component, that won't work. You have to wait a tick before using scrollToIndex. See this Stackblitz. The important bits:

ngAfterViewInit(): void {
    // does nothing
    // this.viewPort.scrollTo({ bottom: 0, behavior: 'smooth' });
    this.scrollToIndex$.next();
  }

ngOnInit(): void {
    this.subscriptions.add(
      this.scrollToIndex$
        .pipe(***-->delay(0)<--***)
        .subscribe(() =>
          this.viewPort.scrollTo({ bottom: 0, behavior: 'smooth' })
        )
    );
  }

delay(0) waits one tick of the Angular lifecycle, which seems to be enough to allow the viewport to finish itself up.

Upvotes: 3

Srinath Kamath
Srinath Kamath

Reputation: 568

There's an alternative to cdk scroll.

Here's a genric version of code to scroll to a given HTML element. It can be used as a service function in angular or as a function in javascript.

scroll(el: HTMLElement, behaviour: any = "smooth", block: any = "start", inline: any = "nearest") {
    el.scrollIntoView({ behavior: behaviour, block: block, inline: inline })
  }

Sample HTML:

<div class="scroll-to-top" [ngClass]="{'show-scrollTop': windowScrolled}">
    Back to top<button mat-button mat-icon-button (click)="scrollToTop()">
        <mat-icon>keyboard_arrow_up</mat-icon>
    </button>
</div>

Sample Typescript component code:

@HostListener("window:scroll")
  onWindowScroll() {
    if (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop > 100) {
      this.windowScrolled = true;
    }
    else if (this.windowScrolled && window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop < 10) {
      this.windowScrolled = false;
    }
  }
  scrollToTop() {
    (function smoothscroll() {
      var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
      if (currentScroll > 0) {
        window.requestAnimationFrame(smoothscroll);
        window.scrollTo(0, currentScroll - (currentScroll / 8));
      }
    })();
  }

Upvotes: 1

Gourav Garg
Gourav Garg

Reputation: 2911

You can use if you are using cdk > 7

this.virtualScrollViewport.scrollToIndex(messages.length-1);

As this will move to the top of last item. You need to call scrollIntoView for that item.

this.virtualScrollViewport.scrollToIndex(this.numbers.length - 1);
setTimeout(() => {
  const items = document.getElementsByClassName("list-item");
  items[items.length - 1].scrollIntoView();
}, 10);
<cdk-virtual-scroll-viewport #virtualScroll style="height: 500px" itemSize="90">
  <ng-container *cdkVirtualFor="let n of numbers">
    <li class="list-item"> {{n}} </li>
  </ng-container>
</cdk-virtual-scroll-viewport>

I have updated your sandbox

Upvotes: 7

Adarsh Thakur
Adarsh Thakur

Reputation: 51

Use can use scrollIntoView() to scroll to the bottom of virtualScrollViewport

this.virtualScrollViewport.scrollIntoView(false);

refer https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

Upvotes: -1

Related Questions