Search code examples
angularangular-material-stepper

Can't get Angular Material Stepper with single form and child component forms working


I'm trying to implement the Angular Material Stepper functionality in our application. Based on the documentation here I came up with the following solution:

civilliability-proposal-detail.component.html

<form [formGroup]="formGroup">
  <mat-horizontal-stepper labelPosition="bottom" formArrayName="formArray" #stepper>
    <mat-step formGroupName="0" [stepControl]="formArray?.get([0])" [editable]="true">
      <div>
        <ng-template matStepLabel>{{ 'proposals.details.civilLiability.stepper.details' | translate }}</ng-template>
        <app-civilliability-step1 [model]="model.details"></app-civilliability-step1>

        <button mat-button matStepperNext type="button">Next</button>
      </div>
    </mat-step>

    <mat-step formGroupName="1" [stepControl]="formArray?.get([1])" [editable]="true">
      <div>
        <ng-template matStepLabel>{{ 'proposals.details.civilLiability.stepper.antecedents' | translate }}</ng-template>
        <!-- <app-civilliability-step2 [model]="model.questionnaire"></app-civilliability-step2> -->

        <button mat-button matStepperPrevious type="button">Back</button>
        <button mat-button matStepperNext type="button">Next</button>
      </div>
    </mat-step>
  </mat-horizontal-stepper>
</form>

civilliability-proposal-detail.component.ts

get formArray(): AbstractControl | null {
  return this.formGroup.get('formArray');
}

constructor(private formBuilder: FormBuilder) {}

ngOnInit() {
  this.buildForms();
}

buildForms() {
  this.formGroup = this.formBuilder.group({
    formArray: this.formBuilder.array([this.buildDetailsForm(), this.buildQuestionnaireForm()])
  });
}

buildDetailsForm(): FormGroup {
  if (typeof this.model === 'undefined') {
    this.model = getEmptyCivilLiabilityRequest();
  }

  const formGroup = this.formBuilder.group({
    horses: new FormControl(this.model.details.horses, Validators.min(0)),
    properties: new FormControl(this.model.details.properties, Validators.min(0)),
    garages: new FormControl(this.model.details.garages, Validators.min(0)),
    ...
  });

  return formGroup;
}

buildQuestionnaireForm(): FormGroup {
  const formGroup = this.formBuilder.group({});

  return formGroup;
}

civilliability-step1.component.html

<form [formGroup]="formGroup">
  <section class="row">
    <section class="col-md-4 full-width-form">
      <mat-form-field>
        <mat-label>{{ 'proposals.details.civilLiability.horses' | translate }}</mat-label>
        <input type="number" matInput formControlName="horses" />
        <mat-error>{{ formGroup.controls['horses'].getError('server-error') }}</mat-error>
      </mat-form-field>
    </section>
    <section class="col-md-4 full-width-form">
      <mat-form-field>
        <mat-label>{{ 'proposals.details.civilLiability.terrainsWithTrees' | translate }}</mat-label>
        <input type="number" matInput formControlName="terrainsWithTrees" />
        <mat-error>{{ formGroup.controls['terrainsWithTrees'].getError('server-error') }}</mat-error>
      </mat-form-field>
    </section>
    <section class="col-md-4 full-width-form">
      <mat-form-field>
        <mat-label>{{ 'proposals.details.civilLiability.garages' | translate }}</mat-label>
        <input type="number" matInput formControlName="garages" />
        <mat-error>{{ formGroup.controls['garages'].getError('server-error') }}</mat-error>
      </mat-form-field>
    </section>
  </section>
</form>

Based on this I get a bunch of errors which I think are related to the fact that I'm using child components in my form, but I'm not quite sure how to tackle this. So, any suggestions?

I've also read somebody not using the formArrayName in the stepper, but I can't seem to find this example anymore, so can't test if that works or not.

enter image description here


Solution

  • Instead of using a form array, I would recommend declaring a form group in each individual step component. Here is how I have done it:

    1. Create a component to keep the stepper.
    2. Create a component for each step and initialise it in the step component.
    3. Use public component variables or define a model to keep user input.
    4. You can then access the public members of each step component in the parent stepper component via data binding.

    Live demo