Is it possible to use the HostListener in Angular 7 to detect any scroll event occurring on a specific element within a webpage?

I am developing a breadcrumb bar component in Angular 7 that should dynamically hide and show based on user scrolling behavior.

To achieve this, I created a directive to track the scroll position of the container element. While my code for capturing the scrollTop works well, I'm facing an issue with triggering the HostListener to execute the code.

The main challenge I'm encountering is finding a way to capture scroll events at the page level without attaching a directive to each individual scrolling container. This would limit the flexibility of the component. Is there a method to globally capture scroll events or propagate them to the document level? See below for the HTML Template and Directive code snippet. Ideally, I prefer not to rely on multiple mouse events either.

Any insights on how to approach this problem would be greatly appreciated.

import { Directive, HostListener, Output, EventEmitter, ElementRef } from '@angular/core';

@Directive({
  selector: '[scrollListener]'
})
export class ScrollListenerDirective {
  previousScrollContainerScrollTop = 0;

  @Output() scroll = new EventEmitter<boolean>();

  constructor(private el: ElementRef) { }

  @HostListener('document:scroll', ['$event'])
  onListenerTriggered(): void {
    let currentScrollTop: number;
    let elementHeight: number;

    const scrollContainerScrollTop = this.el.nativeElement.offsetParent.scrollTop;
    elementHeight = this.el.nativeElement.offsetHeight;
    currentScrollTop = scrollContainerScrollTop;

    if (this.previousScrollContainerScrollTop < currentScrollTop && scrollContainerScrollTop > elementHeight + elementHeight) {
      this.scroll.emit(true);
    } else if (this.previousScrollContainerScrollTop > currentScrollTop && !(scrollContainerScrollTop <= elementHeight)) {
      this.scroll.emit(false);
    }

    this.previousScrollContainerScrollTop = currentScrollTop;
  }
}

<ng-container *ngIf="!hide && breadcrumbs && breadcrumbs.length > 1">
  <div scrollListener (scroll)="trackScroll($event)" class="breadcrumb-container" [class.scroll-up]="breadcrumbBarHidden" data-id="breadcrumb-navigation">
    <div data-id="back-button" class="back-button" (click)="onBackClick()">
      <div class="arrow-container">
        <mat-icon svgIcon="left"></mat-icon>
      </div>
      <span>Back</span>
    </div>

    <div class="crumb-container">
      <ng-container *ngFor="let crumb of breadcrumbs; index as i;">
        <div class="crumb">
          <div class="crumb-label" (click)="onBreadcrumClick(crumb)" [attr.data-id]="'crumb-' + i" [title]="crumb.label">{{crumb.label}}</div>
          <div class="chevron-container">
            <mat-icon svgIcon="chevron-right"></mat-icon>
          </div>
        </div>
      </ng-container>
    </div>
  </div>
</ng-container>

Answer №1

After some exploration, I managed to find a solution. I implemented a pure Javascript event listener that triggers my function through the event emitter with successful results.

import { Directive, HostListener, Output, EventEmitter, ElementRef, Inject } from '@angular/core';


@Directive({
  selector: '[ScrollListener]'
})
export class ScrollListenerDirective {
  previousScrollContainerScrollTop = 0;

  @Output() scroll = new EventEmitter<boolean>();

  constructor(private el: ElementRef) {
    document.addEventListener('scroll', (event) => {
      this.onListenerTriggered(event);
    }, true);  // This is so we can use a UseCapture event listener.  Document scrolls do not bubble back up to the document level
  }

  onListenerTriggered(event): void {
    let currentScrollTop: number;
    let elementHeight: number;

    const scrollContainerScrollTop = this.el.nativeElement.offsetParent.scrollTop;
    elementHeight = this.el.nativeElement.offsetHeight;
    currentScrollTop = scrollContainerScrollTop;

    if (this.previousScrollContainerScrollTop < currentScrollTop && scrollContainerScrollTop > elementHeight + elementHeight) {
      this.scroll.emit(true);
    } else if (this.previousScrollContainerScrollTop > currentScrollTop && !(scrollContainerScrollTop <= elementHeight)) {
      this.scroll.emit(false);
    }

    this.previousScrollContainerScrollTop = currentScrollTop;

  }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Help needed: Encountered an error stating "Module not found: Error can't resolve 'child_process', any solutions to resolve this issue?

I'm currently in the process of developing a JupyterLab extension using TypeScript. After successfully incorporating the package "@types/node" to access functionalities like 'require('http')', I encountered an issue when attemptin ...

Issue with Socket.IO: socket.on not executed

Recently, I devised a custom asynchronous emitter for implementing a server -> client -> server method. Regrettably, the functionality is not meeting my expectations. Although it emits the event, it fails to execute the callback as intended. Upon a ...

What could be causing my mdx files to not display basic markdown elements such as lists and headings? (Next.js, mdx-bundler)

Trying to implement Kent C Dodds mdx-bundler with the Next.js typescript blog starter example is proving challenging. While it successfully renders JSX and certain markdown elements, basic markdown syntax like lists and paragraph spacing seem to be malfunc ...

Is it considered bad form to utilize nearly identical for loops in two separate instances within Angular 6?

I am working on creating two lists for a roster. The first list will display the current members of this year, while the second list will show if individuals have been excused for this year. After analyzing my code, I realized that I am using two identic ...

Error: The class constructor [] must be called with the 'new' keyword when creating instances in a Vite project

I encountered an issue in my small Vue 3 project set up with Vite where I received the error message Class constructor XX cannot be invoked without 'new'. After researching online, it seems that this problem typically arises when a transpiled cla ...

Enhancing State Management with CombineReducers in TypeScript

export const rootReducer = combineReducers({ login: loginReducer, }); Everything is working fine with the current setup, but I encountered an issue when attempting to combine another reducer: export const rootReducer = combineReducers({ login: lo ...

Is it possible to have multiple Mat-Dialogs simultaneously displayed on screen?

Is it possible to show a mat dialog on top of another mat-dialog component? I have a grid with an edit button, and when the user clicks on it, an Edit Dialog window opens for editing data. If the user wants to close this window without saving, I want to di ...

Use leaflet.js in next js to conceal the remainder of the map surrounding the country

I'm currently facing an issue and would appreciate some assistance. My objective is to display only the map of Cameroon while hiding the other maps. I am utilizing Leaflet in conjunction with Next.js to showcase the map. I came across a helpful page R ...

White Background Dialog in Angular

I am struggling to change the default white background of my webpage. Is there a way I can use CSS to blur or darken the background instead? I tried applying CSS code but couldn't locate the correct class. I also attempted setting the "hasBackdrop" at ...

What seems to be the issue with my @typescript-eslint/member-ordering settings?

I am encountering an issue where my lint commands are failing right away with the error message shown below: Configuration for rule "@typescript-eslint/member-ordering" is throwing an error: The value ["signature","public-static-field","pro ...

The attribute 'y' is not found within the data type 'number'

Currently working on a project using Vue.js, Quasar, and TypeScript. However, encountering errors that state the following: Property 'y' does not exist on type 'number | { x: number[]; y: number[]; }'. Property 'y' does not ...

Incorporating observables into an existing axios post request

Currently, I have an API using axios to send HTTP POST requests. These requests contain logs that are stored in a file. The entire log is sent at once when a user signs into the system. Now, I've been instructed to modify the code by incorporating Rx ...

Divide the enhanced document into sections using TypeScript

In my project, I am working with Material UI and TypeScript. I have noticed that I need to declare the Theme interface and ThemeOptions in the same file for it to work properly. Is there a more efficient way to separate these declarations from the main t ...

In Bootstrap 3, rows are aligned on the same line

My issue is that two rows are coming on the same line in Bootstrap 3. Here is the code for a chat screen with two people: Code [<div class="row single-row mt-10" style="float: left !important;> <div class="col-2" styl ...

Automatically convert TypeScript packages from another workspace in Turborepo with transpilation

I have set up a Turborepo-based monorepo with my primary TypeScript application named @myscope/tsapp. This application utilizes another TypeScript package within the same repository called @myscope/tspackage. For reference, you can view the example reposit ...

Is there a way to incorporate the router into the observable within the guard?

Is there a way to inject a router into my guard when I have an Observable method returned? I want to implement routing with a redirect to the login page if a certain condition is met: If the result of the isAccessToLobby method is false, then redirect to t ...

Is the async pipe the best choice for handling Observables in a polling scenario

The situation at hand: I currently have a service that continuously polls a specific URL every 2 seconds: export class FooDataService { ... public provideFooData() { const interval = Observable.interval(2000).startWith(0); return interval ...

Contrast between categories and namespaces in TypeScript

Can you clarify the distinction between classes and namespaces in TypeScript? I understand that creating a class with static methods allows for accessing them without instantiating the class, which seems to align with the purpose of namespaces. I am aware ...

Mapping strings bidirectionally in Typescript

I am currently working on a two-way string mapping implementation; const map = {} as MyMap; // need the correct type here const numbers = "0123456789abcdef" as const; const chars = "ghijklmnopqrstuv" as const; for (let i = 0; i < n ...

Unlock the Power of RxJS Observables in Angular

Issue: My goal is to navigate to the login page if firebase-auth does not have a user logged in, and to an alternate page if a user is already logged in. To achieve this, I plan to call a function within a constructor service. private checkLoginStatus() ...