在 Angular 中,从自定义表单组件获得 FormControl 的访问权限

78

我在我的Angular应用程序中有一个自定义表单控件组件,它实现了ControlValueAccessor接口。

但是,我想要访问与我的组件相关联的FormControl实例。我正在使用具有FormBuilder的响应式表单,并使用formControlName属性提供表单控件。

那么,我如何从我的自定义表单组件内部访问FormControl实例?

6个回答

71

这个解决方案来源于 Angular 仓库中的讨论。如果您对这个问题感兴趣,请务必阅读它,甚至最好参与其中。


我研究了 FormControlName 指令的代码,并受到启发编写了以下解决方案:

@Component({
  selector: 'my-custom-form-component',
  templateUrl: './custom-form-component.html',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: CustomFormComponent,
    multi: true
  }]
})
export class CustomFormComponent implements ControlValueAccessor, OnInit {

  @Input() formControlName: string;

  private control: AbstractControl;


  constructor (
    @Optional() @Host() @SkipSelf()
    private controlContainer: ControlContainer
  ) {
  }


  ngOnInit () {

    if (this.controlContainer) {
      if (this.formControlName) {
        this.control = this.controlContainer.control.get(this.formControlName);
      } else {
        console.warn('Missing FormControlName directive from host element of the component');
      }
    } else {
      console.warn('Can\'t find parent FormGroup directive');
    }

  }

}

我将父FormGroup注入到组件中,然后使用通过formControlName绑定获取的控件名称从中获取特定的FormControl

但是,请注意,此解决方案专门针对在宿主元素上使用FormControlName指令的用例进行了定制。在其他情况下,它将无法正常工作。为此,您需要添加一些额外的逻辑。如果您认为这应该由Angular解决,请确保访问讨论


7
可以不用将父表单组注入,然后再使用formControllerName输入绑定来访问控件,而是直接将表单控件作为输入绑定传递吗?我也有类似的需求,希望能够从自定义表单组件中访问表单控件,我通过将该表单控件作为输入绑定传递给自定义表单组件来实现。 - Ritesh Waghela
2
任何对此印象深刻的人都应该在这里查看 https://material.angular.io/guide/creating-a-custom-form-field-control#-code-ngcontrol-code- - Joey Gough
2
在我看来,在控件中发送控件本身是没有意义的。当然,你可以发送控件并对其进行任何操作...(从这个答案中你不需要在构造函数中添加任何内容)。问题的精神在于询问如何从内部访问正在自定义的控件。这应该是可行的。 - Ben Racicot
对于任何在2020年及以后来到这里的人,请查看@rui下面的低分答案,这个相关的问题,以及从该问题链接的这篇文章。注入FormControl而不是使用NG_VALUE_ACCESSOR提供程序可能是最稳健的解决方案。 - Coderer
我正在尝试使用ngModel来完成相同的操作,但是对于表单而言该怎么做呢?有什么想法可以让它正常工作吗?当在模板表单中注入ControlContainer时,我无法调用controlContainer.control.get,因为control为空。 - Davy
显示剩余2条评论

63

当通过[formControl]指令进行绑定时,使用formControlName作为输入参数是无效的。

这里有一个解决方案,既不需要任何输入参数,又可以双向工作。

export class MyComponent implements AfterViewInit {

  private control: FormControl;

  constructor(
    private injector: Injector,
  ) { }

  // The form control is only set after initialization
  ngAfterViewInit(): void {
    const ngControl: NgControl = this.injector.get(NgControl, null);
    if (ngControl) {
      this.control = ngControl.control as FormControl;
    } else {
      // Component is missing form control binding
    }
  }
}


5
这个解决方案绝对比已接受的答案更好。 - Denys Avilov
尽管这个解决方案对我也非常有效,而且似乎比被接受的方案更优雅,在Angular 7中,我遇到了这个tslint错误:get is deprecated: from v4.0.0 use Type<T> or InjectionToken<T> - AsGoodAsItGets
1
尝试使用const ngControl = this.injector.get<NgControl>(NgControl as Type<NgControl>); - Charles
@AdamMichalski 没错。在这种情况下,最好在 ngOnChanges 生命周期钩子中处理它。这将防止过时的引用继续存在。 - Randy
1
ngAfterViewInit() 给我带来了不一致的状态错误,但将其更改为 ngOnInit() 就像魔法一样奏效!非常感谢,比被接受的答案好多了。 - Alan Sereb
显示剩余2条评论

23

基于之前的回答和在评论中找到的文档,以下是我认为最干净的解决方案,用于基于ControlValueAccessor的组件。

// No FormControl is passed as input to MyComponent
<my-component formControlName="myField"></my-component>
export class MyComponent implements AfterViewInit, ControlValueAccessor  {

  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (ngControl != null) {
      // Setting the value accessor directly (instead of using
      // the providers) to avoid running into a circular import.
      ngControl.valueAccessor = this;
    }
  }

    ngAfterContentInit(): void {
       const control = this.ngControl && this.ngControl.control;
       if (control) {
          // FormControl should be available here
       }
    }
}
请注意,使用此解决方案时,您无需在组件上指定NG_VALUE_ACCESSOR提供程序,因为这将导致CI循环依赖。构造函数将正确设置valueAccessor。

5
好的解决方案。你可能需要在组件声明中删除provide: NG_VALUE_ACCESSOR,以避免循环依赖。请参见:https://material.angular.io/guide/creating-a-custom-form-field-control#ngcontrol - crashbus
我刚刚意识到,使用NG_VALUE_ACCESSOR在提供程序中创建自定义组件并不是正确的方法...我想知道为什么这种方法被推荐到各个地方。我错过了什么?有什么陷阱吗? - Jette
1
@Jette 注意,这个问题已经在将近3年前得到了回答。不确定现在是否有更好的方法,因为Angular不断改进,而我目前并没有积极使用这个框架。 - Rui Marques
@RuiMarques 很好的观点...我会阅读相关资料的。 - Jette

4

正如@Ritesh在评论中已经写过的,您可以将表单控件作为输入绑定传递:

<my-custom-form-component [control]="myForm.get('myField')" formControlName="myField">
</my-custom-form-component>

然后你可以在自定义表单组件内通过以下方式获取表单控件实例:

@Input() control: FormControl;

4
这是一个被简化和优化的答案,适用于FormControlName和FormControl输入:
export class CustomFormComponent implements ControlValueAccessor, OnInit {

  @Input() formControl: FormControl;

  @Input() formControlName: string;

  // get ahold of FormControl instance no matter formControl or formControlName is given.
  // If formControlName is given, then controlContainer.control is the parent FormGroup/FormArray instance.
  get control() {
    return this.formControl || this.controlContainer.control.get(this.formControlName);
  }

  constructor(private controlContainer: ControlContainer) { }
}

0
你可以创建自定义指令(这是一个模板驱动表单的示例,但你可以注入FormControlName而不是NgModel,并且如果需要,可以将选择器更改为formControlName):
@Directive({
    selector: '[ngModel]'
})
export class SetNgModelDirective {
    constructor(@Inject(NgModel) ngModel: NgModel, @Optional() @Inject(NG_VALUE_ACCESSOR) accessors: ControlValueAccessor[]) {
        if (accessors) {
            for (const accessor of accessors) {
                if (typeof accessor.setNgModel === 'function') {
                    accessor.setNgModel(ngModel);
                }
            }
        }
    }
}

export class MyComponent implements ControlValueAccessor  {
    setNgModel(ngModel: NgModel): void {
       const control = this.ngModel.control;
       // ...
    }
}

任何其他方法最终都会导致DI循环依赖错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接