Angular 2 เลื่อนไปด้านล่าง (รูปแบบการแชท)


112

ฉันมีชุดส่วนประกอบเซลล์เดียวภายในไฟล์ ng-forลูป

ฉันมีทุกอย่างเรียบร้อย แต่ดูเหมือนจะคิดไม่ออก

ปัจจุบันฉันมี

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

แต่จะไม่ได้ผลตลอดเวลาเนื่องจากรูปภาพดันวิวพอร์ตลงแบบอะซิงโครนัส

วิธีใดที่เหมาะสมในการเลื่อนไปที่ด้านล่างของหน้าต่างแชทใน Angular 2

คำตอบ:


206

ฉันมีปัญหาเดียวกันฉันใช้ a 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()คุณก็ไม่เป็นไร


ฉันมีส่วนประกอบบ้านในเทมเพลตบ้านฉันมีองค์ประกอบอื่นใน div ซึ่งแสดงข้อมูลและเก็บข้อมูลต่อท้าย แต่แถบเลื่อน css อยู่ในแม่แบบบ้าน div ไม่ได้อยู่ในเทมเพลตภายใน ปัญหาคือฉันเรียกข้อมูล scrolltobottom ทุกครั้งที่เข้ามาภายในส่วนประกอบ แต่ #scrollMe อยู่ในส่วนประกอบภายในบ้านเนื่องจาก div นั้นเลื่อนได้ ... และ # scrollMe ไม่สามารถเข้าถึงได้จากส่วนประกอบภายใน .. ไม่แน่ใจว่าจะใช้ #scrollme จากองค์ประกอบภายใน
อย่างไร

วิธีแก้ปัญหาของคุณ @Basit นั้นเรียบง่ายสวยงามและไม่จบลงด้วยการยิงม้วนที่ไม่มีที่สิ้นสุด / ต้องการวิธีแก้ปัญหาเมื่อผู้ใช้เลื่อนด้วยตนเอง
theotherdy

3
สวัสดีมีวิธีป้องกันการเลื่อนเมื่อผู้ใช้เลื่อนขึ้นหรือไม่?
John Louie Dela Cruz

2
ฉันกำลังพยายามใช้สิ่งนี้ในรูปแบบการแชทด้วย แต่ไม่จำเป็นต้องเลื่อนหากผู้ใช้เลื่อนขึ้น ฉันมองหาและไม่พบวิธีตรวจสอบว่าผู้ใช้เลื่อนขึ้นหรือไม่ ข้อแม้อย่างหนึ่งคือส่วนประกอบการแชทนี้ไม่ได้เต็มหน้าจอเกือบตลอดเวลาและมีแถบเลื่อนของตัวเอง
HarvP

2
@HarvP & JohnLouieDelaCruz คุณพบวิธีการข้ามการเลื่อนหากผู้ใช้เลื่อนด้วยตนเองหรือไม่?
suhailvs

167

ทางออกที่ง่ายและดีที่สุดคือ:

เพิ่ม#scrollMe [scrollTop]="scrollMe.scrollHeight"สิ่งง่ายๆนี้ที่ด้านเทมเพลต

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

นี่คือลิงค์สำหรับWORKING DEMO (พร้อมแอพแชทดัมมี่) และรหัสเต็ม

จะทำงานร่วมกับ Angular2 และไม่เกิน 5 เนื่องจากการสาธิตด้านบนจะทำใน Angular5


บันทึก :

สำหรับข้อผิดพลาด: ExpressionChangedAfterItHasBeenCheckedError

โปรดตรวจสอบ css ของคุณมันเป็นปัญหาของด้าน css ไม่ใช่ด้าน Angular หนึ่งในผู้ใช้ @KHAN ได้แก้ไขโดยการลบออกoverflow:auto; height: 100%;จากdivไฟล์. (โปรดตรวจสอบการสนทนาเพื่อดูรายละเอียด)


3
และคุณจะเรียกการเลื่อนได้อย่างไร?
Burjua

3
มันจะเลื่อนไปที่ด้านล่างของรายการใด ๆ ที่เพิ่มใน ngfor รายการหรือเมื่อความสูงของ div ด้านในเพิ่มขึ้น
Vivek Doshi

2
ขอบคุณค่ะ แม้ว่าจะมีการเพิ่มบางอย่างฉันได้รับข้อผิดพลาดนี้:> ข้อผิดพลาดข้อผิดพลาด: ExpressionChangedAfterItHasBeenCheckedError: นิพจน์มีการเปลี่ยนแปลงหลังจากตรวจสอบแล้ว ค่าก่อนหน้า: '700' ค่าปัจจุบัน: '517'
Vassilis Pits

6
@csharpnewbie ฉันมีปัญหาเดียวกันเมื่อลบข้อความใด ๆ ออกจากรายการ: Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'. แก้แล้วหรือยัง?
Andrey

6
ฉันสามารถแก้ปัญหา "นิพจน์มีการเปลี่ยนแปลงหลังจากตรวจสอบแล้ว" (ในขณะที่รักษาความสูง: 100%; overflow-y: scroll) โดยเพิ่มเงื่อนไขใน [scrollTop] เพื่อป้องกันไม่ให้ตั้งค่าจนกว่าฉันจะมีข้อมูลที่จะเติม ด้วยเช่น <ul class = "chat-history" #chatHistory [scrollTop] = "messages.length === 0? 0: chatHistory.scrollHeight"> <li * ngFor = "let message of messages"> .. การเพิ่มการตรวจสอบ "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) { }
    }

}

โซลูชันที่ใช้งานได้จริงโดยไม่มีข้อผิดพลาดในคอนโซล ขอบคุณ!
Dmitry Vasilyuk 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 ก็ตาม การแก้ปัญหาโดย Mert เป็นเพียงคนเดียวที่ได้ผล โซลูชันของ Vivek จะยิงไม่ว่าจะเพิ่มองค์ประกอบใดก็ตามในขณะที่ Mert สามารถยิงได้ก็ต่อเมื่อมีการเพิ่มองค์ประกอบบางอย่าง
John Hamm

1
นี่เป็นคำตอบเดียวที่ช่วยแก้ปัญหาของฉันได้ ทำงานร่วมกับเชิงมุม 6
suhailvs

2
เจ๋ง !!! ฉันใช้อันบนและคิดว่ามันยอดเยี่ยม แต่แล้วฉันก็ติดขัดในการเลื่อนลงและผู้ใช้ของฉันไม่สามารถอ่านความคิดเห็นก่อนหน้านี้ได้! ขอบคุณสำหรับการแก้ปัญหานี้! สุดยอด! ฉันจะให้ 100+ คะแนนถ้าทำได้ :-D
cheesydoritosandkale

19

ยังไม่เป็นมาตรฐานหากคุณมีความเข้ากันได้ของ crossbrowser คุณจะมีข้อ จำกัด
chavocarlos

1
@chavocarlos - ดูเหมือนว่าจะรองรับทุกอย่างยกเว้น Opera Mini: caniuse.com/#feat=scrollintoview
Boris Yakubchik

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 ไม่ทำงานเนื่องจากข้อผิดพลาด Angular ที่ QueryList เปลี่ยนการสมัครสมาชิกจะเริ่มทำงานเพียงครั้งเดียวแม้ว่าจะประกาศใน ngAfterViewInit ก็ตาม โซลูชันของ Vivek จะยิงไม่ว่าจะเพิ่มองค์ประกอบใดก็ตามในขณะที่ Mert สามารถยิงได้ก็ต่อเมื่อมีการเพิ่มองค์ประกอบบางอย่าง
John Hamm

ขอบคุณ! แต่ละวิธีอื่น ๆ มีข้อเสียบางประการ แค่นี้ก็ใช้งานได้ตามที่ตั้งใจไว้ ขอบคุณสำหรับการแบ่งปันรหัสของคุณ!
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หากผู้ใช้มีการเลื่อนขึ้นมาด้วยตนเอง
Ruslan Stelmachenko

ขอบคุณสำหรับคำแนะนำ @RuslanStelmachenko ฉันใช้มัน (ฉันตัดสินใจที่จะทำngAfterViewCheckedกับบูลีนอื่น ๆ ฉันเปลี่ยนshouldScrollDownชื่อnumberOfMessagesChangedเพื่อให้ชัดเจนว่าบูลีนนี้หมายถึงอะไร
RTYX

ฉันเห็นคุณทำif (this.numberOfMessagesChanged && !isScrolledDown)แต่ฉันคิดว่าคุณไม่เข้าใจเจตนาของคำแนะนำของฉัน ความตั้งใจจริงของฉันคือการไม่เลื่อนลงโดยอัตโนมัติแม้ว่าจะมีการเพิ่มข้อความใหม่ก็ตามหากผู้ใช้เลื่อนขึ้นด้วยตนเอง หากไม่มีสิ่งนี้หากคุณเลื่อนขึ้นเพื่อดูประวัติแชทจะเลื่อนลงทันทีที่มีข้อความใหม่มาถึง นี่อาจเป็นพฤติกรรมที่น่ารำคาญมาก :) ดังนั้นข้อเสนอแนะของฉันคือตรวจสอบว่าการแชทถูกเลื่อนขึ้นก่อนที่ข้อความใหม่จะถูกเพิ่มไปยัง DOM และเพื่อไม่ให้เลื่อนอัตโนมัติหากเป็นเช่นนั้น ngAfterViewCheckedสายเกินไปเพราะ DOM เปลี่ยนไปแล้ว
Ruslan Stelmachenko

อ่าาฉันไม่เข้าใจความตั้งใจแล้ว สิ่งที่คุณพูดนั้นสมเหตุสมผล - ฉันคิดว่าในสถานการณ์นั้นการแชทจำนวนมากเพียงแค่เพิ่มปุ่มเพื่อลงไปข้างล่างหรือทำเครื่องหมายว่ามีข้อความใหม่อยู่ตรงนั้นดังนั้นนี่จึงเป็นวิธีที่ดีในการบรรลุเป้าหมายอย่างแน่นอน ฉันจะแก้ไขและเปลี่ยนแปลงในภายหลัง ขอบคุณสำหรับความคิดเห็น!
RTYX

ขอบคุณพระเจ้าสำหรับการแบ่งปันของคุณด้วยโซลูชัน @Mert มุมมองของฉันเริ่มลดลงในกรณีที่หายาก (การโทรถูกปฏิเสธจากแบ็กเอนด์) ด้วย IterableDiffers ของคุณมันทำงานได้ดี ขอบคุณมาก!
lkaupp

2

คำตอบของ Vivek ใช้ได้ผลสำหรับฉัน แต่ส่งผลให้นิพจน์เปลี่ยนไปหลังจากตรวจสอบข้อผิดพลาด ไม่มีความคิดเห็นใดที่ใช้ได้ผลสำหรับฉัน แต่สิ่งที่ฉันทำคือเปลี่ยนกลยุทธ์การตรวจจับการเปลี่ยนแปลง

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

1

ในเชิงมุมโดยใช้การออกแบบวัสดุ sidenav ฉันต้องใช้สิ่งต่อไปนี้:

let ele = document.getElementsByClassName('md-sidenav-content');
    let eleArray = <Element[]>Array.prototype.slice.call(ele);
    eleArray.map( val => {
        val.scrollTop = val.scrollHeight;
    });


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) { }
  }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.