2

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?

3 Answers3

2

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

Thanks a lot for the code. It was very useful. I have a code similar but the behavior is very strange and couldn't use it just as it was. I had to use a ChangeDetectorRef in this way.

(in base to this post )

I add

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

I declared a variable in the class

showButton:boolean=false;

and inside the subscription I add an if/else statement to control whether the button appears in this way:

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

    });

Then I added a "*ngIf" tag inside the button in this way

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

I don't really know why I had to add the this.changeDetectorRef.detectChanges(); whithin the subscription but using console.log() I could check that the variables where changing accordingly HOWEVER the button was not showed until I added the line.

In case it helps someone else.

Have a nice day

Vito
  • 1
  • 2
-1

You can try this.

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>

Just use the selector for the scrollToTop.component where ever you want.

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

Alternate method: scroll.service.ts


  scroll(el: HTMLElement, behaviour: any = "smooth", block: any = "start", inline: any = "nearest") {
    el.scrollIntoView({ behavior: behaviour, block: block, inline: inline })
  }
Srinath Kamath
  • 542
  • 1
  • 6
  • 17
  • Sorry, for maybe sounding slightly puzzled, but did you read all my code snippets? I aleady tried exactly this. I have a working, component-based back-to-top-button. But those standard scrolling events aren't fired inside a Material Tab. That is my actual problem. –  Feb 20 '21 at 07:36
  • And what's more, also the scrolling commands do not apply here. I read that there must be some solution using cdkScroll or so, but I cannot find any explanation on material's homepage. Quite frustrating. –  Feb 20 '21 at 07:40
  • Please try the service option i have provided. It works for me. – Srinath Kamath Feb 20 '21 at 08:55
  • Sorry, it does not work, no matter where I put the component at. –  Feb 20 '21 at 16:40