在 Angular 8 中将 FormArray 验证器从子组件传播到父组件

Propagating FormArray validators from child component to parent component in Angular 8

编辑:通过重写电子邮件-phone-input.component.ts 以使用 ControlContainer、从父级获取 FormGroup 以及 FormArray 控件,我设法使数据直通和验证工作得很好。 将使用工作代码更新 repo 并回答问题。

我正在尝试让验证器为包含从带有表单数组的子组件发送的对象的表单组工作。

当前层次结构 - 单独的电子邮件组件、单独的 phone 组件(利用外部包),然后是电子邮件 + phone 包含电子邮件和 phone 的 FormArray 的组件,以及然后是一个带有单个表单控件的主表单,该表单控件从电子邮件 + phone 组件获取数据。

我已经获得了所有的数据,但我不知道如何让验证器进入主表单。

Link 到 stackblitz 包含代码 + 演示。 https://stackblitz.com/github/rushvora/nested-form-playground

旁注 - Validators.required 在将新的表单控件添加到表单数组时不适用于电子邮件输入。

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'nested-form-playground';
  form: FormGroup;
  emailsAndPhones: FormGroup;

  constructor(private fb: FormBuilder) { }

  ngOnInit() {
    this.form = this.fb.group({
      notifications: this.fb.group({
        emailsAndPhones: []
      })
    });
    this.emailsAndPhones = this.form.get('notifications.emailsAndPhones') as FormGroup;
  }

  submit() {
    console.log(this.form.valid, this.form.status);
  }
}

app.component.html

<div class="container-fluid mt-3">
  <div class="card">
    <form [formGroup]="form" (ngSubmit)="submit()">
      <div class="card-header bg-light">
        <h4>Nested Form Testing.</h4>
      </div>
      <div class="card-body" formGroupName="notifications">
        <app-email-phone-input formControlName="emailsAndPhones"></app-email-phone-input>
      </div>
      <div class="card-footer bg-light">
        <button>Submit</button>
      </div>
    </form>
  </div>
</div>

邮箱-phone-input.component.ts

import { Component, OnInit, OnDestroy, forwardRef } from '@angular/core';
import {
  FormGroup, FormBuilder, FormArray, FormControl, ControlValueAccessor,
  Validator, NG_VALUE_ACCESSOR, NG_VALIDATORS, Validators, AbstractControl
} from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-email-phone-input',
  templateUrl: './email-phone-input.component.html',
  styleUrls: ['./email-phone-input.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => EmailPhoneInputComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => EmailPhoneInputComponent),
      multi: true
    }
  ]
})
export class EmailPhoneInputComponent implements OnInit, ControlValueAccessor, Validator, OnDestroy {
  emailsAndPhonesForm: FormGroup;
  emails: FormArray;
  phones: FormArray;
  private destroy$ = new Subject();

  constructor(private fb: FormBuilder) { }

  ngOnInit() {

    this.emailsAndPhonesForm = this.fb.group({
      emails: this.fb.array([]),
      phones: this.fb.array([])
    });
    this.emails = this.emailsAndPhonesForm.get('emails') as FormArray;
    this.phones = this.emailsAndPhonesForm.get('phones') as FormArray;
  }

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

  writeValue(val: any): void {
    val && this.emailsAndPhonesForm.setValue(val, { emitEvent: false });
  }

  registerOnChange(fn: any): void {
    this.emailsAndPhonesForm.valueChanges
      .pipe(takeUntil(this.destroy$))
      .subscribe(fn);
  }

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

  validate(_: AbstractControl) {
    let emailsValidity = {};
    let phonesValidity = {};
    this.emails.valid ? emailsValidity = null : { invalidForm: {valid: false, message: `Email is invalid.`}};
    this.phones.valid ? phonesValidity = null : { invalidForm: {valid: false, message: `Phone is invalid.`}};
    console.log('Emails and Phones Form in validate: ');
    console.log(this.emailsAndPhonesForm);
    console.log('Emails in validate: ');
    console.log(this.emails);
    console.log('Phones in validate: ');
    console.log(this.phones);
    if (emailsValidity && phonesValidity) {
      const combinedValidity = { invalidForm: {valid: false, message: `Email & phone are invalid.`}};
      return combinedValidity;
    } else if (emailsValidity) {
      return emailsValidity;
    } else if (phonesValidity) {
      return phonesValidity;
    } else {
      return null;
    }
  }

  addEmail() {
    this.emails.push(new FormControl('', Validators.email));
  }

  addPhone() {
    this.phones.push(new FormControl('', Validators.required));
    console.log('Emails and Phones Form in addPhone: ');
    console.log(this.emailsAndPhonesForm);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

}

邮箱-phone-input.component.html

<!-- Array of email inputs -->
<ng-container [formGroup]="emailsAndPhonesForm">
  <ng-container formArrayName="emails">
    <ng-container *ngFor="let email of emails.controls; index as i">
      <div class="row">
        <app-email-input [index]="i+1" [formControlName]="i"></app-email-input>
      </div>
    </ng-container>
    <button type="button" (click)="addEmail()">Another Email</button>
  </ng-container>
  <ng-container formArrayName="phones">
    <ng-container *ngFor="let phone of phones.controls; index as i">
      <div class="row">
        <div class="form-group">
          <label>Phone {{ i + 1 }}</label>
          <app-phone-input [formControlNameCustom]="i"></app-phone-input>
        </div>
      </div>
    </ng-container>
    <button type="button" (click)="addPhone()">Another Phone</button>
  </ng-container>
</ng-container>
<!-- Array of phone inputs -->
<div class="jumbotron">
  <div *ngFor="let email of emails.value">
    {{email?.email }}
  </div>
  <div *ngFor="let phone of phones.value">
    {{phone?.internationalNumber }}
  </div>
</div>

我通过使用 ControlContainer 从 parent/master 组件获取父表单组解决了这个问题。通过这种方式,我能够更新有效性,直接在父表单控件的子组件中设置验证器。 基本组件仍然需要使用 CVA 实现,这里的电子邮件输入和第 3 方 phone 包确实使用 CVA 来实现它们的控件,但是它们上面的层一直到顶层只需要使用 ControlContainer ,以便在组件之间传递主表单 group/controls。

感谢这个答案引导我找到解决方案 -

更新代码 - https://github.com/rushvora/nested-form-playground