Angular Material Stepper causes mat-formfield to validate for dynamic forms when returning to an older step
Asked Answered
C

2

15

A problem is occurring with the angular materials mat-stepper when working with dynamic forms. For example I have a stepper with 3 steps each having there own form. The first step however uses a hidden form to determine if it is valid or not as this step can dynamically add more forms so binding all of them to the step is impossible.

When you are on the first step you can create multiple forms and everything works as expected without any random validation occurring to the new forms added. The problem occurs if you go to step 2 or 3 and come back to the first step and then create a new form it will automatically start with all the fields highlighted red.

I have tried many different attempts to suppress however I have not been successful. Below is a basic example of how my first step contains a hiddenForm bound to the step control, a default form, and a button to create more forms in that step.

My research into trying to fix this is leading me to believe that the material stepper is making all mat-form-fields to be red if invalid, regardless if newly added or not.

<mat-horizontal-stepper [linear]="true">
    <mat-step [stepControl]="hiddenForm" label="step 1"
      <form [formGroup]="myForm">
        <mat-form-field>
          <mat-label>
            First Name
          </mat-label>

          <input [formControl]="firstName"matInput>

          <mat-error *ngIf="!firstName.valid && firstName.touched">
            First Name required
          </mat-error>
        </mat-form-field>
      </form>

    <button (click)="AddNewForm()">Add New Form</button>
  </mat-step>
</mat-horizontal-stepper>

Failed attempts tried: (Current form is the newly added form)

this.currentForm.markAsUntouched();
this.currentForm.markAsPristine();
this.currentForm.setErrors(null);
this.currentForm.reset();

this.currentForm.get('firstName).setErrors(null);
this.currentForm.get('firstName).reset();
this.currentForm.get('firstName).markAsPristine();
this.currentForm.get('firstName).markAsUntouched();

<mat-step [completed]="true" ..> ( on all steps )
Calycine answered 16/7, 2018 at 18:54 Comment(0)
C
25

Background Information

The nicest solution I have found is changing a parameter on the mat-stepper. There is a step selected at any given time. On step you can change whether the step has been interacted with or not. If a step has been previously visited the interacted parameter will then be set to true. As this is intended and makes sense it will however cause a problems adding a class to all the mat-form-fields causing them to go red.

Scenarios where this is causes a bad user experience:

  1. You complete the first step and go to the second step. After going to the second step you realize you made a mistake on the first step and decide to navigate back to the first step. You make your changes and now proceed once again to the second step. Because you have already visited this step if you have mat-form-fields a class will be added (possibly other changes) and your form fields are now all red. This is a poor user experience as the user has technically not made any mistakes and can be over bearing.

  2. In the first step you have it where you are creating dynamic forms. For simplicity let use the tour of heroes analogy. In our first step you can dynamically add forms. Each form represents a new hero you want to add. You have added 3 heroes and moved on to step 2. Before completing step 2 you realize you forgot a few heroes and proceed back to step 1. Now when you click our button 'create hero' your dynamic form pops up but all your mat-form-fields are now red like the user made a mistake. This is another poor user experience.

The Fix:

hero.stepper.html

<mat-horizontal-stepper [linear]="true" 
  (selectionChange)="stepChanged($event, stepper);">

  <mat-step [stepControl]="hiddenForm" label="step 1"
    <form [formGroup]="heroFormGroupArray[0]">
      <mat-form-field>
        <mat-label>Hero Name</mat-label>

        <input [formControl]="heroName"matInput>

        ...
      </mat-form-field>
    </form>

    <button (click)="AddNewHero()">Add New Hero</button>
  </mat-step>
</mat-horizontal-stepper>

hero.stepper.ts

export class heroStepComponent implements OnInit {
  constructor(){
    ...
  }

  ngOnInit(){
    ...
  }

  stepChanged(event, stepper){
    stepper.selected.interacted = false;
  }
}
Calycine answered 18/7, 2018 at 20:31 Comment(9)
thank you so much. You made my day. I have struck past two days to resolve this issue. thanks a lot.Umbelliferous
No problem, Hit me hard as well hahaCalycine
despide this seems to work it breaks functionality of stepper's next() function. imge.to/i/LRq7FRanking
@GeorgeKnap how so? I have used this for an application for almost a year now and everything works as expected.Calycine
@Calycine Can't really say why. When I implemented this function and used button that calls stepper.next() the internal selectedIndex didn't get changed (see the link to gif in my previous comment).Ranking
@GeorgeKnap the gif did not load for me. I would suggest if you have time to try digging deeper into why yours did not work when you tried implementing. The above has been used in production with no problems reported by users or QA. If there is some specific problem I would be very interested myself to understand why. I would try on my end but I cannot produce this issue. ThanksCalycine
@Calycine how to mark interacted false if im not using (selectionChange)="stepChanged($event, stepper); in <mat-horizontal-header> from where to get stepper value.. im moving the stepper with functions this.myStepper.previous() this.myStepper.next()Plumbing
I dont think you can easily.Calycine
Thankyouuuu so muchPlumbing
R
13

Following on previous post I've found better implementation for stepChanged function:

  stepChanged(event: StepperSelectionEvent) {
    if (event.previouslySelectedIndex > event.selectedIndex) {
     event.previouslySelectedStep.interacted = false;
    }
  }

This code will set interacted property only on steps which you go from to previous steps.

Ranking answered 17/7, 2019 at 11:31 Comment(3)
Brilliant and no words left!! After a couple of hours, found this and it was... amazing! Thank you very much!Salinometer
For me, setting interacted on the current step (not the previous) did the trick.Buatti
I kept troubleshooting the mat-form-field-invalid angle. I changed it to event.selectedStep.interacted = false; on every nav to a specific step index and now angular formgroup and mat-form-field-invalid work.Sevier

© 2022 - 2024 — McMap. All rights reserved.