src/app/shared/widgets/stepper/stepper-widget.component.ts
Properties |
event |
event:
|
Type : StepperSelectionEvent
|
The step selection event containing: selectedIndex, selectedStep, previouslySelectedIndex, previouslySelectedStep |
step |
step:
|
Type : Step
|
Selected step |
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();
}
}