如何将嵌套的 angular 表单标记为已实现 ControlValueAccessor?

How to mark nested angular form as touched implementing ControlValueAccessor?

an example 表单 Angular 了解如何将嵌套表单与 ControlValueAccessor 一起使用。

他们创建了一个单独的表单组件并将其用作子表单:

<div [formGroup]="form">
  ... other form controls
  <address-form formControlName="address" legend="Address"></address-form>
</div>

如果我在父表单上调用 markAllAsTouched 方法,我希望将嵌套表单的所有字段标记为已触摸。

<form [formGroup]="form">
  <app-address-form formControlName="address"></app-address-form>      
</form>
<button (click)="markAsTouched()">Mark as touched</button>
export class AppComponent {
  public form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = fb.group({ address: fb.control({ city: '' }) });
  }

  public markAsTouched() {
    this.form.markAllAsTouched();
  }
}

是否可以使用这种方法?

我建了一个simplified version on StackBlitz

当您点击按钮时实际发生的是将应用程序地址表单标记为已触摸:

<app-address-form formcontrolname="address" class="ng-pristine ng-valid ng-touched">
...
</app-address-form>

但我想将其传播到子表单及其字段。

可能有更好的方法,但是,您可以使用 Input

父组件 ts

export class AppComponent {
  public form: FormGroup;
  setAsTouched: boolean = false;

  constructor(private fb: FormBuilder) {
    this.form = fb.group({ address: fb.control({ city: '' }) });
  }

  public markAsTouched() {
    this.setAsTouched = true;
  }
}

父模板组件

<h1>Form</h1>
<form [formGroup]="form">
  <app-address-form
    formControlName="address"
    [mark]="setAsTouched"
  ></app-address-form>
</form>
<button (click)="markAsTouched()">Mark as touched</button>

子组件 ts

...
...
export class AddressFormComponent implements ControlValueAccessor, Validator {
  @Input() set mark(isTouched: boolean) {
    if (isTouched) {
      this.form.markAllAsTouched();
    }
  }

  ...
  ...
  ...
}

确实,解释 link 的嵌套表单组的管理有点混乱,因为您确实 没有 有嵌套表单组。你有一个带一个控件的FormGroup(控件的值是一个对象,但你只有一个控件)

看到你在你的例子中写道:

 this.form = fb.group({ address: fb.control({ city: '' }) });

嵌套的 formGroup 应该是这样的:

 this.form = fb.group({ address: fb.group({ city: '' }) });

但是这个formGroup不能像大学说的那样管理。方法很简单 使用组件 并将 formGroup 作为 Input(*)

传递

是因为只触及了“控件”

解决方案:您可以使用模板引用变量并传递给您的函数

<form [formGroup]="form">
  <app-address-form #address formControlName="address"></app-address-form>      
</form>
<button (click)="markAsTouched(address)">Mark as touched</button>

  public markAsTouched(address:any) {
    this.form.markAllAsTouched();
    //see that you can access to the form of the "adress" simply
    //using adress.form
    address.form.markAllAsTouched()
  }

(*) 组件(不是从 ControlValueAccessor 实现的)只是

@Component({
  selector: 'app-address-form',
  template: `
    <div [formGroup]="form">
      <label>City</label>
      <input formControlName="city" (blur)="onTouched()">
    </div>
  `
})
export class AddressFormComponent {
    form:FormGroup
    @Input('form') set _(value){
      this.form=value as FormGroup
    }
}

而你使用 as

<app-address-form [form]=form.get('address')><app-address-form>

我找到了以下将 markAsTouched 传播到子控件的解决方案。

以下递归函数有帮助:

const updateFormControlTree = (abstractControl: AbstractControl): void => {
  const forEachChildControl = (
    control: AbstractControl,
    callbackFunction: (abstractControl: AbstractControl) => void): void => {
      const childControls = (control as AbstractControl & { controls?: [] }).controls;

      if (!childControls) {
        return;
      }

      if (typeof childControls === 'object') {
        const extractedChildControls: AbstractControl[] =
          Object.values(childControls);

        extractedChildControls.forEach((childControl) => {
          callbackFunction(childControl);
        });
      }
    };

  forEachChildControl(abstractControl, (control: AbstractControl) =>
    updateFormControlTree(control)
  );
  abstractControl.markAsTouched();
};

我必须用上面的函数覆盖我的控件的 markAsTouched。为此,我需要 FormControl 而不是 formControlName 属性:

<app-address-form [formControl]="addressControl"></app-address-form>

这就是我重写函数的方法:

export class AddressFormComponent implements ControlValueAccessor, Validator, OnInit
{
  @Input() formControl!: FormControl;

  public ngOnInit(): void {
    this.formControl.markAsTouched = () => updateFormControlTree(this.form);
  }
  ...
}

最后要做的是将我的控件标记为已从父窗体触摸:

export class AppComponent  {
  public addressControl = new FormControl();
  public form: FormGroup = this.fb.group({
    address: this.addressControl,
  });

  constructor(private fb: FormBuilder) {    
  }

  markAsTouched() {
    this.addressControl.markAsTouched();
  }
}

我建了一个working example on StackBlitz

P.S。上述解决方案的灵感来自 Angular Framework 源代码中更新值和有效性的方法。这个方法是私有的。不过如果有它就好了 public。