如何检测 Angular Material 选项卡内的滚动事件以及如何以编程方式滚动回顶部?

How to detect the scroll event inside Angular Material Tabs and how to scroll back to top programmatically?

在我的 Angular 应用程序(当前 Angular 11)中,我总是使用用户滚动时出现的返回顶部按钮。该按钮在单击时将 window 滚动回顶部,然后消失。经典行为。

但现在我更改了布局并将 bootstrap 导航栏的内容替换为 Angular Material 标签。

我的 BodyComponent 现在看起来像这样:

<div id="body.component.container" style="margin-top: 62px;">
    <mat-tab-group [(selectedIndex)]="selectedMatTabIndex">
        <mat-tab>
            <ng-template matTabLabel>
                <span style="font-family: 'Arial Narrow', monospace; font-size: 16px; font-weight: bold;">Tab1</span>
            </ng-template>
            <app-content-component-001></app-content-component-001>
        </mat-tab>
    
        <mat-tab>
            <ng-template matTabLabel>
                <span style="font-family: 'Arial Narrow', monospace; font-size: 16px; font-weight: bold;">Tab2</span>
            </ng-template>
            <app-content-component-002></app-content-component-002>
        </mat-tab>
    </mat-tab-group>
</div>

<app-back-to-top
        [acceleration]="1000"
        [animate]="true"
        [scrollDistance]="50"
        [speed]="5000">
</app-back-to-top>

我面临的问题是,不再有可捕获的常见滚动事件。通常,正如你们都知道的那样,在返回顶部按钮组件中会监听 HostEvent window:scroll 但这在 MatTabs 中不起作用。

  @HostListener('window:scroll', [])
  onWindowScroll() {
      if (this.isBrowser()) {
          this.animationState = this.getCurrentScrollTop() > this.scrollDistance / 2 ? 'in' : 'out';
      }
  }

我把这个返回顶部按钮组件放在哪里并不重要。我尝试将它直接放入 body 组件(多年来一直如此),放入容器-div,放入 MatTabGroup 和每个 MatTab。 window:scroll-事件没有出现。

在我通过互联网进行(重新)搜索的过程中,我发现了一些微弱的提示,提示我必须使用 CDK 的一些指令,但没有示例如何使用。

所以我有问题。

  1. 如何检测 Material 选项卡内的 scoll 事件以使我的返回顶部按钮再次淡入?
  2. 如何以编程方式在 Material 选项卡内滚动回到顶部?

你可以试试这个。

scrollToTop.component.ts

import { DOCUMENT } from '@angular/common';
import { Component, HostListener, Inject, OnInit } from '@angular/core';
export class ScrollTopComponent implements OnInit {

  windowScrolled: boolean;
  constructor(@Inject(DOCUMENT) private document: Document) { }

  @HostListener("window:scroll")
  onWindowScroll() {
    if (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop > 100) {
      this.windowScrolled = true;
    }
    else if (this.windowScrolled && window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop < 10) {
      this.windowScrolled = false;
    }
  }
  scrollToTop() {
    (function smoothscroll() {
      var currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
      if (currentScroll > 0) {
        window.requestAnimationFrame(smoothscroll);
        window.scrollTo(0, currentScroll - (currentScroll / 8));
      }
    })();
  }

  ngOnInit(): void {
  }

}

scrollToTop.component.html

<div class="scroll-to-top" [ngClass]="{'show-scrollTop': windowScrolled}">
    Back to top<button mat-button mat-icon-button (click)="scrollToTop()">
        <mat-icon>keyboard_arrow_up</mat-icon>
    </button>
</div>

只需在您想要的地方使用 scrollToTop.component 的选择器。

<app-scroll-top></app-scroll-top>

替代方法:scroll.service.ts


  scroll(el: HTMLElement, behaviour: any = "smooth", block: any = "start", inline: any = "nearest") {
    el.scrollIntoView({ behavior: behaviour, block: block, inline: inline })
  }

我自己找到了解决方案。这实际上是我在此处发布之前尝试采用的方式。我必须使用 cdkScrollable 指令。

现在我知道怎么做了。您必须在每个 MatTab 中显示的组件周围放置一个 div。然后您必须将 cdkScrollable 指令附加到这些 div 上。然后你可以在 TS 代码中使用 ScrollDispatcher 捕获滚动事件。

最后看起来像这样:

我的BodyComponent

HTML
<div id="body.component.container" style="margin-top: 62px;">
    <mat-tab-group [(selectedIndex)]="selectedMatTabIndex">
        <mat-tab>
            <ng-template matTabLabel>
                <span style="font-family: 'Arial Narrow', monospace; font-size: 16px; font-weight: bold;">Tab1</span>
            </ng-template>
            
            <div cdkScrollable>
                <app-content-component-001></app-content-component-001>
            </div>
            
        </mat-tab>
    
        <mat-tab>
            <ng-template matTabLabel>
                <span style="font-family: 'Arial Narrow', monospace; font-size: 16px; font-weight: bold;">Tab2</span>
            </ng-template>
            
            <div cdkScrollable>
                <app-content-component-002></app-content-component-002>
            </div>
            
        </mat-tab>
    </mat-tab-group>
</div>

<app-back-to-top
        [scrollingNativeElement]="scrollingNativeElement">
</app-back-to-top>

我的BodyComponentTS(只有重要的代码行)

import { CdkScrollable, ScrollDispatcher } from '@angular/cdk/overlay';

scrollingNativeElement: HTMLElement;

constructor(public scrollDispatcher: ScrollDispatcher){}

ngOnInit(): void {
    this.scrollDispatcher.scrolled().subscribe((data: CdkScrollable) => {
            this.scrollingNativeElement = data.getElementRef().nativeElement;
    });
}

我的 BackToTopButtonComponent

TS
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({
    selector: 'app-back-to-top',
    templateUrl: './back-to-top.component.html',
    styleUrls: ['./back-to-top.component.css'],
    animations: [
        trigger('appearInOut', [
            state('in', style({
                'display': 'block',
                'opacity': '1'
            })),
            state('out', style({
                'display': 'none',
                'opacity': '0'
            })),
            transition('in => out', animate('400ms ease-in-out')),
            transition('out => in', animate('400ms ease-in-out'))
        ]),
    ]
})

export class BackToTopComponent implements OnInit, OnDestroy, OnChanges {
    animationState = 'out';

    @Input() scrollingNativeElement: HTMLElement;


    ngOnInit(): void
    {
    }

    ngOnDestroy(): void
    {
    }

    ngOnChanges(changes: SimpleChanges): void
    {
        if (changes['scrollingNativeElement'].currentValue)
        {
            this.animationState = (this.scrollingNativeElement.scrollTop > 0) ? 'in' : 'out';
        }
    }

    scrollToTop(): void
    {
        this.scrollingNativeElement.scrollTo(0, 0);
    }
}

我的BackToTopButtonComponentHTML

<button mat-fab type="button"
        id="BT1000"
        aria-label="Back to top of the page"
        class="back-to-top-button"
        [@appearInOut]="animationState"
        (click)="scrollToTop()"
        matTooltip="scroll to top">
    <mat-icon class="back-to-top-mat-icon" svgIcon="YOUR ICON GOES HERE"></mat-icon>
</button>

我的BackToTopButtonComponentCSS

.back-to-top-button {
  position: fixed;
  right: 40px;
  bottom: 40px;
  border: 0;
  outline: none;
  color: black;
  background: #f2f2f2;
  text-decoration: none;
  cursor: pointer;
  z-index: 9999;
}

.back-to-top-mat-icon {
  transform: scale(1.5);
}

非常感谢您提供代码。这非常有用。我有一个类似的代码,但行为很奇怪,无法按原样使用。 我不得不以这种方式使用 ChangeDetectorRef。

(基于此

我加

import { .. ChangeDetectorRef, ... } from '@angular/core';
...
constructor(... private changeDetectorRef: ChangeDetectorRef, ...){}

我在class

中声明了一个变量
showButton:boolean=false;

在订阅中我添加了一个 if/else 语句来控制按钮是否以这种方式出现:

this.scrollDispatcher.scrolled().subscribe((data: CdkScrollable) => {
         //for the update of the variables I had to add here
         this.changeDetectorRef.detectChanges();
            this.scrollingNativeElement = 
         data.getElementRef().nativeElement;

    //this is what I added
    if(this.scrollingNativeElement.scrollTop>200){
        this.showButton=true;
    }else{
        this.showButton=false;
    }

    });

然后我就这样在按钮里面添加了一个"*ngIf"标签

<button 
...
*ngIf="showButton"
...>
    ...
</button>

我真的不知道为什么我必须添加 this.changeDetectorRef.detectChanges(); 在订阅中但使用 console.log() 我可以检查变量是否相应更改但是未显示按钮 直到我添加了这一行。

以防对别人有帮助。

祝你有愉快的一天