ionic 2 + angular 2 : auto scroll to bottom of list / page / chat
Here's how I did it:
chatPage.html
<ion-content #content padding class="chatPage">
<ion-list no-lines>
<ion-item *ngFor="let msg of messages" >
<chat-bubble [message]="msg"></chat-bubble>
</ion-item>
</ion-list>
</ion-content>
The important bit in chatPage.html is #content
on <ion-content>
. I'll use the #content
identifier to obtain a reference to <ion-content>
in my chatPage.js using the ViewChild
.
Now for the actual scrolling logic:
chatPage.js
import {Component, ViewChild} from '@angular/core';
@Component({
templateUrl: 'build/pages/chatPage/chatPage.html',
queries: {
content: new ViewChild('content')
}
})
export class ChatPage {
constructor() {
}
//scrolls to bottom whenever the page has loaded
ionViewDidEnter(){
this.content.scrollToBottom(300);//300ms animation speed
}
}
Also, whenever my chatPage needs to show another chat message in the list (either a new message is received, or a new message is sent), I'm using the below code to scroll to the new bottom:
setTimeout(() => {
this.content.scrollToBottom(300);//300ms animation speed
});
Update for Typescript
Back when I gave this answer I was working with JavaScript version of Ionic 2 project. Over time I switched to TypeScript but I forgot to update the answer, so, here's a small update for chatPage.js(ts):
chatPage.ts
import {Component, ViewChild} from '@angular/core';
@Component({
templateUrl: 'chatPage.html'
})
export class ChatPage {
@ViewChild('content') content:any;
constructor() { }
//scrolls to bottom whenever the page has loaded
ionViewDidEnter(){
this.content.scrollToBottom(300);//300ms animation speed
}
}
Another solution is to "observe" changes in the scroll view and scroll automatically. i.e. if there are new HTML elements where the messages appear then scroll to the bottom.
export class MessagesPage implements OnInit, OnDestroy {
autoScroller: MutationObserver;
ngOnInit() {
this.autoScroller = this.autoScroll();
}
ngOnDestroy() {
this.autoScroller.disconnect();
}
autoScroll(): MutationObserver {
const autoScroller = new MutationObserver(this.scrollDown.bind(this));
autoScroller.observe(this.messagesList, {
childList: true,
subtree: true
});
return autoScroller;
}
scrollDown(): void {
this.scroller.scrollTop = this.scroller.scrollHeight;
this.messageEditor.focus();
}
private get messageEditor(): HTMLInputElement {
return <HTMLInputElement>document.querySelector('ion-input');
}
private get messagesList(): Element {
return document.querySelector('.messages');
}
private get scroller(): Element {
return this.messagesList.querySelector('.scroll-content');
}
}
Template:
<ion-content>
<ion-scroll scrollY="true" class="messages">
<ion-list>
<div *ngFor="let message of messages">
<p [innerHtml]="message.text"></p>
</div>
</ion-list>
</ion-scroll>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-input [(ngModel)]="message" type="text" placeholder="Write a message..." autocomplete="true" spellcheck="true" tappable></ion-input>
<ion-buttons end>
<button ion-button icon-only (click)="sendMessage()">
<ion-icon name="paper-plane"></ion-icon>
</button>
</ion-buttons>
</ion-toolbar>
</ion-footer>
(take from Ionic2CLI-Meteor-WhatsApp on Github)
The Ionic has an option to do this and works pretty good: And this is the most appropriate way with angular 2+ and Ionic.
import { Content } from ‘ionic-angular’; export class CommentsPage{ @ViewChild(Content) content: Content; ionViewWillEnter(): void { this.scrollToBottom(); } scrollToBottom() { setTimeout(() => { this.content.scrollToBottom(); }); } }
First off all, @rinukkusu answer is right but it doesn't work on my case because <ion-content>
(parent of <ion-list>
) has some bugs with it (ionic developers are working on that), so I had to put that element with scroll:hidden
and create a second content inside to apply the auto-scroll.
Finally with the right (s)css I called the function on construtor
when the page loads and then each time the users clicks on "chat" segment.
chat.html
<!-- I create the segment and call the `autoScroll()` when users clicks on "chat" -->
<ion-toolbar primary class="toolbar-segment">
<ion-segment light [(ngModel)]="segment">
<ion-segment-button value="chat" (click)="autoScroll()">
Chat
</ion-segment-button>
<ion-segment-button value="profile">
Profile
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<!--I wrote the css inline just as example.
DON'T do it on your project D: -->
<!-- overflow:hidden prevent the ion-content to scroll -->
<ion-content [ngSwitch]="segment" style="overflow: hidden;">
<!-- height: 100% to force scroll if the content overflows the container.
#chat-autoscroll is used by javascript.-->
<div class="content-scroll" id="chat-autoscroll" *ngSwitchWhen="'chat'" style="height: 100%; overflow: scroll">
(... make sure the content is bigger
than the container to see the auto scroll effect ...)
</div>
<!-- here it's just a normal scroll -->
<div *ngSwitchWhen="'profile'" class="content-scroll" style="height: 100%; overflow: auto">
(... content of profile segment ...)
</div>
</ion-content>
chat.js
constructor () {
// when the user opens the page, it shows the "chat" segment as initial value
this.segment = 'chat';
// when page loads, it calls autoScroll();
if (this.segment == 'chat') {
console.log('chat');
this.autoScroll();
};
}
/*Here comes the tricky.
If you don't use setTimeout, the console will say that
#chat-autoscroll doesn't exist in the first call (inside constructor).
This happens because the script runs before the DOM is ready.
I did a workaround with a timeOut of 10ms.
It's enough time to DOM loads and then autoScroll() works fine.
*/
autoScroll() {
setTimeout(function () {
var itemList = document.getElementById("chat-autoscroll");
itemList.scrollTop = itemList.scrollHeight;
}, 10);
}
Conclusion: The function is called twice. When the page is loaded (constructor) and each time the user comes back to "chat" segment. (click)="autoScroll()"
I hope this helps someone. If you know better way, let me know! I started playing with Angular2 and Ionic2 a couple of weeks ago so there is a lot of concepts/bases that I might be missing here.
Thanks :)