File

projects/isy-angular-widgets/src/lib/wizard/components/wizard/wizard.component.ts

Description

A wizard that guides the user step by step through series of forms. Each side needs to have the WizardDirective.

Implements

OnInit AfterContentInit OnChanges

Metadata

Index

Properties
Methods
Inputs
Outputs
Accessors

Inputs

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

Outputs

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

Methods

closeDialog
closeDialog()

Is closing the dialog

Returns : void
getStepScreenReaderText
getStepScreenReaderText(index: number)
Parameters :
Name Type Optional
index number No
Returns : string
getStepTooltip
getStepTooltip(index: number)
Parameters :
Name Type Optional
index number No
Returns : string | undefined
isStepDisabled
isStepDisabled(index: number)
Parameters :
Name Type Optional
index number No
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 :
Name Type Optional Description
changes SimpleChanges No

Includes all DOM changes

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 :
Name Type Optional Description
event number No
  • The new active index of the wizard.
Returns : void
onStepperValueChange
onStepperValueChange(stepValue: number | undefined)
Parameters :
Name Type Optional
stepValue number | undefined No
Returns : void
onStepSelect
onStepSelect(index: number, activateCallback: () => void)
Parameters :
Name Type Optional
index number No
activateCallback function No
Returns : void
previous
previous()

Moves the wizard to the previous position

Returns : void
save
save()

Informs about the save action

Returns : void

Properties

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)

Accessors

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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""