首页 > 解决方案 > Angular - 创建通用装饰器包装 @HostListener

问题描述

我正在尝试找到一种方法来为我的一些使用 HostListener 的组件制作可重用的装饰器。

我目前拥有的几个功能(组件)非常相似,并且都具有相同的 @HostListener 块:

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

}

HostListener 方法正在调用服务中的某个函数(从后端获取数据)并更新局部变量。将相同的服务注入所有组件,并且所有组件都可以使用相同的变量。事实上 - 逻辑是精确的并且在所有这些组件中重复。

我想做的是找到一种方法来创建一个自定义装饰器,该装饰器将包装重复的 HostListener,例如:

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

如果需要,我将为这些组件创建一个接口,以声明装饰器使用的公共服务和局部变量。

关于如何实现这种装饰器的任何想法、指导或帮助?

提前致谢。

标签: angulartypescriptdecorator

解决方案


您可以使用自定义装饰器在没有 @HostListner 的情况下实现它

在线示例

如何实施

  1. 创建一个函数来实现自定义装饰器 ( WithScrollHostListener)

function WithScrollHostListener() {

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

  1. 将自定义回调扩展到角钩(请参阅装饰器函数)

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

完整代码

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


推荐阅读