专栏首页全栈修仙之路Angular 自定义属性指令

Angular 自定义属性指令

本文将使用 UltimateAngular/angular-pro-src 中的示例,来一步步介绍自定义属性指令的相关知识。在正式开发前,我们可以先看一下,最终效果 Stackblitz - Custom-Attribute-Directive

该示例中定义了两个自定义指令:

  • CreditCardDirective —— 信用卡指令,用于对输入的 16 位信用卡号码,格式化显示(每 4 位数字为一组,中间用空格符分隔)。
  • TooltipDirective —— Tooltip 指令,用于显示提示消息。

现在我们先来定义 CreditCardDirective:

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

@Directive({
  selector: '[credit-card]'
})
export class CreditCardDirective {
  constructor(private element: ElementRef) {
    console.log(this.element);
  }
}

定义完 CreditCardDirective 指令,我们需要在 AppModule 模块中声明该指令:

@NgModule({
  declarations: [
    AppComponent,
    CreditCardDirective,
  ],
})
export class AppModule {}

此后,我们就可以在模板中应用该指令了:

<label>
   Credit Card Number
   <input 
      name="credit-card" 
      type="text"
      placeholder="Enter your 16-digit card number"
      credit-card>
</label>

接下来我们来分析一下需求,对输入的数字格式化显示,即每 4 位数字为一组,中间用空格符分隔。要实现该需求,前提是我们能监听输入框的 input 事件,然后获取该输入框的值,在对输入的数字进行格式化处理。想要监听宿主元素的 input 事件,我们可以利用 Angular 提供的 HostListener 装饰器。

HostListener

HostListener 是属性装饰器,一般用来为宿主元素添加事件监听。它的定义如下:

export interface HostListenerDecorator {
  (eventName: string, args?: string[]): any;
  new (eventName: string, args?: string[]): any;
}

通过以上定义可知 HostListenerDecorator 支持两个参数:eventName 和 args。其中 eventName 用于表示事件名称,而 args 用于表示参数列表。

下面我们来看一下如何使用 HostListenerDecorator :

@Directive({
  selector: '[credit-card]'
})
export class CreditCardDirective {
  @HostListener('input', ['$event'])
  onKeyDown(event: KeyboardEvent) {
    const input = event.target as HTMLInputElement;
    //...
  }
}

需要注意的是,参数列表中 $event 这个参数,它是一个特殊的 token,用于表示事件对象。如果使用其它的名称,比如 event 的话,我们就不能正确获取事件对象。此外,除了监听宿主元素外,我们也可以监听 windowdocument 对象上的事件,如 @HostListener('document:click', ['$event'])

接下来我们来实现格式化显示的功能:

const input = event.target as HTMLInputElement;
let trimmed = input.value.replace(/\s+/g, '');
if (trimmed.length > 16) {
  trimmed = trimmed.substr(0, 16);
}

let numbers = [];
for (let i = 0; i < trimmed.length; i += 4) {
  numbers.push(trimmed.substr(i, 4));
}

input.value = numbers.join(' ');

以上代码很简单,相信大家都能看懂。这里有个问题,当用户在输入框输入非数值类型的时候,我们希望能提醒用户。最简单的方式,就是给当前输入框设置一个红色的边框。要实现这个功能,我们可以利用 HostBinding 装饰器。

HostBinding

HostBinding 是属性装饰器,用来动态设置宿主元素的属性值。它的定义如下:

export interface HostBindingDecorator {
  (hostPropertyName?: string): any;
   new (hostPropertyName?: string): any;
}

对于上述的功能,我们先要为 CreditCardDirective 指令类新增一个 border 属性,然后使用 HostBinding 装饰器,具体如下:

@HostBinding('style.border')
border: string;

在设置完属性绑定后,我们来更新一下 onKeyDown() 方法中的代码,当发现输入非数值时,为当前的输入框设置一个红色的边框:

this.border = '';
  if (/[^\d]+/.test(trimmed)) {
    this.border = '1px solid red';
}

此时,CreditCardDirective 指令的功能已基本完成,下面是完整的实现:

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

@Directive({
  selector: '[credit-card]'
})
export class CreditCardDirective {
  @HostBinding('style.border')
  border: string;

  @HostListener('input', ['$event'])
  onKeyDown(event: KeyboardEvent) {
    const input = event.target as HTMLInputElement;

    let trimmed = input.value.replace(/\s+/g, '');
    if (trimmed.length > 16) {
      trimmed = trimmed.substr(0, 16);
    }

    let numbers = [];
    for (let i = 0; i < trimmed.length; i += 4) {
      numbers.push(trimmed.substr(i, 4));
    }

    input.value = numbers.join(' ');

    this.border = '';
    if (/[^\d]+/.test(trimmed)) {
      this.border = '1px solid red';
    }
  }
}

好的,我们趁热打铁,接着开发 TooltipDirective 指令。该指令实现的功能是,当鼠标移入到指定的元素时(页面中的 ? 元素),显示我们自定义的提示消息。而当鼠标移出指定元素时,要隐藏我们自定义的提示消息。

要实现该功能的一种实现方案是,为应用 TooltipDirective 指令的宿主元素动态添加一个子元素,然后让它作为提示消息的容器,当鼠标移入到指定的元素时,显示前面动态添加的元素。

下面我们来定义 TooltipDirective 指令:

import { Input, Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[tooltip]'
})
export class TooltipDirective implements OnInit {
  constructor(
    private element: ElementRef
  ) {}
}

接着我们按照上述的方案,更新一下 TooltipDirective 指令:

import { Input, Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[tooltip]'
})
export class TooltipDirective implements OnInit {
  tooltipElement = document.createElement('div');

  @Input()
  set tooltip(value) {
    this.tooltipElement.textContent = value;
  }

  constructor(
    private element: ElementRef
  ) {}

  ngOnInit() {
    this.tooltipElement.className = 'tooltip';
    this.element.nativeElement.appendChild(this.tooltipElement);
    this.element.nativeElement.classList.add('tooltip-container');
  }
}

在上面代码中,我们定义了一个输入属性,用于接收用户自定义的提示消息,之后通过调用 DOM API 创建了一个 div 元素,然后在 ngOnInit 生命周期钩子中,执行相关的初始化操作。下面我们再来为该指令新增两个方法,用于控制新建的 div 元素的显示和隐藏:

hide() {
  this.tooltipElement.classList.remove('tooltip--active');
}

show() {
  this.tooltipElement.classList.add('tooltip--active');
}

此时我们的指令功能已基本实现,我们来看一下如何使用:

<label 
   tooltip="3 digits, back of your card"
   #myTooltip="tooltip">
   Enter your security code 
   <span>
      (?)
    </span>
    <input type="text">
</label>

以上代码正常运行后,利用开发者工具,我们能够看到手动创建的 tooltip 元素。 我们的目标是,鼠标移入 (?) 元素时,显示提示消息,而鼠标移出 (?) 元素时,隐藏提示消息。要实现这个功能,我们可以监听 span 元素的 mouseover 和 mouseout 事件,在对应的回调函数中,控制 tooltip 元素的显示和隐藏。

此时,我们的 TooltipDirective 指令,已经包含了控制 tooltip 元素显示和隐藏的方法。那么现在的问题是,我们要如何访问 TooltipDirective 指令的实例。针对这个问题,我们可以在定义指令时,设置 exportAs 属性:

@Directive({
  selector: '[tooltip]',
  exportAs: 'tooltip'
})

之后,我们就可以在模板中,引用该指令对象,具体如下:

<label 
   tooltip="3 digits, back of your card"
   #myTooltip="tooltip">
   Enter your security code 
   <span
     (mouseover)="myTooltip.show()"
     (mouseout)="myTooltip.hide()">
     (?)
   </span>
   <input type="text">
</label>

至此,我们的所有功能就开发完了,以下是 TooltipDirective 指令的完整实现:

import { Input, Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[tooltip]',
  exportAs: 'tooltip'
})
export class TooltipDirective implements OnInit {
  tooltipElement = document.createElement('div');

  @Input()
  set tooltip(value) {
    this.tooltipElement.textContent = value;
  }

  hide() {
    this.tooltipElement.classList.remove('tooltip--active');
  }

  show() {
    this.tooltipElement.classList.add('tooltip--active');
  }

  constructor(
    private element: ElementRef
  ) {}

  ngOnInit() {
    this.tooltipElement.className = 'tooltip';
    this.element.nativeElement.appendChild(this.tooltipElement);
    this.element.nativeElement.classList.add('tooltip-container');
  }
}

事实上在 Angular 表单模块中,也大量使用了 exportAs 属性,比如 ngModel、ngForm、ngModelGroup 及 formControl 指令等。本文通过 CreditCardDirective 和 TooltipDirective 两个指令,介绍了 Angular 自定义属性指令所涉及的相关的基础知识,若想继续深入学习的话,可以阅读 Angular 的官方文档或参考 Github 相关的开源项目。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • TypeScript 函数中的 this 参数

    从 TypeScript 2.0 开始,在函数和方法中我们可以声明 this 的类型,实际使用起来也很简单,比如:

    阿宝哥
  • 你不知道的 Blob

    如果你允许用户从你的网站上下载某些文件,那你可能会遇到 Blob 类型。为了实现上述的功能,你可以很容易从网上找到相关的示例,并根据实际需求进行适当的调整。对于...

    阿宝哥
  • 你不知道的 WeakMap

    相信很多读者对 ES6 引入的 Map 已经不陌生了,其中的一部分读者可能也听说过 WeakMap。既生 Map 何生 WeakMap?带着这个问题,本文将围绕...

    阿宝哥
  • c#透明panel

    冰封一夏
  • 浅谈CGLIB动态代理和JDK动态代理 学习笔记

    用户2032165
  • Spring中@Import的各种用法以及ImportAware接口

    @Import注解提供了和XML中<import/>元素等价的功能,实现导入的一个或多个配置类。@Import即可以在类上使用,也可以作为元注解使用。

    用户1516716
  • 七夕最污代码,单身慎入

    2.找不到对象说爱(Fatal error: Call to a member function on a non-object), 他怎么才能说出爱?

    后端技术探索
  • vscode源码分析【八】加载第一个画面

    先复习一下! 在第一节中,我们提到: app.ts(src\vs\code\electron-main\app.ts)的openFirstWindow方法...

    liulun
  • Spring中@Import的各种用法以及ImportAware接口

    @Import注解提供了和XML中<import/>元素等价的功能,实现导入的一个或多个配置类。@Import即可以在类上使用,也可以作为元注解使用。

    Coder小黑
  • (十)c#Winform自定义控件-横向列表

    GitHub:https://github.com/kwwwvagaa/NetWinformControl

    冰封一夏

扫码关注云+社区

领取腾讯云代金券