Angular 2 滚动到底部(聊天风格)

153

我有一组单元格组件,在ng-for循环中。

我已经准备就绪,但似乎无法找出正确的方法。

目前我有:

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

但是这并不总是有效的,因为图像异步地向下推动视口。

在 Angular 2 中,滚动到聊天窗口底部的适当方法是什么?

20个回答

283

我遇到了同样的问题,我正在使用 AfterViewChecked@ViewChild 的组合(Angular2 beta.3)。

该组件:

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

模板:

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

当然,这很基础。每次检查视图时,AfterViewChecked都会触发:

实现此接口以在组件视图的每次检查后收到通知。

例如,如果您有一个用于发送消息的输入字段,则每次按键松开时都会触发此事件(仅供参考)。但是,如果保存了用户是否手动滚动并跳过了scrollToBottom(),则应该没问题。


6
你好,有没有办法在用户向上滚动时防止页面滚动? - John Louie Dela Cruz
2
我也尝试以聊天风格的方式使用它,但需要在用户向上滚动时不滚动。我一直在寻找方法来检测用户是否向上滚动,但找不到。一个限制是这个聊天组件大多数时候并不全屏,而是有自己的滚动条。 - HarvP
有哪些错误是必须要捕获的? - Christoph
@Cristoph 有时候,当“JS比DOM渲染更快”时,在初始化期间选择器会抛出“NullPointer”(.nativeElement访问器,因为原始元素尚不存在)。至少在我实现这个功能时在Angular beta 3中看到了这种情况 - 这可能已经改变了。 - letmejustfixthat
2
@HarvP和JohnLouieDelaCruz,你们找到了一个解决方案来跳过手动滚动吗? - suhailvs
显示剩余9条评论

221

最简单和最好的解决方案是:

模板端添加这个#scrollMe [scrollTop]="scrollMe.scrollHeight" 简单的代码。

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

这是一个包含虚拟聊天应用的 演示链接 和完整代码 FULL CODE,支持 Angular2 到 5 版本。

注意:

如果出现错误:ExpressionChangedAfterItHasBeenCheckedError

请检查你的 CSS,这是 CSS 方面的问题,而不是 Angular 的问题。其中一位名叫 @KHAN 的用户通过从 div 中删除 overflow:auto; height: 100%; 解决了这个问题(请查看对话以获取详细信息)。



3
你如何触发滚动? - Burjua
4
当向ngfor项目添加任何项或内部div高度增加时,它将自动滚动到底部。 - Vivek Doshi
3
谢谢@VivekDoshi。但是在添加某些内容后,我遇到了这个错误:> 错误:ExpressionChangedAfterItHasBeenCheckedError:表达式已在检查后发生更改。之前的值为“700”。当前值为“517”。 - Vassilis Pits
10
当我从列表中删除任何消息时,出现了与@csharpnewbie相同的问题:“Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'”。你解决了这个问题吗? - Andrey
7
我解决了“检查后表达式已更改”问题(同时保持高度:100%;溢出-y:滚动),通过将条件添加到[scrollTop]中,以防止在我有数据填充之前设置它,例如:<ul class = "chat-history"#chatHistory [scrollTop] = "messages.length === 0?0:chatHistory.scrollHeight"> <li * ngFor =“let message of messages”> ... 添加“messages.length === 0?0”检查似乎修复了该问题,尽管我无法解释原因,所以我不能确定该修复程序是否唯一适用于我的实现。 - Ben
显示剩余33条评论

35

被接受的答案在滚动消息时触发,这样可以避免那种情况。

你想要一个像这样的模板。

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

那么您需要使用ViewChildren注释来订阅添加到页面中的新消息元素。

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

错误信息为空。我认为this.content.nativeElement被设置为null或其他值。尝试打印scrollheight、scrollTop等,但没有显示在日志中。 - csharpnewbie
2
迄今为止最好的答案。谢谢! - cagliostro
1
由于 Angular 存在一个错误,即 QueryList 更改订阅仅在 ngAfterViewInit 中声明时触发一次,因此当前无法工作。Mert 的解决方案是唯一可行的。Vivek 的解决方案会在添加任何元素时都触发,而 Mert 的解决方案只能在添加特定元素时触发。 - John Hamm
1
这是唯一解决我的问题的答案。适用于Angular 6。 - suhailvs
2
太棒了!我之前用的那个也很好,但是当我卡在向下滚动时,我的用户就无法阅读以前的评论了!感谢您提供的解决方案!太棒了!如果可以的话,我会给你100多分的 :-D - cheesydoritosandkale
显示剩余3条评论

32

我添加了一个检查,以查看用户是否尝试向上滚动。

如果有人需要,我就把它放在这里 :)

<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>

并且代码:

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

}

真正可行的解决方案,不会在控制台中出现错误。谢谢! - Dmitry Vasilyuk Just3F

22

如果你想确定在*ngFor完成后滚动到底部,可以使用以下方法。

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

重要的是,"last" 变量定义了您当前是否在最后一个项目,因此您可以触发 "scrollToBottom" 方法。


2
这个答案值得被采纳。它是唯一可行的解决方案。由于 Angular 的一个 bug,Denixtry 的解决方案无法工作,其中 QueryList 更改订阅仅在 ngAfterViewInit 中声明时触发一次。Vivek 的解决方案无论添加什么元素都会触发,而 Mert 的解决方案只有在添加特定元素时才会触发。 - John Hamm
谢谢!其他的方法都有一些缺点。这个只是按照预期工作。感谢您分享您的代码! - lkaupp
在Angular 11.0.9中对我有效,其他版本似乎存在问题。使用外部DIV上的#content,并使用@ViewChild('content') content: ElementRef;来实现建议的ScrollToBottom。 - Pianoman

22

尚未成为标准,如果您具有跨浏览器兼容性,则会有限制。 - chavocarlos
2
@chavocarlos - 似乎被除了Opera Mini之外的所有浏览器支持:https://caniuse.com/#feat=scrollintoview - Boris Yakubchik

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

6
任何帮助提问者朝着正确方向前进的答案都是有用的,但请尽量提及您回答中的任何限制、假设或简化。简洁明了是可以接受的,但更充分的解释更好。 - baduker

9

如果你使用的是 Angular 的最新版本,则只需要以下内容:

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

2
这对于大多数情况来说足够简单了 :) - Kavinda Jayakody

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

3

这个问题的标题提到了“聊天风格”的滚动到底部,这也是我所需要的。然而,这些答案都没有真正地满足我的需求,因为我真正想做的是在子元素添加或删除时滚动到我的div的底部。最终,我使用了这个非常简单的指令来利用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;
    }
}

您只需将此指令附加到列表元素上,每当DOM中的列表项更改时,它就会滚动到底部。这是我个人在寻找的行为。该指令假定您已经处理了列表元素上的heightoverflow规则。

3
这是我上周找到的最棒的代码,非常感谢!它正好是我需要的,而且非常简洁明了,我认为这要归功于使用指令。太棒了! - Benjamin Jesuiter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接