Skip to content

Stepper

@xngui/components/stepper
@xngui/components/stepper

Multi-step flow — compose with x-step panels.

Preview

import { Component, computed, signal, viewChild } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
 
import { XAlert } from '@xngui/components/alert';
import { XButton } from '@xngui/components/button';
import { XCheckbox } from '@xngui/components/checkbox';
import { XForm } from '@xngui/components/form';
import { XFormField } from '@xngui/components/form-field';
import { XInput } from '@xngui/components/input';
import { XRadio } from '@xngui/components/radio';
import { XRadioGroup } from '@xngui/components/radio-group';
import { XStep, XStepper } from '@xngui/components/stepper';
import { email, required } from '@xngui/components/validation';
import type { XChangeEvent } from '@xngui/core';
 
type XWizardModel = {
name: string;
email: string;
plan: string;
acceptTerms: boolean;
};
 
const WIZARD_STEP_COUNT = 3;
 
protected readonly selectedIndex = signal(0);
protected readonly wizardSubmitted = signal(false);
 
protected readonly wizardModel = signal<XWizardModel>({
name: '',
email: '',
plan: 'starter',
acceptTerms: false,
});
 
protected readonly wizardForm = form(this.wizardModel, (path) => {
required(path.name);
required(path.email);
email(path.email);
required(path.acceptTerms);
});
 
private readonly wizardFormHost = viewChild<XForm<XWizardModel>>("wizardFormHost");
 
protected readonly isLastStep = computed(() => this.selectedIndex() === WIZARD_STEP_COUNT - 1);
 
protected goNext(stepper: XStepper): void {
if (!this.validateCurrentStep()) {
this.wizardFormHost()?.focusFirstInvalid();
return;
}
stepper.next();
}
 
protected goBack(stepper: XStepper): void {
this.wizardSubmitted.set(false);
stepper.previous();
}
 
protected canGoBack(): boolean {
return this.selectedIndex() > 0;
}
 
protected onWizardSubmit(value: XWizardModel): void {
this.wizardSubmitted.set(true);
}
 
protected onWizardInvalid(): void {
// focusFirstInvalid() already ran inside x-form
}
 
protected onWizardReset(): void {
this.wizardSubmitted.set(false);
this.selectedIndex.set(0);
}
 
protected onChanged(event: XChangeEvent<number>): void {
const { value, previousValue } = event;
if (value > previousValue && !this.validateBeforeLeavingStep(previousValue)) {
this.selectedIndex.set(previousValue);
this.wizardFormHost()?.focusFirstInvalid();
return;
}
}
 
private validateCurrentStep(): boolean {
return this.validateBeforeLeavingStep(this.selectedIndex());
}
 
private validateBeforeLeavingStep(stepIndex: number): boolean {
if (stepIndex !== 0) {
return true;
}
this.wizardForm.name().markAsTouched();
this.wizardForm.email().markAsTouched();
return !this.wizardForm.name().invalid() && !this.wizardForm.email().invalid();
}

Events

Interact with the preview — events will appear here.