Angular 2 아래로 스크롤 (채팅 스타일)


111

ng-for루프 내에 단일 셀 구성 요소 집합이 있습니다.

모든 것이 제자리에 있지만 적절한 것을 파악할 수없는 것 같습니다.

현재 나는

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

그러나 이미지가 비동기 적으로 뷰포트를 아래로 내리기 때문에 항상 작동하지는 않습니다.

Angular 2에서 채팅 창 하단으로 스크롤하는 적절한 방법은 무엇입니까?

답변:


203

나는 똑같은 문제가 있었고 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()괜찮을 것입니다.


홈 구성 요소가 있습니다. 홈 템플릿에는 데이터를 표시하고 데이터를 계속 추가하는 div에 다른 구성 요소가 있지만 스크롤 막대 CSS는 내부 템플릿이 아닌 홈 템플릿 div에 있습니다. 문제가 메신저이 scrolltobottom 매번 데이터를 callling되어 내부 구성 요소를 제공하지만 DIV 스크롤 ... 그리고 # scrollMe이 .. 내부 구성 요소에서하지 않도록 accessable 한 아니기 때문에 #scrollMe 홈 구성 요소에 얼마나 내부 구성 요소에서 사용 #scrollme에
Anagh Verma

귀하의 솔루션 인 @Basit은 아름답고 간단하며 끝없는 스크롤을 발생시키지 않으며 사용자가 수동으로 스크롤 할 때 해결 방법이 필요하지 않습니다.
theotherdy

3
안녕하세요, 사용자가 위로 스크롤 할 때 스크롤을 방지하는 방법이 있습니까?
John Louie Dela Cruz

2
나는 또한 이것을 채팅 스타일 접근 방식으로 사용하려고 노력하고 있지만 사용자가 위로 스크롤하면 스크롤하지 않아도됩니다. 사용자가 위로 스크롤했는지 감지하는 방법을 찾을 수 없습니다. 한 가지주의 할 점은이 채팅 구성 요소는 대부분의 경우 전체 화면이 아니며 자체 스크롤 막대가 있다는 것입니다.
HarvP 2017-06-08

2
@HarvP & JohnLouieDelaCruz 사용자가 수동으로 스크롤 한 경우 스크롤을 건너 뛰는 솔루션을 찾았습니까?
suhailvs

166

이를위한 가장 간단하고 최상의 솔루션은 다음과 같습니다.

#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. (자세한 내용은 대화를 확인하십시오)


3
그리고 스크롤을 어떻게 트리거합니까?
Burjua

3
ngfor 항목에 추가 된 항목 또는 내부 div의 높이가 증가하면 자동 스크롤됩니다.
Vivek Doshi

2
Thx @VivekDoshi. 무언가가 추가 된 후이 오류가 발생합니다.> ERROR 오류 : ExpressionChangedAfterItHasBeenCheckedError : 확인 된 후식이 변경되었습니다. 이전 값 : '700'. 현재 값 : '517'.
Vassilis Pits 2017 년

6
@csharpnewbie 목록에서 메시지를 제거 할 때 동일한 문제가 Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'있습니다.. 해결 했나요?
Andrey

6
채울 데이터가있을 때까지 [scrollTop]에 조건을 추가하여 "표현식이 확인 된 후 변경되었습니다"(높이 : 100 %; overflow-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

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 19

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

안녕하세요-이 접근 방식을 시도하고 있지만 항상 catch 블록으로 이동합니다. 나는 카드가 있고 그 안에 여러 메시지를 표시하는 div가 있습니다 (카드 div 내부에는 귀하의 구조와 유사합니다). pls는 이것이 css 또는 ts 파일에서 다른 변경이 필요한지 이해하도록 도와 줄 수 있습니까? 감사.
csharpnewbie

2
단연 최고의 답변입니다. 감사 !
cagliostro

1
이것은 현재 ngAfterViewInit에서 선언 된 경우에도 QueryList가 구독을 한 번만 변경하는 Angular 버그로 인해 작동하지 않습니다. Mert의 솔루션이 작동하는 유일한 솔루션입니다. Vivek의 솔루션은 추가 된 요소에 관계없이 실행되는 반면 Mert는 특정 요소가 추가 될 때만 실행됩니다.
John Hamm

1
이것이 내 문제를 해결하는 유일한 대답입니다. 각도 6 일
suhailvs

2
대박!!! 나는 위의 것을 사용했고 그것이 훌륭하다고 생각했지만 스크롤을 내리고 내 사용자가 이전 댓글을 읽을 수 없었습니다! 이 솔루션에 감사드립니다! 환상적입니다! 내가 :-D 수 있다면 나는 당신에게 100 개 이상의 포인트를 줄 것이다
cheesydoritosandkale


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의 솔루션은 ngAfterViewInit에서 선언 된 경우에도 QueryList가 구독을 한 번만 변경하는 Angular 버그로 인해 작동하지 않습니다. Vivek의 솔루션은 추가 된 요소에 관계없이 실행되는 반면 Mert는 특정 요소가 추가 될 때만 실행됩니다.
John Hamm

감사합니다! 다른 방법에는 각각 몇 가지 단점이 있습니다. 이것은 의도 한대로 작동합니다. 코드를 공유해 주셔서 감사합니다!
lkaupp

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

5
질문자가 올바른 방향으로 나아가도록하는 모든 답변은 도움이되지만 답변에 제한, 가정 또는 단순화를 언급하려고합니다. 간결함은 허용되지만 더 자세한 설명이 더 좋습니다.
baduker

2

나머지에 완전히 만족하지 않았기 때문에 내 솔루션을 공유했습니다. 내 문제 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%

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 19.04.02

@RuslanStelmachenko 제안에 감사드립니다. 나는 그것을 구현했다 (나는 그것을하기로 결정했다ngAfterViewChecked . 다른 부울와 나는 변경 shouldScrollDown하기 위해 이름을 numberOfMessagesChanged정확히 부울을 의미한다 무엇에 대한 몇 가지 선명도를 제공 할 수 있습니다.
RTYX

나는 당신이 if (this.numberOfMessagesChanged && !isScrolledDown)봤지만 내 제안의 의도를 이해하지 못했다고 생각합니다. 내 진짜 의도는 사용자가 수동으로 새 메시지를 추가하더라도 자동으로 아래로 스크롤 하지 않는 것입니다. 위로 스크롤 것입니다. 이 기능이 없으면 위로 스크롤하여 기록을 확인하면 새 메시지가 도착하는 즉시 채팅이 아래로 스크롤됩니다. 이것은 매우 성가신 행동이 될 수 있습니다. :) 그래서 내 제안은 새 메시지가 DOM에 추가 되기 전에 채팅이 위로 스크롤되는지 확인 하고 자동 스크롤되지 않는지 확인하는 것입니다. ngAfterViewCheckedDOM이 이미 변경 되었기 때문에 너무 늦었습니다.
Ruslan Stelmachenko

Aaaaah, 나는 그때 의도를 이해하지 못했습니다. 당신이 말한 것은 의미가 있습니다. 그 상황에서 많은 채팅은 내려 가기위한 버튼이나 거기에 새로운 메시지가 있다는 표시를 추가하기 만하면됩니다. 그래서 이것은 확실히 그것을 달성하는 좋은 방법이 될 것입니다. 편집하고 나중에 변경하겠습니다. 피드백 감사드립니다!
RTYX

@Mert 솔루션을 사용하여 감사합니다. IterableDiffers가 잘 작동하는 드문 경우 (백엔드에서 전화가 거부 됨)에 대해 내 견해가 걷어차 게됩니다. 정말 고맙습니다!
lkaupp 19

2

Vivek의 대답은 나를 위해 일했지만 오류가 확인 된 후 표현식이 변경되었습니다. 어떤 의견도 저에게 효과가 없었지만 변경 감지 전략을 변경했습니다.

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

1

머티리얼 디자인 sidenav를 사용하는 각도에서 다음을 사용해야했습니다.

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) { }
  }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.