问题
How to switch between mat-vertical-stepper and mat-horizontal-stepper from angular component with same stepper steps?
回答1:
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;
});
}
}
回答2:
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';
}
回答3:
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);
}
回答4:
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>
回答5:
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();
});
}
}
来源:https://stackoverflow.com/questions/48032538/switch-between-vertical-and-horizontal-stepper-material