Aeseir
Aeseir

Reputation: 8434

Reactive Form Validation not working with ControlValueAccessor

I've created a simple nested form that depends on the child component rather than putting everything into one component. I've implemented a very basic ControlValueAccessor to enable it to work in such fashion, however what I noticed is that validation doesn't get captured at parent component. This means a faulty form can be submitted even though its not valid.

How do i get the validation of child forms to propagate up to the parent form.

Below is the code.

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

EDIT 2: nested approach

Stackblitz Link with Validator and ChangeDetection

Upvotes: 7

Views: 7390

Answers (1)

Amer
Amer

Reputation: 6716

You can achieve that by implementing Validator interface in your child component, which requires to write your validate method within child component, to be checked every time the value changes, and it could be like the following in your case:

@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();
  }
}

Upvotes: 8

Related Questions