7

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.

adrisons
  • 1,923
  • 1
  • 16
  • 28

3 Answers3

4

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,196
  • 9
  • 15
  • 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
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));
      }
    })();
  }
  • 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
0

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