Winkel 2 Nach unten scrollen (Chat-Stil)

111

Ich habe eine Reihe von Einzelzellenkomponenten innerhalb einer ng-forSchleife.

Ich habe alles an Ort und Stelle, aber ich kann nicht scheinen, das richtige herauszufinden

Derzeit habe ich

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

Dies funktioniert jedoch nicht immer, da Bilder das Ansichtsfenster asynchron nach unten drücken.

Was ist der geeignete Weg, um in Angular 2 zum Ende eines Chat-Fensters zu scrollen?

Max Alexander
quelle

Antworten:

203

Ich hatte das gleiche Problem, ich benutze eine AfterViewChecked und @ViewChildKombination (Angular2 Beta.3).

Die Komponente:

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

Die Vorlage:

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

Das ist natürlich ziemlich einfach. DasAfterViewChecked Auslöser jedes Mal, wenn die Ansicht überprüft wurde:

Implementieren Sie diese Schnittstelle, um nach jeder Überprüfung der Ansicht Ihrer Komponente benachrichtigt zu werden.

Wenn Sie beispielsweise ein Eingabefeld zum Senden von Nachrichten haben, wird dieses Ereignis nach jeder Eingabe ausgelöst (um nur ein Beispiel zu nennen). Wenn Sie jedoch speichern, ob der Benutzer manuell gescrollt hat, und dann das überspringen scrollToBottom(), sollte dies in Ordnung sein.

letmejustfixthat
quelle
Ich habe eine Home-Komponente, in der Home-Vorlage habe ich eine andere Komponente in der Div, die Daten anzeigt und weiterhin Daten anhängt, aber die Bildlaufleiste CSS ist in der Home-Vorlage Div nicht in der inneren Vorlage. Das Problem ist, dass ich diesen Bildlauf jedes Mal aufrufe, wenn Daten in die Komponente gelangen, aber #scrollMe sich in der Home-Komponente befindet, da dieses div scrollbar ist ... und # scrollMe nicht von der inneren Komponente aus zugänglich ist. Ich bin mir nicht sicher, wie ich #scrollme von der inneren Komponente aus verwenden soll
Anagh Verma
Ihre Lösung, @Basit, ist sehr einfach und löst keine endlosen Schriftrollen aus / benötigt eine Problemumgehung, wenn der Benutzer manuell scrollt.
Theotherdy
3
Hallo, gibt es eine Möglichkeit, das Scrollen zu verhindern, wenn der Benutzer nach oben gescrollt hat?
John Louie Dela Cruz
2
Ich versuche auch, dies in einem Chat-Ansatz zu verwenden, muss aber nicht scrollen, wenn der Benutzer nach oben gescrollt hat. Ich habe gesucht und kann keinen Weg finden, um festzustellen, ob der Benutzer nach oben gescrollt hat. Eine Einschränkung ist, dass diese Chat-Komponente die meiste Zeit nicht im Vollbildmodus angezeigt wird und über eine eigene Bildlaufleiste verfügt.
HarvP
2
@ HarvP & JohnLouieDelaCruz Haben Sie eine Lösung gefunden, um das Scrollen zu überspringen, wenn der Benutzer manuell gescrollt hat?
Suhailvs
166

Die einfachste und beste Lösung dafür ist:

Fügen Sie diese #scrollMe [scrollTop]="scrollMe.scrollHeight"einfache Sache auf der Vorlagenseite hinzu

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

Hier ist der Link für WORKING DEMO (mit Dummy-Chat-App) UND FULL CODE

Funktioniert mit Angular2 und auch bis zu 5. Wie oben wird die Demo in Angular5 durchgeführt.


Hinweis :

Bei Fehler: ExpressionChangedAfterItHasBeenCheckedError

Bitte überprüfen Sie Ihre CSS, es ist ein Problem der CSS-Seite, nicht der Angular-Seite. Einer der Benutzer @KHAN hat das durch Entfernen overflow:auto; height: 100%;von gelöst div. (Bitte überprüfen Sie die Gespräche für Details)

Vivek Doshi
quelle
3
Und wie löst man die Schriftrolle aus?
Burjua
3
Es wird automatisch nach unten gescrollt, wenn ein Gegenstand zum ngfor-Gegenstand hinzugefügt wurde ODER wenn die Höhe der Innenseite des Divs zunimmt
Vivek Doshi
2
Danke @VivekDoshi. Nachdem etwas hinzugefügt wurde, wird folgende Fehlermeldung angezeigt:> FEHLER Fehler: ExpressionChangedAfterItHasBeenCheckedError: Der Ausdruck hat sich geändert, nachdem er überprüft wurde. Vorheriger Wert: '700'. Aktueller Wert: '517'.
Vassilis Pits
6
@csharpnewbie Ich habe das gleiche Problem beim Entfernen einer Nachricht aus der Liste : Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. Hast du es gelöst?
Andrey
5
Ich konnte den "Ausdruck hat sich geändert, nachdem er überprüft wurde" (unter Beibehaltung der Höhe: 100%; Überlauf-y: Bildlauf) lösen, indem ich [scrollTop] eine Bedingung hinzufügte, um zu verhindern, dass sie gesetzt wird, BIS ich Daten zum Auffüllen hatte es mit, zB: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "Nachricht von Nachrichten lassen"> .. Das Hinzufügen der Prüfung "messages.length === 0? 0" schien das Problem zu beheben, obwohl ich nicht erklären kann, warum, sodass ich nicht mit Sicherheit sagen kann, dass das Update nicht nur für meine Implementierung gilt.
Ben
20

Ich habe eine Überprüfung hinzugefügt, um festzustellen, ob der Benutzer versucht hat, nach oben zu scrollen.

Ich werde das hier einfach lassen, wenn es jemand will :)

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

und der Code:

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

}
Robert King
quelle
wirklich praktikable Lösung ohne Fehler in der Konsole. Vielen Dank!
Dmitry Vasilyuk Just3F
20

Die akzeptierte Antwort wird beim Scrollen durch die Nachrichten ausgelöst. Dadurch wird dies vermieden.

Sie möchten eine solche Vorlage.

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

Anschließend möchten Sie eine ViewChildren-Annotation verwenden, um neue Nachrichtenelemente zu abonnieren, die der Seite hinzugefügt werden.

@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) {}
}
denixtry
quelle
Hi - Ich versuche diesen Ansatz, aber er geht immer in den Fangblock. Ich habe eine Karte und darin habe ich ein Div, das mehrere Nachrichten anzeigt (innerhalb des Karten-Div habe ich eine ähnliche Struktur). Können Sie mir bitte helfen zu verstehen, ob dies eine andere Änderung in CSS- oder TS-Dateien erfordert? Vielen Dank.
Csharpnewbie
2
Mit Abstand die beste Antwort. Vielen Dank !
Cagliostro
1
Dies funktioniert derzeit nicht aufgrund eines Angular-Fehlers, bei dem QueryList-Abonnements nur einmal ausgelöst werden, wenn sie in ngAfterViewInit deklariert sind. Die Lösung von Mert ist die einzige, die funktioniert. Die Lösung von Vivek wird unabhängig vom hinzugefügten Element ausgelöst, während die von Mert nur ausgelöst werden kann, wenn bestimmte Elemente hinzugefügt werden.
John Hamm
1
Dies ist die einzige Antwort, die mein Problem löst. arbeitet mit eckigen 6
suhailvs
2
Genial!!! Ich habe das obige verwendet und fand es wunderbar, aber dann blieb ich beim Scrollen hängen und meine Benutzer konnten frühere Kommentare nicht lesen! Vielen Dank für diese Lösung! Fantastisch! Ich würde Ihnen 100+ Punkte geben, wenn ich könnte :-D
cheesydoritosandkale
13

Wenn Sie sicher sein möchten, dass Sie nach Abschluss von * ngFor bis zum Ende scrollen, können Sie dies verwenden.

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

Wichtig hier ist, dass die Variable "last" definiert, ob Sie sich gerade am letzten Element befinden, sodass Sie die Methode "scrollToBottom" auslösen können

Mert
quelle
1
Diese Antwort verdient es, akzeptiert zu werden. Es ist die einzige Lösung, die funktioniert. Die Lösung von Denixtry funktioniert aufgrund eines Angular-Fehlers nicht, bei dem QueryList-Abonnements nur einmal ausgelöst werden, wenn sie in ngAfterViewInit deklariert sind. Die Lösung von Vivek wird unabhängig vom hinzugefügten Element ausgelöst, während die von Mert nur ausgelöst werden kann, wenn bestimmte Elemente hinzugefügt werden.
John Hamm
DANKE! Jede der anderen Methoden hat einige Nachteile. Dies funktioniert einfach so, wie es beabsichtigt ist. Vielen Dank, dass Sie Ihren Code geteilt haben!
lkaupp
6
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
Stas Kozlov
quelle
5
Jede Antwort, die den Fragesteller in die richtige Richtung bringt, ist hilfreich. Versuchen Sie jedoch, in Ihrer Antwort alle Einschränkungen, Annahmen oder Vereinfachungen zu erwähnen. Kürze ist akzeptabel, aber ausführlichere Erklärungen sind besser.
Baduker
2

Ich teile meine Lösung, weil ich mit dem Rest nicht ganz zufrieden war. Mein Problem dabei AfterViewCheckedist, dass ich manchmal nach oben scrolle und aus irgendeinem Grund dieser Life Hook aufgerufen wird und mich nach unten scrollt, selbst wenn es keine neuen Nachrichten gab. Ich habe versucht, es zu verwenden, OnChangesaber dies war ein Problem, das mich zu dieser Lösung führte. Leider wurde nur DoCheckmit gescrollt, bevor die Nachrichten gerendert wurden, was auch nicht nützlich war. Deshalb habe ich sie so kombiniert, dass DoCheck im Grunde angibt, AfterViewCheckedob es aufgerufen werden sollscrollToBottom .

Freut mich über 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%
RTYX
quelle
Dies kann verwendet werden, um zu überprüfen, ob der Chat nach unten gescrollt ist, bevor dem DOM eine neue Nachricht hinzugefügt wird: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(wobei 3.0 Toleranz in Pixel ist). Es kann verwendet werden, ngDoCheckum bedingt nicht zu setzen shouldScrollDown, truewenn der Benutzer manuell nach oben gescrollt wird.
Ruslan Stelmachenko
Danke für den Vorschlag @RuslanStelmachenko. Ich habe es implementiert (ich habe beschlossen, es ngAfterViewCheckedmit dem anderen Booleschen shouldScrollDownnumberOfMessagesChanged
Wert
Ich sehe dich if (this.numberOfMessagesChanged && !isScrolledDown), aber ich denke, du hast die Absicht meines Vorschlags nicht verstanden. Meine eigentliche Absicht ist es, nicht automatisch nach unten zu scrollen, selbst wenn eine neue Nachricht hinzugefügt wird, wenn der Benutzer manuell nach oben gescrollt hat. Wenn Sie nach oben scrollen, um den Verlauf anzuzeigen, wird der Chat nach unten gescrollt, sobald eine neue Nachricht eintrifft. Dies kann sehr ärgerlich sein. :) Also, mein Vorschlag war zu überprüfen, ob der Chat nach oben gescrollt wird, bevor eine neue Nachricht zum DOM hinzugefügt wird, und nicht automatisch zu scrollen, wenn dies der Fall ist. ngAfterViewCheckedist zu spät, weil DOM bereits geändert wurde.
Ruslan Stelmachenko
Aaaaah, ich hatte die Absicht damals nicht verstanden. Was Sie gesagt haben, macht Sinn - ich denke, in dieser Situation fügen viele Chats einfach einen Knopf hinzu, um nach unten zu gehen, oder eine Markierung, dass es dort unten eine neue Nachricht gibt, also wäre dies definitiv ein guter Weg, um dies zu erreichen. Ich werde es bearbeiten und später ändern. Danke für die Rückmeldung!
RTYX
Gott sei Dank für Ihren Anteil. Mit der @ Metert-Lösung wird meine Ansicht in seltenen Fällen (Anruf wird vom Backend abgelehnt) beeinträchtigt. Mit Ihren IterableDiffers funktioniert dies einwandfrei. Ich danke dir sehr!
lkaupp
2

Die Antwort von Vivek hat bei mir funktioniert, hat jedoch dazu geführt, dass sich ein Ausdruck geändert hat, nachdem der Fehler überprüft wurde. Keiner der Kommentare hat bei mir funktioniert, aber ich habe die Strategie zur Änderungserkennung geändert.

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

Im Winkel mit Material Design Sidenav musste ich Folgendes verwenden:

let ele = document.getElementsByClassName('md-sidenav-content');
    let eleArray = <Element[]>Array.prototype.slice.call(ele);
    eleArray.map( val => {
        val.scrollTop = val.scrollHeight;
    });
Helzgate
quelle
1
const element = document.getElementById('box');
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
Kanomdook
quelle
0

Nachdem Sie andere Lösungen gelesen haben, ist die beste Lösung, die ich mir vorstellen kann, sodass Sie nur das ausführen, was Sie benötigen: Sie verwenden ngOnChanges, um die richtige Änderung zu erkennen

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

}

Und Sie verwenden ngAfterViewChecked, um die Änderung tatsächlich zu erzwingen, bevor sie gerendert wird, aber nachdem die volle Höhe berechnet wurde

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

Wenn Sie sich fragen, wie Sie scrollToBottom implementieren sollen

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
zardilior
quelle