Angular 2 Scorri verso il basso (stile chat)


111

Ho una serie di componenti a cella singola all'interno di un file ng-for ciclo.

Ho tutto a posto ma non riesco a capire il corretto

Al momento ce l'ho

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

Ma questo non funziona sempre poiché le immagini spingono in modo asincrono la visualizzazione verso il basso.

Qual è il modo appropriato per scorrere fino alla fine di una finestra di chat in Angular 2?

Risposte:


204

Ho avuto lo stesso problema, sto usando un AfterViewCheckede@ViewChild combinazione (Angular2 beta.3).

Il componente:

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

Il template:

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

Ovviamente questo è piuttosto semplice. I AfterViewCheckedtrigger ogni volta che la visualizzazione è stata controllata:

Implementa questa interfaccia per ricevere una notifica dopo ogni controllo della visualizzazione del tuo componente.

Se hai un campo di input per l'invio di messaggi, ad esempio, questo evento viene attivato dopo ogni tasto (solo per fare un esempio). Ma se salvi se l'utente ha fatto scorrere manualmente e poi salti scrollToBottom(), dovresti stare bene.


Ho un componente home, nel modello home ho un altro componente nel div che mostra i dati e continuo ad aggiungere dati ma il CSS della barra di scorrimento si trova nel div del modello home non nel modello interno. il problema è che sto richiamando questo scrolltobottom ogni volta che i dati entrano nel componente ma #scrollMe è nel componente home poiché quel div è scorrevole ... e # scrollMe non è accessibile dall'interno del componente .. non sono sicuro di come usare #scrollme dall'interno del componente
Anagh Verma

La tua soluzione, @Basit, è meravigliosamente semplice e non finisce per sparare pergamene infinite / necessita di una soluzione alternativa per quando l'utente scorre manualmente.
theotherdy

3
Ciao, c'è un modo per impedire lo scorrimento quando l'utente scorre verso l'alto?
John Louie Dela Cruz

2
Sto anche cercando di usarlo in un approccio in stile chat, ma non è necessario scorrere se l'utente scorre verso l'alto. Ho cercato e non riesco a trovare un modo per rilevare se l'utente ha fatto scorrere verso l'alto. Un avvertimento è che questo componente di chat non è a schermo intero per la maggior parte del tempo e ha una propria barra di scorrimento.
HarvP

2
@HarvP e JohnLouieDelaCruz hai trovato una soluzione per saltare lo scorrimento se l'utente scorreva manualmente?
suhailvs

167

La soluzione più semplice e migliore per questo è:

Aggiungi questa #scrollMe [scrollTop]="scrollMe.scrollHeight"semplice cosa sul lato del modello

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

Ecco il link per DEMO FUNZIONANTE (con app di chat fittizia) E CODICE COMPLETO

Funzionerà con Angular2 e anche fino a 5, come sopra la demo viene eseguita in Angular5.


Nota :

Per errore: ExpressionChangedAfterItHasBeenCheckedError

Per favore controlla il tuo css, è un problema del lato CSS, non del lato Angular, uno degli utenti @KHAN lo ha risolto rimuovendo overflow:auto; height: 100%;da div. (controlla le conversazioni per i dettagli)


3
E come si attiva lo scorrimento?
Burjua

3
scorrerà automaticamente verso il basso su qualsiasi elemento aggiunto all'elemento ngfor OPPURE quando l'altezza del div del lato interno aumenta
Vivek Doshi

2
Grazie @VivekDoshi. Anche se dopo aver aggiunto qualcosa ottengo questo errore:> ERRORE Errore: ExpressionChangedAfterItHasBeenCheckedError: l'espressione è cambiata dopo essere stata controllata. Valore precedente: "700". Valore corrente: "517".
Vassilis Pits

6
@csharpnewbie Ho lo stesso problema quando la rimozione di qualsiasi messaggio dall'elenco: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. L'hai risolto?
Andrey

6
Sono stato in grado di risolvere il problema "L'espressione è cambiata dopo che è stata controllata" (mantenendo l'altezza: 100%; overflow-y: scroll) aggiungendo una condizione a [scrollTop], per evitare che venga impostata FINO A QUANDO non avessi dati da popolare con, ad esempio: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "let message of messages"> .. L'aggiunta del controllo "messages.length === 0? 0" sembrava risolvere il problema, anche se non posso spiegare perché, quindi non posso dire con certezza che la correzione non sia esclusiva della mia implementazione.
Ben

20

Ho aggiunto un controllo per vedere se l'utente ha provato a scorrere verso l'alto.

Lo lascio qui se qualcuno lo vuole :)

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

e il codice:

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

}

soluzione davvero praticabile senza errori in console. Grazie!
Dmitry Vasilyuk Just3F

20

La risposta accettata si attiva durante lo scorrimento dei messaggi, questo lo evita.

Vuoi un modello come questo.

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

Quindi si desidera utilizzare un'annotazione ViewChildren per iscriversi ai nuovi elementi del messaggio aggiunti alla pagina.

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

Ciao, sto provando questo approccio, ma va sempre nel blocco di cattura. Ho una carta e al suo interno ho un div che mostra più messaggi (all'interno della carta div, ho una struttura simile alla tua). Potete aiutarmi a capire se questo ha bisogno di altre modifiche nei file css o ts? Grazie.
csharpnewbie

2
Di gran lunga la migliore risposta. Grazie !
cagliostro

1
Questo attualmente non funziona a causa di un bug angolare in cui QueryList modifica gli abbonamenti attivandosi solo una volta anche se dichiarato in ngAfterViewInit. La soluzione di Mert è l'unica che funziona. La soluzione di Vivek si attiva indipendentemente dall'elemento aggiunto, mentre quella di Mert può attivarsi solo quando vengono aggiunti determinati elementi.
John Hamm

1
Questa è l'unica risposta che risolve il mio problema. funziona con l'angolo 6
suhailvs

2
Eccezionale!!! Ho usato quello sopra e ho pensato che fosse meraviglioso, ma poi sono rimasto bloccato a scorrere verso il basso ei miei utenti non potevano leggere i commenti precedenti! Grazie per questa soluzione! Fantastico! Ti darei più di 100 punti se potessi :-D
cheesydoritosandkale


13

Se vuoi essere sicuro di scorrere fino alla fine dopo che * ngFor è terminato, puoi usare questo.

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

Importante qui, la variabile "last" definisce se ti trovi attualmente all'ultimo elemento, quindi puoi attivare il metodo "scrollToBottom"


1
Questa risposta merita di essere quella accettata. È l'unica soluzione che funziona. La soluzione di Denixtry non funziona a causa di un bug angolare in cui QueryList modifica gli abbonamenti attivandosi solo una volta anche se dichiarato in ngAfterViewInit. La soluzione di Vivek si attiva indipendentemente dall'elemento aggiunto, mentre quella di Mert può attivarsi solo quando vengono aggiunti determinati elementi.
John Hamm

GRAZIE! Ciascuno degli altri metodi presenta alcuni aspetti negativi. Funziona come previsto. Grazie per aver condiviso il tuo codice!
lkaupp

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

5
Qualsiasi risposta che induca il richiedente ad andare nella giusta direzione è utile, ma cerca di menzionare eventuali limitazioni, ipotesi o semplificazioni nella tua risposta. La brevità è accettabile, ma le spiegazioni più complete sono migliori.
baduker

2

Condividendo la mia soluzione, perché non ero completamente soddisfatto del resto. Il mio problema AfterViewCheckedè che a volte sto scorrendo verso l'alto e, per qualche motivo, questo gancio della vita viene chiamato e mi scorre verso il basso anche se non c'erano nuovi messaggi. Ho provato a utilizzare OnChangesma questo era un problema, che mi ha portato a questa soluzione. Sfortunatamente, usando solo DoCheck, scorreva verso il basso prima che i messaggi venissero visualizzati, il che non era nemmeno utile, quindi li ho combinati in modo che DoCheck in pratica indichi AfterViewCheckedse deve chiamare scrollToBottom.

Felice di ricevere feedback.

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%

Questo può essere usato per controllare se la chat viene fatta scorrere verso il basso prima di aggiungere un nuovo messaggio al DOM: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(dove 3.0 è la tolleranza in pixel). Può essere utilizzato ngDoCheckper non impostare condizionalmente shouldScrollDownsu truese l'utente viene fatto scorrere manualmente verso l'alto.
Ruslan Stelmachenko

Grazie per il suggerimento @RuslanStelmachenko. L'ho implementato (ho deciso di farlo ngAfterViewCheckedsull'altro booleano. Ho cambiato shouldScrollDownnome in numberOfMessagesChangedper dare un po 'di chiarezza su cosa si riferisce esattamente questo booleano.
RTYX

Vedo che lo fai if (this.numberOfMessagesChanged && !isScrolledDown), ma penso che tu non abbia capito l'intenzione del mio suggerimento. La mia vera intenzione è di non scorrere automaticamente verso il basso anche quando viene aggiunto un nuovo messaggio, se l'utente scorre manualmente verso l'alto. Fatto ciò, se scorri verso l'alto per vedere la cronologia, la chat scorrerà verso il basso non appena arriva un nuovo messaggio. Questo può essere un comportamento molto fastidioso. :) Quindi, il mio suggerimento era di controllare se la chat è stata fatta scorrere verso l'alto prima che un nuovo messaggio fosse aggiunto al DOM e di non scorrere automaticamente, se lo è. ngAfterViewCheckedè troppo tardi perché DOM è già cambiato.
Ruslan Stelmachenko

Aaaaah, allora non avevo capito l'intenzione. Quello che hai detto ha senso - Penso che in quella situazione molte chat aggiungano semplicemente un pulsante per andare giù, o un segno che c'è un nuovo messaggio laggiù, quindi questo sarebbe sicuramente un buon modo per ottenerlo. Lo modifico e lo cambio più tardi. Grazie per il feedback!
RTYX

Grazie a dio per la tua condivisione, con la soluzione @Mert, la mia opinione viene respinta in casi rari (la chiamata viene rifiutata dal backend), con il tuo IterableDiffers funziona bene. Grazie mille!
lkaupp

2

La risposta di Vivek ha funzionato per me, ma ha provocato che un'espressione è cambiata dopo che è stato verificato l'errore. Nessuno dei commenti ha funzionato per me, ma quello che ho fatto è stato cambiare la strategia di rilevamento delle modifiche.

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

1

In angolare utilizzando il material design sidenav ho dovuto usare quanto segue:

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

Dopo aver letto altre soluzioni, la migliore soluzione a cui riesco a pensare, quindi esegui solo ciò di cui hai bisogno è la seguente: Usa ngOnChanges per rilevare la modifica corretta

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

}

E si utilizza ngAfterViewChecked per applicare effettivamente la modifica prima del rendering ma dopo aver calcolato l'altezza completa

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

Se ti stai chiedendo come implementare scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.