内に単一セルコンポーネントのセットがあります ng-for
ループます。
私はすべてをきちんと整えていますが、適切なものを理解することができない
現在私は持っています
setTimeout(() => {
scrollToBottom();
});
ただし、画像が非同期でビューポートを押し下げるため、これは常に機能するとは限りません。
Angular 2のチャットウィンドウの一番下までスクロールする適切な方法は何ですか?
内に単一セルコンポーネントのセットがあります ng-for
ループます。
私はすべてをきちんと整えていますが、適切なものを理解することができない
現在私は持っています
setTimeout(() => {
scrollToBottom();
});
ただし、画像が非同期でビューポートを押し下げるため、これは常に機能するとは限りません。
Angular 2のチャットウィンドウの一番下までスクロールする適切な方法は何ですか?
回答:
私は同じ問題を抱えていました、私はとを使用し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>
ワーキングデモ(ダミーチャットアプリを使用)とフルコードのリンクは次のとおりです
Angular2と最大5で動作します。上記のデモはAngular5で行われます。
注意 :
エラーの場合:
ExpressionChangedAfterItHasBeenCheckedError
あなたのCSSを確認してください、それはCSS側の問題であり、Angular側ではありません。@ KHANユーザーの1人が
overflow:auto; height: 100%;
から削除することで解決しましたdiv
。(詳細については会話を確認してください)
Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'
。解決しましたか?
ユーザーが上にスクロールしようとしたかどうかを確認するチェックを追加しました。
誰かがそれを望んでいるなら、私はこれをここに残すつもりです:)
<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) { }
}
}
メッセージをスクロールしている間に、受け入れられた回答が起動します。これにより、それを回避できます。
このようなテンプレートが必要です。
<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) {}
}
使用を検討してください
.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewを参照してください
* 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;
}
ここで重要なのは、「最後の」変数が現在最後のアイテムにいるかどうかを定義するため、「scrollToBottom」メソッドをトリガーできることです。
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
残りの部分に完全に満足していなかったので、私の解決策を共有します。私の問題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%
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に追加される前にチャットがスクロールアップされているかどうかを確認し、そうである場合は自動スクロールしないことでした。DOMがすでに変更されているため、遅すぎます。ngAfterViewChecked
Vivekの答えは私にとってはうまくいきましたが、エラーチェック後に式が変更されました。コメントはどれもうまくいきませんでしたが、私がしたことは変更検出戦略を変更することでした。
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'page1',
templateUrl: 'page1.html',
})
他のソリューションを読んだ後、私が考えることができる最良のソリューションなので、必要なものだけを実行します: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) { }
}