9

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:

  • When a message is too long for one line it breaks in multiple, so its height changes.
  • I have different types of messages with different heights (system messages for example).

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

  • When messages are loaded, scroll up.
  • Send message. (When the new message is loaded, instead of scrolling to bottom, the virtual-scroll stops a little higher)
  • Repeat

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.

Kavinda Senarathne
  • 1,813
  • 13
  • 15
adrisons
  • 3,443
  • 3
  • 32
  • 48

4 Answers4

6

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

Gourav Garg
  • 2,856
  • 11
  • 24
  • Thanks for your response! It has the same behaviour with scrollToIndex. It does not go fully bottom, it leaves the last message below. – adrisons Nov 24 '20 at 11:33
  • Well, you can use a hack. Just add a class on all your items and after above event find last item and use scroll into view for that item as per @adarsh-thakur https://stackblitz.com/edit/angular-material-cdk-virtual-scrolling-r6vklg?file=src/app/app.component.ts – Gourav Garg Nov 25 '20 at 07:01
  • Tried it, does not work. Because of virtual-scroll only X elements are rendered, so when using `getElementsByClassName` only those are retrieved, and it scrolls to the last rendered element. Thanks for your answer – adrisons Nov 25 '20 at 11:04
  • Here is updated one sandbox https://codesandbox.io/s/angular-virtual-scroll-forked-25sz0?fontsize=14&hidenavigation=1&theme=dark – Gourav Garg Nov 25 '20 at 12:23
  • I don't see any difference :/ please follow test steps from my description – adrisons Nov 25 '20 at 14:37
  • Yeah I followed your steps and its working as you needed – Gourav Garg Nov 25 '20 at 15:14
  • Try sending some messages, scrolling top, and send another message. The scroll does not go fully bottom for me – adrisons Nov 25 '20 at 15:21
  • I updated this https://codesandbox.io/s/angular-virtual-scroll-forked-hruxe – Gourav Garg Nov 26 '20 at 10:16
  • Well, it works! The solution was to invoke twice the scroll function with different times in the setTimeout. – adrisons Nov 26 '20 at 11:37
3

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.

Joseph Zabinski
  • 693
  • 4
  • 23
1

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));
      }
    })();
  }
Srinath Kamath
  • 542
  • 1
  • 6
  • 17
  • 1
    My cdk is placed inside a component that does not share the window scroll. I think this would be a good solution for something like a blog. Thanks! – adrisons Nov 30 '20 at 15:12
-1

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