답변:
나는 똑같은 문제가 있었고 a 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 (더미 채팅 앱 포함) 및 전체 코드 링크입니다.
Angular2 및 최대 5에서도 작동합니다. 위의 데모는 Angular5에서 수행됩니다.
노트 :
오류 :
ExpressionChangedAfterItHasBeenCheckedError
당신의 CSS를 확인하십시오, 그것은 CSS 측의 문제가 아닌 각도 측면, 사용자 @KHAN 중 하나는 제거하여 그것을 해결했다입니다
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;
}
여기서 중요한 것은 "last" 변수가 현재 마지막 항목에 있는지 여부를 정의하므로 "scrollToBottom" 메서드를 트리거 할 수 있습니다.
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
나머지에 완전히 만족하지 않았기 때문에 내 솔루션을 공유했습니다. 내 문제 AfterViewChecked
는 때때로 내가 위로 스크롤하고 어떤 이유로이 life hook이 호출되고 새로운 메시지가 없더라도 나를 아래로 스크롤한다는 것입니다. 나는 사용하여 시도 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에 추가 되기 전에 채팅이 위로 스크롤되는지 확인 하고 자동 스크롤되지 않는지 확인하는 것입니다. ngAfterViewChecked
DOM이 이미 변경 되었기 때문에 너무 늦었습니다.
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) { }
}