Search code examples
angularangular-materialangular11

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


In my Angular App (currently Angular 11) I always used a back-to-top-button which appears when the user scrolls. The button scrolls the window back to top when it gets clicked and then disappears. Classic behaviour.

But now I changed my layout and replaced bootstrap navbars 'n stuff by Angular Material Tabs.

My BodyComponent now looks somehow like this:

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

The problem I am facing is, that there is no common scrolling event catchable any more. Usually, As you all surely know, inside a back-to-top-button component one listens to the HostEvent window:scroll but this does not work inside MatTabs.

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

And it does not matter where I put this back-to-top-button-component at. I tried putting it directly into the body component (that's how it worked out for years), into the container-div, into the MatTabGroup and into each MatTab. The window:scroll-event does not show up.

During my (Re)Searching through the internet I found some faint hints that I have to use some directives of CDK's but no example how.

So I've got to questions.

  1. How to detect the scoll event inside Material Tabs in order to get my back-to-top-button fading in again?
  2. How to Scroll back to top inside Material Tabs programmatically?

Solution

  • I found the solution myself. It is actually the way I tried to go before posting here. I have to use the cdkScrollable Directive.

    And now I know how. You have to put a div around the component that is displayed inside each MatTab. And then you have to attach the cdkScrollable Directive to these divs. Then you can catch the scroll-event with a ScrollDispatcher inside the TS-code.

    Finally it looks like this:

    The HTML of my 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>
                
                <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>
    

    The TS of my BodyComponent (only the important lines of code)

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

    The TS of my BackToTopButtonComponent

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

    The HTML of my BackToTopButtonComponent

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

    The CSS of my BackToTopButtonComponent

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