Elazar Zadiki
Elazar Zadiki

Reputation: 1137

Material Mat-Stepper horizontal - How to position the header below the content?

Im using the horizontal mat-stepper as shown in this stackblitz: stackblitz here

I want to position the header (where the steps are) below the content rather than above it. I see its easily done when just swapping the elements one below the other in the Elements tab inside Chrome devtools, but those elements arent exposed. how would I do that? thanks.

Upvotes: 0

Views: 4056

Answers (2)

Gieted
Gieted

Reputation: 895

There is no official way of doing this.

But you can "hack" into the stepper component very easily using attribute directive.

First execute ng generate directive stepper-position to generate new directive. Then go to stepper-postion.directive.ts and paste this code:

import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';

@Directive({
  selector: '[appStepperPosition]'
})
export class StepperPositionDirective implements AfterViewInit {
  @Input('appStepperPosition') position: 'top' | 'bottom';
  element: any;

  constructor(private elementRef: ElementRef) {
    this.element = elementRef.nativeElement;
  }

  ngAfterViewInit(): void {
    if (this.position === 'bottom') {
      const header = this.element.children[0];
      const content = this.element.children[1];
      this.element.insertBefore(content, header);
    }
  }
}

At last go to a html template where you've declared your mat-horizontal-stepper and add appStepperPosition="bottom" attribute.

For example: <mat-horizontal-stepper appStepperPosition="bottom" [linear]="isLinear" #stepper>

And now you have stepper content above the header 😀

Upvotes: 2

Todd Skelton
Todd Skelton

Reputation: 7239

You'll need to create a custom stepper component. I had to do this because I wanted the vertical stepper to show a summary of the steps that were completed. Luckily, all the component code is on GitHub.

Copy over a couple the files from https://github.com/angular/components/tree/master/src/material/stepper into a component folder.

  • stepper-horizontal.html
  • stepper.scss

Create a new custom stepper component that uses the new layout. I'll call it CustomHorizontalStepper.

custom-horizontal-stepper.ts
import { MatStepper, matStepperAnimations } from "@angular/material";

@Component({
  selector: 'custom-horizontal-stepper',
  exportAs: 'customHorizontalStepper',
  templateUrl: 'stepper-horizontal.html',
  styleUrls: ['stepper.css'],
  inputs: ['selectedIndex'],
  host: {
    'class': 'mat-stepper-horizontal',
    '[class.mat-stepper-label-position-end]': 'labelPosition == "end"',
    '[class.mat-stepper-label-position-bottom]': 'labelPosition == "bottom"',
    'aria-orientation': 'horizontal',
    'role': 'tablist',
  },
  animations: [matStepperAnimations.horizontalStepTransition],
  providers: [
    {provide: MatStepper, useExisting: CustomHorizontalStepper},
    {provide: CdkStepper, useExisting: CustomHorizontalStepper}
  ],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomHorizontalStepper extends MatStepper {
  /** Whether the label should display in bottom or end position. */
  @Input()
  labelPosition: 'bottom' | 'end' = 'end';

  static ngAcceptInputType_editable: BooleanInput;
  static ngAcceptInputType_optional: BooleanInput;
  static ngAcceptInputType_completed: BooleanInput;
  static ngAcceptInputType_hasError: BooleanInput;
}

Alter the stepper-horizontal.html file.

stepper-horizontal.html
<div class="mat-horizontal-content-container">
  <div *ngFor="let step of steps; let i = index"
       class="mat-horizontal-stepper-content" role="tabpanel"
       [@stepTransition]="_getAnimationDirection(i)"
       (@stepTransition.done)="_animationDone.next($event)"
       [id]="_getStepContentId(i)"
       [attr.aria-labelledby]="_getStepLabelId(i)"
       [attr.aria-expanded]="selectedIndex === i">
    <ng-container [ngTemplateOutlet]="step.content"></ng-container>
  </div>
</div>

<div class="mat-horizontal-stepper-header-container">
  <ng-container *ngFor="let step of steps; let i = index; let isLast = last">
    <mat-step-header class="mat-horizontal-stepper-header"
                     (click)="step.select()"
                     (keydown)="_onKeydown($event)"
                     [tabIndex]="_getFocusIndex() === i ? 0 : -1"
                     [id]="_getStepLabelId(i)"
                     [attr.aria-posinset]="i + 1"
                     [attr.aria-setsize]="steps.length"
                     [attr.aria-controls]="_getStepContentId(i)"
                     [attr.aria-selected]="selectedIndex == i"
                     [attr.aria-label]="step.ariaLabel || null"
                     [attr.aria-labelledby]="(!step.ariaLabel && step.ariaLabelledby) ? step.ariaLabelledby : null"
                     [index]="i"
                     [state]="_getIndicatorType(i, step.state)"
                     [label]="step.stepLabel || step.label"
                     [selected]="selectedIndex === i"
                     [active]="step.completed || selectedIndex === i || !linear"
                     [optional]="step.optional"
                     [errorMessage]="step.errorMessage"
                     [iconOverrides]="_iconOverrides"
                     [disableRipple]="disableRipple">
    </mat-step-header>
    <div *ngIf="!isLast" class="mat-stepper-horizontal-line"></div>
  </ng-container>
</div>

Register your component in the module and use it as you would the regular stepper. Just use the new selector.

<custom-horizontal-stepper></custom-horizontal-stepper>

Upvotes: 1

Related Questions