Angular 2 Scroll to bottom (Chat style)

ScrollTypescriptAngular

Scroll Problem Overview


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?

Scroll Solutions


Solution 1 - Scroll

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.

Solution 2 - Scroll

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)

Solution 3 - Scroll

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) {}
}

Solution 4 - Scroll

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) { }
    }

}

Solution 5 - Scroll

Solution 6 - Scroll

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

Solution 7 - Scroll

this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});

Solution 8 - Scroll

const element = document.getElementById('box');
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });

Solution 9 - Scroll

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>

Solution 10 - Scroll

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',
})

Solution 11 - Scroll

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%

Solution 12 - Scroll

Here's another good solution on stackblitz.

Alternatively:

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>

Solution 13 - Scroll

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;
    });

Solution 14 - Scroll

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.

Solution 15 - Scroll

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) { }
  }

Solution 16 - Scroll

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();
    }

https://stackoverflow.com/questions/45467881/expressionchangedafterithasbeencheckederror-expression-has-changed-after-it-was

Solution 17 - Scroll

To add smooth scroll do this

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

and

this.cdref.detectChanges();

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMax AlexanderView Question on Stackoverflow
Solution 1 - ScrollletmejustfixthatView Answer on Stackoverflow
Solution 2 - ScrollVivek DoshiView Answer on Stackoverflow
Solution 3 - ScrolldenixtryView Answer on Stackoverflow
Solution 4 - Scrollrobert kingView Answer on Stackoverflow
Solution 5 - ScrollBoris YakubchikView Answer on Stackoverflow
Solution 6 - ScrollMertView Answer on Stackoverflow
Solution 7 - ScrollStas KozlovView Answer on Stackoverflow
Solution 8 - ScrollKanomdookView Answer on Stackoverflow
Solution 9 - ScrollbirajadView Answer on Stackoverflow
Solution 10 - Scrolluser3610386View Answer on Stackoverflow
Solution 11 - ScrollRTYXView Answer on Stackoverflow
Solution 12 - ScrollKagiso RaserokaView Answer on Stackoverflow
Solution 13 - ScrollPost ImpaticaView Answer on Stackoverflow
Solution 14 - ScrollMikeView Answer on Stackoverflow
Solution 15 - ScrollzardiliorView Answer on Stackoverflow
Solution 16 - ScrollRaluca ConstandaView Answer on Stackoverflow
Solution 17 - ScrollRutendoView Answer on Stackoverflow