Last active
December 12, 2022 15:57
-
-
Save davideas/3534a5c2567d6668013426ab961990f6 to your computer and use it in GitHub Desktop.
Angular 8.x Responsive Stepper with headers disable feature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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:
I just corrected it as follow, line: 175:
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.