如何使用 ControlValueAccessor Angular 在自定义输入中使用指令

Posted

技术标签:

【中文标题】如何使用 ControlValueAccessor Angular 在自定义输入中使用指令【英文标题】:How To Use A Directive In A Custom Input With ControlValueAccessor Angular 【发布时间】:2020-06-03 08:26:03 【问题描述】:

我已经在我的 Angular 应用程序中使用 ControlValueAccessor 创建了一个简单的自定义 input 组件。所以,当我想创建一个表单input元素时,我不必调用<input />,只需调用<my-input>即可。

我有一个问题,当我使用<input />时,我可以使用myDirective。例如:

<input type="text" class="form-control" formControlName="name" myDirective />

但是,当我使用my-input 时,我不能使用myDirective。例如:

<my-input formControlName="name" myDirective></my-input>

myDirective 在我的输入中不起作用

这是my-input组件使用ControlValueAccessor代码:

import  Component, forwardRef  from '@angular/core';
import  NG_VALUE_ACCESSOR, ControlValueAccessor  from '@angular/forms';

@Component(
  selector: 'my-input',
  templateUrl: './my-input.component.html',
  styleUrls: ['./my-input.component.scss'],
  providers: [
    
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(()  => MyInputComponent ),
      multi: true
    
  ]
)

export class MyInputComponent implements ControlValueAccessor 

  onChange: () => void;
  onTouched: () => void;

  value: string;

  writeValue(value: string): void 
    this.value = value ? value : '';
  

  registerOnChange(fn: any): void 
    this.onChange = fn;
  

  registerOnTouched(fn: any): void 
    this.onTouched = fn;
  

更新: myDirective 代码:

import  Directive, HostListener  from '@angular/core';
import  FormControlName  from '@angular/forms';

@Directive(
    selector: '[myDirective]'
)

export class MyDirective
    constructor(private formControlName: FormControlName)  

    @HostListener('input', ['$event']) 
    onInputChange() 
        this.formControlName.control.setValue(this.formControlName.value.replace(/[^0-9]/g, ''));
    

myDirective 有没有办法在my-input 组件中使用?

提前致谢。

【问题讨论】:

发布myDirective 实施 我之前被添加到我的问题中:&lt;my-input formControlName="name" myDirective&gt;&lt;/my-input&gt; 【参考方案1】:

您的指令有问题。注入一个NgControl 并控制这个ngControl

export class MyDirective
    constructor(private control: NgControl)   //<--inject NgControl

    @HostListener('input', ['$event']) 
    onInputChange() 
        this.control.control.setValue(this.control.value.replace(/[^0-9]/g, ''));
    

你可以在stackblitz看到

注意:不要忘记包含在模块声明中

@NgModule(
  imports:      [ BrowserModule, FormsModule,ReactiveFormsModule ],
  declarations: [ AppComponent, MyInputComponent,MyDirective ],
  bootstrap:    [ AppComponent ]
)
export class AppModule  

【讨论】:

感谢您的回复,非常感谢,但我使用的是formGroupformControlName Titus,该指令应用于 NgControl(NgControl 是带有 [(NgModel)]、[formControl] or formControlName 的输入)。在 stackblitz 中,我使用 formControl 来不创建 formGroup,但如果您有 formGroup,该指令也可以使用 works 因为我一直在尝试上面的代码,并将 myDirective 放入 my-input 选择器:&lt;my-input formControlName="name" myDirective&gt;&lt;/my-input&gt; 但它并没有改变值。但我敢肯定,从你的代码来看,它是有效的,这就是你回答我的问题的原因,我明白了,它是有效的。但在我的,使用formGroupformControlName,它不能改变值。但感谢您的回复。提前致谢。 我使用 FormGroup 更新了 stackblitz。更改自定义表单控件中的值时是否忘记调用 onChange(value)? 当然,这就是我在上面的问题中所说的原因。请检查一下。它在input 中工作,但不在my-input 中。【参考方案2】:

您不能直接使用,但可以使用指令查询进行一些 Hack

根据 Angular Directives 文档:

将类标记为 Angular 指令的装饰器。您可以定义自己的指令来将自定义行为附加到 DOM 中的元素。

装饰器是附加到 DOM 元素的行为,因此它会影响使用该指令的元素。

但是(出于某种原因,这是一个很大的 BUT - 检查说明),您可以使用指令查询来破解此行为。

但是记住你必须定义特定的指令,这些指令只适用于特定的查询,不能用于任何 DOM 元素。

解决方案

解决方案基于以下几点:

我想定义一个适用于子元素而非元素本身的指令。

您可以使用@ContentChildren 和 QueryList 来检查您是否有一些具有特定指令的孩子。

例如,我有一个背景指令应用于输入父级和一个 CustomFormControlDirective 来查询子级:

import 
  Directive,
  ElementRef,
  Input,
  ContentChildren,
  ViewChildren,
  QueryList
 from "@angular/core";

@Directive(
  selector: "[customformcontrol]"
)
export class CustomFormControlDirective 
  constructor(public elementRef: ElementRef) 


@Directive(
  selector: "[backgroundColor]",
  queries: 
    contentChildren: new ContentChildren(CustomFormControlDirective),
    viewChildren: new ViewChildren(CustomFormControlDirective)
  
)
export class BackgroundColorDirective 
  @Input()
  set backgroundColor(color: string) 
    this.elementRef.nativeElement.style.backgroundColor = color;
  

  @ContentChildren(CustomFormControlDirective,  descendants: true )
  contentChildren: QueryList<CustomFormControlDirective>;
  viewChildren: QueryList<CustomFormControlDirective>;

  constructor(private elementRef: ElementRef) 

  ngAfterContentInit() 
    // contentChildren is set
    console.log("%o", this.contentChildren);
  

[...]
<div backgroundColor="red">
  <input customformcontrol />
</div>

当然,这个指令会将 bg 颜色应用到父 div:

那么我们如何设置这个属性给孩子呢?

我们的 contentChildren 变量中有孩子:

所以我们需要为子元素应用所需的背景,我们可以遍历查询结果并尝试应用样式:

  [...]
  _backgroundColor = null;
  @Input()
  set backgroundColor(color: string) 
    /// save the bg color instead of apply style
    this._backgroundColor = color;
  
  ngAfterContentInit() 
    if (this.contentChildren.length) 
      /// then loop through childrens to apply style
      this.contentChildren.forEach(customFormControl => 
        customFormControl.elementRef.nativeElement.style.backgroundColor = this._backgroundColor;
      );
    
  
  [...]

它会将样式应用于儿童:

如果有超过 1 个孩子:

注意事项

这是一个示例,请勿按原样执行此实现,您可以定义自己的方法或使用更好的方法,但请尝试了解 QueryList 选择器和 ContentChildren。 可能不需要使用自定义指令来获取子项,您可以直接使用 ReactiveForm 指令(AbstractControlDirective / FormControlDirective),但我认为它们不会让您访问 DOM,因为它是私有的。 这些指令仅适用于儿童,因此您可以选择更好的命名约定(例如 ApplyToControlBackgroundDirective)

【讨论】:

请记住,此解决方案适用于任意嵌套元素,不仅适用于 Reactive Forms 元素

以上是关于如何使用 ControlValueAccessor Angular 在自定义输入中使用指令的主要内容,如果未能解决你的问题,请参考以下文章

嵌套 ControlValueAccessor 的角度验证状态未在父级中正确传播,如何实现此验证?

在 Angular 中使用 ControlValueAccessor 继承验证

带有 ControlValueAccessor 测试的 Angular 2(4) 组件

带有 ControlValueAccessor 和 formControlName 的 Angular Material Datepicker [重复]

Angular 7 - ControlValueAccessor - 修剪绑定到表单的输入值

带有表单组的子 ControlValueAccessor 组件 - 检查后表达式已更改