【问题标题】:Switch between vertical and horizontal stepper material在垂直和水平步进材料之间切换
【发布时间】:2017-12-30 09:15:37
【问题描述】:

如何在ma​​t-vertical-stepperma​​t-h​​orizo​​​​ntal-stepper之间切换具有相同步进步骤的角度分量?

【问题讨论】:

  • Angular 不只是使用 mat-stepper 并避免所有这些水平垂直分布,这是创造的奇迹之一。当前的方法使响应时间过长。哦,角....

标签: angular angular-material2


【解决方案1】:

由于 Angular 12(特别是这些 PR:https://github.com/angular/components/pull/21940https://github.com/angular/components/pull/22139),您现在可以只使用 MatStepper(请注意,这些 PR 中不推荐使用 MatHorizontalStepperMatVerticalStepper)并设置方向输入为您需要:

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

DEMO

【讨论】:

    【解决方案2】:

    为避免重写相同的 html 内容,请这样做。创建模板并使用#hashtag 为他们提供参考。然后您可以使用ng-container *ngTemplateOutlet="hashtag"&gt;&lt;/ng-container&gt; 插入它们。

    这是一个制作响应式 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;
          });
         }
    }
    

    【讨论】:

    • 如果你放入 ng-template matStepperNext、matStepLabel 等将不起作用。所以它不是一个真正使用过的解决方案
    • 如果你想让模板中的步进器从matStepperNext更改为(click)="stepper.next()",你可以将matStepLabel放在ng-container之前
    【解决方案3】:

    我将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';
    }
    

    【讨论】:

      【解决方案4】:

      我想做同样的事情,最后想出了如何让它工作,完全嵌入步骤等,另外你可以在水平和垂直之间同步当前选择的索引,以便页面大小发生变化不会将该人重置回第 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);
      }
      

      【讨论】:

      • 真的很棒,非常感谢。但是在包装器组件 HTML 中,您忘记了包含模板:&lt;ng-container *ngTemplateOutlet="template"&gt;&lt;/ng-container&gt;
      • 非常好,干得好。但对我来说,这仍然不完美。请参阅此示例中的stackblitz.com/angular/… - 当在未填写第一步的情况下单击第二步时,将触发第一步的 mat-error。您的版本中似乎不存在此行为。我已经复制了您的示例并将所需的验证器添加到表单组中 - 这种行为不存在.....您对此有什么想法吗?
      • 听起来正确。我在这里主要感兴趣的是展示如何使组件响应而不是重置人的进度。显然,需要考虑表单验证,因为每个设置可能不同,有些设置允许您在没有完成的情况下进入另一个步骤,而通常它可能会像您概述的那样。看起来你已经为你的案件处理了这部分。
      • 我找到了方法。在您的 StepComponent 中有 @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"。然后验证将起作用。
      • 你们有这方面的工作例子吗? Stackblitz 会很棒 :-)
      【解决方案5】:

      您可能想要创建两个单独的步进器并使用 *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>
      

      【讨论】:

      • 但是步进步骤有太多的html代码我不能重复。
      • 我猜不可能以编程方式更改指令,如果是这种情况,这是您唯一的选择,但是,为了使其更具可读性,您可以创建一个新组件,即 stepper.component并将步进器的代码与其余代码分开。
      • 有趣的是,Material sidenav 有一个模式。 Over,Side等。这是我根据断点切换的东西。但我不能用步进器做到这一点。我希望我可以并且也许提交功能请求是一种选择,但是在那之前我喜欢创建自己的具有模式的组件的想法。我自己在水平和垂直步进器之间切换,只需使用内容投影和输入来获取我的数据。
      • 好像有人已经建议过了。 github.com/angular/material2/issues/7700
      • 如何连接两个步进器?就像标志改变时一样......在同一个步骤上?
      【解决方案6】:
      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。
      【解决方案7】:

      这就是我的做法,有两种方法,一种是使用 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>

      【讨论】: