Açısal 2 Aşağı kaydır (Sohbet stili)


112

Bir ng-fordöngü içinde bir dizi tek hücre bileşenim var .

Her şeyim yerinde ama doğru olanı bulamıyorum

Şu anda sahibim

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

Ancak, görüntüler eşzamansız olarak görüntü alanını aşağı ittiğinden, bu her zaman çalışmaz.

Angular 2'de bir sohbet penceresinin altına kaydırmanın uygun yolu nedir?

Yanıtlar:


205

Aynı sorunu yaşadım, bir AfterViewCheckedve @ViewChildkombinasyonunu kullanıyorum (Angular2 beta.3).

Bileşen:

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

Şablon:

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

Elbette bu oldukça basit. AfterViewCheckedGörünümü kontrol edildi her zaman tetikler:

Bileşeninizin görünümü her kontrol edildikten sonra bildirim almak için bu arayüzü uygulayın.

Örneğin, mesaj göndermek için bir giriş alanınız varsa, bu olay her tuşlamadan sonra tetiklenir (sadece bir örnek vermek gerekirse). Ama kullanıcı el kaydırılan olmadığını kaydedebilir ve daha sonra atlarsanız scrollToBottom()olman gereken cezayı.


Bir ev bileşenim var, ev şablonunda, div'de verileri gösteren ve veri eklemeye devam eden başka bir bileşen var, ancak kaydırma çubuğu css, iç şablonda değil ana şablon div'de. sorun, bu kaydırma çubuğunu çağırmaktır, ancak veri bileşenin içine her girdiğinde, ancak #scrollMe ana bileşenindedir çünkü bu div kaydırılabilir ... ve # scrollMe'ye içeriden erişilemez .. #scrollme bileşeninin içinden nasıl kullanılacağından emin değilim
Anagh Verma

Çözümünüz, @Basit, güzel bir şekilde basittir ve sonunda sonsuz kaydırma yapmakla kalmaz / kullanıcı manuel olarak kaydırdığında bir geçici çözüme ihtiyaç duymaz.
theotherdy

3
Merhaba, kullanıcı yukarı kaydırdığında kaydırmayı önlemenin bir yolu var mı?
John Louie Dela Cruz

2
Bunu sohbet tarzı bir yaklaşımda da kullanmaya çalışıyorum, ancak kullanıcı yukarı kaydırdığında kaydırmamam gerekiyor. Bakıyorum ve kullanıcının yukarı kaydırıp kaydırmadığını tespit etmenin bir yolunu bulamıyorum. Bir uyarı, bu sohbet bileşeninin çoğu zaman tam ekran olmaması ve kendi kaydırma çubuğuna sahip olmasıdır.
HarvP

2
@HarvP & JohnLouieDelaCruz, kullanıcı manuel olarak kaydırırsa kaydırmayı atlamak için bir çözüm buldunuz mu?
suhailvs

167

Bunun için en basit ve en iyi çözüm şudur:

Bu #scrollMe [scrollTop]="scrollMe.scrollHeight"basit şeyi Şablon tarafına ekleyin

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

İşte ÇALIŞMA DEMOSU (Kukla sohbet uygulaması ile) VE TAM KOD bağlantısı

Angular2 ile çalışacak ve ayrıca 5'e kadar, Yukarıdaki demo Angular5'te yapılır.


Not :

Hata için: ExpressionChangedAfterItHasBeenCheckedError

Lütfen css'nizi kontrol edin, bu bir css tarafı sorunu, Açısal taraf değil, @KHAN kullanıcılarından biri bunu ' overflow:auto; height: 100%;dan kaldırarak çözdü div. (ayrıntılar için lütfen konuşmaları kontrol edin)


3
Ve parşömeni nasıl tetiklersiniz?
Burjua

3
öğeye eklenen herhangi bir öğede aşağıya otomatik kaydırılır VEYA iç tarafın yüksekliği arttığında
Vivek Doshi

2
Thx @VivekDoshi. Yine de bir şey eklendikten sonra şu hatayı alıyorum:> ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: İfade kontrol edildikten sonra değişti. Önceki değer: '700'. Mevcut değer: '517'.
Vassilis Pits

6
listeden herhangi bir mesaj çıkarılırken @csharpnewbie ben aynı sorunu vardır: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. Çözdün mü?
Andrey

6
"İfade kontrol edildikten sonra değişti" sorununu çözebildim (yüksekliği korurken:% 100; taşma-y: kaydırma), doldurulacak verilerim OLANA KADAR ayarlanmasını önlemek için [scrollTop] öğesine bir koşul ekleyerek örn: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "let message of messages"> .. . "messages.length === 0? 0" kontrolünün eklenmesi sorunu çözmüş görünüyordu, ancak nedenini açıklayamasam da, düzeltmenin benim uygulamama özgü olmadığını kesin olarak söyleyemem.
Ben

20

Kullanıcının yukarı kaydırmayı deneyip denemediğini görmek için bir kontrol ekledim.

Biri isterse bunu burada bırakacağım :)

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

ve kod:

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

}

konsolda hatasız gerçekten uygulanabilir bir çözüm. Teşekkürler!
Dmitry Vasilyuk Just3F

20

Kabul edilen cevap, mesajlar arasında gezinirken tetiklenir, bu bundan kaçınır.

Bunun gibi bir şablon istiyorsunuz.

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

Ardından sayfaya eklenen yeni mesaj öğelerine abone olmak için bir ViewChildren ek açıklaması kullanmak istiyorsunuz.

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

Merhaba - Bu yaklaşımı deniyorum, ama her zaman yakalama bloğuna giriyor. Bir kartım var ve içinde birden fazla mesaj görüntüleyen bir div var (kartın içinde, yapınıza benzer bir şey var). Bunun css veya ts dosyalarında başka bir değişikliğe ihtiyacı olup olmadığını anlamama yardımcı olabilir misiniz? Teşekkürler.
csharpnewbie

2
Şimdiye kadar en iyi cevap. Teşekkürler !
cagliostro

1
Bu, şu anda QueryList'in ngAfterViewInit'te bildirildiğinde bile yalnızca bir kez tetiklenen abonelikleri değiştirdiği bir Angular hata nedeniyle çalışmıyor. Mert'in çözümü, işe yarayan tek çözümdür. Vivek'in çözümü, hangi element eklenirse eklensin ateşlenirken, Mert'in çözümü ancak belirli elementler eklendiğinde ateş edebiliyor.
John Hamm

1
Sorunumu çözen tek cevap bu. açısal 6 ile çalışır
suhailvs

2
Müthiş !!! Yukarıdakini kullandım ve harika olduğunu düşündüm ama sonra aşağı kaydırırken takılıp kaldım ve kullanıcılarım önceki yorumları okuyamadı! Bu çözüm için teşekkürler! Fantastik!
Yapabilseydim


13

* NgFor bittikten sonra sonuna kadar ilerlediğinizden emin olmak istiyorsanız, bunu kullanabilirsiniz.

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

Burada önemli olan "son" değişken, şu anda son öğede olup olmadığınızı tanımlar, böylece "scrollToBottom" yöntemini tetikleyebilirsiniz.


1
Bu cevap kabul edilen cevap olmayı hak ediyor. İşe yarayan tek çözüm bu. Denixtry'nin çözümü, QueryList'in ngAfterViewInit'te bildirildiğinde bile yalnızca bir kez tetiklenen abonelikleri değiştirdiği bir Angular hata nedeniyle çalışmıyor. Vivek'in çözümü, hangi element eklenirse eklensin ateşlenirken, Mert'in çözümü ancak belirli elementler eklendiğinde ateş edebiliyor.
John Hamm

TEŞEKKÜR EDERİM! Diğer yöntemlerin her birinin bazı dezavantajları vardır. Bu sadece amaçlandığı gibi çalışır. Kodunuzu paylaştığınız için teşekkürler!
lkaupp

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

5
Soruyu soranın doğru yöne gitmesini sağlayan herhangi bir yanıt yararlıdır, ancak yanıtınızda herhangi bir sınırlama, varsayım veya basitleştirmeden bahsetmeye çalışın. Kısalık kabul edilebilir, ancak daha kapsamlı açıklamalar daha iyidir.
baduker

2

Çözümümü paylaşıyorum, çünkü geri kalanından tamamen memnun değildim. Benim sorunum AfterViewChecked, bazen yukarı kaydırıyorum ve bazı nedenlerden dolayı, bu yaşam kancası çağrılıyor ve yeni mesaj olmasa bile beni aşağı kaydırıyor. Kullanmayı denedim OnChangesama bu beni bu çözüme götüren bir sorundu . Ne yazık ki, yalnızca kullanarak DoCheck, mesajlar işlenmeden önce aşağı kaydırılıyordu, bu da yararlı değildi, bu yüzden onları birleştirdim, böylece DoCheck temelde AfterViewCheckedçağrı yapıp yapmayacağını belirtiyor scrollToBottom.

Geri bildirim almaktan mutluluk duyarız.

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%

Bu, DOM'a yeni mesaj eklemeden önce sohbetin aşağı kaydırılıp kaydırılmadığını kontrol etmek için kullanılabilir: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(burada 3.0 piksel cinsinden toleranstır). Kullanıcı manuel olarak yukarı kaydırılırsa, ngDoCheckkoşullu olarak ayarlanmaması shouldScrollDowniçin kullanılabilir true.
Ruslan Stelmachenko

@RuslanStelmachenko öneriniz için teşekkürler. Onu uyguladım (bunu ngAfterViewCheckeddiğer shouldScrollDownnumberOfMessagesChanged
booleanla

Anladığını görüyorum if (this.numberOfMessagesChanged && !isScrolledDown)ama sanırım önerimin amacını anlamadın. Benim gerçek niyeti etmektir değil kullanıcı eğer yeni mesaj, eklense bile otomatik olarak aşağı kaydırma elle yukarı kaydırılan. Bu olmadan, geçmişi görmek için yukarı kaydırırsanız, yeni mesaj gelir gelmez sohbet aşağı kayacaktır. Bu çok can sıkıcı bir davranış olabilir. :) Bu yüzden, benim önerim, DOM'a yeni mesaj eklenmeden önce sohbetin yukarı kaydırılıp kaydırılmadığını kontrol etmek ve eğer öyleyse otomatik kaydırmamaktı . ngAfterViewCheckedDOM zaten değiştiği için çok geç.
Ruslan Stelmachenko

Aaaaah, o zamanlar niyeti anlamamıştım. Söylediğiniz şey mantıklı - bence bu durumda birçok sohbet aşağı inmek için bir düğme veya aşağıda yeni bir mesaj olduğunu belirten bir işaret ekler, bu yüzden bu kesinlikle bunu başarmanın iyi bir yolu olacaktır. Düzenleyip daha sonra değiştireceğim. Geri dönüşünüz için teşekkür ederiz!
RTYX

Paylaştığınız için tanrıya teşekkürler, @Mert çözümü ile, görüşüm nadir durumlarda (arka uçtan reddedilme çağrısı) reddediliyor, IterableDiffers'ınız güzel çalışıyor. Çok teşekkür ederim!
lkaupp

2

Vivek'in cevabı benim için işe yaradı, ancak hata kontrol edildikten sonra bir ifadenin değişmesine neden oldu. Yorumların hiçbiri benim için işe yaramadı, ancak yaptığım şey değişiklik tespit stratejisini değiştirmekti.

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

1

Açısal malzeme tasarımında sidenav kullanımında aşağıdakileri kullanmak zorunda kaldım:

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

Diğer çözümleri okuduktan sonra, aklıma gelen en iyi çözüm, sadece ihtiyacınız olanı çalıştırmanız için şudur: Doğru değişikliği tespit etmek için ngOnChanges kullanıyorsunuz

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

}

Değişimi oluşturmadan önce ancak tam yükseklik hesaplandıktan sonra gerçekten zorlamak için ngAfterViewChecked kullanırsınız.

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

ScrollToBottom'un nasıl uygulanacağını merak ediyorsanız

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.