projects/isy-angular-widgets/src/lib/wizard/components/wizard/wizard.component.ts
A wizard that guides the user step by step through series of forms. Each side needs to have the WizardDirective.
OnInit
AfterContentInit
OnChanges
| providers |
MessageService
|
| selector | isy-wizard |
| standalone | true |
| imports |
CommonModule
StepperModule
DialogModule
ButtonModule
ToastModule
TooltipModule
|
| styleUrls | ./wizard.component.scss |
| templateUrl | ./wizard.component.html |
Properties |
|
Methods |
Inputs |
Outputs |
Accessors |
| allowFreeNavigation | |
Type : boolean
|
|
Default value : false
|
|
| allowNext | |
Type : boolean
|
|
Default value : false
|
|
|
Controls whether the next button is enabled which is to be controlled from the outside (e.g. for validation) |
|
| autoDisableFutureSteps | |
Type : boolean
|
|
Default value : false
|
|
|
Automatically disables all following steps while the current step is not allowed to proceed. Typical use: <isy-wizard [autoDisableFutureSteps]="true"> |
|
| breaktpoints | |
Type : literal type
|
|
Default value : {
'3840px': '95vw',
'1920px': '95vw',
'1366px': '85vw',
'768px': '95vw',
'412px': '95vw'
}
|
|
|
Breakpoint for PrimeNg dialog responsiveness |
|
| closable | |
Type : boolean
|
|
Default value : true
|
|
|
Determines whether the wizard is closable |
|
| disabledStepAriaText | |
Type : string
|
|
|
Screenreader text used for auto-disabled steps. Example: <isy-wizard [disabledStepAriaText]="'Schritt deaktiviert'"> |
|
| disabledStepTooltip | |
Type : string
|
|
|
Tooltip text used for auto-disabled steps. Example: <isy-wizard [disabledStepTooltip]="'Bitte zuerst Pflichtfelder ausfüllen'"> |
|
| draggable | |
Type : boolean
|
|
Default value : false
|
|
|
Determines whether the wizard is draggable |
|
| headerTitle | |
Type : string
|
|
Default value : ''
|
|
|
A title to show |
|
| height | |
Type : number
|
|
Default value : defaultHeight
|
|
|
The wizard height in %. Default is 30 |
|
| index | |
Type : number
|
|
Default value : 0
|
|
|
The current wizard index |
|
| isSaved | |
Type : boolean
|
|
Default value : false
|
|
|
Determines if the system is saving now |
|
| isVisible | |
Type : boolean
|
|
Default value : false
|
|
|
Determines whether the wizard is visible |
|
| labelBackButton | |
Type : string
|
|
Default value : 'Zurück'
|
|
|
The text of the back button |
|
| labelCloseButton | |
Type : string
|
|
Default value : 'Schließen'
|
|
|
The text of the close button |
|
| labelNextButton | |
Type : string
|
|
Default value : 'Weiter'
|
|
|
The text of the next button |
|
| labelSaveButton | |
Type : string
|
|
Default value : 'Speichern'
|
|
|
The text of the save button |
|
| modal | |
Type : boolean
|
|
Default value : true
|
|
|
Determines if the modal behind the wizard dialog is displayed |
|
| stepStates | |
Type : WizardStepState[]
|
|
Default value : []
|
|
|
Optional states for individual steps. Array positions correspond to step indices. Use this only when single steps need different disabled/tooltip/aria behavior. |
|
| width | |
Type : number
|
|
Default value : defaultWidth
|
|
|
The wizard width in %. Default is 50 |
|
| indexChange | |
Type : EventEmitter
|
|
|
Emits the currently displayed page |
|
| isVisibleChange | |
Type : EventEmitter
|
|
|
Emits the current visibility status |
|
| savingChange | |
Type : EventEmitter
|
|
|
Emits when the user is currently trying to save to be handled from outside |
|
| closeDialog |
closeDialog()
|
|
Is closing the dialog
Returns :
void
|
| getStepScreenReaderText | ||||||
getStepScreenReaderText(index: number)
|
||||||
|
Parameters :
Returns :
string
|
| getStepTooltip | ||||||
getStepTooltip(index: number)
|
||||||
|
Parameters :
Returns :
string | undefined
|
| isStepDisabled | ||||||
isStepDisabled(index: number)
|
||||||
|
Parameters :
Returns :
boolean
|
| next |
next()
|
|
Moves the wizard to the next position
Returns :
void
|
| ngAfterContentInit |
ngAfterContentInit()
|
|
Fired after content initialization
Returns :
void
|
| ngOnChanges | ||||||||
ngOnChanges(changes: SimpleChanges)
|
||||||||
|
Fired on changes
Parameters :
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Fired on initialization
Returns :
void
|
| onActiveIndexChange | ||||||||
onActiveIndexChange(event: number)
|
||||||||
|
Handles the change of the active index in the wizard component. Updates the current index, emits the index change event, and displays a toast message indicating the step change.
Parameters :
Returns :
void
|
| onStepperValueChange | ||||||
onStepperValueChange(stepValue: number | undefined)
|
||||||
|
Parameters :
Returns :
void
|
| onStepSelect |
onStepSelect(index: number, activateCallback: () => void)
|
|
Returns :
void
|
| previous |
previous()
|
|
Moves the wizard to the previous position
Returns :
void
|
| save |
save()
|
|
Informs about the save action
Returns :
void
|
| configService |
Type : unknown
|
Default value : inject(WidgetsConfigService)
|
|
A service used to translate labels within the widgets library. |
| Optional content |
Type : QueryList<WizardDirective>
|
Decorators :
@ContentChildren(WizardDirective)
|
|
Stores the content that will be projected inside the template |
| Optional footerTemplate |
Type : WizardFooterDirective
|
Decorators :
@ContentChild(WizardFooterDirective)
|
|
Stores an optional projected custom footer template. |
| items |
Type : MenuItem[]
|
Default value : []
|
|
Stores the items of the wizard |
| Readonly messageService |
Type : unknown
|
Default value : inject(MessageService)
|
| stepCount |
getstepCount()
|
| isFirstStep |
getisFirstStep()
|
| isLastStep |
getisLastStep()
|
| showBackButton |
getshowBackButton()
|
| showNextButton |
getshowNextButton()
|
| showSaveButton |
getshowSaveButton()
|
| canClose |
getcanClose()
|
| footerContext |
getfooterContext()
|
import {
AfterContentInit,
Component,
ContentChild,
ContentChildren,
EventEmitter,
inject,
Input,
OnChanges,
OnInit,
Output,
QueryList,
SimpleChanges
} from '@angular/core';
import {MenuItem, MessageService} from 'primeng/api';
import {WizardDirective} from '../../directives/wizard.directive';
import {WizardFooterDirective} from '../../directives/wizard-footer.directive';
import {WidgetsConfigService} from '../../../i18n/widgets-config.service';
import {CommonModule} from '@angular/common';
import {StepperModule} from 'primeng/stepper';
import {DialogModule} from 'primeng/dialog';
import {ButtonModule} from 'primeng/button';
import {ToastModule} from 'primeng/toast';
import {TooltipModule} from 'primeng/tooltip';
export interface WizardStepState {
/**
* Explicitly disables a single step independent of autoDisableFutureSteps.
*/
disabled?: boolean;
/**
* Optional tooltip for explicitly disabled steps.
*/
disabledTooltip?: string;
/**
* Optional screen-reader text for explicitly disabled steps.
*/
disabledScreenReaderText?: string;
}
export interface WizardFooterContext {
index: number;
stepCount: number;
isFirstStep: boolean;
isLastStep: boolean;
allowNext: boolean;
isSaved: boolean;
closable: boolean;
showBack: boolean;
showNext: boolean;
showSave: boolean;
canClose: boolean;
next: () => void;
previous: () => void;
save: () => void;
close: () => void;
}
/**
* The width of the wizard of not otherwise specified by the user.
* @internal
*/
const defaultWidth = 50;
/**
* The height of the wizard of not otherwise specified by the user.
* @internal
*/
const defaultHeight = 30;
/**
* A wizard that guides the user step by step through series of forms.
* Each side needs to have the {@link WizardDirective}.
*/
@Component({
standalone: true,
selector: 'isy-wizard',
templateUrl: './wizard.component.html',
styleUrls: ['./wizard.component.scss'],
imports: [CommonModule, StepperModule, DialogModule, ButtonModule, ToastModule, TooltipModule],
providers: [MessageService]
})
export class WizardComponent implements OnInit, AfterContentInit, OnChanges {
/**
* Stores the content that will be projected inside the template
*/
@ContentChildren(WizardDirective) content?: QueryList<WizardDirective>;
/**
* Stores an optional projected custom footer template.
*/
@ContentChild(WizardFooterDirective) footerTemplate?: WizardFooterDirective;
/**
* Emits the currently displayed page
*/
@Output() indexChange = new EventEmitter<number>();
/**
* Emits when the user is currently trying to save to be handled from outside
*/
@Output() savingChange = new EventEmitter<boolean>();
/**
* Emits the current visibility status
*/
@Output() isVisibleChange = new EventEmitter<boolean>();
/**
* Determines whether the wizard is visible
*/
@Input() isVisible: boolean = false;
/**
* Determines whether the wizard is draggable
*/
@Input() draggable: boolean = false;
/**
* Determines whether the wizard is closable
*/
@Input() closable: boolean = true;
/**
* Determines if the modal behind the wizard dialog is displayed
*/
@Input() modal: boolean = true;
/**
* A title to show
*/
@Input() headerTitle: string = '';
/**
* The wizard width in %.
* Default is 50
*/
@Input() width: number = defaultWidth;
/**
* The wizard height in %.
* Default is 30
*/
@Input() height: number = defaultHeight;
/**
* The text of the back button
*/
@Input() labelBackButton = 'Zurück';
/**
* The text of the next button
*/
@Input() labelNextButton = 'Weiter';
/**
* The text of the save button
*/
@Input() labelSaveButton = 'Speichern';
/**
* The text of the close button
*/
@Input() labelCloseButton = 'Schließen';
/**
* Controls whether the next button is enabled which is to be controlled from the outside (e.g. for validation)
*/
@Input() allowNext: boolean = false;
/**
* Determines if the system is saving now
*/
@Input() isSaved: boolean = false;
/**
* The current wizard index
*/
@Input() index: number = 0;
/**
* Breakpoint for PrimeNg dialog responsiveness
*/
@Input() breaktpoints: {[key: string]: string} = {
'3840px': '95vw',
'1920px': '95vw',
'1366px': '85vw',
'768px': '95vw',
'412px': '95vw'
};
@Input() allowFreeNavigation = false;
/**
* Automatically disables all following steps while the current step is not allowed to proceed.
* Typical use:
* <isy-wizard [autoDisableFutureSteps]="true"></isy-wizard>
*/
@Input() autoDisableFutureSteps = false;
/**
* Tooltip text used for auto-disabled steps.
* Example:
* <isy-wizard [disabledStepTooltip]="'Bitte zuerst Pflichtfelder ausfüllen'"></isy-wizard>
*/
@Input() disabledStepTooltip?: string;
/**
* Screenreader text used for auto-disabled steps.
* Example:
* <isy-wizard [disabledStepAriaText]="'Schritt deaktiviert'"></isy-wizard>
*/
@Input() disabledStepAriaText?: string;
/**
* Optional states for individual steps. Array positions correspond to step indices.
* Use this only when single steps need different disabled/tooltip/aria behavior.
*/
@Input() stepStates: WizardStepState[] = [];
/**
* Stores the items of the wizard
*/
items: MenuItem[] = [];
/**
* A service used to translate labels within the widgets library.
*/
configService = inject(WidgetsConfigService);
readonly messageService = inject(MessageService);
get stepCount(): number {
return this.items.length;
}
get isFirstStep(): boolean {
return this.index === 0;
}
get isLastStep(): boolean {
return this.index === this.stepCount - 1;
}
get showBackButton(): boolean {
return !this.isSaved && !this.isFirstStep;
}
get showNextButton(): boolean {
return !this.isSaved && !this.isLastStep;
}
get showSaveButton(): boolean {
return this.isLastStep && !this.isSaved;
}
get canClose(): boolean {
return !this.isSaved || this.closable;
}
get footerContext(): WizardFooterContext {
return {
index: this.index,
stepCount: this.stepCount,
isFirstStep: this.isFirstStep,
isLastStep: this.isLastStep,
allowNext: this.allowNext,
isSaved: this.isSaved,
closable: this.closable,
showBack: this.showBackButton,
showNext: this.showNextButton,
showSave: this.showSaveButton,
canClose: this.canClose,
next: () => this.next(),
previous: () => this.previous(),
save: () => this.save(),
close: () => this.closeDialog()
};
}
/**
* Fired on initialization
*/
ngOnInit(): void {
this.indexChange.emit(this.index);
}
/**
* Fired after content initialization
*/
ngAfterContentInit(): void {
if (this.content) {
this.items = this.content.map((item) => {
return {
label: item.isyWizardDirective
};
});
}
}
/**
* Fired on changes
* @param changes Includes all DOM changes
*/
ngOnChanges(changes: SimpleChanges): void {
if (changes.isVisible?.previousValue === true && changes.isVisible?.currentValue === false) {
this.resetWizard();
}
}
/**
* Moves the wizard to the next position
*/
next(): void {
this.index++;
this.indexChange.emit(this.index);
}
/**
* Moves the wizard to the previous position
*/
previous(): void {
this.index--;
this.indexChange.emit(this.index);
}
/**
* Is closing the dialog
*/
closeDialog(): void {
this.resetWizard();
this.close();
}
/**
* Resets the wizard position
*/
private resetWizard(): void {
this.index = 0;
}
/**
* Is closing the wizard
*/
private close(): void {
this.isVisible = false;
this.isVisibleChange.emit(this.isVisible);
}
/**
* Informs about the save action
*/
save(): void {
this.savingChange.emit(true);
}
/**
* Handles the change of the active index in the wizard component.
* Updates the current index, emits the index change event, and displays
* a toast message indicating the step change.
* @param event - The new active index of the wizard.
*/
onActiveIndexChange(event: number): void {
if (event < 0 || event >= this.items.length) {
return;
}
this.index = event;
this.indexChange.emit(this.index);
const label = this.items[event]?.label ?? '';
this.messageService.add({
severity: 'info',
summary: this.configService.getTranslation('wizard.toast.stepChanged'),
detail: label
});
}
onStepperValueChange(stepValue: number | undefined): void {
if (stepValue == null) {
return;
}
const newIndex = stepValue - 1;
if (newIndex < 0 || newIndex >= this.items.length) {
return;
}
if (this.allowFreeNavigation && !this.isStepDisabled(newIndex)) {
this.onActiveIndexChange(newIndex);
}
}
onStepSelect(index: number, activateCallback: () => void): void {
if (this.isStepDisabled(index)) {
return;
}
activateCallback();
}
private isAutoDisabledStep(index: number): boolean {
return this.autoDisableFutureSteps && !this.allowNext && index > this.index;
}
private isExplicitlyDisabledStep(index: number): boolean {
return this.stepStates[index]?.disabled ?? false;
}
isStepDisabled(index: number): boolean {
return this.isExplicitlyDisabledStep(index) || this.isAutoDisabledStep(index);
}
getStepTooltip(index: number): string | undefined {
if (!this.isStepDisabled(index)) {
return undefined;
}
if (this.isExplicitlyDisabledStep(index)) {
return this.stepStates[index]?.disabledTooltip;
}
if (this.isAutoDisabledStep(index)) {
return this.disabledStepTooltip;
}
return undefined;
}
getStepScreenReaderText(index: number): string {
if (!this.isStepDisabled(index)) {
return '';
}
const configuredText = this.isExplicitlyDisabledStep(index)
? this.stepStates[index]?.disabledScreenReaderText?.trim()
: this.disabledStepAriaText?.trim();
if (configuredText) {
return configuredText;
}
const disabledStateText = this.configService.getTranslation('wizard.aria.disabledStep');
const tooltip = this.getStepTooltip(index)?.trim();
return tooltip ? `${disabledStateText}. ${tooltip}` : disabledStateText;
}
}
<p-toast [baseZIndex]="2000" />
<p-dialog
[header]="headerTitle"
[closeAriaLabel]="configService.getTranslation('wizard.aria.close')"
[(visible)]="isVisible"
[modal]="modal"
[closable]="closable"
[draggable]="draggable"
[breakpoints]="breaktpoints"
[style]="{
width: width + 'vw',
height: height + 'vw'
}"
(visibleChange)="isVisibleChange.emit($event)"
>
<p-stepper [value]="index + 1" [linear]="!allowFreeNavigation" (valueChange)="onStepperValueChange($event)">
<p-step-list>
@for (item of items; track $index; let i = $index) {
<p-step [value]="i + 1">
<ng-template pTemplate="content" let-activateCallback="activateCallback" let-value="value">
<span
class="isy-wizard-step-trigger"
[pTooltip]="getStepTooltip(i)"
tooltipPosition="top"
[attr.aria-disabled]="isStepDisabled(i)"
[attr.aria-label]="isStepDisabled(i) ? getStepScreenReaderText(i) : null"
[attr.tabindex]="isStepDisabled(i) ? 0 : null"
>
<button
type="button"
class="p-step-header"
[disabled]="isStepDisabled(i)"
[attr.aria-controls]="null"
[attr.tabindex]="-1"
(click)="onStepSelect(i, activateCallback)"
>
<span class="p-step-number">{{ value }}</span>
<span class="p-step-title">
<span class="isy-wizard-step-label" [class.is-disabled]="isStepDisabled(i)">
<span>{{ item.label }}</span>
@if (isStepDisabled(i)) {
<span class="visually-hidden">{{ getStepScreenReaderText(i) }}</span>
}
</span>
</span>
</button>
</span>
</ng-template>
</p-step>
}
</p-step-list>
</p-stepper>
<ng-container [ngTemplateOutlet]="this.content?.get(index)?.templateRef ?? null"></ng-container>
<ng-template pTemplate="footer">
@if (footerTemplate) {
<ng-container
[ngTemplateOutlet]="footerTemplate.templateRef"
[ngTemplateOutletContext]="footerContext"
></ng-container>
} @else {
<div class="flex align-items-center justify-content-between flex-wrap gap-3 card-container w-full">
<div class="flex align-items-center">
<button
id="close-button"
type="button"
pRipple
pButton
[outlined]="true"
class="flex align-items-center justify-content-center"
[disabled]="!canClose"
(click)="closeDialog()"
>
{{ labelCloseButton }}
</button>
</div>
<div class="flex align-items-center justify-content-center flex-wrap gap-2">
@if (showBackButton) {
<button
id="back-button"
type="button"
pRipple
pButton
[outlined]="true"
class="flex align-items-center justify-content-center"
(click)="previous()"
>
{{ labelBackButton }}
</button>
}
@if (showNextButton) {
<button
id="next-button"
type="button"
pRipple
pButton
class="flex align-items-center justify-content-center"
[disabled]="!allowNext"
(click)="next()"
>
{{ labelNextButton }}
</button>
}
</div>
<div class="flex align-items-center justify-content-end">
@if (showSaveButton) {
<button
id="save-button"
type="button"
pRipple
pButton
class="flex align-items-center justify-content-center"
[disabled]="!allowNext"
(click)="save()"
>
{{ labelSaveButton }}
</button>
}
</div>
</div>
}
</ng-template>
</p-dialog>
./wizard.component.scss
.isy-wizard-step-trigger {
display: inline-flex;
}
.isy-wizard-step-trigger:focus-visible {
outline: 2px solid var(--p-focus-ring-color, var(--p-primary-color, #1a73e8));
outline-offset: 2px;
border-radius: 0.25rem;
}
.isy-wizard-step-trigger .p-step-header[disabled] {
pointer-events: none;
}
.isy-wizard-step-label {
display: inline-flex;
align-items: center;
}
.isy-wizard-step-label.is-disabled {
color: var(--p-text-muted-color, #6b7280);
cursor: not-allowed;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}