ET-CS
ET-CS

Reputation: 7200

Angular - Create common decorator wrapping @HostListener

I'm trying to find a way to make a reusable decorator for some of my components that are using HostListener.

What I have currently are several features (components) That are very similiar and all having the same @HostListener block:

@Component({...})
export class MySearchComponent implements OnInit, OnDestroy {

   @HostListener('window:scroll', [])
   onScroll(): void {
      this.loading = true;
      this.commonService.getData(
         this.tab,
         this.query,
         ...
      ).subscribe(results => {
         this.results = results;
         this.loading = false;
      })
   }

}

The HostListener method is calling some function in a service (to get data from backend) and updates a local variables. The same service is injected to all components and the same variables is available in all of them. Infact - the logic is EXACT and repeated in all of those components.

What I would like to do, is to find a way to create a custom decorator that will wrap the repeated HostListener, such as:

@Component({...})
@WithScrollHostListener()
export class MySearchComponent implements OnInit, OnDestroy {
}

If required, I will create an interface for those components to declare of the common service and local variable to be used by the decorator.

Any idea, guidance or assistance on how to implement such decorator?

Thanks in advance.

Upvotes: 3

Views: 1364

Answers (1)

Chunbin Li
Chunbin Li

Reputation: 2244

You can use custom decorator to implement it without @HostListner

online example

How to implement it

  1. create a function to implement custom decorator (WithScrollHostListener)

function WithScrollHostListener() {

  return function decorator(constructor) {
  
     ...
  }
  
 }

  1. extend custom callback into the angular hook ( see decorator function )

function WithScrollHostListener() {

  // required
  function extendHook(arg: {
    hookName: string;
    target: {
      prototype;
    };
    fn: (hookArg: { componentInstance }) => void;
  }) {
    const original = arg.target.prototype[arg.hookName];

    arg.target.prototype[arg.hookName] = function(...args) {
      arg.fn({
        componentInstance: this
      });
      original && original.apply(this, args);
    };
  }
 
  // required
  return function decorator(constructor) {
    extendHook({
      // hook's name according to you (e.x. ngOnInit , ngAfterViewInit)
      hookName: "ngOnInit",
      target: constructor,
      // setup your custom logic
      fn: hookArg => {
        window.addEventListener("scroll", () =>
          scrollFn({
            commonComponent: hookArg.componentInstance
          })
        );
      }
    });
  };
  
 }

Full codes

import { Component, Injectable } from "@angular/core";
import { CommonService } from "./common.service";

// optional (shared the same structure with other component)
export interface CommonComponent {
  loading?;
  tab?;
  query?;
  results?;

  commonService?: CommonService;
}

function WithScrollHostListener() {
  // custom logic
  function scrollFn(arg: { commonComponent: CommonComponent }) {
    arg.commonComponent.loading = true;
    arg.commonComponent.commonService
      .getData(arg.commonComponent.tab, arg.commonComponent.query)
      .subscribe(results => {
        console.log(results);
        arg.commonComponent.results = results;
        arg.commonComponent.loading = false;
      });
  }

  // required
  function extendHook(arg: {
    hookName: string;
    target: {
      prototype;
    };
    fn: (hookArg: { componentInstance }) => void;
  }) {
    const original = arg.target.prototype[arg.hookName];

    arg.target.prototype[arg.hookName] = function(...args) {
      arg.fn({
        componentInstance: this
      });
      original && original.apply(this, args);
    };
  }
 
  // required
  return function decorator(constructor) {
    extendHook({
      // hook's name according to you (e.x. ngOnInit , ngAfterViewInit)
      hookName: "ngOnInit",
      target: constructor,
      // setup your custom logic
      fn: hookArg => {
        window.addEventListener("scroll", () =>
          scrollFn({
            commonComponent: hookArg.componentInstance
          })
        );
      }
    });
  };
}

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
@WithScrollHostListener()
export class AppComponent implements CommonComponent {
  constructor(public commonService: CommonService) {
  }
}

Upvotes: 2

Related Questions