反应式表单验证不适用于 ControlValueAccessor

Reactive Form Validation not working with ControlValueAccessor

我创建了一个简单的嵌套表单,它依赖于 child 组件,而不是将所有内容都放入一个组件中。 我已经实现了一个非常基本的 ControlValueAccessor 以使其能够以这种方式工作,但是我注意到验证不会在 parent 组件处被捕获。这意味着可以提交错误的表单,即使它无效。

我如何获得 child 表单的验证以传播到 parent 表单。

下面是代码。

Parent HTML

<form [formGroup]="parentForm" (ngSubmit)="submitData()">
    <h3>Parent</h3>
    <br>
    <ng-container formArrayName="children">
        <ng-container *ngFor="let c of children?.controls; index as j">
            <app-child [formControlName]="j"></app-child>
        </ng-container>
    </ng-container>

    <button mat-raised-button type="button" (click)="addChild()">Add Activity</button>
    <button mat-raised-button type="submit" color="warn" [disabled]="!parentForm.valid">Submit</button>
</form>

<pre>
{{parentForm.valid}} // this is always true because its not getting validator state from children
</pre>
<pre>
{{parentForm.value | json}}
</pre>

Parent TS

export class ParentComponent implements OnInit {
  parentForm: FormGroup;

  constructor(private fb: FormBuilder) {
    
  }

  ngOnInit(): void {
    this.createForm();
  }

  private createForm() {
    this.parentForm = this.fb.group({
      children: this.fb.array([])
    });
  }

  get children(): FormArray {
    return this.parentForm.get("children") as FormArray;
  }

  addChild() {
    const tempChild = new FormControl({
      description: null
    });

    this.children.push(tempChild);
  }

  submitData() {
    console.info(JSON.stringify(this.parentForm.value));
  }
}

Child HTML

<form [formGroup]="newChildForm">
    <tr>
        <td>
            <mat-form-field>
                <mat-label>Description</mat-label>
                <input type="text" matInput formControlName="description" required>
            </mat-form-field>
        </td>
    </tr>
</form>

Child TS

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ChildComponent),
      multi: true
    }
  ]
})
export class ChildComponent implements ControlValueAccessor, OnInit {
  newChildForm: FormGroup;

  constructor(private fb: FormBuilder) {
    this.createForm();
  }

  private createForm() {
    this.newChildForm = this.fb.group({
      description: [null, Validators.required],
    });
  }

  onTouched: () => void = () => { };

  writeValue(value: any) {
    if (value) {
      this.newChildForm.setValue(value, { emitEvent: true });
    }
  }

  registerOnChange(fn: (v: any) => void) {
    this.newChildForm.valueChanges.subscribe((val) => {
      fn(val);
    });
  }

  setDisabledState(disabled: boolean) {
    disabled ? this.newChildForm.disable() : this.newChildForm.enable();
  }

  registerOnTouched(fn: () => void) {
    this.onTouched = fn;
  }

  ngOnInit(): void {  }
}

Stackblitz Link

编辑 2:嵌套方法

Stackblitz Link with Validator and ChangeDetection

您可以通过在子组件中实现 Validator 接口来实现,这需要在子组件中编写 validate 方法,以便在每次值更改时进行检查,并且可以是如您的情况如下:

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ChildComponent),
      multi: true,
    },
    {
      // >>>>>> 1- The NG_VALIDATORS should be provided here <<<<<
      provide: NG_VALIDATORS,
      useExisting: ChildComponent,
      multi: true,
    },
  ],
})
export class ChildComponent implements ControlValueAccessor, Validator, OnInit {
  newChildForm: FormGroup;

  onChange: any = () => {};
  onValidationChange: any = () => {};

  constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
    this.createForm();
  }

  private createForm() {
    this.newChildForm = this.fb.group({
      description: [null, Validators.required],
      children: this.fb.array([]),
    });
  }

  onTouched: () => void = () => {};

  writeValue(value: any) {
    if (value) {
      this.newChildForm.setValue(value, { emitEvent: true });
    }
  }

  registerOnChange(fn: (v: any) => void) {
    this.onChange = fn;
  }

  setDisabledState(disabled: boolean) {
    disabled ? this.newChildForm.disable() : this.newChildForm.enable();
  }

  registerOnTouched(fn: () => void) {
    this.onTouched = fn;
  }

  ngOnInit(): void {
    this.newChildForm.valueChanges.subscribe((val) => {
      this.onChange(val);

      // >>>> 4- call registerOnValidatorChange after every changes to check validation of the form again <<<<
      this.onValidationChange();
    });
  }

  // >>>> 2- validate method should be added here <<<<
  validate(): ValidationErrors | null {
    if (this.newChildForm?.invalid) {
      return { invalid: true };
    } else {
      return null;
    }
  }

  // >>>> 3- add registerOnValidatorChange to call it after every changes <<<<
  registerOnValidatorChange?(fn: () => void): void {
    this.onValidationChange = fn;
  }

  get children(): FormArray {
    return this.newChildForm.get("children") as FormArray;
  }

  addChild() {
    const tempChild = new FormControl({
      description: null,
      children: [],
    });

    this.children.push(tempChild);
    this.cdr.detectChanges();
  }
}