【发布时间】:2017-12-30 09:15:37
【问题描述】:
如何在mat-vertical-stepper和mat-horizontal-stepper之间切换具有相同步进步骤的角度分量?
【问题讨论】:
-
Angular 不只是使用 mat-stepper 并避免所有这些水平垂直分布,这是创造的奇迹之一。当前的方法使响应时间过长。哦,角....
如何在mat-vertical-stepper和mat-horizontal-stepper之间切换具有相同步进步骤的角度分量?
【问题讨论】:
由于 Angular 12(特别是这些 PR:https://github.com/angular/components/pull/21940 和 https://github.com/angular/components/pull/22139),您现在可以只使用 MatStepper(请注意,这些 PR 中不推荐使用 MatHorizontalStepper 和 MatVerticalStepper)并设置方向输入为您需要:
<mat-stepper [orientation]="orientation">
<mat-step>Step 1</mat-step>
<mat-step>Step 2</mat-step>
</mat-stepper>
【讨论】:
为避免重写相同的 html 内容,请这样做。创建模板并使用#hashtag 为他们提供参考。然后您可以使用ng-container *ngTemplateOutlet="hashtag"></ng-container> 插入它们。
这是一个制作响应式 stepepr 的示例,即角度材质方式。
<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>
您可以像这样使用角度 cdk 布局来跟踪屏幕大小。
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;
});
}
}
【讨论】:
matStepperNext更改为(click)="stepper.next()",你可以将matStepLabel放在ng-container之前
我将Teradata's Covalent 组件与 Google 的 Material 组件一起使用。他们使用材料设计,甚至以与 Google 的材料模块相同的方式导入模块。
Covalent 的步进器在设置时考虑了模式输入,因此您可以像这样实现 HTML 模板:
<td-steps [mode]="stepperMode">
<td-step>
...
</td-step>
...
</td-steps>
然后在组件的打字稿文件中,您可以根据需要将变量设置为水平或垂直:
if (condition) {
stepperMode = 'horizontal';
} else {
stepperMode = 'vertical';
}
【讨论】:
我想做同样的事情,最后想出了如何让它工作,完全嵌入步骤等,另外你可以在水平和垂直之间同步当前选择的索引,以便页面大小发生变化不会将该人重置回第 1 步。
这是一个完整的例子。
包装器组件 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>
包装组件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();
}
}
步骤组件 HTML:
<ng-template #template>
<ng-content></ng-content>
</ng-template>
步骤组件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);
}
}
在组件 HTML 中使用响应式 Angular Material Stepper:
<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>
以及表单需要的组件功能:
form1Submit(formValues: any): void {
console.log(formValues);
}
【讨论】:
<ng-container *ngTemplateOutlet="template"></ng-container>
@Input() form: FormGroup;,您应该将其更改为两种方式绑定,例如 formGroupValue: FormGroup; @Input() get formGroup() { return this.formGroupValue; } @Output() formGroupChange = new EventEmitter(); set formGroup(value) { this.formGroupValue = value; this.formGroupChange.emit(this.formGroupValue); } (blog.thoughtram.io/angular/2016/10/13/…)。在父对象中实例化。做[(formGroup)]="formGroup"。然后验证将起作用。
您可能想要创建两个单独的步进器并使用 *ngIf 在它们之间切换
<mat-vertical-stepper *ngIf="verticalFlag">
<mat-step>
</mat-step>
</mat-vertical-stepper>
<mat-horizontal-stepper *ngIf="!verticalFlag">
<mat-step>
</mat-step>
</mat-horizontal-stepper>
【讨论】:
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();
});
}
}
【讨论】:
No provider for CdkStepper!,现在 DOCUMENT 来自 angular/common。
这就是我的做法,有两种方法,一种是使用 css 属性,另一种是使用 angular 提供的 fxLayout。所以它是一样的,我希望你会知道如何使用 css,所以我将向你展示如何使用 fxLayout。您可以在 https://tburleson-layouts-demos.firebaseapp.com/#/docs
中查看 fxLayout<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>
【讨论】: