151

I have a set of single cell components within an ng-for loop.

I have everything in place but I cannot seem to figure out the proper

Currently I have

setTimeout(() => {
  scrollToBottom();
});

But this doesn't work all the time as images asynchronously push the viewport down.

Whats the appropriate way to scroll to the bottom of a chat window in Angular 2?

Sangwin Gawande
  • 7,658
  • 8
  • 48
  • 66
Max Alexander
  • 5,471
  • 6
  • 38
  • 52

19 Answers19

282

I had the same problem, I'm using a AfterViewChecked and @ViewChild combination (Angular2 beta.3).

The Component:

import {..., AfterViewChecked, ElementRef, ViewChild, OnInit} from 'angular2/core'
@Component({
    ...
})
export class ChannelComponent implements OnInit, AfterViewChecked {
    @ViewChild('scrollMe') private myScrollContainer: ElementRef;

    ngOnInit() { 
        this.scrollToBottom();
    }

    ngAfterViewChecked() {        
        this.scrollToBottom();        
    } 

    scrollToBottom(): void {
        try {
            this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
        } catch(err) { }                 
    }
}

The Template:

<div #scrollMe style="overflow: scroll; height: xyz;">
    <div class="..." 
        *ngFor="..."
        ...>  
    </div>
</div>

Of course this is pretty basic. The AfterViewChecked triggers every time the view was checked:

Implement this interface to get notified after every check of your component's view.

If you have an input-field for sending messages for instance this event is fired after each keyup (just to give an example). But if you save whether the user scrolled manually and then skip the scrollToBottom() you should be fine.

letmejustfixthat
  • 3,379
  • 2
  • 15
  • 30
  • I have a home component, in home template i have another component in the div which is showing data and keep appending data but scroll bar css is in the home template div not in the inside template. the problem is i m callling this scrolltobottom everytime data comes inside component but #scrollMe is in home component since that div is scrollable ... and # scrollMe is not accessable from inside component.. not sure how to used #scrollme from inside component – Anagh Verma Feb 09 '17 at 15:30
  • Your solution, @Basit, is beautifully simple and doesn't end up firing endless scrolls/need a workaround for when user manually scrolls. – theotherdy May 30 '17 at 21:01
  • 6
    Hi, is there a way to prevent scroll when user scrolled up? – John Louie Dela Cruz Jun 06 '17 at 02:24
  • To achieve a similar objective (scroll the main window so a particular element is visible), I used: this.myScrollContainer.nativeElement.scrollIntoView() – John Langford Jun 07 '17 at 21:56
  • 2
    I'm also trying to use this in a chat style approach, but needs to not scroll if the user scrolled up. I've been looking and can't find a way to detect if the user scrolled up. One caveat is this chat component is not full screen most of the time and has its own scroll bar. – HarvP Jun 08 '17 at 12:35
  • What kind of error could occur that must be catched? – Christoph Sep 29 '17 at 11:03
  • @Cristoph sometimes, when the "JS is faster than the dom rendering" the selector throws an 'NullPointer" during init (.nativeElement accessor, since the native element does not exist yet). At least in I saw this when implementing this in angular beta 3 - this may have changed – letmejustfixthat Sep 30 '17 at 17:49
  • is there a way to do this without accessing the native element? that way the DOM manipulation will still be compatible with mobile? – FussinHussin Oct 16 '17 at 18:15
  • 2
    @HarvP & JohnLouieDelaCruz did you find a solution to skip scroll if user scrolled manually? – suhailvs Apr 02 '19 at 06:44
  • Beautiful. People should learn how to take the core and not the code. – Aakash Verma Apr 28 '19 at 13:28
  • Are you some kind of magician? Are you a Super Hero? Are you from another planet!?!?!? Thank you so much for this fix =D I have tried about a million things, and it was such an annoying bug Thank you! – cheesydoritosandkale Jun 28 '19 at 18:44
  • @JohnLouieDelaCruz @HarvP Implementing `AfterViewInit` and `ngAfterViewInit()`, respectively, solved this issue for me. That way when the user scrolls in the div `ngAfterViewChecked()` is not called again, and the autoscroll only happens once. – Kurtis Jungersen Mar 26 '20 at 16:36
  • @letmejustfixthat thank you! saved me some time and it does just what I needed. – Farasi78 Apr 28 '20 at 14:14
  • work around for not to scroll to the bottom when the user scrolled up, is to initialize scroll to bottom after it, checks every time if it not already scrolled to bottom then do not scroll – Sumit Yadav Jun 10 '20 at 10:42
220

Simplest and the best solution for this is :

Add this #scrollMe [scrollTop]="scrollMe.scrollHeight" simple thing on Template side

<div style="overflow: scroll; height: xyz;" #scrollMe [scrollTop]="scrollMe.scrollHeight">
    <div class="..." 
        *ngFor="..."
        ...>  
    </div>
</div>

Here is the link for WORKING DEMO (With dummy chat app) AND FULL CODE

Will work with Angular2 and also upto 5, As above demo is done in Angular5.


Note :

For error : ExpressionChangedAfterItHasBeenCheckedError

Please check your css,it's a issue of css side,not the Angular side , One of the user @KHAN has solved that by removing overflow:auto; height: 100%; from div. (please check conversations for detail)

Vivek Doshi
  • 56,649
  • 12
  • 110
  • 122
  • 3
    And how do you trigger the scroll? – Burjua Aug 23 '17 at 16:15
  • 4
    it will autoscroll to bottom on any item added to the ngfor item OR when inner side div's height increase – Vivek Doshi Aug 24 '17 at 04:00
  • 3
    Thx @VivekDoshi. Though after something is added I get this error: >ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '700'. Current value: '517'. – Vassilis Pits Sep 28 '17 at 12:03
  • @VassilisPits, have any jsfiddle or env to check the code? – Vivek Doshi Sep 29 '17 at 05:28
  • @VivekDoshi no but it's exactly as you have it on your code. In 4.4.0+ angular this will thought the error told you. In order to bypass it I had to change the `scrollMe.scrollHeight` with a value from the component and wrap in in an setTimeout function to push it at the end of the code execution. – Vassilis Pits Sep 29 '17 at 09:10
  • @VassilisPits Hi I have the same issue, can you explain how exactly you made the workaround with the setTimeout? – Jonas Ostergaard Oct 12 '17 at 13:40
  • @VassilisPits , please provide me the link of jsfiddle or plunkr, so I can check the issue and also update the answer. – Vivek Doshi Oct 12 '17 at 14:16
  • @JonasOstergaard, please check the updated answer also provided working demo now. – Vivek Doshi Nov 15 '17 at 08:08
  • @VassilisPits, please check the updated answer also provided working demo now, – Vivek Doshi Nov 15 '17 at 08:08
  • @KHAN, will you please check given link , demo + code , or please provide error online, so I can test and debug it. – Vivek Doshi Nov 20 '17 at 11:29
  • Ye i got the code from your demo. "ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '0'. Current value: '464'." when i load the page it gives me that error – Kay Nov 20 '17 at 11:30
  • @KHAN,as you can see there is no error in code , working perfectly fine online and also in my local, I want your code so I can check what's wrong, might be issue with your parent child relations. – Vivek Doshi Nov 20 '17 at 11:32
  • This is where i put it
    – Kay Nov 20 '17 at 11:34
  • @KHAN, can you provide the demo link , or full code ? same code working fine mine side. I also want to know the reason. – Vivek Doshi Nov 20 '17 at 11:35
  • 2
    I found the problem, the class example-scrolling-content i had these css classes there. overflow: auto; height: 100%;. Once i removed this class from the div it worked. – Kay Nov 20 '17 at 11:37
  • @KHAN, thanks man, that was the only issue I want to solve it, Some of the user also posted the issue, but in my demo all was working perfectly, Thanks for posting the solution also, added in my answer too. – Vivek Doshi Nov 20 '17 at 12:10
  • There is still a problem. The scrollbar is not the full height of the container. If i set the height to 100% instead of xyz i then get this error messsage again. – Kay Nov 21 '17 at 10:10
  • @KHAN, please provide the link of your demo ,so I can find the solution for it. – Vivek Doshi Nov 21 '17 at 10:22
  • @VivekDoshi - Hi - Am trying this approach, but it does nt scroll.I have a card and inside that I have a div which displays multiple messages (inside the card div, I have similar to your structure). I have this style for my card - style="height:425px;overflow:scroll;overflow-x: hidden;" Can you pls help me understand if this needs any other change in css or ts files? Thanks – csharpnewbie Feb 06 '18 at 17:15
  • @csharpnewbie , yeah sure , it would be helpfull if you can show me real example – Vivek Doshi Feb 06 '18 at 17:17
  • 1
    Can someone explain to me why this works? (It did for me) – Mario Flores Feb 12 '18 at 20:24
  • 1
    @VivekDoshi - Hi, I added this to my div and scroll is working fine now. Thanks for the code snippet. But I have a problem now. When the div is initialized and started, it has only few messages and scroll wont be triigered that time. But in that scenario, I get an error "Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '470'. Current value: '448'". I tried checking for the error and tried adding ngAfterViewInit and called this.cd.detectChanges(); But still I get the error. Any suggestions to fix it? Thanks. – csharpnewbie Mar 07 '18 at 17:04
  • 10
    @csharpnewbie I have the same issue when removing any message from the list: `Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'`. Did you solve it? – Andrey Mar 17 '18 at 23:56
  • @Andrey - No. I was not able to solve it still. Pls keep me posted if you find a solution as well. – csharpnewbie Mar 19 '18 at 00:22
  • 7
    I was able to solve the "Expression has changed after it was checked" (while keeping height: 100%; overflow-y: scroll) by adding a condition to [scrollTop], to prevent it from being set UNTIL I had data to populate it with, ex:
    • ... the addition of the "messages.length === 0 ? 0" check seemed to fix the issue, although I cannot explain why, so I can't say for certain that the fix is not unique to my implementation.
    – Ben May 16 '18 at 15:15
  • @Ben , Glad to here that , I can't produce the issue by my self , That's why I have created the whole working demo and its working without any error. Issue is almost related to css properties. – Vivek Doshi May 17 '18 at 03:59
  • I just had to do one change in my css. I had to replace overflow-y:auto; to overflow-y:scroll; and it worked perfectly. Great Solution. – ATHER Jun 25 '18 at 15:46
  • SUPERB solution. – tm1701 Oct 19 '18 at 13:03
  • 1
    Is there any way to make it animate? – Reza Nov 12 '18 at 02:31
  • 1
    https://stackoverflow.com/a/35278480/2351696 solved the bug "Expression has changed after it was checked" – suhailvs Apr 01 '19 at 07:01
  • 1
    This did not work for me, I went through pretty much every suggestion in the comments. lol I had to use the main answer. I would have rather done it like this, as this is how I originally had it setup. There was probably something else interfering on my end. Thanks though! – cheesydoritosandkale Jun 28 '19 at 19:14
  • 1
    It works, but don't forget to add the css styles!! It's stupid, but I focused in the js code mainly and went through it like crazy, as simple as it is. – Gonzalo Nov 08 '19 at 10:18
  • 2
    This should have been the accepted answer! working on angular 8 – BrunoMartinsPro Apr 16 '20 at 10:46
  • 1
    Perfect solution for my scenario(a chat screen) – Arul Rozario Jul 31 '20 at 06:36
  • it's way easier to use this solution inside code, binding with @ViewChild and then setting the properties in js. No errors whatsoever. Also do this in ngAfterViewChecked(). – John White Oct 08 '20 at 10:43
  • 3
    Nice solution but I kept having the error "Expression has changed after it was checked", changing the change detection strategy to `changeDetection: ChangeDetectionStrategy.OnPush` did the trick for me – TCH Oct 15 '20 at 12:37
  • 1
    The only working solution for me was to add a `detectChanges()` call to `ngAfterViewChecked()`, see https://stackoverflow.com/a/52865608/16831793 –  Nov 11 '21 at 18:12
  • Working on Angular 13! – Uri Gross Feb 15 '22 at 09:25
34

The accepted answer fires while scrolling through the messages, this avoids that.

You want a template like this.

<div #content>
  <div #messages *ngFor="let message of messages">
    {{message}}
  </div>
</div>

Then you want to use a ViewChildren annotation to subscribe to new message elements being added to the page.

@ViewChildren('messages') messages: QueryList<any>;
@ViewChild('content') content: ElementRef;

ngAfterViewInit() {
  this.scrollToBottom();
  this.messages.changes.subscribe(this.scrollToBottom);
}

scrollToBottom = () => {
  try {
    this.content.nativeElement.scrollTop = this.content.nativeElement.scrollHeight;
  } catch (err) {}
}
Flecibel
  • 166
  • 2
  • 11
denixtry
  • 2,928
  • 1
  • 21
  • 19
  • Hi - Am trying this approach, but it always goes into the catch block. I have a card and inside that I have a div which displays multiple messages (inside the card div, I have similar to your structure). Can you pls help me understand if this needs any other change in css or ts files? Thanks. – csharpnewbie Feb 06 '18 at 16:56
  • Can you log your error message? You have to match the `#content` id into the `ViewChild` annotation. – denixtry Feb 06 '18 at 18:02
  • The error message is just empty. I think this.content.nativeElement is getting set as null or so. tried printing scrollheight, scrollTop etc, but none show up in the logs. – csharpnewbie Feb 06 '18 at 18:24
  • 2
    By far the best answer. Thanks ! – cagliostro Oct 11 '18 at 07:35
  • 1
    This currently does not work due to an Angular bug where QueryList changes subscriptions only firing once even when declared in ngAfterViewInit. The solution by Mert is the only one that works. Vivek's solution fires no matter what element is added, whereas Mert's can fire only when certain elements are added. – John Hamm Nov 11 '18 at 06:45
  • 1
    This is the only answer that solves my problem. works with angular 6 – suhailvs Apr 04 '19 at 06:20
  • 2
    Awesome!!! I used the one above and thought it was wonderful but then I got stuck scrolling down and my users couldn't read previous comments! Thank you for this solution! Fantastic! I would give you 100+ points if I could :-D – cheesydoritosandkale Jul 12 '19 at 15:19
  • This would go really wrong for lazy loading when scrolling up – zardilior Apr 19 '20 at 14:27
32

I added a check to see if the user tried to scroll up.

I'm just going to leave this here if anyone wants it :)

<div class="jumbotron">
    <div class="messages-box" #scrollMe (scroll)="onScroll()">
        <app-message [message]="message" [userId]="profile.userId" *ngFor="let message of messages.slice().reverse()"></app-message>
    </div>

    <textarea [(ngModel)]="newMessage" (keyup.enter)="submitMessage()"></textarea>
</div>

and the code:

import { AfterViewChecked, ElementRef, ViewChild, Component, OnInit } from '@angular/core';
import {AuthService} from "../auth.service";
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/concatAll';
import {Observable} from 'rxjs/Rx';

import { Router, ActivatedRoute } from '@angular/router';

@Component({
    selector: 'app-messages',
    templateUrl: './messages.component.html',
    styleUrls: ['./messages.component.scss']
})
export class MessagesComponent implements OnInit {
    @ViewChild('scrollMe') private myScrollContainer: ElementRef;
    messages:Array<MessageModel>
    newMessage = ''
    id = ''
    conversations: Array<ConversationModel>
    profile: ViewMyProfileModel
    disableScrollDown = false

    constructor(private authService:AuthService,
                private route:ActivatedRoute,
                private router:Router,
                private conversationsApi:ConversationsApi) {
    }

    ngOnInit() {

    }

    public submitMessage() {

    }

     ngAfterViewChecked() {
        this.scrollToBottom();
    }

    private onScroll() {
        let element = this.myScrollContainer.nativeElement
        let atBottom = element.scrollHeight - element.scrollTop === element.clientHeight
        if (this.disableScrollDown && atBottom) {
            this.disableScrollDown = false
        } else {
            this.disableScrollDown = true
        }
    }


    private scrollToBottom(): void {
        if (this.disableScrollDown) {
            return
        }
        try {
            this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
        } catch(err) { }
    }

}
Rusty Rob
  • 16,489
  • 8
  • 100
  • 116
22

Consider using

.scrollIntoView()

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

Boris Yakubchik
  • 3,861
  • 3
  • 34
  • 41
22

If you want to be sure, that you are scrolling to the end after *ngFor is done, you can use this.

<div #myList>
 <div *ngFor="let item of items; let last = last">
  {{item.title}}
  {{last ? scrollToBottom() : ''}}
 </div>
</div>

scrollToBottom() {
 this.myList.nativeElement.scrollTop = this.myList.nativeElement.scrollHeight;
}

Important here, the "last" variable defines if you are currently at the last item, so you can trigger the "scrollToBottom" method

Mert
  • 1,333
  • 1
  • 12
  • 15
  • 2
    This answer deserves to be the accepted one. It is the only solution that works. Denixtry's solution does not work due to an Angular bug where QueryList changes subscriptions only firing once even when declared in ngAfterViewInit. Vivek's solution fires no matter what element is added, whereas Mert's can fire only when certain elements are added. – John Hamm Dec 05 '18 at 01:33
  • THANK YOU! Each of the other methods have some downsides. This just works as it is intended. Thanks for sharing your code! – lkaupp Jul 05 '19 at 13:05
  • Works for me in Angular: 11.0.9. the other ones seem to have issues. Using the #content on the outer DIV, and `@ViewChild('content') content: ElementRef;` to implement the suggested ScrollToBottom – Pianoman Feb 25 '21 at 15:51
9

If you are in recent version of Angular, following is enough:

<div #scrollMe style="overflow: scroll; height: xyz;" [scrollTop]="scrollMe.scrollHeight>
    <div class="..." 
        *ngFor="..."
        ...>  
    </div>
</div>
birajad
  • 482
  • 1
  • 8
  • 16
8
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
Stas Kozlov
  • 109
  • 2
  • 6
  • 6
    Any answer that gets the asker going in the right direction is helpful, but do try to mention any limitations, assumptions or simplifications in your answer. Brevity is acceptable, but fuller explanations are better. – baduker Mar 28 '18 at 07:41
6
const element = document.getElementById('box');
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
Prashant Pimpale
  • 10,349
  • 9
  • 44
  • 84
Kanomdook
  • 715
  • 8
  • 6
3

Vivek's answer has worked for me, but resulted in an expression has changed after it was checked error. None of the comments worked for me, but what I did was change the change detection strategy.

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})
user3610386
  • 109
  • 11
  • I had exactly the same issue, trying to use Vivek's answer and keep having the error. Now it works perfectly thank you !!! – TCH Oct 15 '20 at 12:34
3

The accepted answer is a good solution, but it can be improved since your content/chat may often scroll to the bottom involuntarily given how the ngAfterViewChecked() lifecycle hook works.

Here's an improved version...

COMPONENT

import {..., AfterViewChecked, ElementRef, ViewChild, OnInit} from 'angular2/core'
@Component({
    ...
})
export class ChannelComponent implements OnInit, AfterViewChecked {
    @ViewChild('scrollMe') private myScrollContainer: ElementRef;

    /**Add the variable**/
    scrolledToBottom = false;

    ngAfterViewChecked() {        
        this.scrollToBottom();        
    } 

    scrollToBottom(): void {
        try {
          /**Add the condition**/
          if(!this.scrolledToBottom){
             this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
          }   
        } catch(err) { }                 
    }

    /**Add the method**/
    onScroll(){
      this.scrolledToBottom = true;
    }
}

TEMPLATE

<!--Add a scroll event listener-->
<div #scrollMe 
     style="overflow: scroll; height: xyz;"
     (scroll)="onScroll()">
    <div class="..." 
        *ngFor="..."
        ...>  
    </div>
</div>

Alternatively: Here's another good solution on stackblitz.

3

The title of the question mentions "Chat Style" scroll to bottom, which I also needed. None of these answers really satisfied me, because what I really wanted to do was scroll to the bottom of my div whenever child elements were added or destroyed. I ended up doing that with this very simple Directive that leverages the MutationObserver API

@Directive({
    selector: '[pinScroll]',
})
export class PinScrollDirective implements OnInit, OnDestroy {
    private observer = new MutationObserver(() => {
        this.scrollToPin();
    });

    constructor(private el: ElementRef) {}

    ngOnInit() {
        this.observer.observe(this.el.nativeElement, {
            childList: true,
        });
    }

    ngOnDestroy() {
        this.observer.disconnect();
    }

    private scrollToPin() {
        this.el.nativeElement.scrollTop = this.el.nativeElement.scrollHeight;
    }
}

You just attach this directive to your list element, and it will scroll to the bottom whenever a list item changes in the DOM. It's the behavior I was personally looking for. This directive assumes that you are already handling height and overflow rules on the list element.

Mike
  • 1,266
  • 1
  • 11
  • 17
  • 3
    This is the most awesome pice of code I've found in the last week, thank you very much! It's exactly what i needed and it's also very clean, in my opinion through the use of a directive for this. It's great! – Benjamin Jesuiter Jul 20 '21 at 17:23
2

Sharing my solution, because I was not completely satisfied with the rest. My problem with AfterViewChecked is that sometimes I'm scrolling up, and for some reason, this life hook gets called and it scrolls me down even if there were no new messages. I tried using OnChanges but this was an issue, which lead me to this solution. Unfortunately, using only DoCheck, it was scrolling down before the messages were rendered, which was not useful either, so I combined them so that DoCheck is basically indicating AfterViewChecked if it should call scrollToBottom.

Happy to receive feedback.

export class ChatComponent implements DoCheck, AfterViewChecked {

    @Input() public messages: Message[] = [];
    @ViewChild('scrollable') private scrollable: ElementRef;

    private shouldScrollDown: boolean;
    private iterableDiffer;

    constructor(private iterableDiffers: IterableDiffers) {
        this.iterableDiffer = this.iterableDiffers.find([]).create(null);
    }

    ngDoCheck(): void {
        if (this.iterableDiffer.diff(this.messages)) {
            this.numberOfMessagesChanged = true;
        }
    }

    ngAfterViewChecked(): void {
        const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;

        if (this.numberOfMessagesChanged && !isScrolledDown) {
            this.scrollToBottom();
            this.numberOfMessagesChanged = false;
        }
    }

    scrollToBottom() {
        try {
            this.scrollable.nativeElement.scrollTop = this.scrollable.nativeElement.scrollHeight;
        } catch (e) {
            console.error(e);
        }
    }

}

chat.component.html

<div class="chat-wrapper">

    <div class="chat-messages-holder" #scrollable>

        <app-chat-message *ngFor="let message of messages" [message]="message">
        </app-chat-message>

    </div>

    <div class="chat-input-holder">
        <app-chat-input (send)="onSend($event)"></app-chat-input>
    </div>

</div>

chat.component.sass

.chat-wrapper
  display: flex
  justify-content: center
  align-items: center
  flex-direction: column
  height: 100%

  .chat-messages-holder
    overflow-y: scroll !important
    overflow-x: hidden
    width: 100%
    height: 100%
RTYX
  • 1,244
  • 1
  • 14
  • 32
  • This can be used to check if chat is scrolled down before adding new message to the DOM: `const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;` (where 3.0 is tolerance in pixels). It can be used in `ngDoCheck` to conditionally not set `shouldScrollDown` to `true` if user is manually scrolled up. – Ruslan Stelmachenko Apr 02 '19 at 17:15
  • Thanks for the suggestion @RuslanStelmachenko. I implemented it (I decided to do it on the ```ngAfterViewChecked``` with the other boolean. I changed ```shouldScrollDown``` name to ```numberOfMessagesChanged``` to give some clarity about what exactly this boolean refers to. – RTYX Apr 09 '19 at 07:56
  • I see you do `if (this.numberOfMessagesChanged && !isScrolledDown)`, but I think you didn't understand the intention of my suggestion. My real intention is to **not** scroll down automatically even when new message is added, if user **manually** scrolled up. Without this, if you scroll up to see the history, the chat will scroll down as soon as new message arrives. This can be very annoying behaviour. :) So, my suggestion was to check if chat is scrolled up **before** new message is added to the DOM and to not autoscroll, if it is. `ngAfterViewChecked` is too late because DOM already changed. – Ruslan Stelmachenko Apr 09 '19 at 17:19
  • Aaaaah, I had not understood the intention then. What you said makes sense - I think in that situation many chats just add a button to go down, or a mark that there's a new message down there, so this would definitely be a good way to achieve that. I'll edit it and change it later. Thanks for the feedback! – RTYX Apr 10 '19 at 09:05
  • Thanks god for your share, with @Mert solution, my view get kicked down on rare cases (call get rejected from backend), with your IterableDiffers it functions nicely. Thank you so much! – lkaupp Jul 05 '19 at 14:53
2

In case anyone has this problem with Angular 9, this is how I manage to fix it.

I started with the solution with #scrollMe [scrollTop]="scrollMe.scrollHeight" and I got the ExpressionChangedAfterItHasBeenCheckedError error as people mentioned.

In order to fix this one I just add in my ts component:

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
...})

 constructor(private cdref: ChangeDetectorRef) {}

 ngAfterContentChecked() {
        this.cdref.detectChanges();
    }

ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'

1

In angular using material design sidenav I had to use the following:

let ele = document.getElementsByClassName('md-sidenav-content');
    let eleArray = <Element[]>Array.prototype.slice.call(ele);
    eleArray.map( val => {
        val.scrollTop = val.scrollHeight;
    });
Post Impatica
  • 14,999
  • 9
  • 67
  • 78
0

After reading other solutions, the best solution I can think of, so you run only what you need is the following: You use ngOnChanges to detect the proper change

ngOnChanges() {
  if (changes.messages) {
      let chng = changes.messages;
      let cur  = chng.currentValue;
      let prev = chng.previousValue;
      if(cur && prev) {
        // lazy load case
        if (cur[0].id != prev[0].id) {
          this.lazyLoadHappened = true;
        }
        // new message
        if (cur[cur.length -1].id != prev[prev.length -1].id) {
          this.newMessageHappened = true;
        }
      }
    }

}

And you use ngAfterViewChecked to actually enforce the change before it renders but after the full height is calculated

ngAfterViewChecked(): void {
    if(this.newMessageHappened) {
      this.scrollToBottom();
      this.newMessageHappened = false;
    }
    else if(this.lazyLoadHappened) {
      // keep the same scroll
      this.lazyLoadHappened = false
    }
  }

If you are wondering how to implement scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
zardilior
  • 2,810
  • 25
  • 30
0

Just in case someone is using Ionic and Angular, here is a link that uses a very simple code to do that king of scroll to bottom (or top) :

https://forum.ionicframework.com/t/scroll-content-to-top-bottom-using-ionic-4-solution/163048

kimo
  • 45
  • 1
  • 6
0

This angular code worked for me

<div id="focusBtn"></div>

const element = document.getElementById("focusBtn");
element.scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
Mukul Raghav
  • 349
  • 2
  • 5
-1

To add smooth scroll do this

#scrollMe [scrollTop]="scrollMe.scrollHeight" style="scroll-behavior: smooth;"

and

this.cdref.detectChanges();
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Rutendo
  • 39
  • 2