如何从实现ControlValueAccessor的组件中获取对FormControl的引用?

7
我创建了一个源自于这篇博客文章的Angular组件。我将其放在一个响应式表单中,我想要获取表单控件上的错误信息,组件本身将呈现一个样式化的错误消息,当控件有错误时会渲染此消息。然而,当我尝试将NgControl类注入到组件中时,我遇到了循环引用问题,那么我该如何访问控件上的错误信息呢?
以下是当前的代码,它还不完整,但应该能够基本说明我想要实现的内容:
import { Component, Output, EventEmitter, Input, forwardRef } from '@angular/core';
import {
    NgControl,
    NG_VALUE_ACCESSOR,
    ControlValueAccessor,
    Validator,
    AbstractControl,
    FormControl,
    NG_VALIDATORS
} from '@angular/forms';

@Component({
    selector: 'form-field-input',
    templateUrl: './form-field-input.component.html',
    styleUrls: ['./form-field-input.component.less'],
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => FormFieldInputComponent),
        multi: true
    }]
})
export class FormFieldInputComponent implements ControlValueAccessor {

    private propagateChange = (_: any) => { };
    private propagateTouch = (_: any) => { };

    @Input('label') label: string;
    @Input('type') type: string;
    @Input('id') id: string;
    @Input('formControlName') formControlName: string;
    @Input('error') error: string;
    @Input('classes') classes: any;

    private value: string;
    private data: any;

    constructor() {
        debugger;
    }

    private onChange(event) {
        this.data = event.target.value;
        this.propagateChange(this.data);
        this.propagateTouch(this.data);
    }

    writeValue(obj: any): void {
        this.data = obj;
    }
    registerOnChange(fn: any): void {
        this.propagateChange = fn;
    }
    registerOnTouched(fn: any): void {
        this.propagateTouch = fn;
    }
}

模板文件:

<div class="form-field-input-component">
    <input id="{{id}}"
           type="{{type}}"
           class="form-field-input"
           [value]="data"
           (change)="onChange($event)"
           (keyup)="onChange($event)" />
    <span class="context-icon fa fa-lock"></span>
    <span class="info-icon fa fa-info-circle"></span>
    <!-- I will have an NGIF here to check for errors before rendering the error -->
    <div class="form-error">
        {{ error }}
    </div>
</div>
2个回答

1
你可以像这样将ngControl注入到自定义输入元素中,

(ts文件)

import {NgControl} from "@angular/forms";

...

constructor(public ngControl: NgControl) {}

(模板)

<label>Custom Input</label>
<input [formControl]="ngControl.control"/>

然后在父组件中像这样调用它:
<custom-input
  [formControl]="youControlHere"
  ngDefualtControl> <!-- This is the part that gives the parent control -->
</custom-input>

如果您设置正确,它应该像普通输入一样使用formControl!希望对您有所帮助。

我遇到了循环依赖错误: ERROR in Cannot instantiate cyclic dependency! NgControl但是我也使用了一个指令。 - JoCa
@JoCa 我不确定你的错误,你是否尝试在非指令中执行并得到相同的错误? - Aaron Turkel

1
我希望通过某种依赖注入或声明式的方式来实现这一点。由于我找不到使用这些方法的任何内容,因此我将分享如何为我的情况修复此问题。
我只需将formGroup作为组件的输入参数添加,以及传递的formControlName,即可获取对控件的引用。
这是我的组件最终结果。
//Typescript code file for component
/// ... necessary imports
@Component({
    selector: 'form-field-input',
    templateUrl: './form-field-input.component.html',
    styleUrls: ['./form-field-input.component.less'],
    providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => FormFieldInputComponent),
        multi: true
    }]
})
export class FormFieldInputComponent implements ControlValueAccessor {

    private propagateChange = (_: any) => { };
    private propagateTouch = (_: any) => { };

    @Input('label') label: string;
    @Input('type') type: string;
    @Input('id') id: string;
    @Input('contextIconName') contextIconName: string;

    //Here I take in both the parent form and the form control name
    //in ngOnInit I throw if there is no parent form passed
    @Input('formControlName') formControlName: string;
    @Input('parentForm') parentForm: FormGroup;

    @Input('classes') classes: any;
    @Input('errorDefs') errorDefs: any;

    private error: string;
    private value: string;
    private data: any;
    private control: AbstractControl;

    constructor() {}

    ngOnInit() {
        if (!this.parentForm) {
            throw "Form Field input component must be a part of a form group"
        }

        //It ain't pretty but here we get access to the control and all of it's errors
        this.control = this.parentForm.get(this.formControlName);
        if (!this.control) {
            throw "Form Field input component must be a part of a form group"
        }
    }

    private setError() {
        if (this.errorDefs && this.control.errors) {
            var errorKeys = Object.keys(this.control.errors).filter(x => !!x);
            if (errorKeys) {
                var errorKey = errorKeys[0];
                var error = this.errorDefs[errorKey] || null;
                this.error = error;
                return;
            }
        }
        this.error = null;
    }

    //Now on our on change event we can propagate the events 
    //To the registered handlers, which should set the form field errors
    //and at the end we can check the reference to the control for those errors
    //so that we can display the appropriate messages
    private onChange(event) {
        this.data = event ? event.target.value : this.data;
        this.propagateChange(this.data);
        this.propagateTouch(this.data);
        this.setError();
    }

    writeValue(obj: any): void {
        this.data = obj;
    }
    registerOnChange(fn: any): void {
        this.propagateChange = fn;
    }
    registerOnTouched(fn: any): void {
        this.propagateTouch = fn;
    }
}

//HTML template file
<div class="form-field-input-component">
    <input id="{{id}}"
           type="{{type}}"
           class="form-field-input {{class}}"
           [value]="data"
           (change)="onChange($event)"
           (keyup)="onChange($event)"
           (blur)="onChange($event)" />
    <span class="context-icon fa {{contextIconName || 'fa-cog'}}"></span>
    <span class="info-icon fa fa-info-circle" *ngIf="error"></span>
    <div class="form-field-error" *ngIf="error">
        {{ error }}
    </div>
</div>

//EXAMPLE USAGE:
<form novalidate [formGroup]="myFormGroup">
    <form-field-input
                      formControlName="firstName"
                      [parentForm]="myFormGroup"

                      <!-- example: When the Validators.required sets it's error message we can map that to a user friendly error -->
                      [errorDefs]="{
                           'required': 'this field is required' 
                      }"
                      <!-- Other inputs and stuff-->
                      >
    </form-field-input>
</form>

5
如果您将 formGroup 传递给自定义控件,为什么需要 ControlValueAccessor 和所有这些用于自定义控件的实现呢? 只需将 FormGroup 的引用传递给自定义控件模板的父级 div,然后就完成了。不需要任何 ControlValueAccessor 和实现。 - Sanjeet A
我知道我来晚了,但是为什么你想把FormControl作为输入传递呢?你可以将整个逻辑封装在子组件下,并使用ControlValueAccessor接口。在这种方法中,父组件不知道控件的内部细节,只知道数据,而控件管理(验证等)只在一个地方进行——子组件。 - refaelio

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