Reputation: 291
How to switch between mat-vertical-stepper and mat-horizontal-stepper from angular component with same stepper steps?
Upvotes: 28
Views: 19932
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
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>
Upvotes: 8
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
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
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
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
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
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