Кутова 2 Прокрутіть донизу (стиль чату)


111

У мене є набір компонентів з однієї комірки в ng-forциклі.

У мене все на місці, але я не можу зрозуміти належне

В даний час у мене є

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

Але це не працює весь час, оскільки зображення асинхронно відсувають вікно перегляду вниз.

Який підходящий спосіб прокрутити до нижньої частини вікна чату в кутовій 2?

Відповіді:


203

У мене була така ж проблема, я використовую комбінацію AfterViewCheckedта @ViewChild(комбінація Angular2 beta.3).

Компонент:

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

Шаблон:

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

Звичайно, це досить просто. AfterViewCheckedСпрацьовує кожен раз , коли перевірявся вид:

Реалізуйте цей інтерфейс, щоб отримувати сповіщення після кожної перевірки перегляду компонента.

Якщо у вас є поле для введення, наприклад, ця подія запускається після кожної клавіатури (просто для прикладу). Але якщо ви збережете, чи користувач прокручував вручну, а потім пропускав, scrollToBottom()вам слід добре.


У мене є домашній компонент, у домашньому шаблоні у мене є інший компонент у діві, який відображає дані та зберігає додані дані, але смуга прокрутки css знаходиться в домашньому шаблоні div не у внутрішньому шаблоні. проблема їм callling цей scrolltobottom EVERYTIME даних приходить всередині компонента , але #scrollMe в домашньому компоненті , так що ДІВ прокручувати ... і # scrollMe не з всередині , доступну компонента .. не знаю , як б #scrollme з всередині компонента
Anagh Верма

Ваше рішення, @Basit, дуже просте і не закінчується запуском нескінченних сувоїв / потребує вирішення, коли користувач прокручує вручну.
theotherdy

3
Привіт, чи існує спосіб запобігти прокрученню, коли користувач прокручується вгору?
Джон Луї Дела Крус

2
Я також намагаюся використовувати це в підході до стилю чату, але потрібно не прокручувати, якщо користувач прокручується вгору. Я шукав і не можу знайти спосіб визначити, чи користувач прокрутився вгору. Одне застереження - цей компонент чату не є повноекранним та має власну панель прокрутки.
HarvP

2
@HarvP & JohnLouieDelaCruz Ви знайшли рішення пропустити прокрутку, якщо користувач прокручував вручну?
suhailvs

166

Найпростіше і найкраще рішення для цього:

Додайте цю #scrollMe [scrollTop]="scrollMe.scrollHeight"просту річ на стороні шаблону

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

Ось посилання для РОБОТИ ДЕМО (З додатком чат-чату) І ПОЛНІЙ КОД

Працюватиме з Angular2, а також до 5, як вище демонструється в Angular5.


Примітка :

Для помилки: ExpressionChangedAfterItHasBeenCheckedError

Перевірте свій css, це проблема css сторони, а не кутової сторони. Один з користувачів @KHAN вирішив це, видаливши overflow:auto; height: 100%;з div. (будь ласка, перевірте розмови для деталізації)


3
А як запустити прокрутку?
Буржуа

3
він автоматично прокрутить донизу будь-який предмет, доданий до елемента ngfor АБО при збільшенні висоти внутрішньої сторони Div
Vivek Doshi

2
Thx @VivekDoshi. Хоча після чого щось додано, я отримую цю помилку:> Помилка помилки: ExpressionChangedAfterItHasBeenCheckedError: Вираз змінився після його перевірки. Попереднє значення: '700'. Поточне значення: '517'.
Vassilis Pits

6
@csharpnewbie У мене така ж проблема при видаленні будь-якого повідомлення зі списку: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. Ви вирішили це?
Андрій

6
Я зміг вирішити "Вираз змінився після його перевірки" (зберігаючи висоту: 100%; переповнення-у: прокручування), додавши умову до [scrollTop], щоб запобігти його встановленню ДО UNITIL У мене були дані для заповнення це з, напр .: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "нехай повідомлення повідомлень"> .. . Додавання перевірки "messages.length === 0? 0", здавалося, вирішує проблему, хоча я не можу пояснити чому, тому я не можу сказати напевно, що виправлення не властиве моїй реалізації.
Бен

20

Я додав чек, щоб побачити, чи намагався користувач прокрутити вгору.

Я просто збираюся залишити це тут, якщо хто хоче цього :)

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

і код:

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

}

дійсно працездатне рішення без помилок у консолі. Дякую!
Дмитро Василюк Just3F

20

Прийнята відповідь спрацьовує під час прокрутки повідомлень, це уникає.

Ви хочете, як такий шаблон.

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

Потім потрібно використовувати анотацію ViewChildren, щоб підписатися на нові елементи повідомлення, що додаються на сторінку.

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

Привіт - Я намагаюся такий підхід, але він завжди переходить у блок лову. У мене є картка, а всередині мене є div, який відображає кілька повідомлень (всередині картки div, я схожа на вашу структуру). Чи можете ви допомогти мені зрозуміти, чи потрібна будь-яка інша зміна файлів css чи ts? Дякую.
csharpnewbie

2
На сьогодні найкраща відповідь. Дякую !
cagliostro

1
Наразі це не працює через кутову помилку, де QueryList змінює підписки лише один раз, навіть коли це оголошено в ngAfterViewInit. Рішення Мерт - єдине, що працює. Рішення Вівека спрацьовує незалежно від того, який елемент додано, тоді як Мерт може запускати лише тоді, коли додаються певні елементи.
Джон Хамм

1
Це єдина відповідь, яка вирішує мою проблему. працює з кутовим 6
suhailvs

2
Дивовижно !!! Я скористався вищезгаданим і подумав, що це чудово, але потім я застряг прокручуючись і мої користувачі не могли прочитати попередні коментарі! Дякую за таке рішення! Фантастичний! Я дав би вам 100 балів, якби міг :-D
cheesydoritosandkale

19

Подумайте про використання

.scrollIntoView()

Дивіться https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView


Ще не стандарт, якщо у вас є сумісність з кросс-браузером, у вас будуть обмеження.
чавокарлос

1
@chavocarlos - здається, підтримує все, крім Opera Mini: caniuse.com/#feat=scrollintoview
Борис Якубчик

13

Якщо ви хочете бути впевнені, що ви прокручуєте до кінця після завершення * ngFor, ви можете скористатися цим.

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

Тут важливо, що "остання" змінна визначає, чи ви зараз перебуваєте на останньому елементі, тому ви можете запустити метод "scrollToBottom"


1
Ця відповідь заслуговує на прийняття. Це єдине рішення, яке працює. Рішення Denixtry не працює через кутову помилку, де QueryList змінює підписки лише один раз, навіть коли оголошено в ngAfterViewInit. Рішення Вівека спрацьовує незалежно від того, який елемент додано, тоді як Мерт може запускати лише тоді, коли додаються певні елементи.
Джон Хамм

СПАСИБІ! Кожен з інших методів має деякі недоліки. Це просто працює так, як задумано. Дякуємо, що поділилися кодом!
lkaupp

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

5
Будь-яка відповідь, яка вимагає, щоб запитуючий рухався в правильному напрямку, є корисним, але намагайтеся згадати будь-які обмеження, припущення чи спрощення у своїй відповіді. Стислість прийнятна, але більш повні пояснення краще.
baduker

2

Ділиться своїм рішенням, оскільки я не був повністю задоволений рештою. Моя проблема AfterViewCheckedполягає в тому, що іноді я прокручую вгору, і чомусь цей життєвий гачок називається, і він прокручує мене вниз, навіть якщо нових повідомлень не було. Я намагався використовувати, OnChangesале це була проблема, яка привела мене до цього рішення. На жаль, використовуючи лише DoCheck, він прокручувався вниз до винесення повідомлень, що теж не було корисно, тому я поєднав їх так, що DoCheck в основному вказує, AfterViewCheckedчи варто дзвонити scrollToBottom.

Раді отримати відгуки.

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%

Це можна використовувати, щоб перевірити, чи прокручується чат перед тим, як додати нове повідомлення до DOM: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(де 3.0 - допуск у пікселях). З його допомогою можна ngDoCheckумовно не встановити shouldScrollDownзначення, trueякщо користувач прокручується вручну.
Руслан

Дякую за пропозицію @RuslanStelmachenko. Я реалізував це (я вирішив зробити це ngAfterViewCheckedз іншим булевим. Я змінив shouldScrollDownім'я, numberOfMessagesChangedщоб дати ясність щодо того, що саме стосується цього булевого.
RTYX

Я бачу, що ви робите if (this.numberOfMessagesChanged && !isScrolledDown), але я думаю, ви не зрозуміли наміру моєї пропозиції. Мій реальний намір - не прокручуватися автоматично вниз, навіть коли додається нове повідомлення, якщо користувач вручну прокручує вгору. Без цього, якщо прокрутити вгору, щоб переглянути історію, чат прокрутить вниз, як тільки надійде нове повідомлення. Це може дуже дратувати поведінку. :) Отже, моя пропозиція полягала в тому, щоб перевірити, чи прокручується чат до того, як до DOM буде додано нове повідомлення, і чи не відбуватиметься автоматична прокрутка, якщо вона є. ngAfterViewCheckedзанадто пізно, оскільки DOM вже змінився.
Руслан

Ааааа, я тоді не зрозумів наміру. Те, що ви сказали, має сенс - я думаю, що в такій ситуації багато чатів просто додають кнопку для спуску або позначку про те, що внизу є нове повідомлення, тому це, безумовно, буде хорошим способом досягти цього. Я відредагую його та згодом змінити. Дякуємо за відгук!
RTYX

Дякую Богу за вашу частку, з рішенням @Mert, мій погляд стає непридатним у рідкісних випадках (дзвінок відхилений з бекенда), і ваш IterableDiffers він прекрасно функціонує. Дуже дякую!
lkaupp

2

Відповідь Вівека працювала на мене, але в результаті вираз змінився після перевірки помилки. Жоден із коментарів не працював для мене, але те, що я зробив, було змінити стратегію виявлення змін.

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

1

У кутовому використанні матеріалів дизайну сиденів мені довелося використовувати наступне:

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

Прочитавши інші рішення, найкраще рішення, про яке я можу придумати, тож ви запускаєте лише те, що вам потрібно, це наступне: Ви використовуєте ngOnChanges для виявлення правильної зміни

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

}

І ви використовуєте ngAfterViewChecked, щоб фактично застосувати зміну до того, як вона відобразиться, але після розрахунку повної висоти

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

Якщо вам цікаво, як реалізувати scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.