Angular Material Stepper with Separate Component for each step - ExpressionChangedAfterItHasBeenCheckedError

6.8k Views Asked by At

I have a material stepper with forms in each step of the stepper. There, each step should be controlled by the form associated with it.

Even though this question has already been asked in SO, the answers to those questions do not resolve my issue. Hence I am asking.

Parent HTML

<mat-horizontal-stepper linear #stepper>
    <mat-step [stepControl]="frmStepOne">
        <ng-template matStepLabel>Step One Details</ng-template>
        <app-first-step #stepOne></app-first-step>
    </mat-step>
    <mat-step [stepControl]="frmStepTwo">
        <ng-template matStepLabel>Step Two Details</ng-template>
        <app-second-step #stepTwo></app-second-step>
    </mat-step>
</mat-horizontal-stepper> 

In my parent component, I have following.

@ViewChild('stepOne') stepOneComponent: FirstStepComponent;
@ViewChild('stepTwo') stepTwoComponent: SecondStepComponent;


get frmStepOne() {
    return this.stepOneComponent ? this.stepOneComponent.frmStepOne : null;
}

get frmStepTwo() {
    return this.stepTwoComponent ? this.stepTwoComponent.frmStepTwo : null;
}

My Child class Component

frmStepOne: FormGroup;

constructor(private formBuilder: FormBuilder) {

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

Child Class HTML

<mat-card>

  <form [formGroup]="frmStepOne">

    <mat-form-field>
      <input matInput formControlName="name" matInput placeholder="Name" required>
    </mat-form-field>

    <mat-card-actions>
      <button mat-raised-button matStepperNext class="nav-btn pull-right">Next</button>
    </mat-card-actions>

  </form>

</mat-card>

Now, when I run the app, In the console, I see following.

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'stepControl: null'. Current value: 'stepControl: [object Object]'.

As I mentioned before, there are discussions on the same in SO. For example, answer and the comments of the following linked question suggest to move form initialization to the constructor instead of having it in the onInit. I have done so.

Angular Material Stepper Component For Each Step

However, I still get the error.

This is my project on Stackblitz - https://stackblitz.com/github/vigamage/stepper-component-wise

Can someone suggest how to get rid of the issue?

Thank you..

3

There are 3 best solutions below

0
On BEST ANSWER

Just change the variable that causes this error to an Observable and pipe it with the delay(0) operator

here is a link to a working solution - no error and no need to use change detection.

here is an example

Let's say that I'm using a variable name of type string inside my html template and it causes the error mentioned above.

<div>{{ name }}</div> // BOOM ERROR why? 

because at first name was something and it was changed after angular already checked it.

so to solve this.. first change name to an Observable like name$

name$: Observable<string>; // name$ replaces name    

private myName$ = new BehaviorSubject<string>("");
myNameListener$: Observable<string> = this.myName$.asObservable();
myName(name: string) {
    this.myName$.next(name)
}

now in ngOnInit() just listen to that Observable

ngOnInit() {

    this.name$ = this.myNameListener$.pipe(
        delay(0)
    )

}

subscribe to that Observable in your html template using the async pipe

<div *ngIf=" (name$ | async) as name ">{{ name }}</div> // render your variable

now pass your actual data to the observable where ever you want.

this.myName(data);

here is a link to a working solution - no error and no need to use change detection.

good luck!

5
On

Basically, the angular is detecting the change after it has already set the value as null for both values. So, you need to explicitly tell angular that it should run change detection. Check out this demo code

I have removed the logic of using get because its a bad practice to call a function directly from an HTML. It'll be called multiple times. Try putting console log in your code and see.

I have used ngAfterViewInit along with ChangeDetectorRef to inform.

export class AppComponent implements AfterViewInit {
  title = 'mat-stepper';

  form1: FormGroup;
  form2: FormGroup;
  @ViewChild('stepOne') stepOneComponent: FirstStepComponent;
  @ViewChild('stepTwo') stepTwoComponent: SecondStepComponent;

  constructor(private cdr :ChangeDetectorRef){}

  ngAfterViewInit(){
    this.form1 = this.stepOneComponent.frmStepOne;
    this.form2 = this.stepTwoComponent.frmStepTwo
    this.cdr.detectChanges();
  }

}

This will solve the error

0
On

A really easy way to handle this is by declaring the form group in the child, and in the parent template, you have the ability to reference it! The main benefit of this is that it is wonderfully type safe and a lot cleaner than all the other options.

<mat-step [stepControl]="step1.step1Form">
  <app-step1 #step1></app-step1>
</mat-step>