是否只能通过手动调用ngOnChanges来为像NgxMaskDirective这样的宿主指令配置动态输入?

前端开发 2026-07-10

我在用Angular 21构建一个可复用的掩码指令,它把 NgxMaskDirective 作为宿主指令来包装。目标是为常见的巴西文档和格式化模式(CPF、CNPJ、电话号码、邮编等)提供命名掩码,同时仍允许将原始掩码字符串作为回退传入。

Here's what I have so far:

export type NamedMask = 'cnpj' | 'cpf' | 'phone' | 'cnpj-cpf' | 'zip-code';

const MASKS: Record<NamedMask, string> = {
  cnpj: '00.000.000/0000-00',           // Brazilian company registry number
  cpf: '000.000.000-00',                // Brazilian individual taxpayer number
  phone: '(00) 0000-0000||(00) 00000-0000', // 8 or 9-digit phone numbers
  'cnpj-cpf': '000.000.000-00||00.000.000/0000-00', // accepts both CPF and CNPJ
  'zip-code': '00000-000',              // Brazilian ZIP code (CEP)
};

@Directive({
  selector: '[mascara]',
  hostDirectives: [
    {
      directive: NgxMaskDirective,
      inputs: ['patterns', 'prefix', 'suffix'],
    },
  ],
})
export class MaskDirective {
  readonly mascara = input.required<NamedMask | string>();

  private readonly maskDir = inject(NgxMaskDirective);

  private readonly resolvedMask = computed(() =>
    MASKS[this.mascara() as NamedMask] ?? this.mascara()
  );

  constructor() {
    effect(() => {
      this.maskDir.ngOnChanges({
        mask: new SimpleChange(undefined, this.resolvedMask(), true),
      });
    });
  }
}

问题: NgxMaskDirectivemascaradropSpecialCharactersclearIfNotMatch 公开为标准的 @Input() 属性,但由于我使用的是 hostDirectives,因此只能转发在 inputs 数组中明确列出的输入。这三个没有被列出,因为我需要拦截并转换它们(特别是 mascara,它把友好名称映射到实际的模式字符串)。

唯一可以在解析完毕后将值推送到宿主指令的办法,是用一个 SimpleChange 对象手动调用 ngOnChanges(),这感觉像是在滥用内部API。

我的问题:

  1. 在这个场景中,对注入的宿主指令手动调用 ngOnChanges() 是预期/推荐的做法吗,还是被视为投机取巧?
  2. 是否存在更干净的Angular 19 API(基于signals的,或其他)可以在不经过 ngOnChanges() 的情况下,将解析/转换后的值传递给宿主指令的输入?
  3. 在这里是否更好完全避免使用 hostDirectives,改为以编程方式应用 NgxMaskDirective,还是直接复刻其逻辑?

解决方案

与其把 mascara 作为宿主指令的输入进行转发。

我们可以直接设置 _maskInternal(私有且内部的属性)(WORKAROUND!)来配置指令使用的掩码。

@Directive({
  selector: '[mascara]',
  hostDirectives: [
    {
      directive: NgxMaskDirective,
      inputs: ['patterns', 'prefix', 'suffix'],
    },
  ],
})
export class MaskDirective {
  readonly mascara = input.required<NamedMask | string>();

  private readonly maskDir = inject(NgxMaskDirective);

  private readonly resolvedMask = computed(
    () => MASKS[this.mascara() as NamedMask] ?? this.mascara()
  );

  constructor() {
    effect(() => {
      this.maskDir['_maskValue'].set(this.resolvedMask()); // set the internal private property
      this.maskDir._maskService.maskExpression = this.maskDir['_maskValue'](); // set the service property also.
    });
  }
}

你的代码也做了同样的事情,但这并不使用事件钩子。

完整代码:

import {
  Component,
  computed,
  Directive,
  effect,
  inject,
  input,
  SimpleChange,
} from '@angular/core';
import { NgxMaskDirective } from 'ngx-mask';

export type NamedMask = 'cnpj' | 'cpf' | 'phone' | 'cnpj-cpf' | 'zip-code';

const MASKS: Record<NamedMask, string> = {
  cnpj: '00.000.000/0000-00', // Brazilian company registry number
  cpf: '000.000.000-00', // Brazilian individual taxpayer number
  phone: '(00) 0000-0000||(00) 00000-0000', // 8 or 9-digit phone numbers
  'cnpj-cpf': '000.000.000-00||00.000.000/0000-00', // accepts both CPF and CNPJ
  'zip-code': '00000-000', // Brazilian ZIP code (CEP)
};

@Directive({
  selector: '[mascara]',
  hostDirectives: [
    {
      directive: NgxMaskDirective,
      inputs: ['patterns', 'prefix', 'suffix'],
    },
  ],
})
export class MaskDirective {
  readonly mascara = input.required<NamedMask | string>();

  private readonly maskDir = inject(NgxMaskDirective);

  private readonly resolvedMask = computed(
    () => MASKS[this.mascara() as NamedMask] ?? this.mascara()
  );

  constructor() {
    effect(() => {
      this.maskDir['_maskValue'].set(this.resolvedMask()); // set the internal private property
      this.maskDir._maskService.maskExpression = this.maskDir['_maskValue'](); // set the service property also.
    });
  }
}
@Component({
  selector: 'app-root',
  imports: [MaskDirective],
  template: `
    <div>
      <label>Number (00) 0000 00</label>
      <input
        type="text"
        prefix="+963 "
        mask="00 0000 000"
        mascara="cnpj"
      />
    </div>
  `,
})
export class App {}

Stackblitz Demo

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章