Can someone share with me the best practices for implementing @HostListener within a method in my Angular 16 project?

Currently, I've been involved in developing a Single Page Application using Angular 16, TypeScript, and The Movie Database (TMDB).

My task at hand is to implement the "infinite scroll" functionality on a particular component.

To achieve this, I have:

export class MoviesByGenre {

  constructor(
    private activatedRoute: ActivatedRoute,
    private movieService: MovieService
  ) { }

  public genreName: string | undefined = '';

  public movieResponse!: MovieResponse;
  public movies: Movie[] | undefined = [];

  public genreResponse!: GenreResponse;
  public genres: Genre[] | undefined = [];

  public getMoviesByGenre(): void {

    // Retrieving genre id from URL parameter
    const genreId = Number(this.activatedRoute.snapshot.paramMap.get('id'));

    const maxPage: number = 10;
    let pageNumber: number = 1;
    let isLoading: boolean = false;

    // Fetching genre name from genres array
    this.movieService.getAllMovieGenres().subscribe((response) => {
      this.genreResponse = response;
      this.genres = this.genreResponse.genres;

      if (this.genres && this.genres.length) {
        let currentGenre = this.genres.find(genre => genre.id === genreId);
        if (currentGenre) {
          this.genreName = currentGenre.name || '';
          this.movieService.defaultTitle = this.genreName;
        }
      }
    });

    // Obtaining movies by genre id
    this.movieService.getMoviesByGenre(genreId, pageNumber).subscribe((response) => {
      this.movieResponse = response;
      this.movies = this.movieResponse.results;
   
       @HostListener('window:scroll', ['$event'])
       onWindowScroll(event) {
        if (window.innerHeight + window.scrollY >= document.body.offsetHeight &&! isLoading) {
          if (pageNumber < maxPage) {
            isLoading = true;
        
            // Appending to the movies array
            this.movies?.push(...this.movies);
        
            // Incrementing page number
            pageNumber++;
        
            isLoading = false;
          }
        }
      }
    })
  }

  ngOnInit() {
    this.activatedRoute.params.subscribe(() => {
      this.getMoviesByGenre();
    });
  }

  ngOnDestroy() {
    this.movieService.defaultTitle = '';
  }

}

The Dilemma

I did not anticipate @HostListener causing issues within my getMoviesByGenre() method, but it indeed does.

An error is thrown as follows:

TS1146: Declaration expected.
@HostListener('window:scroll', ['$event'])

A demonstration has been set up with a stackblitz link here.

Inquiries

  1. What might be the flaw in my approach?
  2. Are there any suitable substitutes for utilizing @HostListener?

Answer №1

Appreciate it, the stackblitz is now up and running smoothly. Below is the implementation code for reference.

Infinite Scroll Implementation in JS <- this article was a great help

We initiate the API call on load with page number 1. Subsequent scrolls trigger an increment of the page number, leading to another API call where new results are added to the array, creating an infinite scroll effect!

ngAfterViewInit() {
    fromEvent(window, 'scroll')
      .pipe(
        startWith(0),
        map((x) => window?.scrollY),
        distinctUntilChanged()
      )
      .subscribe((scrollPos: any) => {
        if (!this.movies?.length) {
          return;
        }
        console.log(scrollPos + window.innerHeight); //scrolled from top
        console.log(document.documentElement.scrollHeight); //visible part of screen
        if (
          Math.round(scrollPos + window.innerHeight) >=
            document.documentElement.scrollHeight &&
          this.pageNumber < this.maxPage
        ) {
          this.pageNumber++;
          this.getMoviesByPage(this.genreId, this.pageNumber);
        }
      });
  }

FULL CODE:

TS:

import { Component, EventEmitter, HostListener } from '@angular/core';
import { GenreResponse, Genre } from '../../models/Genre';
import { MovieResponse, Movie } from '../../models/Movie';
import { MovieService } from '../../services/movie-service.service';
import { ActivatedRoute } from '@angular/router';
import { distinctUntilChanged, fromEvent, map, startWith } from 'rxjs';

@Component({
  selector: 'app-movies-by-genre',
  templateUrl: './movies-by-genre.component.html',
  styleUrls: ['./movies-by-genre.component.scss'],
})
export class MoviesByGenre {
  constructor(
    private activatedRoute: ActivatedRoute,
    private movieService: MovieService
  ) {}

  public genreName: string | undefined = '';

  public movieResponse!: MovieResponse;
  public movies: Movie[] = [];
  public moviesBuffer: Movie[] | undefined = [];

  public genreResponse!: GenreResponse;
  public genres: Genre[] | undefined = [];
  scrollCount = 10;
  genreId!: number;
  maxPage: number = 10;
  pageNumber: number = 1;
  isLoading: boolean = false;
  public getMoviesByGenre(): void {
    // Get genre id (from URL parameter)
    this.genreId = Number(this.activatedRoute.snapshot.paramMap.get('id'));

    // Get genre name from genres array
    this.movieService.getAllMovieGenres().subscribe((response) => {
      this.genreResponse = response;
      this.genres = this.genreResponse.genres;

      if (this.genres && this.genres.length) {
        let currentGenre = this.genres.find(
          (genre) => genre.id === this.genreId
        );
        if (currentGenre) {
          this.genreName = currentGenre.name || '';
          this.movieService.defaultTitle = this.genreName;
        }
      }
    });
    this.getMoviesByPage(this.genreId, this.pageNumber);
  }

  getMoviesByPage(genreId: number, pageNumber: number) {
    // Get movies by genre id
    this.movieService
      .getMoviesByGenre(genreId, pageNumber)
      .subscribe((response) => {
        this.movieResponse = response;
        this.movies.push(...(this.movieResponse?.results || []));
      });
  }

  ngAfterViewInit() {
    fromEvent(window, 'scroll')
      .pipe(
        startWith(0),
        map((x) => window?.scrollY),
        distinctUntilChanged()
      )
      .subscribe((scrollPos: any) => {
        if (!this.movies?.length) {
          return;
        }
        console.log(scrollPos + window.innerHeight); //scrolled from top
        console.log(document.documentElement.scrollHeight); //visible part of screen
        if (
          Math.round(scrollPos + window.innerHeight) >=
            document.documentElement.scrollHeight &&
          this.pageNumber < this.maxPage
        ) {
          this.pageNumber++;
          this.getMoviesByPage(this.genreId, this.pageNumber);
        }
      });
  }

  ngOnInit() {
    this.activatedRoute.params.subscribe(() => {
      this.getMoviesByGenre();
    });
  }

  ngOnDestroy() {
    this.movieService.defaultTitle = '';
  }
}

HTML

<ng-container *ngIf="movieResponse">
  <div class="row grid">
    <div
      *ngFor="let movie of movies"
      class="col-xs-12 col-sm-6 col-lg-4 col-xl-3"
    >
      <app-movie-card class="movie card" [movie]="movie"></app-movie-card>
    </div>
  </div>
</ng-container>

Check out the Stackblitz Demo


To implement infinite scroll, we need to access the scrollable div using ViewChild.

@ViewChild('scroller') scroller!: ElementRef<any>;

In the ngAfterViewInit, utilize rxjs fromEvent to listen for scroll events and apply necessary logic accordingly.

ngAfterViewInit() {
    if(scroller?.nativeElement) {
        // for window or anything else
        // fromEvent(window, 'scroll')
        // for some div
        fromEvent(scroller.nativeElement, 'scroll').pipe(
            startWith(0), 
            map(x => window?.scrollY), 
            distinctUntilChanged(),
        ).subscribe((scrollPos: any) => {
            // apply logic here!
        });
    }
}

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

``Is there a specific scenario where the use of getInitialProps is recommended when automatically redirecting from one

Within my application, I have set up an auto-redirect from the root directory '/' to '/PageOne' with the following code: const Home = () => { const router = useRouter(); useEffect(() => { router.push('/pageone', ...

Can you guide me on how to specify the return type in NestJS for the Session User in a request?

async authenticated(@Req() request: Request) { const user = request.user return user } It is important for the 'user' variable to have the correct type globally. While working with express passport, I came across the following: decl ...

Displaying images dynamically in Angular using ng-repeat with URL strings

I'm working on a project where I need to dynamically call pictures inside an ng-repeat loop. Initially, I tried adding a key/value pair of 'img' to the JSON object I'm fetching and then dropping it inside the URL. However, this approach ...

How can I retrieve the line number of a code during runtime in JavaScript?

Is there a way to add a console.log statement that would indicate the line number it is on in JavaScript? For example: console.log ('Line:', [code to get the line]). The output in the console would be Line: [line number], helping me identify wher ...

Exploring jQuery: Techniques for Hovering, Changing, and Toggling Images

Currently, I am busy working on my project and I am attempting to achieve this by... I ideally want everything to be operational through click actions for each individual image, allowing them to have their unique "paper". I am aiming for a hover effect an ...

Transferring Cookies through FETCH API using a GET method from the client-side to the server-side

Struggling with a challenge here: Attempting to send a cookie via a GET request to determine if the user is logged in. The cookie is successfully transmitted to my browser and is visible in the developer tools. When I manually make a request through the UR ...

I am unsuccessful in transferring the "side-panel content" to the side panel located on the main menu page

I am facing an issue where I want to pass My left and right Panel to the main menu page (dashboard), but it is not working as expected. The problem arises because the first page that needs to be declared is the login page (/ root) in my case. If I pass it ...

Issue with serving static files in ExpressJs

I'm facing an issue with my Express app where the static files are not working properly for certain routes. For example, when I visit '/', all styles and images load correctly as expected when the index.ejs is rendered. However, when I navi ...

Updating the scope value in AngularJS with an asynchronous response is a crucial task for

I am facing an issue with sharing data between AngularJS controllers. The data is obtained through an http request, but when I try to access it in the controller, it returns null. Strangely, if I manually refresh through the UI, the data becomes available. ...

Having trouble with the jQuery each function's functionality

I am creating circular counters for surveys by generating a counter for each answer option. Currently, I am utilizing this "plugin": Issue: The problem lies in the plugin not fetching the text value from the <div> element and failing to draw coun ...

Using JavaScript regex to split text by line breaks

What is the best way to split a long string of text into individual lines? And why does this code snippet return "line1" twice? /^(.*?)$/mg.exec('line1\r\nline2\r\n'); ["line1", "line1"] By enabling the multi-line modifi ...

Choose a Range of DOM Elements

My challenge is to select a range of DOM elements, starting from element until element. This can be done in jQuery like this: (Source) $('#id').nextUntil('#id2').andSelf().add('#id2') I want to achieve the same using JavaScr ...

Dynamically insert innerHTML content into table rows using JavaScript in PHP is a fantastic way to customize

Having trouble with innerHTML in this scenario, looking to achieve something along these lines: <table width="100%" border="0" id="table2"> <?php include("connection.php"); $sql=mysql_query("select* from b1"); while($res=mys ...

An error was encountered: SyntaxError - An unexpected token '!' was found

I am having trouble creating a react cluster map. I encountered a SyntaxError, and I'm not sure what went wrong. Initially, my map was working fine, but after using the use-supercluster npm package, it started showing an Uncaught SyntaxError: Unexpect ...

Silhouettes dancing across handcrafted designs

I have been trying to create a custom geometry using vertices and faces, but I am facing an issue where the geometry does not cast shadows on itself and the faces all have the same color as it rotates. I have attempted various solutions but haven't ha ...

Looking to incorporate Functional Components in React using the package "@types/react" version "^18.0.17"? Learn how here!

With the removal of the children prop from React.FC type, what is the new approach for typing components? ...

Iterate through an array and append individual elements to a fresh array - ensuring only a single item is present in the new

I have been working on a project where I need to call a backend API and retrieve a JSON response. I have come across various solutions, but none seem to address my specific problem. The JSON data returned from the API is structured like this: [ { ...

Using Angular 4 to retrieve a dynamic array from Firebase

I have a dilemma while creating reviews for the products in my shop. I am facing an issue with the button and click event that is supposed to save the review on the database. Later, when I try to read those reviews and calculate the rating for the product, ...

Are there performance concerns associated with invoking functions in templates within Angular 2+?

I'm currently adjusting to Angular's change detection mechanism, and I'm uncertain if invoking functions in templates can impact performance. For instance, which is better: <mat-tab-group> <mat-tab label="First"> {{ getFirstT ...

Choose to either check or uncheck boxes using ReactJS

After successfully creating a function to select either single or multiple boxes, I encountered an issue with the "Select all" feature. Any suggestions on how to resolve this? (I'm utilizing the material-ui library for my checkboxes, which are essenti ...