Angular 2下にスクロール(チャットスタイル)


111

内に単一セルコンポーネントのセットがあります ng-forループます。

私はすべてをきちんと整えていますが、適切なものを理解することができない

現在私は持っています

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

ただし、画像が非同期でビューポートを押し下げるため、これは常に機能するとは限りません。

Angular 2のチャットウィンドウの一番下までスクロールする適切な方法は何ですか?

回答:


203

私は同じ問題を抱えていました、私はとを使用し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にあります。内部コンポーネントから確認してくださいどのように使用#scrollmeに...ない問題イムは、このscrolltobottom毎回データをcalllingさは、コンポーネント内に来るが、そのdiv要素がスクロール可能であるため、#scrollMeは、ホームコンポーネントである...と#scrollMe内部コンポーネントからアクセス可能ではありません
Anaghバーマ

あなたのソリューション、@ Basitは美しくシンプルで、エンドレススクロールが発生することはなく、ユーザーが手動でスクロールする場合の回避策も必要ありません。
theotherdy

3
こんにちは、ユーザーが上にスクロールしたときにスクロールを防ぐ方法はありますか?
John Louie Dela Cruz

2
私はこれをチャットスタイルのアプローチでも使用しようとしていますが、ユーザーが上にスクロールしてもスクロールする必要はありません。私は探していましたが、ユーザーが上にスクロールしたかどうかを検出する方法を見つけることができません。注意点の1つは、このチャットコンポーネントがほとんどの場合フルスクリーンではなく、独自のスクロールバーを持っていることです。
HarvP 2017年

2
@HarvP&JohnLouieDelaCruzユーザーが手動でスクロールした場合、スクロールをスキップする解決策を見つけましたか?
suhailvs

166

このための最も簡単で最良の解決策は次のとおりです。

テンプレート側にこの#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。(詳細については会話を確認してください)


3
そして、どのようにしてスクロールをトリガーしますか?
ブルジュア2017

3
ngforアイテムに追加されたアイテム、または内側のdivの高さが増加すると、アイテムが下に自動スクロールします
Vivek Doshi

2
Thx @VivekDoshi。何かが追加された後でも、次のエラーが発生します。>エラーエラー: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
[チェック後に変更された式](高さを100%に保ちながら、オーバーフロー-y:スクロール)を[scrollTop]に条件を追加して解決し、データを入力するまで設定されないようにしました。例:<ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0?0:chatHistory.scrollHeight"> <li * ngFor = "メッセージのメッセージ"> .. 。 "messages.length === 0?0"チェックを追加すると問題が解決したようですが、理由は説明できません。そのため、この修正が実装に固有のものではないことは確かではありません。
ベン

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

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内では、私はあなたの構造に似ています)。これがcssまたはtsファイルの他の変更が必要かどうかを理解するのに役立ちますか?ありがとう。
csharpnewbie

2
断然最良の答えです。よろしくお願いします!
cagliostro 2018年

1
これは、ngAfterViewInitで宣言されている場合でも、QueryListの変更サブスクリプションが1回だけ発生するというAngularのバグのため、現在は機能しません。Mertによる解決策が機能する唯一のものです。どの要素が追加されてもVivekのソリューションは起動しますが、Mertのソリューションは特定の要素が追加された場合にのみ起動できます。
John Hamm、

1
これが私の問題を解決する唯一の答えです。角度6で動作
suhailvs

2
驚くばかり!!!私は上記のものを使用し、それが素晴らしいと思いましたが、下にスクロールするのが行き詰まり、ユーザーは以前のコメントを読むことができませんでした!この解決策をありがとう!素晴らしい!できれば100ポイント以上を差し上げます:-D
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;
}

ここで重要なのは、「最後の」変数が現在最後のアイテムにいるかどうかを定義するため、「scrollToBottom」メソッドをトリガーできることです。


1
この答えは受け入れられるべきものです。これが機能する唯一のソリューションです。ngAfterViewInitで宣言されている場合でも、QueryListがサブスクリプションを一度だけ変更するというAngularのバグのため、Denixtryのソリューションは機能しません。どの要素が追加されてもVivekのソリューションは起動しますが、Mertのソリューションは特定の要素が追加された場合にのみ起動できます。
John Hamm、2018

ありがとうございました!他の方法にはそれぞれ欠点があります。これは意図したとおりに機能します。コードを共有していただきありがとうございます。
lkaupp

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

5
質問者を正しい方向に導くどんな答えも役に立ちますが、あなたの答えの制限、仮定、または単純化について言及するようにしてください。簡潔さは許容されますが、完全な説明の方が優れています。
baduker 2018年

2

残りの部分に完全に満足していなかったので、私の解決策を共有します。私の問題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%

これは、新しいメッセージをDOMに追加する前にチャットが下にスクロールされているかどうかを確認するために使用できますconst isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(3.0はピクセル単位の許容値)。これは、ユーザーが手動で上にスクロールngDoCheckshouldScrollDownれたtrue場合に条件付きで設定されない場合に使用できます。
Ruslan Stelmachenko

提案@RuslanStelmachenkoをありがとう。私は(それを実現し、私は上にそれを行うことを決めたngAfterViewChecked。他のブールと私は変更shouldScrollDownする名前をnumberOfMessagesChanged正確に、このブール値が参照するかについて、いくつかの明快さを与える。
RTYX

私はあなたがそうif (this.numberOfMessagesChanged && !isScrolledDown)だと思いますが、あなたは私の提案の意図を理解していなかったと思います。私の本当の目的は、ユーザーが手動で上にスクロールした場合、新しいメッセージが追加されても自動的に下にスクロールしないことです。これがない場合、上にスクロールして履歴を表示すると、新しいメッセージが到着するとすぐにチャットが下にスクロールします。これは非常に迷惑な動作になる可能性があります。:)したがって、私の提案は、新しいメッセージがDOMに追加される前にチャットがスクロールアップされているかどうかを確認し、そうである場合は自動スクロールしないことでした。DOMがすでに変更されているため、遅すぎます。ngAfterViewChecked
Ruslan Stelmachenko

ああ、当時はその意図がわからなかった。あなたが言ったことは理にかなっている-私はそのような状況では多くのチャットが下に行くためのボタンを追加するか、そこに新しいメッセージがあるというマークを付けていると思うので、これは間違いなくそれを達成する良い方法だろう。編集して後で変更します。フィードバックをお寄せいただきありがとうございます!
RTYX

共有してくれてありがとう。@ Mertソリューションを使用すると、IterableDiffersがうまく機能するというまれなケース(バックエンドから呼び出しが拒否される)で私のビューがキックダウンされます。どうもありがとうございます!
lkaupp

2

Vivekの答えは私にとってはうまくいきましたが、エラーチェック後に式が変更されました。コメントはどれもうまくいきませんでしたが、私がしたことは変更検出戦略を変更することでした。

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

1

マテリアルデザインのサイドナブを使用した角度で​​は、以下を使用する必要がありました。

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.