-
-
Save davideas/3534a5c2567d6668013426ab961990f6 to your computer and use it in GitHub Desktop.
| import { Directionality } from '@angular/cdk/bidi'; | |
| import { CdkStep, CdkStepper, StepperSelectionEvent } from '@angular/cdk/stepper'; | |
| import { | |
| AfterViewChecked, | |
| AfterViewInit, | |
| 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/common'; | |
| const MAT_STEPPER_PROXY_FACTORY_PROVIDER = { | |
| provide: MatStepper, | |
| deps: [ | |
| forwardRef(() => ResponsiveStepperComponent), | |
| [new Optional(), Directionality], | |
| ChangeDetectorRef, | |
| [new Inject(DOCUMENT)] | |
| ], | |
| useFactory: MAT_STEPPER_PROXY_FACTORY | |
| }; | |
| const CDK_STEPPER_PROXY_FACTORY_PROVIDER = { ...MAT_STEPPER_PROXY_FACTORY_PROVIDER, provide: CdkStepper }; | |
| export function MAT_STEPPER_PROXY_FACTORY(component: ResponsiveStepperComponent, directionality: Directionality, | |
| changeDetectorRef: ChangeDetectorRef, document: 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) | |
| }); | |
| } | |
| /** | |
| * Configurable vertical/horizontal layout.<br> | |
| * Keeps input fields state.<br> | |
| * Allow to make headers un-clickable (disabled) with normal cursor: see updateStepState(). | |
| * | |
| * Authors: @grant77, @davideas | |
| */ | |
| @Component({ | |
| selector: 'responsive-stepper', | |
| // templateUrl: './stepper.component.html', | |
| // styleUrls: ['./stepper.component.scss'], | |
| changeDetection: ChangeDetectionStrategy.OnPush, | |
| providers: [ | |
| MAT_STEPPER_PROXY_FACTORY_PROVIDER, | |
| CDK_STEPPER_PROXY_FACTORY_PROVIDER | |
| ], | |
| template: ` | |
| <ng-container [ngSwitch]="orientation"> | |
| <mat-horizontal-stepper *ngSwitchDefault | |
| [labelPosition]="labelPosition" | |
| [linear]="linear" | |
| [selected]="selected" | |
| [selectedIndex]="selectedIndex" | |
| (animationDone)="animationDone.emit($event)" | |
| (selectionChange)="selectionChange.emit($event)"> | |
| </mat-horizontal-stepper> | |
| <mat-vertical-stepper *ngSwitchCase="'vertical'" | |
| [linear]="linear" | |
| [selected]="selected" | |
| [selectedIndex]="selectedIndex" | |
| (animationDone)="animationDone.emit($event)" | |
| (selectionChange)="selectionChange.emit($event)"> | |
| </mat-vertical-stepper> | |
| </ng-container>` | |
| }) | |
| export class ResponsiveStepperComponent implements AfterViewInit, AfterViewChecked { | |
| // 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>(); | |
| @Output() orientationChange = new EventEmitter<string>(); | |
| // 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; | |
| private htmlSteps: Array<HTMLElement> = []; | |
| constructor(private changeDetectorRef: ChangeDetectorRef) { | |
| } | |
| ngAfterViewInit() { | |
| this.reset(); | |
| this.stepperList.changes.subscribe(() => this.reset()); | |
| // Emitted from (animationDone) event | |
| this.selectionChange.subscribe((e: StepperSelectionEvent) => this.lastSelectedIndex = e.selectedIndex); | |
| this.syncHTMLSteps(); | |
| // Initial step selection with enter animation if initial step > 1 | |
| setTimeout(() => this.stepper.selectedIndex = this.selectedIndex, 400); | |
| } | |
| ngAfterViewChecked() { | |
| if (this.needsFocus) { | |
| this.needsFocus = false; | |
| const { _elementRef, _keyManager, selectedIndex } = this.stepper as any; | |
| _elementRef.nativeElement.focus(); | |
| _keyManager.setActiveItem(selectedIndex); | |
| } | |
| } | |
| get isHorizontal(): boolean { | |
| return this.orientation === 'horizontal'; | |
| } | |
| get isVertical(): boolean { | |
| return this.orientation === 'vertical'; | |
| } | |
| next() { | |
| this.stepper.next(); | |
| } | |
| previous() { | |
| this.stepper.previous(); | |
| } | |
| /** | |
| * Enable/Disable the click on the step header. | |
| * | |
| * @param step The step number | |
| * @param enabled The new state | |
| */ | |
| updateStepState(step: number, enabled: boolean) { | |
| if (this.htmlSteps.length > 0) { | |
| this.htmlSteps[step - 1].style.pointerEvents = enabled ? '' : 'none'; | |
| } | |
| } | |
| /** | |
| * Sync from the dom the list of HTML elements for the steps. | |
| */ | |
| private syncHTMLSteps() { | |
| this.htmlSteps = []; | |
| let increment = 1; | |
| let stepper: HTMLElement = document.querySelector('.mat-stepper-vertical'); | |
| if (!stepper) { | |
| increment = 2; // 2, because Angular adds 2 elements for each horizontal step | |
| stepper = document.querySelector('.mat-horizontal-stepper-header-container'); | |
| } | |
| for (let i = 0; i < stepper.children.length; i += increment) { | |
| this.htmlSteps.push(stepper.children[i] as HTMLElement); | |
| } | |
| } | |
| private reset() { | |
| // Delay is necessary (Too early in AfterViewInit: HTMLElements not loaded) | |
| setTimeout(() => this.syncHTMLSteps(), 100); | |
| const { stepper, steps, changeDetectorRef, lastSelectedIndex } = this; | |
| stepper.steps.reset(steps.toArray()); | |
| stepper.steps.notifyOnChanges(); | |
| if (lastSelectedIndex) { | |
| stepper.selectedIndex = lastSelectedIndex; | |
| // After htmlSteps have been synced | |
| setTimeout(() => this.orientationChange.emit(this.orientation), 101); | |
| } | |
| Promise.resolve().then(() => { | |
| this.needsFocus = true; | |
| changeDetectorRef.markForCheck(); | |
| }); | |
| } | |
| } |
Excellent, is there any online example?
Hi guys, this is awesome, I just putted an online version of it this: https://stackblitz.com/edit/angular-stepper-responsive
In my project, it was returning an error:
core.js:6014 ERROR TypeError: Cannot read property 'children' of null
at ResponsiveStepperComponent.syncHTMLSteps (responsive-stepper.component.ts:229)
at responsive-stepper.component.ts:236
at ZoneDelegate.invokeTask (zone-evergreen.js:391)
at Object.onInvokeTask (core.js:39679)
at ZoneDelegate.invokeTask (zone-evergreen.js:390)
at Zone.runTask (zone-evergreen.js:168)
at invokeTask (zone-evergreen.js:465)
at ZoneTask.invoke (zone-evergreen.js:454)
at timer (zone-evergreen.js:2650)
I just corrected it as follow, line: 175:
for (let i = 0; stepper && i < stepper.children.length; i += increment) {
this.htmlSteps.push(stepper.children[i] as HTMLElement);
}
I needed, in my project, to replace the default icons, I did get it done putting the icons inside the proxies, the component receive a list of iconSet to show on steps. It can be checked on the stackblitz. The documentation way didn´t worked for me, even on the regular mat-stepper.
@davideas, that works super.
Please send files to this email: [email protected]
Thank you so much for your time. And I appreciate the efforts you made in flexible adapter.
Good luck on your newest projects.