【发布时间】:2016-02-05 20:14:14
【问题描述】:
我在ng-for 循环中有一组单单元组件。
我什么都准备好了,但我似乎找不到合适的
目前我有
setTimeout(() => {
scrollToBottom();
});
但这并不总是有效,因为图像会异步将视口向下推。
在 Angular 2 中滚动到聊天窗口底部的合适方式是什么?
【问题讨论】:
标签: scroll typescript angular
我在ng-for 循环中有一组单单元组件。
我什么都准备好了,但我似乎找不到合适的
目前我有
setTimeout(() => {
scrollToBottom();
});
但这并不总是有效,因为图像会异步将视口向下推。
在 Angular 2 中滚动到聊天窗口底部的合适方式是什么?
【问题讨论】:
标签: scroll typescript angular
我遇到了同样的问题,我正在使用 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()你应该没问题。
【讨论】:
最简单也是最好的解决方案是:
在模板端添加这个#scrollMe [scrollTop]="scrollMe.scrollHeight"简单的东西
<div style="overflow: scroll; height: xyz;" #scrollMe [scrollTop]="scrollMe.scrollHeight">
<div class="..."
*ngFor="..."
...>
</div>
</div>
这是WORKING DEMO(使用虚拟聊天应用程序)和FULL CODE
的链接将与 Angular2 以及最多 5 个一起使用,如上演示在 Angular5。
注意:
对于错误:
ExpressionChangedAfterItHasBeenCheckedError请检查您的 css,这是 css 方面的问题,而不是 Angular 方面的问题 ,其中一位用户@KHAN 通过从
div中删除overflow:auto; height: 100%;解决了这个问题。 (详情请查看对话)
【讨论】:
Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'。你解决了吗?
在滚动消息时触发接受的答案,这可以避免这种情况。
你想要这样的模板。
<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 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) { }
}
}
【讨论】:
【讨论】:
如果您想确定在 *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"方法
【讨论】:
@ViewChild('content') content: ElementRef; 来实现建议的 ScrollToBottom
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
【讨论】:
const element = document.getElementById('box');
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
【讨论】:
如果您使用的是最新版本的 Angular,以下内容就足够了:
<div #scrollMe style="overflow: scroll; height: xyz;" [scrollTop]="scrollMe.scrollHeight>
<div class="..."
*ngFor="..."
...>
</div>
</div>
【讨论】:
Vivek 的回答对我有用,但在检查错误后导致表达式发生了变化。没有一个 cmets 对我有用,但我所做的是改变了变化检测策略。
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'page1',
templateUrl: 'page1.html',
})
【讨论】:
分享我的解决方案,因为我对其余部分并不完全满意。我对AfterViewChecked 的问题是有时我向上滚动,并且由于某种原因,这个生命钩子被调用并且即使没有新消息它也会向下滚动我。我尝试使用OnChanges 但this 是一个问题,这导致我使用this 解决方案。不幸的是,仅使用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%
【讨论】:
const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(其中 3.0 是像素容差)。如果用户手动向上滚动,它可以用于ngDoCheck 有条件地不将shouldScrollDown 设置为true。
ngAfterViewChecked 上使用另一个布尔值。我将 shouldScrollDown 名称更改为 numberOfMessagesChanged 以明确这个布尔值到底指的是什么。
if (this.numberOfMessagesChanged && !isScrolledDown),但我认为你没有理解我的建议的意图。如果用户手动向上滚动,我的真正意图是不自动向下滚动,即使添加了新消息。如果没有这个,如果您向上滚动查看历史记录,一旦有新消息到达,聊天就会向下滚动。这可能是非常烦人的行为。 :) 所以,我的建议是检查聊天是否向上滚动之前 新消息添加到 DOM 并且不自动滚动,如果是的话。 ngAfterViewChecked 为时已晚,因为 DOM 已经改变了。
这是stackblitz 上的另一个很好的解决方案。
或者:
接受的答案是一个很好的解决方案,但可以改进它,因为考虑到 ngAfterViewChecked() 生命周期挂钩的工作原理,您的内容/聊天可能经常不由自主地滚动到底部。
这是一个改进的版本...
组件
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;
}
}
模板
<!--Add a scroll event listener-->
<div #scrollMe
style="overflow: scroll; height: xyz;"
(scroll)="onScroll()">
<div class="..."
*ngFor="..."
...>
</div>
</div>
【讨论】:
在使用材料设计 sidenav 的角度中,我必须使用以下内容:
let ele = document.getElementsByClassName('md-sidenav-content');
let eleArray = <Element[]>Array.prototype.slice.call(ele);
eleArray.map( val => {
val.scrollTop = val.scrollHeight;
});
【讨论】:
问题的标题提到“聊天风格”滚动到底部,这也是我需要的。这些答案都没有让我真正满意,因为我真正想要做的是在添加或销毁子元素时滚动到我的 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 中发生更改,它就会滚动到底部。这是我个人一直在寻找的行为。该指令假定您已经在处理列表元素上的height 和overflow 规则。
【讨论】:
在阅读了其他解决方案之后,我能想到的最好的解决方案,所以你只运行你需要的如下: 您使用 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) { }
}
【讨论】:
如果有人在使用 Angular 9 时遇到这个问题,这就是我设法解决的方法。
我从 #scrollMe [scrollTop]="scrollMe.scrollHeight" 的解决方案开始,我得到了人们提到的 ExpressionChangedAfterItHasBeenCheckedError 错误。
为了解决这个问题,我只需在我的 ts 组件中添加:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
...})
constructor(private cdref: ChangeDetectorRef) {}
ngAfterContentChecked() {
this.cdref.detectChanges();
}
【讨论】:
要添加平滑滚动,请执行此操作
#scrollMe [scrollTop]="scrollMe.scrollHeight" style="scroll-behavior: smooth;"
和
this.cdref.detectChanges();
【讨论】: