Kątowy 2 Przewiń w dół (styl czatu)

112

Mam zestaw komponentów pojedynczej komórki w ng-forpętli.

Mam wszystko na swoim miejscu, ale nie mogę znaleźć właściwego rozwiązania

Obecnie mam

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

Ale to nie działa przez cały czas, ponieważ obrazy asynchronicznie przesuwają okno w dół.

Jaki jest właściwy sposób przewijania do dołu okna czatu w Angular 2?

Max Alexander
źródło

Odpowiedzi:

205

Miałem ten sam problem, używam AfterViewCheckedi@ViewChild kombinacji (Angular2 beta.3).

Składnik:

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

Szablon:

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

Oczywiście jest to dość podstawowe. AfterViewCheckedWyzwala za każdym razem widok sprawdzono:

Zaimplementuj ten interfejs, aby otrzymywać powiadomienia po każdym sprawdzeniu widoku komponentu.

Jeśli masz pole wejściowe do wysyłania wiadomości, na przykład to zdarzenie jest wywoływane po każdym naciśnięciu klawisza (tylko dla przykładu). Ale jeśli zapiszesz, czy użytkownik przewinął ręcznie, a następnie pominiesz scrollToBottom(), powinno być dobrze.

letmejustfixthat
źródło
Mam komponent domowy, w szablonie domowym mam inny komponent w div, który wyświetla dane i kontynuuje dodawanie danych, ale pasek przewijania css znajduje się w głównym div szablonu, a nie w szablonie wewnętrznym. Problem polega na tym, że wywołuję to scrolltobottom za każdym razem, gdy dane wchodzą do komponentu, ale #scrollMe jest w komponencie domowym, ponieważ ten element div jest przewijalny ... i # scrollMe nie jest dostępny z komponentu wewnętrznego ... nie wiem, jak używać #scrollme z komponentu wewnętrznego
Anagh Verma
Twoje rozwiązanie, @Basit, jest pięknie proste i nie kończy się na uruchamianiu niekończących się przewijania / wymaga obejścia problemu, gdy użytkownik ręcznie przewija.
theotherdy
3
Cześć, czy istnieje sposób, aby zapobiec przewijaniu po przewinięciu przez użytkownika?
John Louie Dela Cruz
2
Próbuję również użyć tego w podejściu w stylu czatu, ale nie muszę przewijać, jeśli użytkownik przewinął w górę. Szukałem i nie mogę znaleźć sposobu, aby sprawdzić, czy użytkownik przewinął w górę. Jedynym zastrzeżeniem jest to, że ten element czatu przez większość czasu nie jest wyświetlany na pełnym ekranie i ma własny pasek przewijania.
HarvP,
2
@HarvP i JohnLouieDelaCruz Czy znaleźliście rozwiązanie pozwalające pominąć przewijanie, jeśli użytkownik przewija ręcznie?
suhailvs
167

Najprostszym i najlepszym rozwiązaniem jest:

Dodaj tę #scrollMe [scrollTop]="scrollMe.scrollHeight"prostą rzecz po stronie szablonu

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

Oto link do PRACY DEMO (z fałszywą aplikacją do czatu) I PEŁNY KOD

Będzie działać z Angular2, a także do 5, Jak powyżej demo jest zrobione w Angular5.


Uwaga :

W przypadku błędu: ExpressionChangedAfterItHasBeenCheckedError

Sprawdź swój css, jest to problem strony css, a nie strony Angular, Jeden z użytkowników @KHAN rozwiązał to, usuwając overflow:auto; height: 100%;z div. (proszę sprawdzić rozmowy, aby uzyskać szczegółowe informacje)

Vivek Doshi
źródło
3
A jak uruchamiasz zwoje?
Burjua
3
automatycznie przewinie w dół po każdym elemencie dodanym do ngfor lub gdy wzrośnie wysokość elementu div po wewnętrznej stronie
Vivek Doshi
2
Thx @VivekDoshi. Chociaż po dodaniu czegoś pojawia się ten błąd:> ERROR Błąd: ExpressionChangedAfterItHasBeenCheckedError: Wyrażenie zmieniło się po sprawdzeniu. Poprzednia wartość: „700”. Aktualna wartość: „517”.
Vassilis Pits
6
@csharpnewbie Mam ten sam problem przy usuwaniu dowolną wiadomość z listy: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. Czy rozwiązałeś to?
Andrey
6
Udało mi się rozwiązać problem „Wyrażenie zmieniło się po sprawdzeniu” (zachowując wysokość: 100%; przepełnienie-y: przewiń) dodając warunek do [scrollTop], aby zapobiec jego ustawieniu DO DOPÓKI miałem dane do wypełnienia to z, np: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "let message of messages"> .. . dodanie opcji „messages.length === 0? 0” wydawało się rozwiązać problem, chociaż nie potrafię wyjaśnić dlaczego, więc nie mogę stwierdzić na pewno, że poprawka nie jest unikalna dla mojej implementacji.
Ben
20

Dodałem sprawdzenie, czy użytkownik próbował przewijać w górę.

Zostawię to tutaj, jeśli ktoś tego chce :)

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

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

}
Robert King
źródło
naprawdę sprawne rozwiązanie bez błędów w konsoli. Dzięki!
Dmitry Vasilyuk Just3F
20

Zaakceptowana odpowiedź odpala się podczas przewijania wiadomości, aby tego uniknąć.

Chcesz takiego szablonu.

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

Następnie chcesz użyć adnotacji ViewChildren, aby zasubskrybować nowe elementy wiadomości dodawane do strony.

@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
źródło
Cześć - próbuję tego podejścia, ale zawsze trafia do bloku catch. Mam kartę, a wewnątrz mam div, który wyświetla wiele wiadomości (wewnątrz karty div mam podobną do twojej struktury). Czy możesz mi pomóc zrozumieć, czy to wymaga innych zmian w plikach css lub ts? Dzięki.
csharpnewbie
2
Zdecydowanie najlepsza odpowiedź. Dzięki !
cagliostro
1
Obecnie nie działa to z powodu błędu kątowego, w którym QueryList zmienia subskrypcje tylko raz uruchamiane, nawet jeśli zostały zadeklarowane w ngAfterViewInit. Rozwiązanie Merta jest jedynym, które działa. Rozwiązanie Viveka strzela bez względu na dodany element, podczas gdy rozwiązanie Merta może działać tylko wtedy, gdy zostaną dodane pewne elementy.
John Hamm
1
To jedyna odpowiedź, która rozwiązuje mój problem. działa z kątowym 6
suhailvs
2
Niesamowite!!! Użyłem powyższego i pomyślałem, że jest wspaniały, ale potem utknąłem przewijając w dół, a moi użytkownicy nie mogli przeczytać poprzednich komentarzy! Dziękuję za to rozwiązanie! Fantastyczny! Dałbym ci
ponad
13

Jeśli chcesz mieć pewność, że przewijasz do końca po zakończeniu * ngFor, możesz użyć tego.

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

Ważne tutaj, zmienna „last” określa, czy aktualnie znajdujesz się przy ostatniej pozycji, więc możesz wywołać metodę „scrollToBottom”

Mert
źródło
1
Ta odpowiedź zasługuje na akceptację. To jedyne rozwiązanie, które działa. Rozwiązanie Denixtry nie działa z powodu błędu kątowego, w którym QueryList zmienia subskrypcje tylko raz uruchamiane, nawet po zadeklarowaniu w ngAfterViewInit. Rozwiązanie Viveka strzela bez względu na dodany element, podczas gdy rozwiązanie Merta może działać tylko wtedy, gdy zostaną dodane pewne elementy.
John Hamm,
DZIĘKUJĘ CI! Każda z pozostałych metod ma pewne wady. Działa to po prostu zgodnie z przeznaczeniem. Dziękujemy za udostępnienie kodu!
lkaupp,
6
this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
Stas Kozlov
źródło
5
Każda odpowiedź, która sprawia, że ​​osoba pytająca idzie we właściwym kierunku, jest pomocna, ale spróbuj wspomnieć o wszelkich ograniczeniach, założeniach lub uproszczeniach w swojej odpowiedzi. Zwięzłość jest akceptowalna, ale pełniejsze wyjaśnienia są lepsze.
baduker
2

Dzielenie się moim rozwiązaniem, ponieważ nie byłem w pełni zadowolony z reszty. Mój problem AfterViewCheckedpolega na tym, że czasami przewijam w górę iz jakiegoś powodu ten haczyk życia jest wywoływany i przewija mnie w dół, nawet jeśli nie ma nowych wiadomości. Próbowałem użyć, OnChangesale to był problem, który doprowadził mnie do tego rozwiązania. Niestety, używając tylko DoCheck, przewijało się w dół przed renderowaniem wiadomości, co też nie było przydatne, więc połączyłem je tak, aby DoCheck w zasadzie wskazywał, AfterViewCheckedczy powinien wywołaćscrollToBottom .

Chętnie otrzymam informację zwrotną

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
źródło
Można to wykorzystać do sprawdzenia, czy czat jest przewijany w dół przed dodaniem nowej wiadomości do DOM: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(gdzie 3,0 to tolerancja w pikselach). Może być stosowany w ngDoCheckcelu warunkowo nie jest ustawiona shouldScrollDownna truejeśli użytkownik ręcznie przewijane w górę.
Ruslan Stelmachenko
Dzięki za sugestię @RuslanStelmachenko. Zaimplementowałem to (zdecydowałem się to zrobić ngAfterViewCheckedz drugą wartością logiczną. Zmieniłem shouldScrollDownnazwę na, numberOfMessagesChangedaby
wyjaśnić,
Widzę, że rozumiesz if (this.numberOfMessagesChanged && !isScrolledDown), ale myślę, że nie zrozumiałeś intencji mojej sugestii. Moim prawdziwym zamiarem jest, aby nie przewijać automatycznie w dół, nawet po dodaniu nowej wiadomości, jeśli użytkownik ręcznie przewija w górę. Bez tego, jeśli przewiniesz w górę, aby zobaczyć historię, czat przewinie się w dół, gdy tylko nadejdzie nowa wiadomość. Może to być bardzo irytujące zachowanie. :) Tak więc moją sugestią było sprawdzenie, czy czat jest przewijany w górę przed dodaniem nowej wiadomości do DOM i nie przewijanie automatyczne, jeśli tak jest. ngAfterViewCheckedjest za późno, ponieważ DOM już się zmienił.
Ruslan Stelmachenko
Aaaaach, nie rozumiałem wtedy intencji. To, co powiedziałeś, ma sens - myślę, że w tej sytuacji wiele czatów po prostu dodaje przycisk, aby przejść w dół, lub znak, że jest tam nowa wiadomość, więc na pewno byłby to dobry sposób na osiągnięcie tego. Zmienię to i zmienię później. Dziękujemy za opinię!
RTYX
Dzięki Bogu za twój udział, dzięki rozwiązaniu @Mert, mój widok jest wyrzucany w rzadkich przypadkach (połączenie zostaje odrzucone z zaplecza), z twoim IterableDiffers działa ładnie. Dziękuję bardzo!
lkaupp,
2

Odpowiedź Viveka zadziałała dla mnie, ale spowodowała, że ​​wyrażenie zmieniło się po sprawdzeniu błędu. Żaden z komentarzy nie działał dla mnie, ale zmieniłem strategię wykrywania zmian.

import {  Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'page1',
  templateUrl: 'page1.html',
})
user3610386
źródło
1

W kątowym używaniu sidenav Material Design musiałem użyć następującego:

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

Po przeczytaniu innych rozwiązań najlepszym rozwiązaniem, które przychodzi mi do głowy, więc uruchamiasz tylko to, czego potrzebujesz, jest następujące: Używasz ngOnChanges do wykrycia właściwej zmiany

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

}

Używasz ngAfterViewChecked, aby faktycznie wymusić zmianę przed renderowaniem, ale po obliczeniu pełnej wysokości

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

Jeśli zastanawiasz się, jak zaimplementować scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
zardilior
źródło