Ashish Gurjar
Ashish Gurjar

Reputation: 291

Switch between vertical and horizontal stepper material

How to switch between mat-vertical-stepper and mat-horizontal-stepper from angular component with same stepper steps?

Upvotes: 28

Views: 19932

Answers (8)

Vega
Vega

Reputation: 28738

No need to duplicate your templates. Implement the screen size checker from the answer https://stackoverflow.com/a/52989737/5468463, and then:

HTML:

<mat-stepper [orientation]="orientation">
...

Typescript:

...
    orientation: StepperOrientation = "horizontal";
...
    breakpointObserver.observe([
      Breakpoints.XSmall,
      Breakpoints.Small
    ]).subscribe(result => {
      this.orientation = result.matches ? 'vertical' : 'horizontal';
    })

Upvotes: 0

developer033
developer033

Reputation: 24894

Since Angular 12 (specifically these PRs: https://github.com/angular/components/pull/21940 and https://github.com/angular/components/pull/22139) you can now just use MatStepper (note that both MatHorizontalStepper and MatVerticalStepper were deprecated in these PRs) and set orientation input as you need:

<mat-stepper [orientation]="orientation">
  <mat-step>Step 1</mat-step>
  <mat-step>Step 2</mat-step>
</mat-stepper>

DEMO

Upvotes: 8

lodey
lodey

Reputation: 304

here is how i did there is two ways one is using css property and another is using fxLayout which angular provide. so its same i hope you will know how to use css so i will show you how to make using fxLayout. you can check out about fxLayout in https://tburleson-layouts-demos.firebaseapp.com/#/docs

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<mat-horizontal-stepper linear fxHide.lt-md>
          <mat-step [stepControl]="firstFormGroup" editable="true">
            <form [formGroup]="firstFormGroup">
              <ng-template matStepLabel>Fill out your name</ng-template>
              <mat-form-field>
                <mat-label>Name</mat-label>
                <input matInput formControlName="firstCtrl" placeholder="Last name, First name" required>
              </mat-form-field>
              <div>
                <button mat-button matStepperNext>Next</button>
              </div>
            </form>
          </mat-step>
          <mat-step [stepControl]="secondFormGroup" editable="true">
            <form [formGroup]="secondFormGroup">
              <ng-template matStepLabel>Fill out your address</ng-template>
              <mat-form-field>
                <mat-label>Address</mat-label>
                <input matInput formControlName="secondCtrl" placeholder="Ex. 1 Main St, New York, NY"
                       required>
              </mat-form-field>
              <div>
                <button mat-button matStepperPrevious>Back</button>
                <button mat-button matStepperNext>Next</button>
              </div>
            </form>
          </mat-step>
          <mat-step>
            <ng-template matStepLabel>Done</ng-template>
            <p>You are now done.</p>
            <div>
              <button mat-button matStepperPrevious>Back</button>
              <button mat-button (click)="done()">done</button>
            </div>
          </mat-step>
        </mat-horizontal-stepper>
        
        
        
        
        <mat-vertical-stepper linear fxHide.gt-sm>
          <mat-step [stepControl]="firstFormGroup" editable="true">
            <form [formGroup]="firstFormGroup">
              <ng-template matStepLabel>Fill out your name</ng-template>
              <mat-form-field>
                <mat-label>Name</mat-label>
                <input matInput formControlName="firstCtrl" placeholder="Last name, First name" required>
              </mat-form-field>
              <div>
                <button mat-button matStepperNext>Next</button>
              </div>
            </form>
          </mat-step>
          <mat-step [stepControl]="secondFormGroup" editable="true">
            <form [formGroup]="secondFormGroup">
              <ng-template matStepLabel>Fill out your address</ng-template>
              <mat-form-field>
                <mat-label>Address</mat-label>
                <input matInput formControlName="secondCtrl" placeholder="Ex. 1 Main St, New York, NY"
                       required>
              </mat-form-field>
              <div>
                <button mat-button matStepperPrevious>Back</button>
                <button mat-button matStepperNext>Next</button>
              </div>
            </form>
          </mat-step>
          <mat-step>
            <ng-template matStepLabel>Done</ng-template>
            <p>You are now done.</p>
            <div>
              <button mat-button matStepperPrevious>Back</button>
              <button mat-button (click)="done()">done</button>
            </div>
          </mat-step>
        </mat-horizontal-stepper>

Upvotes: 0

Matej Maloča
Matej Maloča

Reputation: 974

import { Directionality } from '@angular/cdk/bidi';
import { CdkStep, StepperSelectionEvent } from '@angular/cdk/stepper';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Inject, Input, Optional, Output, QueryList, ViewChildren } from '@angular/core';
import { MatStep, MatStepper } from '@angular/material';
import { DOCUMENT } from '@angular/platform-browser';

const MAT_STEPPER_PROXY_FACTORY_PROVIDER = {
    provide: MatStepper,
    deps: [forwardRef(() => StepperComponent), [new Optional(), Directionality], ChangeDetectorRef, [new Inject(DOCUMENT)]],
    useFactory: MAT_STEPPER_PROXY_FACTORY
};

export function MAT_STEPPER_PROXY_FACTORY(component: StepperComponent, directionality: Directionality,
    changeDetectorRef: ChangeDetectorRef, docuement: Document) {
    // We create a fake stepper primarily so we can generate a proxy from it.  The fake one, however, is used until 
    // our view is initialized.  The reason we need a proxy is so we can toggle between our 2 steppers 
    // (vertical and horizontal) depending on  our "orientation" property.  Probably a good idea to include a polyfill 
    // for the Proxy class: https://github.com/GoogleChrome/proxy-polyfill.

    const elementRef = new ElementRef(document.createElement('mat-horizontal-stepper'));
    const stepper = new MatStepper(directionality, changeDetectorRef, elementRef, document);
    return new Proxy(stepper, {
        get: (target, property) => Reflect.get(component.stepper || target, property),
        set: (target, property, value) => Reflect.set(component.stepper || target, property, value)
    });
}

@Component({
    selector: 'app-stepper',
    // templateUrl: './stepper.component.html',
    // styleUrls: ['./stepper.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [MAT_STEPPER_PROXY_FACTORY_PROVIDER],
    template: `
<ng-container [ngSwitch]="orientation">
    <mat-horizontal-stepper *ngSwitchCase="'horizontal'"
                            [labelPosition]="labelPosition"
                            [linear]="linear"
                            [selected]="selected"
                            [selectedIndex]="selectedIndex"
                            (animationDone)="animationDone.emit($event)"
                            (selectionChange)="selectionChange.emit($event)">
    </mat-horizontal-stepper>


    <mat-vertical-stepper *ngSwitchDefault
                            [linear]="linear"
                            [selected]="selected"
                            [selectedIndex]="selectedIndex"
                            (animationDone)="animationDone.emit($event)"
                            (selectionChange)="selectionChange.emit($event)">
    </mat-vertical-stepper>
</ng-container>
`
})
export class StepperComponent {
    // public properties
    @Input() labelPosition?: 'bottom' | 'end';
    @Input() linear?: boolean;
    @Input() orientation?: 'horizontal' | 'vertical';
    @Input() selected?: CdkStep;
    @Input() selectedIndex?: number;

    // public events
    @Output() animationDone = new EventEmitter<void>();
    @Output() selectionChange = new EventEmitter<StepperSelectionEvent>();

    // internal properties
    @ViewChildren(MatStepper) stepperList!: QueryList<MatStepper>;
    @ContentChildren(MatStep) steps!: QueryList<MatStep>;
    get stepper(): MatStepper { return this.stepperList && this.stepperList.first; }

    // private properties
    private lastSelectedIndex?: number;
    private needsFocus = false;

    // public methods
    constructor(private changeDetectorRef: ChangeDetectorRef) { }
    ngAfterViewInit() {
        this.reset();
        this.stepperList.changes.subscribe(() => this.reset());
        this.selectionChange.subscribe((e: StepperSelectionEvent) => this.lastSelectedIndex = e.selectedIndex);
    }
    ngAfterViewChecked() {
        if (this.needsFocus) {
            this.needsFocus = false;
            const { _elementRef, _keyManager, selectedIndex } = <any>this.stepper;
            _elementRef.nativeElement.focus();
            _keyManager.setActiveItem(selectedIndex);
        }
    }

    // private properties
    private reset() {
        const { stepper, steps, changeDetectorRef, lastSelectedIndex } = this;
        stepper.steps.reset(steps.toArray());
        stepper.steps.notifyOnChanges();
        if (lastSelectedIndex) {
            stepper.selectedIndex = lastSelectedIndex;
        }

        Promise.resolve().then(() => {
            this.needsFocus = true;
            changeDetectorRef.markForCheck();
        });
    }
}

Upvotes: 0

Joey Gough
Joey Gough

Reputation: 3103

to avoid rewriting identical html content, do like this. Create the template and give them a reference using a #hashtag. then you can instert them using ng-container *ngTemplateOutlet="hashtag"></ng-container>.

here is an example of making a responsive stepepr, the angular material way.

<ng-template #stepOne>
  <div>step one</div>
</ng-template>

<ng-template #stepTwo>
  <div>step two</div>
</ng-template>

<ng-template #stepThree>
  <div>step three</div>
</ng-template>

<ng-template #stepFour>
  <div>step four</div>
</ng-template>

<ng-template [ngIf]="smallScreen" [ngIfElse]="bigScreen">
  <mat-vertical-stepper linear #stepper >
    <mat-step>
      <ng-container *ngTemplateOutlet="stepOne"></ng-container>
    </mat-step>
    <mat-step>
      <ng-container *ngTemplateOutlet="stepTwo"></ng-container>
    </mat-step>
    <mat-step>
      <ng-container *ngTemplateOutlet="stepThree"></ng-container>
    </mat-step>
    <mat-step>
      <ng-container *ngTemplateOutlet="stepFour"></ng-container>
    </mat-step>
  </mat-vertical-stepper>
</ng-template>

<ng-template #bigScreen>
  <mat-horizontal-stepper linear #stepper >
    <mat-step>
      <ng-container *ngTemplateOutlet="stepOne"></ng-container>
    </mat-step>
    <mat-step >
      <ng-container *ngTemplateOutlet="stepTwo"></ng-container>
    </mat-step>
    <mat-step>
      <ng-container *ngTemplateOutlet="stepThree"></ng-container>
    </mat-step>
    <mat-step>
      <ng-container *ngTemplateOutlet="stepFour"></ng-container>
    </mat-step>
  </mat-horizontal-stepper>
</ng-template>

You can use the angular cdk layout to track screen size like this.

import { Component } from '@angular/core';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';

@Component({
  selector: 'app-responsive-stepper',
  templateUrl: './responsive-stepper.component.html',
  styleUrls: ['./responsive-stepper.component.scss']
})
export class ResponsiveStepperComponent implements OnInit {

    smallScreen: boolean;

    constructor(
       private breakpointObserver: BreakpointObserver
      ) {
        breakpointObserver.observe([
          Breakpoints.XSmall,
          Breakpoints.Small
        ]).subscribe(result => {
          this.smallScreen = result.matches;
      });
     }
}

Upvotes: 18

Aaron Hazelton
Aaron Hazelton

Reputation: 789

I wanted to do the same thing and finally figured out how to get it to work, with complete transclusion for the steps, etc, plus you can sync the currently-selected index between the horizontal and vertical so that the page size changing won't reset the person back to step 1.

Here's a complete example.

Wrapper component HTML:

<ng-template #horizontal>
  <mat-horizontal-stepper #stepper
    [linear]="isLinear"
    (selectionChange)="selectionChanged($event)">
    <mat-step *ngFor="let step of steps; let i = index"
      [stepControl]="step.form"
      [label]="step.label"
      [optional]="step.isOptional">
      <ng-container *ngTemplateOutlet="step.template"></ng-container>
      <div class="actions">
        <div class="previous">
          <button *ngIf="i > 0" 
            type="button" mat-button
            color="accent"
            (click)="reset()"
            matTooltip="All entries will be cleared">Start Over</button>
          <button *ngIf="i > 0"
            type="button" mat-button
            matStepperPrevious>Previous</button>
        </div>
        <div class="next">
          <button type="button" mat-button
            color="primary"
            matStepperNext
            (click)="step.submit()">{{i === steps.length - 1 ? 'Finish' : 'Next'}}</button>
        </div>
      </div>
    </mat-step>
  </mat-horizontal-stepper>
</ng-template>

<ng-template #vertical>
  <mat-vertical-stepper #stepper
    [linear]="isLinear"
    (selectionChange)="selectionChanged($event)">
    <mat-step *ngFor="let step of steps; let i = index"
      [stepControl]="step.form"
      [label]="step.label"
      [optional]="step.isOptional">
      <ng-container *ngTemplateOutlet="step.template"></ng-container>
      <div class="actions">
        <div class="previous">
          <button *ngIf="i > 0" 
            type="button" mat-button
            color="accent"
            (click)="reset()"
            matTooltip="All entries will be cleared">Start Over</button>
          <button *ngIf="i > 0"
            type="button" mat-button
            matStepperPrevious>Previous</button>
        </div>
        <div class="next">
          <button type="button" mat-button
            color="primary"
            matStepperNext
            (click)="step.submit()">{{i === steps.length - 1 ? 'Finish' : 'Next'}}</button>
        </div>
      </div>
    </mat-step>
  </mat-vertical-stepper>
</ng-template>

Wrapper component ts:

import { Component, OnInit, OnDestroy, Input, ContentChildren, QueryList, ViewChild, TemplateRef } from '@angular/core';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { StepComponent } from './step/step.component';
import { Subscription } from 'rxjs';
import { MatStepper } from '@angular/material';

@Component({
  selector: 'stepper',
  templateUrl: './stepper.component.html',
  styleUrls: ['./stepper.component.scss']
})
export class StepperComponent implements OnInit, OnDestroy {

  public selectedIndex: number = 0;
  public isMobile: boolean;
  public template: TemplateRef<any>;
  @Input() isLinear: boolean = true;
  @Input() startAtIndex: number;
  @ContentChildren(StepComponent) private steps: QueryList<StepComponent>;
  @ViewChild('horizontal') templHorizontal: TemplateRef<any>;
  @ViewChild('vertical') templVertical: TemplateRef<any>;
  @ViewChild('stepper') stepper: MatStepper;

  private _bpSub: Subscription;

  constructor(private bpObserver: BreakpointObserver) { }

  ngOnInit() {
    this._bpSub = this.bpObserver
      .observe(['(max-width: 599px)'])
      .subscribe((state: BreakpointState) => {
        this.setMobileStepper(state.matches);
      });

    if (this.startAtIndex) {
      this.selectedIndex = this.startAtIndex;
    }
  }

  selectionChanged(event: any): void {
    this.selectedIndex = event.selectedIndex;
  }

  setMobileStepper(isMobile: boolean): void {
    this.isMobile = isMobile;
    if (isMobile) {
      this.template = this.templVertical;
    }
    else {
      this.template = this.templHorizontal;
    }
    setTimeout(() => {
      // need async call since the ViewChild isn't ready
      // until after this function runs, thus the setTimeout hack
      this.stepper.selectedIndex = this.selectedIndex;
    });
  }

  reset(): void {
    this.stepper.reset();
  }

  ngOnDestroy(): void {
    this._bpSub.unsubscribe();
  }

}

Step component HTML:

<ng-template #template>
  <ng-content></ng-content>
</ng-template>

Step component ts:

import { Component, Input, Output, TemplateRef, ViewChild, EventEmitter } from '@angular/core';
import { FormGroup } from '@angular/forms';

@Component({
  selector: 'stepper-step',
  templateUrl: './step.component.html',
  styleUrls: ['./step.component.scss']
})
export class StepComponent {

  @Input() isOptional: boolean = false;
  @Input() label: string;
  @Input() form: FormGroup;
  @ViewChild('template') template: TemplateRef<any>;
  @Output() formSubmitted: EventEmitter<any> = new EventEmitter();

  constructor() { }

  submit(): void {
    this.formSubmitted.emit(this.form.value);
  }

}

Using responsive Angular Material Stepper in component HTML:

<stepper>
  <stepper-step
    label="Step 1 label"
    [form]="step1form"
    (formSubmitted)="form1Submit($event)">
    content
    <form [formGroup]="frmStep1">
      <mat-form-field>
        <input matInput name="firstname" formControlName="firstname" placeholder="First Name" />
      </mat-form-field>
      content
    </form>
  </stepper-step>
  <stepper-step
    label="Step 2 label"
    [form]="step2form">
    step 2 content
  </stepper-step>
</stepper>

And the component function needed for forms:

form1Submit(formValues: any): void {
  console.log(formValues);
}

Upvotes: 5

Samshel
Samshel

Reputation: 928

I use Teradata's Covalent components alongside Google's Material components. They use material design and the modules are even imported in the same manner as Google's material modules.

Covalent's stepper is setup with a mode input in mind so you would implement the HTML template like this:

<td-steps [mode]="stepperMode">
  <td-step>
    ...
  </td-step>
  ...
</td-steps>

Then in your component's typescript file you can setup the variable to be either horizontal or vertical according to your needs:

if (condition) {
  stepperMode = 'horizontal';
} else {
  stepperMode = 'vertical';
}

Upvotes: 8

Bilal Alam
Bilal Alam

Reputation: 894

you might want to create two separate steppers and use *ngIf to switch between them

<mat-vertical-stepper *ngIf="verticalFlag">
  <mat-step>
  </mat-step>
</mat-vertical-stepper>

<mat-horizontal-stepper *ngIf="!verticalFlag">
  <mat-step>
  </mat-step>
</mat-horizontal-stepper>

Upvotes: 0

Related Questions