chrome:shell.skip

generated:catalog.entry.stepper.title

generated:catalog.entry.stepper.description

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

chrome:demo.section

chrome:demo.preview

demo-preview:hints.category.navigation:
Tab
demo-preview:hints.category.focus:
Focus
demo-preview:hints.category.shortcuts:
Enter / Space
demo-preview:projection.slots-label:
x-step
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();
}

chrome:demo.events.title

chrome:demo.events.empty

chrome:demo.api.title

x-step

chrome:demo.api.col.property
chrome:demo.api.col.type
chrome:demo.api.col.default
chrome:demo.api.col.description
label chrome:demo.api.required
string
—
Step title shown on the indicator trigger.
description
string
—
Secondary text shown below the step label.
optional
boolean
false
When true, marks the step as optional in the indicator.
optionalLabel
string
—
Label beside optional steps — falls back to the `step.optional` i18n key.
disabled
boolean
false
When true, this step cannot be selected.
completed
boolean
false
When true, marks this step completed regardless of selection index.