Angular 2 Cuộn xuống dưới cùng (Kiểu trò chuyện)


111

Tôi có một tập hợp các thành phần ô đơn trong một ng-forvòng lặp.

Tôi có mọi thứ tại chỗ nhưng dường như tôi không thể tìm ra cách thích hợp

Hiện tại tôi có

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

Nhưng điều này không hoạt động mọi lúc vì hình ảnh đẩy khung nhìn xuống một cách không đồng bộ.

Cách thích hợp để cuộn xuống cuối cửa sổ trò chuyện trong Angular 2 là gì?

Câu trả lời:


203

Tôi đã gặp vấn đề tương tự, tôi đang sử dụng một AfterViewChecked@ViewChildkết hợp (Angular2 beta.3).

Thanh phân:

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

Bản mẫu:

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

Tất nhiên điều này là khá cơ bản. Các AfterViewCheckedkích hoạt mỗi khi chế độ xem được kiểm tra:

Triển khai giao diện này để nhận thông báo sau mỗi lần kiểm tra chế độ xem thành phần của bạn.

Nếu bạn có một trường đầu vào để gửi tin nhắn, chẳng hạn như sự kiện này sẽ được kích hoạt sau mỗi lần gõ phím (chỉ để đưa ra một ví dụ). Nhưng nếu bạn lưu cho dù người dùng cuộn theo cách thủ công và sau đó bỏ qua thì scrollToBottom()bạn sẽ ổn.


Tôi có một thành phần trang chủ, trong mẫu nhà tôi có một thành phần khác trong div đang hiển thị dữ liệu và tiếp tục nối dữ liệu nhưng css thanh cuộn nằm trong mẫu trang chủ div không có trong mẫu bên trong. vấn đề là tôi đang gọi dữ liệu scrolltobottom này mỗi khi dữ liệu đến bên trong thành phần nhưng #scrollMe nằm trong thành phần chính vì div đó có thể cuộn được ... và # scrollMe không thể truy cập được từ bên trong thành phần .. không chắc chắn cách sử dụng #scrollme từ bên trong thành phần
Anagh Verma

Giải pháp của bạn, @Basit, rất đơn giản và không kết thúc việc kích hoạt các cuộn vô tận / cần một giải pháp thay thế khi người dùng cuộn theo cách thủ công.
theotherdy

3
Xin chào, có cách nào để ngăn chặn cuộn khi người dùng cuộn lên không?
John Louie Dela Cruz

2
Tôi cũng đang cố gắng sử dụng điều này trong cách tiếp cận kiểu trò chuyện, nhưng không cần cuộn nếu người dùng cuộn lên. Tôi đã tìm kiếm và không thể tìm ra cách phát hiện xem người dùng có cuộn lên hay không. Một lưu ý là thành phần trò chuyện này hầu như không ở toàn màn hình và có thanh cuộn riêng.
HarvP

2
@HarvP & JohnLouieDelaCruz bạn có tìm thấy giải pháp để bỏ qua cuộn nếu người dùng cuộn theo cách thủ công không?
suhailvs

166

Đơn giản nhất và giải pháp tốt nhất cho việc này là:

Thêm điều #scrollMe [scrollTop]="scrollMe.scrollHeight"đơn giản này vào bên Mẫu

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

Đây là liên kết cho LÀM VIỆC DEMO (Với ứng dụng trò chuyện giả) VÀ MÃ ĐẦY ĐỦ

Sẽ hoạt động với Angular2 và tối đa 5, Như bản demo trên được thực hiện trong Angular5.


Ghi chú :

Đối với lỗi: ExpressionChangedAfterItHasBeenCheckedError

Vui lòng kiểm tra css của bạn, đó là vấn đề của phía css, không phải phía Angular, Một trong những người dùng @KHAN đã giải quyết vấn đề đó bằng cách xóa overflow:auto; height: 100%;khỏi div. (vui lòng kiểm tra các cuộc trò chuyện để biết chi tiết)


3
Và làm thế nào để bạn kích hoạt cuộn?
Burjua

3
nó sẽ autoscroll xuống dưới vào bất kỳ mục bổ sung vào mục ngfor HOẶC khi bên phía div của tăng chiều cao
Vivek Doshi

2
Thx @VivekDoshi. Mặc dù sau khi thêm thứ gì đó, tôi vẫn gặp lỗi này:> Lỗi LỖI: ExpressionChangedAfterItHasBeenCheckedError: Biểu thức đã thay đổi sau khi nó được kiểm tra. Giá trị trước đó: '700'. Giá trị hiện tại: '517'.
Vassilis Pits

6
@csharpnewbie Tôi có vấn đề tương tự khi loại bỏ bất kỳ tin nhắn từ danh sách: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. Bạn đã giải quyết nó?
Andrey

6
Tôi đã có thể giải quyết vấn đề "Biểu thức đã thay đổi sau khi nó được kiểm tra" (trong khi vẫn giữ chiều cao: 100%; tràn-y: cuộn) bằng cách thêm điều kiện vào [scrollTop], để ngăn nó được đặt CHO ĐẾN KHI tôi có dữ liệu để điền nó với, ví dụ: <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "let message of messages"> .. . Việc thêm dấu kiểm "messages.length === 0? 0" dường như đã khắc phục được sự cố, mặc dù tôi không thể giải thích tại sao, vì vậy tôi không thể chắc chắn rằng bản sửa lỗi không phải là duy nhất đối với việc triển khai của tôi.
Ben

20

Tôi đã thêm một kiểm tra để xem liệu người dùng có cố gắng cuộn lên hay không.

Tôi chỉ sẽ để cái này ở đây nếu có ai muốn nó :)

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

và mã:

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

}

giải pháp thực sự khả thi mà không có lỗi trong bảng điều khiển. Cảm ơn!
Dmitry Vasilyuk Just3F

20

Câu trả lời được chấp nhận sẽ kích hoạt khi cuộn qua các thư, điều này tránh được điều đó.

Bạn muốn một mẫu như thế này.

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

Sau đó, bạn muốn sử dụng chú thích ViewChildren để đăng ký các phần tử thông báo mới được thêm vào trang.

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

Xin chào - Tôi đang thử cách tiếp cận này, nhưng nó luôn đi vào khối khó hiểu. Tôi có một thẻ và bên trong tôi có một div hiển thị nhiều thông báo (bên trong thẻ div, tôi có cấu trúc tương tự như cấu trúc của bạn). Bạn có thể giúp tôi hiểu nếu điều này cần bất kỳ thay đổi nào khác trong tệp css hoặc ts? Cảm ơn.
csharpnewbie

2
Cho đến nay câu trả lời tốt nhất. Cảm ơn !
cagliostro

1
Điều này hiện không hoạt động do một lỗi Angular trong đó QueryList thay đổi đăng ký chỉ kích hoạt một lần ngay cả khi được khai báo trong ngAfterViewInit. Giải pháp của Mert là giải pháp duy nhất hoạt động. Giải pháp của Vivek kích hoạt bất kể phần tử nào được thêm vào, trong khi giải pháp của Mert chỉ có thể kích hoạt khi một số phần tử được thêm vào.
John Hamm

1
Đây là câu trả lời duy nhất giải quyết được vấn đề của tôi. làm việc với góc 6
suhailvs

2
Tuyệt vời!!! Tôi đã sử dụng cái ở trên và nghĩ rằng nó thật tuyệt vời nhưng sau đó tôi gặp khó khăn khi cuộn xuống và người dùng của tôi không thể đọc các nhận xét trước đó! Cảm ơn bạn cho giải pháp này! Tuyệt diệu! Tôi sẽ cung cấp cho bạn 100 điểm nếu tôi có thể :-D
cheesydoritosandkale


13

Nếu bạn muốn chắc chắn rằng bạn đang cuộn đến cuối sau khi * ngFor hoàn tất, bạn có thể sử dụng điều này.

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

Quan trọng ở đây, biến "cuối cùng" xác định nếu bạn hiện đang ở mục cuối cùng, vì vậy bạn có thể kích hoạt phương thức "scrollToBottom"


1
Câu trả lời này xứng đáng được chấp nhận. Nó là giải pháp duy nhất hoạt động. Giải pháp của Denixtry không hoạt động do một lỗi Angular trong đó QueryList thay đổi đăng ký chỉ kích hoạt một lần ngay cả khi được khai báo trong ngAfterViewInit. Giải pháp của Vivek kích hoạt bất kể phần tử nào được thêm vào, trong khi giải pháp của Mert chỉ có thể kích hoạt khi một số phần tử được thêm vào.
John Hamm

CẢM ƠN BẠN! Mỗi phương pháp khác đều có một số nhược điểm. Điều này chỉ hoạt động như nó được dự định. Cảm ơn vì đã chia sẻ mã của bạn!
lkaupp

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

5
Bất kỳ câu trả lời nào khiến người hỏi đi đúng hướng đều hữu ích, nhưng hãy cố gắng đề cập đến bất kỳ hạn chế, giả định hoặc đơn giản hóa nào trong câu trả lời của bạn. Sự ngắn gọn có thể chấp nhận được, nhưng giải thích đầy đủ hơn sẽ tốt hơn.
baduker

2

Chia sẻ giải pháp của tôi, vì tôi không hoàn toàn hài lòng với phần còn lại. Vấn đề của tôi AfterViewCheckedlà đôi khi tôi đang cuộn lên, và vì một lý do nào đó, cái móc cuộc sống này được gọi và nó cuộn tôi xuống ngay cả khi không có tin nhắn mới. Tôi đã thử sử dụng OnChangesnhưng đây là một vấn đề, dẫn tôi đến giải pháp này . Thật không may, chỉ sử dụng DoCheck, nó đã cuộn xuống trước khi các thông báo được hiển thị, điều này cũng không hữu ích, vì vậy tôi kết hợp chúng để DoCheck về cơ bản chỉ ra AfterViewCheckedliệu nó có nên gọi hay không scrollToBottom.

Rất vui khi nhận được phản hồi.

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%

Điều này có thể được sử dụng để kiểm tra xem trò chuyện có được cuộn xuống hay không trước khi thêm thông báo mới vào DOM: const isScrolledDown = Math.abs(this.scrollable.nativeElement.scrollHeight - this.scrollable.nativeElement.scrollTop - this.scrollable.nativeElement.clientHeight) <= 3.0;(trong đó 3.0 là dung sai tính bằng pixel). Nó có thể được sử dụng trong ngDoCheckđể có điều kiện không được thiết lập shouldScrollDownđể truenếu người dùng được bằng tay cuộn lên.
Ruslan Stelmachenko

Cảm ơn đề xuất @RuslanStelmachenko. Tôi đã triển khai nó (Tôi quyết định làm điều đó ngAfterViewCheckedvới boolean khác. Tôi đã đổi shouldScrollDowntên thành numberOfMessagesChangedđể cung cấp một số thông tin rõ ràng về những gì chính xác mà boolean này đề cập đến.
RTYX

Tôi thấy bạn làm if (this.numberOfMessagesChanged && !isScrolledDown), nhưng tôi nghĩ bạn không hiểu ý định của đề nghị của tôi. Ý định thực sự của tôi là không tự động cuộn xuống ngay cả khi tin nhắn mới được thêm vào, nếu người dùng cuộn lên theo cách thủ công . Không có điều này, nếu bạn cuộn lên để xem lịch sử, cuộc trò chuyện sẽ cuộn xuống ngay khi có tin nhắn mới. Đây có thể là hành vi rất khó chịu. :) Vì vậy, đề xuất của tôi là kiểm tra xem trò chuyện có được cuộn lên trước khi tin nhắn mới được thêm vào DOM hay không và không tự động cuộn , nếu có. ngAfterViewCheckedquá muộn vì DOM đã thay đổi.
Ruslan Stelmachenko

Aaaaah, tôi không hiểu ý định lúc đó. Những gì bạn nói đều có lý - Tôi nghĩ trong tình huống đó, nhiều cuộc trò chuyện chỉ cần thêm một nút để chuyển xuống hoặc đánh dấu rằng có một tin nhắn mới ở dưới đó, vì vậy đây chắc chắn sẽ là một cách tốt để đạt được điều đó. Tôi sẽ chỉnh sửa nó và thay đổi nó sau. Cảm ơn vì bạn đã phản hồi!
RTYX

Cảm ơn chúa vì sự chia sẻ của bạn, với giải pháp @Mert, quan điểm của tôi bị loại bỏ trong các trường hợp hiếm gặp (cuộc gọi bị từ chối từ chương trình phụ trợ), với IterableDiffers của bạn, nó hoạt động rất tốt. Cảm ơn bạn rất nhiều!
lkaupp

2

Câu trả lời của Vivek đã phù hợp với tôi, nhưng dẫn đến một biểu thức đã thay đổi sau khi nó được kiểm tra lỗi. Không có nhận xét nào phù hợp với tôi, nhưng những gì tôi đã làm là thay đổi chiến lược phát hiện thay đổi.

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

1

Trong thiết kế góc cạnh bằng material design, tôi phải sử dụng những thứ sau:

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

Sau khi đọc các giải pháp khác, giải pháp tốt nhất mà tôi có thể nghĩ ra, vì vậy bạn chỉ chạy những gì bạn cần như sau: Bạn sử dụng ngOnChanges để phát hiện thay đổi thích hợp

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

}

Và bạn sử dụng ngAfterViewChecked để thực thi thay đổi trước khi nó hiển thị nhưng sau khi tính toàn bộ chiều cao

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

Nếu bạn đang tự hỏi làm thế nào để triển khai scrollToBottom

@ViewChild('scrollWrapper') private scrollWrapper: ElementRef;
scrollToBottom(){
    try {
      this.scrollWrapper.nativeElement.scrollTop = this.scrollWrapper.nativeElement.scrollHeight;
    } catch(err) { }
  }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.