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조건 설정하지 않도록 shouldScrollDown에 true사용자가 수동으로 스크롤합니다.
— 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.