Sudut 2 Gulir ke bawah (Gaya obrolan)


111

Saya memiliki satu set komponen sel tunggal dalam satu ng-forlingkaran.

Saya memiliki segalanya di tempat tetapi saya tidak bisa menemukan yang tepat

Saat ini saya punya

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

Tapi ini tidak bekerja sepanjang waktu karena gambar secara asinkron mendorong viewport ke bawah.

Apa cara yang tepat untuk menggulir ke bagian bawah jendela obrolan di Angular 2?

Jawaban:


204

Saya memiliki masalah yang sama, saya menggunakan kombinasi AfterViewCheckeddan @ViewChild(Angular2 beta.3).

Komponen:

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

Template:

<div #scrollMe style="overflow: scroll; height: xyz;">
    <div class="..." 
        *ngFor="..."
        ...>  
    </div>
</div>

Tentu saja ini sangat mendasar. The AfterViewCheckedmemicu setiap kali pandangan diperiksa:

Terapkan antarmuka ini untuk mendapatkan pemberitahuan setelah setiap pemeriksaan tampilan komponen Anda.

Jika Anda memiliki input-field untuk mengirim pesan misalnya, event ini dijalankan setelah setiap keyup (hanya untuk memberikan contoh). Tetapi jika Anda menyimpan apakah pengguna menggulir secara manual dan kemudian melewati scrollToBottom()Anda harus baik-baik saja.


Saya memiliki komponen home, di template home saya memiliki komponen lain di div yang menampilkan data dan terus menambahkan data tetapi scroll bar css ada di div template home bukan di template dalam. Masalahnya adalah saya memanggil scrolltobottom ini setiap kali data masuk ke dalam komponen tetapi #scrollMe ada di komponen rumah karena div itu dapat digulir ... dan # scrollMe tidak dapat diakses dari dalam komponen .. tidak yakin bagaimana menggunakan #scrollme dari dalam komponen
Anagh Verma

Solusi Anda, @Basit, sangat sederhana dan tidak berakhir dengan menggulir tanpa henti / perlu solusi saat pengguna menggulir secara manual.
theotherdy

3
Hai, apakah ada cara untuk mencegah scroll saat pengguna menggulir ke atas?
John Louie Dela Cruz

2
Saya juga mencoba menggunakan ini dalam pendekatan gaya obrolan, tetapi tidak perlu menggulir jika pengguna menggulir ke atas. Saya telah mencari dan tidak dapat menemukan cara untuk mendeteksi jika pengguna menggulir ke atas. Satu peringatan adalah komponen obrolan ini sebagian besar tidak layar penuh dan memiliki bilah gulir sendiri.
HarvP

2
@HarvP & JohnLouieDelaCruz apakah Anda menemukan solusi untuk melewati scroll jika pengguna menggulir secara manual?
suhailvs

167

Solusi paling sederhana dan terbaik untuk ini adalah:

Tambahkan hal #scrollMe [scrollTop]="scrollMe.scrollHeight"sederhana ini di sisi Template

<div style="overflow: scroll; height: xyz;" #scrollMe [scrollTop]="scrollMe.scrollHeight">
    <div class="..." 
        *ngFor="..."
        ...>  
    </div>
</div>

Berikut ini tautan untuk WORKING DEMO (Dengan aplikasi obrolan boneka) DAN KODE LENGKAP

Akan bekerja dengan Angular2 dan juga hingga 5, Seperti demo di atas dilakukan di Angular5.


Catatan :

Untuk kesalahan: ExpressionChangedAfterItHasBeenCheckedError

Silakan periksa css Anda, ini masalah sisi css, bukan sisi Angular, Salah satu pengguna @KHAN telah menyelesaikannya dengan menghapus overflow:auto; height: 100%;dari div. (silakan periksa percakapan untuk detailnya)


3
Dan bagaimana Anda memicu gulungan itu?
Burjua

3
itu akan berputar otomatis ke bawah pada item apa pun yang ditambahkan ke item ngfor ATAU ketika tinggi div sisi dalam meningkat
Vivek Doshi

2
Terima kasih @VivekDoshi. Meskipun setelah sesuatu ditambahkan saya mendapatkan kesalahan ini:> ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression telah berubah setelah diperiksa. Nilai sebelumnya: '700'. Nilai saat ini: '517'.
Vassilis Pits

6
@csharpnewbie Saya memiliki masalah yang sama saat melepas pesan apapun dari daftar: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. Apakah Anda menyelesaikannya?
Andrey

6
Saya dapat memecahkan "Ekspresi telah berubah setelah dicentang" (sambil menjaga tinggi: 100%; overflow-y: scroll) dengan menambahkan kondisi ke [scrollTop], untuk mencegahnya disetel SAMPAI saya memiliki data untuk diisi dengan, misalnya: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "let message of messages"> .. . penambahan centang "messages.length === 0? 0" tampaknya memperbaiki masalah, meskipun saya tidak dapat menjelaskan alasannya, jadi saya tidak dapat mengatakan dengan pasti bahwa perbaikan tersebut tidak unik untuk penerapan saya.
Ben

20

Saya menambahkan tanda centang untuk melihat apakah pengguna mencoba menggulir ke atas.

Saya hanya akan meninggalkan ini di sini jika ada yang menginginkannya :)

<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>

dan kode:

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

}

solusi yang benar-benar bisa diterapkan tanpa kesalahan di konsol. Terima kasih!
Dmitry Vasilyuk Just3F

20

Jawaban yang diterima menyala saat menggulir pesan, ini menghindarinya.

Anda menginginkan template seperti ini.

<div #content>
  <div #messages *ngFor="let message of messages">
    {{message}}
  </div>
</div>

Kemudian Anda ingin menggunakan anotasi ViewChildren untuk berlangganan elemen pesan baru yang ditambahkan ke halaman.

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

Hai - Saya mencoba pendekatan ini, tetapi selalu masuk ke blok tangkap. Saya memiliki kartu dan di dalamnya saya memiliki div yang menampilkan banyak pesan (di dalam div kartu, saya mirip dengan struktur Anda). Bisakah Anda membantu saya memahami jika ini memerlukan perubahan lain dalam file css atau ts? Terima kasih.
csharpnewbie

2
Sejauh ini jawaban terbaik. Terima kasih!
cagliostro

1
Ini saat ini tidak berfungsi karena bug Angular di mana QueryList mengubah langganan yang hanya diaktifkan sekali bahkan ketika dideklarasikan dalam ngAfterViewInit. Solusi dari Mert adalah satu-satunya solusi yang berhasil. Solusi Vivek menyala tidak peduli elemen apa yang ditambahkan, sedangkan Mert's hanya dapat menyala ketika elemen tertentu ditambahkan.
John Hamm

1
Ini adalah satu-satunya jawaban yang memecahkan masalah saya. bekerja dengan sudut 6
suhailvs

2
Luar biasa !!! Saya menggunakan yang di atas dan berpikir itu luar biasa tetapi kemudian saya macet saat menggulir ke bawah dan pengguna saya tidak dapat membaca komentar sebelumnya! Terima kasih atas solusi ini! Fantastis! Saya akan memberi Anda 100+ poin jika saya bisa :-D
cheesydoritosandkale


13

Jika Anda ingin memastikan, bahwa Anda menggulir ke akhir setelah * ngFor selesai, Anda dapat menggunakan ini.

<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;
}

Penting di sini, "lalu" mendefinisikan variabel jika Anda saat ini di item terakhir, sehingga Anda dapat memicu "scrollToBottom" Metode


1
Jawaban ini layak menjadi jawaban yang diterima. Ini adalah satu-satunya solusi yang berhasil. Solusi Denixtry tidak berfungsi karena bug Angular di mana QueryList mengubah langganan hanya diaktifkan sekali bahkan saat dideklarasikan dalam ngAfterViewInit. Solusi Vivek menyala tidak peduli elemen apa yang ditambahkan, sedangkan Mert's hanya dapat menyala ketika elemen tertentu ditambahkan.
John Hamm

TERIMA KASIH! Masing-masing metode lain memiliki beberapa kelemahan. Ini hanya berfungsi sebagaimana mestinya. Terima kasih telah membagikan kode Anda!
lkaupp

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

5
Jawaban apa pun yang mengarahkan penanya ke arah yang benar sangat membantu, tetapi cobalah untuk menyebutkan batasan, asumsi, atau penyederhanaan apa pun dalam jawaban Anda. Singkatnya bisa diterima, tetapi penjelasan yang lebih lengkap lebih baik.
baduker

2

Berbagi solusi saya, karena saya tidak sepenuhnya puas dengan yang lain. Masalah saya AfterViewCheckedadalah terkadang saya menggulir ke atas, dan untuk beberapa alasan, pengait kehidupan ini dipanggil dan menggulung saya ke bawah meskipun tidak ada pesan baru. Saya mencoba menggunakan OnChangestetapi ini adalah masalah, yang mengarahkan saya ke solusi ini . Sayangnya, hanya dengan menggunakan DoCheck, itu bergulir ke bawah sebelum pesan dirender, yang juga tidak berguna, jadi saya menggabungkannya sehingga DoCheck pada dasarnya menunjukkan AfterViewCheckedapakah itu harus memanggil scrollToBottom.

Senang menerima umpan balik.

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%

Ini dapat digunakan untuk memeriksa apakah obrolan digulir ke bawah sebelum menambahkan pesan baru ke DOM: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(di mana 3,0 adalah toleransi dalam piksel). Hal ini dapat digunakan dalam ngDoCheckkondisional tidak diatur shouldScrollDownuntuk truejika pengguna secara manual menggulir up.
Ruslan Stelmachenko

Terima kasih atas saran @RuslanStelmachenko. Saya menerapkannya (saya memutuskan untuk melakukannya ngAfterViewCheckeddengan boolean lain. Saya mengganti shouldScrollDownnama menjadi numberOfMessagesChangeduntuk memberikan kejelasan tentang apa sebenarnya yang dimaksud boolean ini.
RTYX

Saya melihat Anda melakukannya if (this.numberOfMessagesChanged && !isScrolledDown), tetapi saya pikir Anda tidak mengerti maksud dari saran saya. Maksud saya sebenarnya adalah untuk tidak menggulir ke bawah secara otomatis bahkan ketika pesan baru ditambahkan, jika pengguna menggulir ke atas secara manual . Tanpa ini, jika Anda menggulir ke atas untuk melihat riwayat, obrolan akan menggulir ke bawah segera setelah pesan baru tiba. Ini bisa menjadi perilaku yang sangat mengganggu. :) Jadi, saran saya adalah untuk memeriksa apakah obrolan di-scroll ke atas sebelum pesan baru ditambahkan ke DOM dan jangan autoscroll, jika ya. ngAfterViewCheckedsudah terlambat karena DOM sudah berubah.
Ruslan Stelmachenko

Aaaaah, saya belum mengerti maksudnya saat itu. Apa yang Anda katakan masuk akal - Saya pikir dalam situasi itu banyak obrolan hanya menambahkan tombol untuk turun, atau tanda bahwa ada pesan baru di sana, jadi ini pasti cara yang baik untuk mencapai itu. Saya akan mengeditnya dan mengubahnya nanti. Terima kasih untuk umpan baliknya!
RTYX

Terima kasih Tuhan untuk bagian Anda, dengan solusi @Mert, pandangan saya ditendang pada kasus yang jarang terjadi (panggilan ditolak dari backend), dengan IterableDiffers Anda berfungsi dengan baik. Terima kasih banyak!
lkaupp

2

Jawaban Vivek telah berhasil untuk saya, tetapi menghasilkan ekspresi yang berubah setelah diperiksa kesalahannya. Tidak ada komentar yang berhasil untuk saya, tetapi yang saya lakukan adalah mengubah strategi deteksi perubahan.

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

1

Dalam sudut menggunakan desain material sidenav saya harus menggunakan yang berikut:

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

Setelah membaca solusi lain, solusi terbaik yang dapat saya pikirkan, jadi Anda hanya menjalankan yang Anda butuhkan adalah sebagai berikut: Anda menggunakan ngOnChanges untuk mendeteksi perubahan yang tepat

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;
        }
      }
    }

}

Dan Anda menggunakan ngAfterViewChecked untuk benar-benar menerapkan perubahan sebelum dirender, tetapi setelah ketinggian penuh dihitung

ngAfterViewChecked(): void {
    if(this.newMessageHappened) {
      this.scrollToBottom();
      this.newMessageHappened = false;
    }
    else if(this.lazyLoadHappened) {
      // keep the same scroll
      this.lazyLoadHappened = false
    }
  }

Jika Anda bertanya-tanya bagaimana cara mengimplementasikan scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.