Search code examples
angularangular-materialangular-formsangular-validation

Angular 8 / Material : subform not showing errors


I created a form with a conditional subform this way:

In the parent form:

<form [formGroup]="layerFormGroup" (submit)="submit()" #f="ngForm">
  ...
  <div *ngIf="layerFormGroup.value.mode == 'features'">
    <app-edit-layer-features formControlName="modeFormGroup"></app-edit-layer-features>
  </div>

with the TS:

ngOnInit() {

  this.layerFormGroup = this.formBuilder.group({
    ...
    modeFormGroup: ['', Validators.required]
  });
}

public submit() {

  if (!this.layerFormGroup.valid) {
    return;
  }

Then the child form is:

<ng-container [formGroup]="modeFormGroup">
<mat-horizontal-stepper>

    <mat-step label="Collection" [stepControl]="modeFormGroup">
        <mat-form-field>
            <mat-label>Collection</mat-label>
            <mat-select formControlName="collectionCtrl">
                <mat-option value="col1">col1</mat-option>
                <mat-option value="col2">col2</mat-option>
            </mat-select>
        </mat-form-field>
        <div>
            <button mat-button matStepperNext type="button">Next</button>
        </div>
    </mat-step>

    <mat-step label="Rendered geometry" [stepControl]="modeFormGroup">
        <mat-form-field>
            <mat-label>Rendered geometry</mat-label>
            <mat-select formControlName="geometryCtrl">
                <mat-option value="geoshape">a geopoint</mat-option>
                <mat-option value="geopoint">a geoshape</mat-option>
            </mat-select>
        </mat-form-field>
        <div>
            <button mat-button matStepperNext type="button">Next</button>
        </div>
    </mat-step>

</mat-horizontal-stepper>

and finally the child view:

@Component({
selector: 'app-edit-layer-features',
templateUrl: './edit-layer-features.component.html',
styleUrls: ['./edit-layer-features.component.scss'],
providers: [
  {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => EditLayerFeaturesComponent),
    multi: true
  },
  {
    provide: NG_VALIDATORS,
    useExisting: forwardRef(() => EditLayerFeaturesComponent),
    multi: true
  }
]
})
// ControlValueAccessor: see https://christianlydemann.com/form-validation-with-controlvalueaccessor/
export class EditLayerFeaturesComponent implements OnInit, ControlValueAccessor, Validator {

  public modeFormGroup: FormGroup = this.formBuilder.group({
    collectionCtrl: ['', Validators.required],
    geometryCtrl: ['', Validators.required],
  });

  constructor(
    private formBuilder: FormBuilder) { }

  ngOnInit() {
  }

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

  writeValue(obj: any): void {
    if (obj) {
      this.modeFormGroup.patchValue(obj, { emitEvent: false });
      this.onTouched();
    }
  }
  registerOnChange(fn: any): void {
    this.modeFormGroup.valueChanges.subscribe(fn);
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    isDisabled ? this.modeFormGroup.disable() : this.modeFormGroup.enable();
  }
  validate(control: AbstractControl): import('@angular/forms').ValidationErrors {
    return this.modeFormGroup.valid ? null : { invalidForm: { valid: false, message: 'Features fields are invalid' } };
  }
  registerOnValidatorChange?(fn: () => void): void {
    this.modeFormGroup.valueChanges.subscribe(fn);
  }
}

The issue is the following: when I call the submit() method in the parent form, then the mat-error tags are added in the parent form but not in the child form, i.a. validation errors are shown in the parent form but not in the child one.

After some tests, I found out that calling

this.modeFormGroup.markAllAsTouched()

in the child form will indeed show the child errors. Anyway I this should be called from the parent submit() method and I can't find how to do it (this.layerFormGroup.get('modeFormGroup').valid doesn't do the job).

I feel like I am missing something related to material, any clue?

Thx.

Edit 1 : I created a stackblitz where you can see the problem : https://stackblitz.com/edit/angular-eldyyc . If you only choose the mode = Features and you click on save layer then only the name is displayed as an error, not the subform fields


Solution

  • The problem is the communication between the components and the reference to the original parent form group. You see, you create the form group in the parent component with modeFormGroup as a nested form group without any controls. Then you don't pass the reference to the layerFormGroup to the child component.

    On the other hand, in the child component you create local property modeFormGroup. This modeFormGroup is not the same as the modeFormGroup in layerFormGroup.

    this.layerFormGroup = this.formBuilder.group({
          name: ['', Validators.required],
          mode: ['', Validators.required],
          id: [''],
          modeFormGroup: this.formBuilder.group({})  //not the same as in the child component
    });
    

    Note that modeFormGroup is an empty form group, without any controls. Now, in the child component, you create the child form group as:

    // totally different form group
    public modeFormGroup: FormGroup = this.formBuilder.group({
        collectionCtrl: ['', Validators.required],
        geometryCtrl: ['', Validators.required]
    });
    

    What you need to do is, once the child component is initialized, within the ngOnInit() function to emit the created form group with EventEmitter, and assign the form group to the modeFormGroup control in the parent form group.

    I edited your stackblitz