angularangular-materialangular11angular-material-stepper

Angular Material stepper not proceeding to next step when being called from nested component. (Angular 11)


When calling

this.stepper.next()

using a button on the child component, the stepper is not progressed until the second time the button is clicked.

Parent Component Html

<mat-card>
  <mat-card-header>Test</mat-card-header>
  <mat-card-content>
    <mat-horizontal-stepper #stepper linear>
      <mat-step [completed]='this.CheckIfStepCompleted(1)'>
        <mat-card>
          <mat-card-content>
            <app-comp1 [steps]='this.steps'></app-comp1>
          </mat-card-content>
          <mat-card-actions align='start'>
           <!-- <button mat-raised-button matStepperNext color='primary' [disabled]='!this.CheckIfStepCompleted(1)'>Next</button> -->
          </mat-card-actions>
        </mat-card>
      </mat-step>
      <mat-step [completed]='this.CheckIfStepCompleted(2)'>
        <mat-card>
          <mat-card-content>
            <app-comp2 [steps]='this.steps'></app-comp2>
          </mat-card-content>
          <mat-card-actions align='start'>
            <button mat-raised-button matStepperNext color='primary' [disabled]='!this.CheckIfStepCompleted(2)'>Next</button>
          </mat-card-actions>
        </mat-card>
      </mat-step>
      <mat-step [completed]='this.CheckIfStepCompleted(2)'>
        <mat-card>
          <mat-card-content>
            <app-comp3 [steps]='this.steps'></app-comp3>
          </mat-card-content>
          <mat-card-actions align='start'>
            <button mat-raised-button matStepperPrevious color='primary' [disabled]='!this.CheckIfStepCompleted(2)'>Back</button>
          </mat-card-actions>
        </mat-card>
      </mat-step>
    </mat-horizontal-stepper>
  </mat-card-content>
</mat-card>

Child Component Html

  <button (click)='this.Complete()' mat-raised-button color='accent'>Complete</button>

Child Component ts file

import { Component, Input, NgZone, OnInit, ViewChild, ViewChildren } from '@angular/core';
import { MatHorizontalStepper } from '@angular/material/stepper';
import { Step, SteppperService } from '../../services/stepper/steppper.service';

@Component({
  selector: 'app-comp1',
  templateUrl: './comp1.component.html',
  styleUrls: ['./comp1.component.css']
})
export class Comp1Component implements OnInit {

  @Input() steps: Step[] = [];

  constructor(private _stepSvc: SteppperService, private readonly stepper: MatHorizontalStepper, private ngZone: NgZone) { }
  selectedIndex: number = this.stepper.selectedIndex;
  ngOnInit(): void {
  }
  Complete() {
    this._stepSvc.CompleteStep(this.steps, 1).then(() => this.stepper.next());
    // this.ProgressStep();
  }
  ProgressStep() {
    this.ngZone.run(() => {
      this.stepper.next();
    });
  }
}

We have also tried creating a function on the parent and having a event be emitted from the child component to trigger the next().

TLDR: I need to trigger the next() for the stepper located on the parent component, but I need to trigger it from the child component.


Solution

  • So I got it working. I was able to use Akkonrad's answer but that wasn't what finally worked.

    Ultimately I had to not use the "completed" attribute on the template tile.

    What I found was on the ngAfterViewInit() that I would access the stepper as a ViewChild and then iterate through the steps and set them all to Completed=false.

    Then if I used a EventEmitter on the child component I could trigger the completion of the step on the parent. They key is that I CANNOT use the attribute for it to work this way. It can only be controlled by the ts file.

    Now to me, this seems like a bug. That being said I've only had a year in angular and I'm by far not an expert. If someone can explain why this happens this way I would love to know.

    Working Code is below:

    Parent html:

    <mat-card>
      <mat-card-header>Test</mat-card-header>
      <mat-card-content>
        <mat-horizontal-stepper #stepper linear>
          <mat-step>
            <mat-card>
              <mat-card-content>
                <app-comp1 (CompleteStep)='this.CompleteStep(0)'></app-comp1>
              </mat-card-content>
            </mat-card>
          </mat-step>
          <mat-step>
            <mat-card>
              <mat-card-content>
                <app-comp2 (CompleteStep)='this.CompleteStep(1)'></app-comp2>
              </mat-card-content>
            </mat-card>
          </mat-step>
          <mat-step>
            <mat-card>
              <mat-card-content>
                <app-comp3 (CompleteStep)='this.CompleteStep(2)'></app-comp3>
              </mat-card-content>
            </mat-card>
          </mat-step>
        </mat-horizontal-stepper>
      </mat-card-content>
    </mat-card>
    

    Parent TS:

    import { Component, OnInit, ViewChild, AfterViewInit, ViewEncapsulation } from '@angular/core';
    import { MatStepper } from '@angular/material/stepper';
    
    @Component({
      selector: 'app-step',
      templateUrl: './step.component.html',
      styleUrls: ['./step.component.css'],
      encapsulation: ViewEncapsulation.None
    })
    export class StepComponent implements OnInit, AfterViewInit {
    
      @ViewChild('stepper', { static: false }) stepper: MatStepper;
    
      constructor() { }
    
      ngOnInit() {
      }
    
      ngAfterViewInit() {
        this.Initialize()
      }
      Initialize() {
        this.stepper.steps.forEach(Step => {
          Step.completed = false;
        });
      }
    
      CompleteStep(stepNumer: number) {
        debugger;
        this.stepper.steps.get(stepNumer).completed = true;
        this.stepper.next();
      }
    
    }
    

    Child Html:

      <button (click)='this.Complete()' mat-raised-button color='accent'>Complete</button>
    

    Child TS:

    import { Component, EventEmitter, OnInit, Output } from '@angular/core';
    
    @Component({
      selector: 'app-comp1',
      templateUrl: './comp1.component.html',
      styleUrls: ['./comp1.component.css']
    })
    export class Comp1Component implements OnInit {
    
      @Output() CompleteStep = new EventEmitter<any>();
    
      constructor() { }
    
      ngOnInit(): void {
      }
    
      Complete() {
        this.CompleteStep.emit();
      }
    }