0

我正在处理 Angular 中的一个棘手问题。我想创建一个自定义表单组件,它“捆绑”了几个子输入元素,并且在所有子元素都有效时有效。

我以为我只需创建一个实现 ControlValueAccessor 和 Validator 接口的组件。我会将 ViewChildren 装饰器的输入作为 NgModel 注入,这样我就可以遍历它们并在组件的 validate 方法中收集任何验证错误。这是我制作的地址输入组件的示例实现,它对两个都需要一个值的输入(街道和街道号码)执行此操作:

import { AfterViewInit, Component, forwardRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NgModel, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { Address } from './address';

@Component({
    selector: `address-input`,
    template: `
        <label for="street">Street</label>
        <input id="street" name="street" type="text" [(ngModel)]="Street" (blur)="onTouchedAField()" required /><br />
        <label for="number">Number</label>
        <input id="number" name="number" type="number" [(ngModel)]="Number" (blur)="onTouchedAField()" required />`,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: AddressInputComponent,
            multi: true,
        },
        {
            provide: NG_VALIDATORS,
            useExisting: AddressInputComponent,
            multi: true,
        },
    ],
})
export class AddressInputComponent implements ControlValueAccessor, Validator {
    @ViewChildren(NgModel) public validatedFields!: QueryList<NgModel>;

    private address: Address = new Address(null, null);

    private onFormCtrlChanged!: (_: any) => void;
    private onFormCtrlTouched!: (_: any) => void;

    public get Street(): string | null {
        if (this.address) {
            return this.address.Street;
        } else {
            return null;
        }
    }
    public set Street(val: string) {
      this.address = new Address(val, this.address.Number);

        this.emitChanged();
    }

    public get Number(): number | null {
        if (this.address) {
            return this.address.Number;
        } else {
            return null;
        }
    }
    public set Number(val: number | null) {
        this.address = new Address(this.address.Street, val);

        this.emitChanged();
    }

    public onTouchedAField(): void {
        this.emitTouched();
    }

    public writeValue(newAddress: Address): void {
        if (newAddress !== undefined) {
            this.address = newAddress;
        }
    }

    public registerOnChange(fn: (_: any) => void): void {
        this.onFormCtrlChanged = fn;
    }

    public registerOnTouched(fn: (_: any) => void): void {
        this.onFormCtrlTouched = fn;
    }

    public validate(control: AbstractControl): ValidationErrors | null {
        let validationErrors: ValidationErrors | null = null;
        this.validatedFields.forEach((ngm: NgModel) => {
            console.log(`ValidationErrs for ${ngm.name} with value ${ngm.value}`, ngm.errors);
            if (ngm.errors !== null) {
                if (validationErrors === null)
                    validationErrors = {};
                validationErrors[ngm.name] = ngm.errors;
            }
        });

        console.log(`validationErrors `, validationErrors);
        return validationErrors;
    }

    private emitChanged(): void {
        this.onFormCtrlChanged(this.address);
    }

    private emitTouched(): void {
        this.onFormCtrlTouched(this.address);
    }
}

当我将地址组件嵌入到这样的表单中时,这通常可以正常工作:

<form (ngSubmit)="onSubmit()" #myForm="ngForm">
    <address-input name="address" [(ngModel)]="address" #addressInput="ngModel"></address-input>

    <button type="submit">SUBMIT</button>
</form>

唯一但令人讨厌的问题是,当地址输入组件将通过模型绑定正确填写时,地址输入组件仍将保持无效状态。

即当包含自定义表单组件的表单的应用程序组件定义它绑定到自定义表单组件的地址字段时,如下所示:

public address: Address = new Address('Baker Street', 123);

自定义表单组件应该有效但仍然无效。

例如,当我在字段中添加字母或数字时,自定义表单组件立即生效。

经过一番调查,我注意到这是因为 Angular 在调用自定义表单组件上的 ControlValueAccessor 接口的 writeValue 方法后立即调用了 Validator 接口的 validate 方法。这样,当 Angular 在自定义表单组件上调用 validate 方法时,自定义表单组件的子输入的值及其验证状态尚未更新。

为了解决我的问题,我想知道是否有任何方法可以强制 Angular 更新 ngModels 的值和验证状态,或者在更新子输入/组件时再次运行验证。或者,如果有任何其他方法可以使这项工作。更广泛地说,我还想知道是否有更好的方法来实现一个表单组件,当它的所有子组件都有效时,它是有效的,因为我觉得解决这个问题需要一些“黑客”或低效的代码。

您可以在此处找到包含该问题的简单再现的Stackblitz 。

提前感谢您的时间和帮助,约书亚

4

4 回答 4

0

如果您使用 ReactiveForms,您的代码会更简单,在 ReactiveForms 中,您的真实来源是模型,并且您始终可以访问您的单个控件或控件组的状态,而无需使用 @ViewChildren 扫描组件,这是一个很好的示例这是从角度文档https://angular.io/guide/form-validation#cross-field-validation实现的

于 2019-07-24T09:56:41.810 回答
0

您可以尝试使用 FormGroup 控件嵌套表单: https ://angular.io/guide/reactive-forms#step-1-creating-a-nested-group

于 2019-07-24T10:06:18.720 回答
0

感谢您提供如此详细的解释...我遇到了类似的问题,自定义组件上设置的默认值未验证包装组件。调试后意识到 onChangeCb() 仍然是我初始化的而不是 registerOnChange 分配的。所以这些变化没有传播。以下是我尝试过的更改并且似乎正在工作。请检查

...
public registerOnChange(fn) {
            this._onChange = fn;
            /* value is model */
            this.value && setTimeout(()=>{this._onChange(this.value)},0);
        }
...
于 2020-01-22T08:29:17.597 回答
0

除了上面的一个,我觉得下面的解决方案更好,因为它不涉及任何'setTimeout'

public writeValue(val) {
// EXISING CODE
...

(val!==this.value) && this._onChange(this.value);

}
于 2020-01-23T08:15:41.387 回答