nm-stepper
src/app/shared/widgets/stepper/stepper-widget.component.ts
providers |
{
: , : { : false },
}
|
selector | nm-stepper |
styleUrls | stepper-widget.component.scss |
templateUrl | ./stepper-widget.component.html |
Widget inputs |
Widget outputs |
Properties |
|
Methods |
|
Public configureWidget | ||||||
configureWidget(configuration: WidgetConfig
|
||||||
Decorators : WidgetConfigure
|
||||||
Parameters :
Returns :
void
|
Public getCssClass | ||||||
getCssClass(hiddenHeaderSteps: number[])
|
||||||
Parameters :
Returns :
string
|
Private getHiddenHeaderSteps |
getHiddenHeaderSteps()
|
Returns :
number[]
|
ngOnDestroy |
ngOnDestroy()
|
Returns :
void
|
Public onSelectionChange | ||||||
onSelectionChange(cdkEvent: StepperSelectionEvent)
|
||||||
Parameters :
Returns :
void
|
Private active |
active:
|
Default value : new ReplaySubject<StepSelectedEvent>(1)
|
Decorators : WidgetOutput
|
Outputs the currently active step |
Private activeIndex |
activeIndex:
|
Type : number
|
Default value : 0
|
Private completeAndNext |
completeAndNext:
|
Default value : new Subject()
|
Decorators : WidgetInput
|
Completes the current step and selects the next step |
Public configuration |
configuration:
|
Type : WidgetConfig<StepperConfiguration>
|
Decorators : WidgetConfiguration
|
Public cssClass |
cssClass:
|
Type : string
|
Public disabledStates |
disabledStates:
|
Type : boolean[]
|
Default value : []
|
Private disableStep |
disableStep:
|
Default value : new Subject<{ stepNo: number; state: boolean }>()
|
Decorators : WidgetInput
|
Disabled a specified step |
Public disableStepsHeader |
disableStepsHeader:
|
Type : boolean
|
Public header |
header:
|
Type : string
|
Public linear |
linear:
|
Type : boolean
|
Private navigateToThisStep |
navigateToThisStep:
|
Default value : new Subject<number>()
|
Decorators : WidgetInput
|
Navigate to a specific step |
Private next |
next:
|
Default value : new Subject()
|
Decorators : WidgetInput
|
Selects the next step if possible |
Private previous |
previous:
|
Default value : new Subject()
|
Decorators : WidgetInput
|
Selects the previous step if possible |
Private reset |
reset:
|
Default value : new Subject()
|
Decorators : WidgetInput
|
Resets the stepper |
Public stepper |
stepper:
|
Type : MatStepper
|
Decorators : ViewChild
|
Private unsubscribe |
unsubscribe:
|
Default value : NgUnsubscribe.create()
|
Public validStates |
validStates:
|
Type : boolean[]
|
Default value : []
|
Public widgetId |
widgetId:
|
Type : string
|
Decorators : WidgetId
|
Public withBorder |
withBorder:
|
Type : boolean
|
Public withHeader |
withHeader:
|
Type : boolean
|
import { Component, OnDestroy, ViewChild } from "@angular/core";
import {
WidgetComponent,
WidgetConfiguration,
WidgetConfigure,
WidgetId,
WidgetInput,
WidgetOutput,
} from "../widget.metadata";
import {
getOrDefault,
throwErrorIfUndefined,
WidgetConfig,
} from "../widget.configuration";
import { NgUnsubscribe } from "../../ng-unsubscribe";
import { ReplaySubject, Subject } from "rxjs";
import {
STEPPER_GLOBAL_OPTIONS,
StepperSelectionEvent,
} from "@angular/cdk/stepper";
import { takeUntil, debounceTime } from "rxjs/operators";
import { MatStepper } from "@angular/material/stepper";
import { BaseConfiguration } from "../widgetframe/widgetframe.component";
export interface StepperConfiguration extends BaseConfiguration {
/**
* Is it needed that previous steps are completed before starting the next one. @default(true)
*/
linear?: boolean;
/**
* The steps this component has
*/
steps: Step[];
/**
* It is needed to disable clicking steps header. @default(false)
*/
disableStepsHeader?: boolean;
}
export interface Step {
/**
* The label of this step - should be a translation key
*/
label: string;
/**
* The component that should be used to render this step
*/
component: string;
/**
* If this step is optional, @default(false)
*/
optional?: boolean;
/**
* Hides/Shows step header, @default(false)
*/
hiddenHeader?: boolean;
/**
* Step is editable on return to previously completed steps @default(true)
*/
editable?: boolean;
/**
* If this step is disabled, @default(false)
*/
disabled?: boolean;
}
export interface StepSelectedEvent {
/**
* Selected step
*/
step: Step;
/**
* The step selection event containing: selectedIndex, selectedStep, previouslySelectedIndex, previouslySelectedStep
*/
event: StepperSelectionEvent;
}
@WidgetComponent("nm-stepper")
@Component({
selector: "nm-stepper",
templateUrl: "./stepper-widget.component.html",
styleUrls: ["./stepper-widget.component.scss"],
providers: [
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: { displayDefaultIndicatorType: false },
},
],
})
export class StepperWidgetComponent implements OnDestroy {
public header: string;
public withHeader: boolean;
public withBorder: boolean;
public linear: boolean;
public disableStepsHeader: boolean;
public cssClass: string;
private unsubscribe = NgUnsubscribe.create();
public validStates: boolean[] = [];
public disabledStates: boolean[] = [];
@ViewChild("stepper")
public stepper: MatStepper;
/**
* Input to change the valid state of a step, takes an object as parameter, where index is the index of the step and valid is the new value
*/
@WidgetInput("valid")
private valid = new ReplaySubject<{ index: number; valid: boolean }>(1);
/**
* Input to change the valid state from a step until to the last step.
* Use case:
* - all steps are valid because of user interaction
* - the user changed step x
* - because of the change all steps after x should be changed to invalid
*/
@WidgetInput("validFrom")
private validFrom = new Subject<{ startIndex: number; valid: boolean }>();
/**
* Outputs the currently active step
*/
@WidgetOutput("active")
private active = new ReplaySubject<StepSelectedEvent>(1);
/**
* Selects the next step if possible
*/
@WidgetInput("next")
private next = new Subject();
/**
* Completes the current step and selects the next step
*/
@WidgetInput("completeAndNext")
private completeAndNext = new Subject();
/**
* Navigate to a specific step
*/
@WidgetInput()
private navigateToThisStep = new Subject<number>();
/**
* Selects the previous step if possible
*/
@WidgetInput("previous")
private previous = new Subject();
/**
* Resets the stepper
*/
@WidgetInput("reset")
private reset = new Subject();
/**
* Disabled a specified step
*/
@WidgetInput("disableStep")
private disableStep = new Subject<{ stepNo: number; state: boolean }>();
@WidgetId()
public widgetId: string;
@WidgetConfiguration()
public configuration: WidgetConfig<StepperConfiguration>;
private activeIndex = 0;
@WidgetConfigure()
public configureWidget(configuration: WidgetConfig<StepperConfiguration>) {
throwErrorIfUndefined("steps", configuration.configuration);
this.header = getOrDefault(configuration.configuration.header, "primary");
this.withHeader = getOrDefault(
configuration.configuration.withHeader,
true
);
this.withBorder = getOrDefault(
configuration.configuration.withBorder,
true
);
configuration.configuration.steps.forEach((step, index) => {
this.validStates[index] = false;
this.disabledStates[index] = step.disabled;
});
this.linear = getOrDefault(this.configuration.configuration.linear, true);
this.disableStepsHeader = getOrDefault(
this.configuration.configuration.disableStepsHeader,
false
);
const hiddenHeaderSteps: number[] = this.getHiddenHeaderSteps();
this.cssClass = this.getCssClass(hiddenHeaderSteps);
this.next.pipe(takeUntil(this.unsubscribe)).subscribe(() => {
if (this.disabledStates[this.activeIndex + 1]) {
this.stepper.next();
this.next.next();
return;
}
this.stepper.next();
});
this.completeAndNext.pipe(takeUntil(this.unsubscribe)).subscribe(() => {
this.stepper.selected.completed = true;
this.validStates[this.activeIndex] = true;
this.next.next();
});
this.navigateToThisStep
.pipe(takeUntil(this.unsubscribe))
.subscribe((targetIndex) => {
if (targetIndex > this.validStates.length || targetIndex < 0) {
return;
}
if (this.disabledStates[targetIndex]) {
this.navigateToThisStep.next(targetIndex + 1);
return;
}
this.stepper.selected.completed = true;
this.validStates[targetIndex] = true;
this.activeIndex = targetIndex;
const currentIndex = this.stepper.selectedIndex;
if (currentIndex < targetIndex) {
for (let i = 0; i < targetIndex - currentIndex; i++) {
this.stepper.next();
}
} else {
for (let i = 0; i < currentIndex - targetIndex; i++) {
this.stepper.previous();
}
}
});
this.previous.pipe(takeUntil(this.unsubscribe)).subscribe(() => {
if (this.activeIndex > 1 && this.disabledStates[this.activeIndex - 1]) {
this.stepper.previous();
this.previous.next();
return;
}
this.stepper.previous();
});
this.reset
.pipe(
takeUntil(this.unsubscribe),
//is needed because of an infinite recursion (from 'active' to 'reset')
//case: change the step over the head and when active step is 0, reset the stepper (occurred in vwproductdatazadb)
debounceTime(100)
)
.subscribe(() => {
this.stepper.reset();
configuration.configuration.steps.forEach((step, index) => {
this.validStates[index] = false;
this.stepper.steps.get(index).state = step.disabled
? "disabled"
: undefined;
this.disabledStates[index] = step.disabled;
});
});
this.valid.pipe(takeUntil(this.unsubscribe)).subscribe((data) => {
this.validStates[data.index] = data.valid;
});
this.validFrom.pipe(takeUntil(this.unsubscribe)).subscribe((data) => {
for (var i = data.startIndex; i < this.validStates.length; i++) {
this.validStates[i] = data.valid;
}
});
this.disableStep.pipe(takeUntil(this.unsubscribe)).subscribe((data) => {
this.stepper.steps.get(data.stepNo).state = data.state
? "disabled"
: undefined;
this.disabledStates[data.stepNo] = data.state;
this.stepper._stateChanged();
});
}
public onSelectionChange(cdkEvent: StepperSelectionEvent) {
this.activeIndex = cdkEvent.selectedIndex;
const event: StepSelectedEvent = {
event: cdkEvent,
step: this.configuration.configuration.steps[this.activeIndex],
};
this.active.next(event);
}
public getCssClass(hiddenHeaderSteps: number[]): string {
var cssClass = "";
if (this.disableStepsHeader) {
cssClass += "nm-stepper__header--disabled ";
}
if (hiddenHeaderSteps.length > 0) {
hiddenHeaderSteps.forEach((hiddenIndex) => {
const stepIndex: number = hiddenIndex + 1;
cssClass += "nm-stepper__step" + stepIndex + "-hidden ";
});
}
return cssClass;
}
private getHiddenHeaderSteps(): number[] {
const steps = this.configuration.configuration.steps;
const hiddenHeaderSteps = [];
for (var i = 0; i < steps.length; i++) {
if (steps[i].hiddenHeader) {
hiddenHeaderSteps.push(i);
}
}
return hiddenHeaderSteps;
}
ngOnDestroy(): void {
this.unsubscribe.destroy();
}
}
<nm-widgetframe
[header]="header"
[configuration]="configuration"
[infoTitle]="configuration.configuration.infoTitle"
[widgetId]="widgetId"
[infoText]="configuration.configuration.infoText"
[infoPlacement]="'bottom'"
[wikiLink]="configuration.configuration.wikiLink"
[toolbarInvisible]="!withHeader"
[withBorder]="withBorder"
>
<div slot="title" class="nm-widgetframe__title">
<span class="title-font"
>{{ configuration.configuration.title | translate }}
</span>
</div>
<div slot="content" class="nm-widgetframe__content">
<mat-horizontal-stepper
[ngClass]="cssClass"
[linear]="linear"
#stepper
(selectionChange)="onSelectionChange($event)"
>
<ng-container
*ngFor="let step of configuration.configuration.steps; let i = index"
>
<mat-step
[label]="step.label | translate"
[completed]="validStates[i]"
[optional]="step.optional"
[editable]="step.editable !== false"
[state]="step.disabled ? 'disabled' : undefined"
[aria-labelledby]="step.disabled ? 'disabled' : undefined"
>
<nm-container
*ngIf="step.component"
[configuration]="step.component | widgetFor: configuration"
[parent]="widgetId"
[id]="step.component"
>
</nm-container>
</mat-step>
<ng-template matStepperIcon="disabled">
<mat-icon>remove_circle_outline</mat-icon>
</ng-template>
</ng-container>
</mat-horizontal-stepper>
</div>
</nm-widgetframe>