Angular 2滚动到底部(聊天样式)


111

我在ng-for循环中有一组单电池组件。

我已经准备好一切,但似乎无法找出合适的方法

目前我有

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

但这并非一直有效,因为图像会异步将视口向下推。

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

Answers:


203

我遇到了同样的问题,我使用的是AfterViewCheckedand @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()应该没问题。


我有一个home组件,在home模板中,我在div中有另一个组件,该组件显示数据并保持追加数据,但滚动条css在home模板div中,而不在内部模板中。问题IM callling这个scrolltobottom每次数据来自内部的元件,但#scrollMe是家庭成分,因为该分区是滚动...以及#scrollMe是不可访问,从内部的元件..不知道如何从内部组件使用#scrollme
Anagh Verma

您的解决方案@Basit非常简单,不会导致无休止的滚动/当用户手动滚动时不需要解决方法。
Thetherdy

3
嗨,有没有一种方法可以防止用户向上滚动?
John Louie Dela Cruz

2
我也在尝试以聊天方式使用它,但是如果用户向上滚动,则不需要滚动。我一直在寻找并且找不到找到用户是否向上滚动的方法。需要注意的是,此聊天组件大部分时间并非全屏显示,并且具有自己的滚动条。
HarvP

2
@HarvP和JohnLouieDelaCruz是否找到了一种解决方案,如果用户手动滚动则跳过滚动?
suhailvs

166

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

#scrollMe [scrollTop]="scrollMe.scrollHeight"模板一侧添加此简单的内容

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

这是工作演示(带有虚拟聊天应用程序)和完整代码的链接

可以与Angular2一起使用,最多也可以与Angular5一起使用,因为上面的演示是在Angular5中完成的。


注意 :

对于错误: ExpressionChangedAfterItHasBeenCheckedError

请检查您的css,这是css方面的问题,而不是Angular方面,@KHAN用户之一已通过overflow:auto; height: 100%;从中删除来解决了此问题div。(请检查对话以了解详细信息)


3
以及如何触发滚动?
布尔贾

3
它将在任何添加到ngfor项目的项目上自动滚动到底部,或者在内侧div的高度增加时自动滚动至底部
Vivek Doshi

2
谢谢@VivekDoshi。尽管添加了某些内容后,我得到以下错误:> ERROR错误:ExpressionChangedAfterItHasBeenCheckedError:在检查表达式后,表达式已更改。上一个值:“ 700”。当前值:“ 517”。
瓦西里斯坑

6
@csharpnewbie从列表中删除任何消息时,我也遇到同样的问题:Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'。解决了吗
安德烈(Andrey)

5
我可以通过在[scrollTop]中添加一个条件来解决“检查后表达式已更改”(同时保持高度:100%;溢流y:滚动)的问题,以防止在我有数据要填充之前将其设置为例如:<ul class =“ chat-history” #chatHistory [scrollTop] =“ messages.length === 0?0:chatHistory.scrollHeight”> <li * ngFor =“让消息成为消息”>。 。添加“ messages.length === 0?0”检查似乎可以解决该问题,尽管我无法解释原因,所以我不能肯定地说该修复不是我的实现所独有的。

20

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

如果有人要的话,我就把它留在这里:)

<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

20

滚动浏览消息时会触发接受的答案,这避免了这种情况。

您需要这样的模板。

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

嗨-我正在尝试这种方法,但是它总是会陷入困境。我有一张卡,里面有一个div,可以显示多个消息(在div里面,我的结构类似于您的结构)。您能否帮助我了解是否需要对CSS或ts文件进行任何其他更改?谢谢。
csharpnewbie

2
迄今为止最好的答案。谢谢 !
cagliostro

1
当前由于Angular错误而无法使用,其中QueryList更改订阅仅触发一次,即使在ngAfterViewInit中声明时也是如此。Mert的解决方案是唯一可行的解​​决方案。无论添加什么元素,Vivek的解决方案都会触发,而只有添加某些元素时,Mert的解决方案才能触发。
约翰·哈姆

1
这是解决我的问题的唯一答案。可以与角度6一起使用
-suhailvs

2
太棒了!!! 我使用了上面的那个,并认为它很棒,但是后来我陷入了向下滚动的状态,并且我的用户无法阅读以前的评论!谢谢您的解决方案!太棒了!如果可以的话,我会给你100+分:-D
cheesydoritosandkale '19


13

如果要确保在完成* 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”方法


1
这个答案值得被接受。这是唯一可行的解​​决方案。Denixtry的解决方案由于一个Angular错误而无法使用,其中QueryList更改订阅仅触发一次,即使在ngAfterViewInit中声明也是如此。无论添加什么元素,Vivek的解决方案都会触发,而只有添加某些元素时,Mert的解决方案才能触发。
约翰·哈姆

谢谢!其他每种方法都有一些缺点。这只是按预期工作。感谢您分享您的代码!
lkaupp

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

5
使提问者朝正确方向前进的任何答案都是有帮助的,但请尝试在答案中提及任何限制,假设或简化。简短是可以接受的,但是更充分的解释会更好。
baduker

2

分享我的解决方案,因为我对其余的部分并不完全满意。我的问题AfterViewChecked是,有时我会向上滚动,并且由于某种原因,这个生命挂钩被调用,即使没有新消息,它也会使我向下滚动。我尝试使用OnChanges,但这个是一个问题,这使我这个解决方案。不幸的是,仅使用DoCheck,它就在呈现消息之前向下滚动,这也没有用,因此我将它们组合在一起,以便DoCheck基本上指示AfterViewChecked它是否应该调用scrollToBottom

很高兴收到反馈。

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%

这可用于检查在将新消息添加到DOM之前,是否向下滚动了聊天:(const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;其中3.0为像素公差)。如果用户手动向上滚动,则可以使用它ngDoCheck有条件地不设置shouldScrollDowntrue
Ruslan Stelmachenko,

感谢您的建议@RuslanStelmachenko。我实现了它(我决定ngAfterViewChecked与另一个布尔值一起使用。我更改了shouldScrollDown名称,numberOfMessagesChanged以使此布尔值所指的
含义更加清楚。– RTYX

我看得出您的意思if (this.numberOfMessagesChanged && !isScrolledDown),但我认为您不理解我的建议的意图。我的真实意图是即使用户手动向上滚动,即使添加了新消息也不会自动向下滚动。没有此功能,如果您向上滚动以查看历史记录,则聊天消息将在收到新消息后立即向下滚动。这可能是非常令人讨厌的行为。:)因此,我的建议是在将新消息添加到DOM 之前检查聊天是否向上滚动,如果不是,则不要自动滚动。为时已晚,因为DOM已经更改。ngAfterViewChecked
Ruslan Stelmachenko

啊啊,那时候我还不明白意图。您所说的是有道理的-我认为在这种情况下,很多聊天只是添加了一个向下的按钮,或者标记着那里有新消息,因此,这绝对是实现此目标的好方法。我将对其进行编辑,然后再进行更改。感谢您的反馈!
RTYX

感谢上帝的分享,通过@Mert解决方案,我的观点在少数情况下受到了打击(从后端拒绝了呼叫),而IterableDiffers的功能很好。非常感谢!
lkaupp

2

Vivek的答案对我有用,但是导致表达式在检查错误后发生了变化。没有任何评论对我有用,但是我所做的是更改更改检测策略。

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})

1

在角度使用材质设计方面,我不得不使用以下内容:

let ele = document.getElementsByClassName('md-sidenav-content');
    let eleArray = <Element[]>Array.prototype.slice.call(ele);
    eleArray.map( val => {
        val.scrollTop = val.scrollHeight;
    });

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

0

阅读完其他解决方案后,我能想到的最佳解决方案是,您仅运行所需的内容:使用ngOnChanges来检测适当的更改

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

}

然后您使用ngAfterViewChecked在渲染之前但在计算出整个高度之后实际实施更改

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

如果您想知道如何实现scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.