14

I have a angular material linear stepper each step is a separate angular component containing a form which needs validation

The validation simply just isn't working. I can progress through to the next step without completing the form.

To illustrate what I mean I have created a condensed version on stackblitz.

The main things to look at (I think) is the create-profile.component.html

<mat-horizontal-stepper linear #stepper>
    <mat-step [stepControl]="frmStepOne">
        <ng-template matStepLabel>Step One Details</ng-template>
        <step-one-component></step-one-component>
    </mat-step>
    <mat-step [stepControl]="frmStepTwo">
        <ng-template matStepLabel>Step Two Details</ng-template>
        <step-two-component></step-two-component>
    </mat-step>
    <mat-step [stepControl]="frmStepThree">
        <ng-template matStepLabel>Step Three Details</ng-template>
        <step-three-component></step-three-component>
    </mat-step>
</mat-horizontal-stepper>

And each step-X-component

Here is the stackblitz. https://stackblitz.com/edit/angular-vpoj5j

Ben Donnelly
  • 1,191
  • 5
  • 13
  • 30

3 Answers3

28

The problem is in your CreateProfileComponent:

@Component({
    selector: 'create-profile-component',
    templateUrl: './create-profile.component.html'
})
export class CreateProfileComponent {

    frmStepOne: FormGroup;
    frmStepTwo: FormGroup;
    frmStepThree: FormGroup;

    constructor(private fb: FormBuilder) { }

}

There is no relation between your defined FormGroups in CreateProfileComponent and your stepper components. You tried to extend every StepComponent with CreateProfileComponent, but with this approach every StepComponent has its own instance of CreateProfileComponent and so their own FormGroup declaration.

To solve your problem you can declare template variables for every StepComponent in your html (starting with #) and pass the formControl to [stepControl]:

<mat-horizontal-stepper linear #stepper>
    <mat-step [stepControl]="step1.frmStepOne">
        <ng-template matStepLabel>Step One Details</ng-template>
        <step-one-component #step1></step-one-component>
    </mat-step>
    <mat-step [stepControl]="step2.frmStepTwo">
        <ng-template matStepLabel>Step Two Details</ng-template>
        <step-two-component #step2></step-two-component>
    </mat-step>
    <mat-step [stepControl]="step3.frmStepThree">
        <ng-template matStepLabel>Step Three Details</ng-template>
        <step-three-component #step3></step-three-component>
    </mat-step>
</mat-horizontal-stepper>

Or you leave your html as it is and work with ViewChild() (my preferred approach):

@Component({
    selector: 'create-profile-component',
    templateUrl: './create-profile.component.html'
})

export class CreateProfileComponent {

    @ViewChild(StepOneComponent) stepOneComponent: StepOneComponent;
    @ViewChild(StepTwoComponent) stepTwoComponent: StepTwoComponent;
    @ViewChild(StepTwoComponent) stepThreeComponent: StepThreeComponent;

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

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

    get frmStepThree() {
       return this.stepThreeComponent ? this.stepThreeComponent.frmStepThree : null;
    }

}

Either way there is no need to extend your StepComponents with CreateProfileComponent and it doesn't make any sense.

@Component({
    selector: 'step-x-component',
    templateUrl: './step-x.component.html',
})
export class StepXComponent {

    public frmStepX: FormGroup;

    constructor(private formBuilder: FormBuilder) {
    }

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

    }

}
Tim
  • 5,435
  • 7
  • 42
  • 62
SplitterAlex
  • 2,755
  • 2
  • 20
  • 23
  • Thank you so much for the clear explanation. You're right, there wasn't really any need to extend CreateProfile component. Again, thanks for the solution. – Ben Donnelly Jan 29 '18 at 11:15
  • 1
    I had to use the "ViewChild" approach, because the other one triggered a "ExpressionHasChangedAfterCheck" error! Although it worked, it may cause problems in the future (or not if they solve in the core this issue). Anyway thanks for this (I upped yesterday) – Sampgun Apr 06 '18 at 08:53
  • 1
    Moving the frmStepX declaration into the constructor solves the problem. – Sampgun Apr 06 '18 at 09:00
  • 7
    just a note: I was getting `ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: '[object Object]'.` error and solved it by initialising individual step components' forms in the constructor. – umutesen Apr 19 '18 at 15:34
  • 1
    Just create the form group in the constructor `this.form = new FormGroup({});` – Ricardo Saracino Sep 04 '18 at 19:41
  • Shining example of what a SO response should be. Well explained and an elegant, simple solution. Helped me with my issues as well. Nice work @SplitterAlex – ustad Nov 27 '19 at 15:07
  • @splitterAlex, I've got the forms split up in separate components but the linear property is ignored and the valid status of the children are not being considered when using matStepperNext/matStepperPrevious. Any help would be appreciated! – ustad Nov 28 '19 at 00:19
  • 1
    @ustad Can you prepare a small and simple demo of your problem on Stackblitz? That way it makes it easy for me to help you. – SplitterAlex Nov 28 '19 at 02:39
  • 2
    Thanks for the quick response @SplitterAlex, I did not include the step component references in the template (#step1, #step2) as I was a bit confused when you said, "Or you leave your html as it is...". My bad :) Works perfectly now and my code is much easier to maintain. Thanks again. – ustad Nov 29 '19 at 17:15
  • moving the form initialization from onInit to constructor does not solve the issue for me :( – vigamage Sep 04 '20 at 07:23
  • ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: '[object Object]'. this error solved by this post https://stackoverflow.com/questions/45467881/expressionchangedafterithasbeencheckederror-expression-has-changed-after-it-was – Sam Arul Raj T Apr 12 '21 at 14:13
0

Your stepper and forms components works on different form objects. You need to set super's forms objects in step component's ngOnInit()

ngOnInit() {
    super.frmStepTwo = this.formBuilder.group({
        address: ['', Validators.required]
    });
}

instead

ngOnInit() {
    this.frmStepTwo = this.formBuilder.group({
        address: ['', Validators.required]
    });
}
Tim
  • 5,435
  • 7
  • 42
  • 62
Mehmet Sunkur
  • 2,373
  • 16
  • 22
0

To have a mat-stepper with each step as its own component, create the buttons to traverse through the stepper outside the component and show/hide the traversal buttons based on form validation done inside the individual component and expose the form info to the parent stepper.
For Example:

<mat-horizontal-stepper  #stepper linear  iseditable>
<mat-step 
 [stepControl]="firstFormGroup" 
 [completed]="primaryIsTrue"
 >
  <app-primary-settings  
  (formIsValid)="formValidity($event)"></app-primary-settings>

  <button mat-button matStepperNext 
   *ngIf="primaryIsTrue">
    Next
   </button>
</mat-step>

</mat-horizontal-stepper>
Terrorbladezz
  • 81
  • 1
  • 5