Search code examples
angularng-templatemat-stepperangular-material-11

Angular Material Stepper custom icons ng-template has undefined context variable


I am trying to use an ng-template with the matStepperIcon property to override the default Angular Material's matStepper's icons. I also want to pass some data, and I tried using ng-container and *ngTemplateOutlet, however it only partially works.

As you can see from the following code, I would expect the print function to always print a defined value, however, after correctly printing the seven IDs of the steps it prints undefined seven times. Why is that and how do I prevent that from happening?

<mat-horizontal-stepper labelPosition="bottom">
  <ng-container *ngFor="let step of form.steps">
    <ng-template #ref let-step="id" matStepperIcon="edit">
      <mat-icon>clear</mat-icon>
      {{ print(step) }}
    </ng-template>
    
    <ng-template #ref let-step="id" matStepperIcon="number">
      <mat-icon>clear</mat-icon>
      {{ print(step) }}
    </ng-template>

    <ng-template #ref let-step="id" matStepperIcon="done">
      <mat-icon>clear</mat-icon>
      {{ print(step) }}
    </ng-template>
    
    <ng-container *ngTemplateOutlet="ref; context: step"></ng-container>

    <mat-step>
      <ng-template matStepLabel>
        {{ step.id }}:
        {{ translateService.currentLang === "it" ? step.name : step.nameEn }}
      </ng-template>

      <!-- Step content-->
      </mat-step>
  </ng-container>
</mat-horizontal-stepper>

Link to StackBlitz example

Changing my code to the following, on the other hand, causes the print function to print all the correct IDs but it also prints the ID of the last step 8 times.

<mat-horizontal-stepper labelPosition="bottom">
  <ng-container *ngFor="let step of form.steps">
    <ng-template #ref matStepperIcon="edit">
      {{ print(step.id) }}
      <mat-icon>clear</mat-icon>
    </ng-template>

    <ng-template #ref matStepperIcon="number">
      {{ print(step.id) }}
      <mat-icon>clear</mat-icon>
    </ng-template>

    <ng-template #ref matStepperIcon="done">
      {{ print(step.id) }}
      <mat-icon>clear</mat-icon>
    </ng-template>

    <ng-container *ngTemplateOutlet="ref; context: step"></ng-container>

    <mat-step>
      <ng-template matStepLabel>
        {{ step.id }}:
        {{ translateService.currentLang === "it" ? step.name : step.nameEn }}
      </ng-template>

      <!-- Step content-->
    </mat-step>
  </ng-container>
</mat-horizontal-stepper>

Solution

  • If you want to use custom icons with MatStepper you should do several things:

    • Teach library to take into account your set of icons.

    We could disable displayDefaultIndicatorType like

    providers: [{
       provide: STEPPER_GLOBAL_OPTIONS, useValue: {displayDefaultIndicatorType: false}
    }]
    

    But it won't help since Angular Material has predefined logic to show specific icon depending on internal step state https://github.com/angular/components/blob/28c36f8a02f72e51a4d6c6a797e2f913e5dede9b/src/cdk/stepper/stepper.ts#L442-L465

    So, you can override base logic like here https://github.com/angular/components/issues/18307

    @ViewChild(MatHorizontalStepper, { static: true }) stepper: MatHorizontalStepper;
    
    ngOnInit() {
      this.stepper._getIndicatorType = (i, state) => state || 'number';
    }
    
    • Decide which icon to show depending on step

      <mat-step [state]="step.state">
      
      
      steps: [
        {
          id: 218,
          name: 'Dati richiedente',
          nameEn: 'Applicant data',
          state: 'home',
        },
        {
          id: 219,
          name: 'Richiesta Meeting',
          nameEn: 'Meeting request',
          state: 'edit'
        },
      
    • Use built-in matStepperIcon context to access rendering index. With this index you can access corresponding step:

      <ng-template let-i="index" matStepperIcon="home">
        <mat-icon>home</mat-icon>
        {{ print(form.steps[i]) }}
      </ng-template>
      

    Forked Stackblitz