【问题标题】:Angular 2 Scroll to bottom (Chat style)Angular 2 滚动到底部(聊天风格)
【发布时间】:2016-02-05 20:14:14
【问题描述】:

我在ng-for 循环中有一组单单元组件。

我什么都准备好了,但我似乎找不到合适的

目前我有

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

但这并不总是有效,因为图像会异步将视口向下推。

在 Angular 2 中滚动到聊天窗口底部的合适方式是什么?

【问题讨论】:

    标签: scroll typescript angular


    【解决方案1】:

    我遇到了同样的问题,我正在使用 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()你应该没问题。

    【讨论】:

    • 我有一个 home 组件,在 home 模板中我在 div 中有另一个组件,它显示数据并继续附加数据,但滚动条 css 在 home 模板 div 中而不是在内部模板中。问题是我每次数据进入组件时都会调用这个scrolltobottom,但#scrollMe在主组件中,因为该div是可滚动的......并且#scrollMe无法从组件内部访问..不确定如何从内部组件使用#scrollme跨度>
    • 您的解决方案 @Basit 非常简单,不会导致无限滚动/需要解决用户手动滚动时的问题。
    • 嗨,有没有办法防止用户向上滚动时滚动?
    • 我也在尝试在聊天风格的方法中使用它,但如果用户向上滚动则不需要滚动。我一直在寻找并且找不到检测用户是否向上滚动的方法。需要注意的是,这个聊天组件大部分时间都不是全屏的,并且有自己的滚动条。
    • @HarvP & JohnLouieDelaCruz 如果用户手动滚动,您是否找到了跳过滚动的解决方案?
    【解决方案2】:

    最简单也是最好的解决方案是:

    模板端添加这个#scrollMe [scrollTop]="scrollMe.scrollHeight"简单的东西

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

    这是WORKING DEMO(使用虚拟聊天应用程序)和FULL CODE

    的链接

    将与 Angular2 以及最多 5 个一起使用,如上演示在 Angular5。


    注意:

    对于错误:ExpressionChangedAfterItHasBeenCheckedError

    请检查您的 css,这是 css 方面的问题,而不是 Angular 方面的问题 ,其中一位用户@KHAN 通过从div 中删除overflow:auto; height: 100%; 解决了这个问题。 (详情请查看对话)

    【讨论】:

    • 你如何触发滚动?
    • 它将自动滚动到添加到 ngfor 项目的任何项目的底部,或者当内侧 div 的高度增加时
    • 谢谢@VivekDoshi。虽然添加某些内容后我收到此错误:>ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it is checked。以前的值:'700'。当前值:'517'。
    • @csharpnewbie 从列表中删除任何消息时我遇到了同样的问题:Expression has changed after it was checked. Previous value: 'scrollTop: 1758'. Current value: 'scrollTop: 1734'。你解决了吗?
    • 我能够通过向 [scrollTop] 添加条件来解决“检查后表达式已更改”(同时保持高度:100%;溢出-y:滚动),以防止它设置直到我有数据填充它,例如:
      • ... 添加“messages.length === 0 ? 0”检查似乎解决了这个问题,虽然我无法解释原因,所以我不能肯定地说修复不是对我的实现来说是独一无二的。
    【解决方案3】:

    在滚动消息时触发接受的答案,这可以避免这种情况。

    你想要这样的模板。

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

    【讨论】:

    • 嗨 - 我正在尝试这种方法,但它总是进入 catch 块。我有一张卡片,里面有一个显示多条消息的 div(在卡片 div 里面,我的结构类似于你的结构)。您能否帮助我了解这是否需要对 css 或 ts 文件进行任何其他更改?谢谢。
    • 迄今为止最好的答案。谢谢!
    • 这目前不起作用,因为 Angular 错误,其中 QueryList 更改订阅仅触发一次,即使在 ngAfterViewInit 中声明也是如此。 Mert 的解决方案是唯一有效的解决方案。无论添加什么元素,Vivek 的解决方案都会触发,而 Mert 的解决方案只有在添加某些元素时才会触发。
    • 这是解决我问题的唯一答案。适用于角度 6
    • 太棒了!!!我使用了上面的那个,认为它很棒,但后来我被卡住了向下滚动,我的用户无法阅读以前的 cmets!感谢您提供此解决方案!极好的!如果可以的话,我会给你100+分:-D
    【解决方案4】:

    我添加了一个检查以查看用户是否尝试向上滚动。

    如果有人想要,我将把它留在这里:)

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

    【讨论】:

    • 真正可行的解决方案,在控制台中没有错误。谢谢!
    【解决方案5】:

    【讨论】:

    • 还不是标准,如果你有跨浏览器的兼容性,你会有限制。
    • @chavocarlos - 似乎除了 Opera Mini 之外的所有东西都支持:caniuse.com/#feat=scrollintoview
    【解决方案6】:

    如果您想确定在 *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;
    }
    

    这里很重要,"last"变量定义了你当前是否在最后一项,所以你可以触发"scrollToBottom"方法

    【讨论】:

    • 这个答案值得被接受。这是唯一有效的解决方案。 Denixtry 的解决方案由于 Angular 错误而无法工作,其中 QueryList 更改订阅仅触发一次,即使在 ngAfterViewInit 中声明也是如此。无论添加什么元素,Vivek 的解决方案都会触发,而 Mert 的解决方案只有在添加某些元素时才会触发。
    • 谢谢!其他每种方法都有一些缺点。这只是按预期工作。感谢您分享您的代码!
    • 在 Angular 中为我工作:11.0.9。其他的似乎有问题。使用外部 DIV 上的 #content 和 @ViewChild('content') content: ElementRef; 来实现建议的 ScrollToBottom
    【解决方案7】:
    this.contentList.nativeElement.scrollTo({left: 0 , top: this.contentList.nativeElement.scrollHeight, behavior: 'smooth'});
    

    【讨论】:

    • 任何让提问者朝着正确方向前进的答案都是有帮助的,但请尝试在您的答案中提及任何限制、假设或简化。简洁是可以接受的,但更全面的解释会更好。
    【解决方案8】:
    const element = document.getElementById('box');
    element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
    

    【讨论】:

      【解决方案9】:

      如果您使用的是最新版本的 Angular,以下内容就足够了:

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

      【讨论】:

      • 这对于大多数情况来说已经足够简单了 :)
      【解决方案10】:

      Vivek 的回答对我有用,但在检查错误后导致表达式发生了变化。没有一个 cmets 对我有用,但我所做的是改变了变化检测策略。

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

      【讨论】:

      • 我遇到了完全相同的问题,尝试使用 Vivek 的答案并继续出现错误。现在它可以完美运行了,谢谢!!!
      【解决方案11】:

      分享我的解决方案,因为我对其余部分并不完全满意。我对AfterViewChecked 的问题是有时我向上滚动,并且由于某种原因,这个生命钩子被调用并且即使没有新消息它也会向下滚动我。我尝试使用OnChangesthis 是一个问题,这导致我使用this 解决方案。不幸的是,仅使用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) &lt;= 3.0;(其中 3.0 是像素容差)。如果用户手动向上滚动,它可以用于ngDoCheck 有条件地不将shouldScrollDown 设置为true
      • 感谢@RuslanStelmachenko 的建议。我实现了它(我决定在 ngAfterViewChecked 上使用另一个布尔值。我将 shouldScrollDown 名称更改为 numberOfMessagesChanged 以明确这个布尔值到底指的是什么。
      • 我看到你这样做if (this.numberOfMessagesChanged &amp;&amp; !isScrolledDown),但我认为你没有理解我的建议的意图。如果用户手动向上滚动,我的真正意图是自动向下滚动,即使添加了新消息。如果没有这个,如果您向上滚动查看历史记录,一旦有新消息到达,聊天就会向下滚动。这可能是非常烦人的行为。 :) 所以,我的建议是检查聊天是否向上滚动之前 新消息添加到 DOM 并且不自动滚动,如果是的话。 ngAfterViewChecked 为时已晚,因为 DOM 已经改变了。
      • 啊啊啊,当时我还没明白用意。你说的很有道理——我认为在这种情况下,许多聊天只是添加一个按钮来向下,或者一个标记那里有一条新消息,所以这绝对是实现这一目标的好方法。我将对其进行编辑并稍后更改。感谢您的反馈!
      • 感谢上帝的分享,使用@Mert 解决方案,我的观点在极少数情况下被踢掉(调用被后端拒绝),而您的 IterableDiffers 它运行良好。非常感谢!
      【解决方案12】:

      这是stackblitz 上的另一个很好的解决方案。

      或者

      接受的答案是一个很好的解决方案,但可以改进它,因为考虑到 ngAfterViewChecked() 生命周期挂钩的工作原理,您的内容/聊天可能经常不由自主地滚动到底部。

      这是一个改进的版本...

      组件

      import {..., AfterViewChecked, ElementRef, ViewChild, OnInit} from 'angular2/core'
      @Component({
          ...
      })
      export class ChannelComponent implements OnInit, AfterViewChecked {
          @ViewChild('scrollMe') private myScrollContainer: ElementRef;
      
          /**Add the variable**/
          scrolledToBottom = false;
      
          ngAfterViewChecked() {        
              this.scrollToBottom();        
          } 
      
          scrollToBottom(): void {
              try {
                /**Add the condition**/
                if(!this.scrolledToBottom){
                   this.myScrollContainer.nativeElement.scrollTop = this.myScrollContainer.nativeElement.scrollHeight;
                }   
              } catch(err) { }                 
          }
      
          /**Add the method**/
          onScroll(){
            this.scrolledToBottom = true;
          }
      }
      

      模板

      <!--Add a scroll event listener-->
      <div #scrollMe 
           style="overflow: scroll; height: xyz;"
           (scroll)="onScroll()">
          <div class="..." 
              *ngFor="..."
              ...>  
          </div>
      </div>
      

      【讨论】:

        【解决方案13】:

        在使用材料设计 sidenav 的角度中,我必须使用以下内容:

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

        【讨论】:

          【解决方案14】:

          问题的标题提到“聊天风格”滚动到底部,这也是我需要的。这些答案都没有让我真正满意,因为我真正想要做的是在添加或销毁子元素时滚动到我的 div 底部。我最终用这个非常简单的指令做到了这一点,它利用了MutationObserver API

          @Directive({
              selector: '[pinScroll]',
          })
          export class PinScrollDirective implements OnInit, OnDestroy {
              private observer = new MutationObserver(() => {
                  this.scrollToPin();
              });
          
              constructor(private el: ElementRef) {}
          
              ngOnInit() {
                  this.observer.observe(this.el.nativeElement, {
                      childList: true,
                  });
              }
          
              ngOnDestroy() {
                  this.observer.disconnect();
              }
          
              private scrollToPin() {
                  this.el.nativeElement.scrollTop = this.el.nativeElement.scrollHeight;
              }
          }
          

          您只需将此指令附加到您的列表元素,只要列表项在 DOM 中发生更改,它就会滚动到底部。这是我个人一直在寻找的行为。该指令假定您已经在处理列表元素上的heightoverflow 规则。

          【讨论】:

          • 这是我上周发现的最棒的一段代码,非常感谢!这正是我所需要的,而且它也非常干净,在我看来,通过为此使用指令。太棒了!
          【解决方案15】:

          在阅读了其他解决方案之后,我能想到的最好的解决方案,所以你只运行你需要的如下: 您使用 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) { }
            }
          

          【讨论】:

            【解决方案16】:

            如果有人在使用 Angular 9 时遇到这个问题,这就是我设法解决的方法。

            我从 #scrollMe [scrollTop]="scrollMe.scrollHeight" 的解决方案开始,我得到了人们提到的 ExpressionChangedAfterItHasBeenCheckedError 错误。

            为了解决这个问题,我只需在我的 ts 组件中添加:

            @Component({
                changeDetection: ChangeDetectionStrategy.OnPush,
            ...})
            
             constructor(private cdref: ChangeDetectorRef) {}
            
             ngAfterContentChecked() {
                    this.cdref.detectChanges();
                }
            

            ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'

            【讨论】:

              【解决方案17】:

              要添加平滑滚动,请执行此操作

              #scrollMe [scrollTop]="scrollMe.scrollHeight" style="scroll-behavior: smooth;"
              

              this.cdref.detectChanges();
              

              【讨论】:

              • 那是哪个库/控件?更好的是,您至少应该提供一些上下文
              猜你喜欢
              • 2016-09-21
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2018-09-20
              • 1970-01-01
              • 2014-12-08
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多