DevExtreme v25.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

Angular Stepper - Form Integration

This demo uses the DevExtreme Stepper component to guide users through a multi-page hotel registration form. The Stepper lacks a built-in form but can connect to it or other controls through APIs. Input controls at each step are organized using DevExtreme MultiView and Form components.

There are three key parts to this demo:

  • Stepper
  • Step content where the MultiView component displays content for each step. Each view with input fields contains a Form.
  • A navigation panel that displays the current step ("Step 1 of 5") and buttons for moving between steps (Next/Back).
Backend API
<dx-stepper [focusStateEnabled]="!isStepperReadonly" [class.readonly]="isStepperReadonly" [(selectedIndex)]="selectedIndex" (onSelectionChanging)="onSelectionChanging($event)" > <dxi-stepper-item *ngFor="let step of steps" [label]="step.label" [icon]="step.icon" [isValid]="step.isValid" [hint]="step.hint" [optional]="step.optional" ></dxi-stepper-item> </dx-stepper> <div class="content"> <dx-multi-view [(selectedIndex)]="selectedIndex" [animationEnabled]="false" [focusStateEnabled]="false" [swipeEnabled]="false" [height]="400" > <dxi-multi-view-item> <div *dxTemplate> <dates-form [formData]="formData" [validationGroup]="validationGroups[0]" ></dates-form> </div> </dxi-multi-view-item> <dxi-multi-view-item> <div *dxTemplate> <guests-form [formData]="formData" [validationGroup]="validationGroups[1]" ></guests-form> </div> </dxi-multi-view-item> <dxi-multi-view-item> <div *dxTemplate> <room-meal-plan-form [formData]="formData" [validationGroup]="validationGroups[2]" ></room-meal-plan-form> </div> </dxi-multi-view-item> <dxi-multi-view-item> <div *dxTemplate> <additional-form [formData]="formData"></additional-form> </div> </dxi-multi-view-item> <dxi-multi-view-item> <div *dxTemplate> <confirmation [formData]="formData" [isConfirmed]="isConfirmed" ></confirmation> </div> </dxi-multi-view-item> </dx-multi-view> <div class="nav-panel"> <div class="current-step"> <span *ngIf="!isConfirmed"> Step <span class="selected-index">{{ selectedIndex + 1 }}</span> of {{ steps.length }} </span> </div> <div class="nav-buttons"> <dx-button id="prevButton" [visible]="selectedIndex !== 0 && !isConfirmed" text="Back" type="normal" (onClick)="onPrevButtonClick($event)" [width]="100" > </dx-button> <dx-button id="nextButton" [text]="getNextButtonText()" type="default" (onClick)="onNextButtonClick($event)" [width]="100" > </dx-button> </div> </div> </div>
import { NgModule, Component, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { DxButtonModule, DxDateRangeBoxModule, DxFormModule, DxMultiViewModule, DxNumberBoxModule, DxSelectBoxModule, DxTextAreaModule, } from 'devextreme-angular'; import { DxStepperModule, type DxStepperTypes } from 'devextreme-angular/ui/stepper'; import { AppService } from './app.service'; import { BookingFormData } from './app.types'; import { DatesFormComponent } from "./dates-form/dates-form.component"; import { GuestsFormComponent } from "./guests-form/guests-form.component"; import { RoomMealPlanFormComponent } from "./room-meal-plan-form/room-meal-plan-form.component"; import { AdditionalFormComponent } from "./additional-form/additional-form.component"; import { ConfirmationComponent } from "./confirmation/confirmation.component"; import validationEngine from 'devextreme/ui/validation_engine'; if (!/localhost/.test(document.location.host)) { enableProdMode(); } let modulePrefix = ''; // @ts-ignore if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'demo-app', templateUrl: `app/app.component.html`, styleUrls: [`app/app.component.css`], }) export class AppComponent { steps: DxStepperTypes.Item[]; formData: BookingFormData; selectedIndex: number; isConfirmed: boolean; isStepperReadonly: boolean; validationGroups = ['dates', 'guests', 'roomAndMealPlan']; constructor(private readonly appService: AppService) { this.steps = this.appService.getInitialSteps(); this.formData = this.appService.getInitialFormData(); this.selectedIndex = 0; this.isConfirmed = false; this.isStepperReadonly = false; } getValidationResult(index: number){ if (index >= this.validationGroups.length) { return true; } return validationEngine.validateGroup(this.validationGroups[index]).isValid; } setStepValidationResult(index: number, isValid: boolean | undefined){ this.steps[index].isValid = isValid; } onSelectionChanging(e: DxStepperTypes.SelectionChangingEvent) { const { component, addedItems, removedItems } = e; const { items = [] } = component.option(); const addedIndex = items.findIndex((item: DxStepperTypes.Item) => item === addedItems[0]); const removedIndex = items.findIndex((item: DxStepperTypes.Item) => item === removedItems[0]); const isMoveForward = removedIndex > -1 && addedIndex > removedIndex; if (isMoveForward) { const isValid = this.getValidationResult(removedIndex); this.setStepValidationResult(removedIndex, isValid); if (isValid === false) { e.cancel = true; } } } getNextButtonText() { if (this.selectedIndex < this.steps.length - 1) { return 'Next'; } return this.isConfirmed ? 'Reset' : 'Confirm'; } onPrevButtonClick() { this.selectedIndex -= 1; } moveNext() { const isValid = this.getValidationResult(this.selectedIndex); this.setStepValidationResult(this.selectedIndex, isValid); if (isValid) { this.selectedIndex += 1; } } reset(){ this.isConfirmed = false; this.selectedIndex = 0; this.steps = this.appService.getInitialSteps(); this.formData = this.appService.getInitialFormData(); this.isStepperReadonly = false; } confirm(){ this.isConfirmed = true; this.setStepValidationResult(this.selectedIndex, true); this.isStepperReadonly = true; } onNextButtonClick() { if (this.selectedIndex < this.steps.length - 1) { this.moveNext(); } else if (this.isConfirmed) { this.reset(); } else { this.confirm(); } } } @NgModule({ imports: [ BrowserModule, DxButtonModule, DxDateRangeBoxModule, DxFormModule, DxMultiViewModule, DxNumberBoxModule, DxSelectBoxModule, DxStepperModule, DxTextAreaModule, ], declarations: [ AppComponent, DatesFormComponent, GuestsFormComponent, RoomMealPlanFormComponent, AdditionalFormComponent, ConfirmationComponent, ], bootstrap: [AppComponent], providers: [AppService], }) export class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);
::ng-deep .demo-container { min-height: 580px; } ::ng-deep demo-app { display: flex; flex-direction: column; justify-content: center; row-gap: 20px; height: 580px; min-width: 620px; } ::ng-deep .content { padding-inline: 40px; flex: 1; display: flex; flex-direction: column; row-gap: 20px; } ::ng-deep .nav-panel { display: flex; align-items: center; justify-content: space-between; } ::ng-deep .current-step { color: var(--dx-color-icon); } ::ng-deep .nav-buttons { display: flex; gap: 8px; } ::ng-deep .readonly { pointer-events: none; }
import { Injectable } from '@angular/core'; import { type DxStepperTypes } from 'devextreme-angular/ui/stepper'; import { BookingFormData } from "./app.types"; @Injectable({ providedIn: 'root', }) export class AppService { initialSteps: DxStepperTypes.Item[]; initialFormData: BookingFormData; roomTypes: string[]; mealPlans: string[]; constructor() { this.initialSteps = [ { label: 'Dates', hint: 'Dates', icon: 'daterangepicker', }, { label: 'Guests', hint: 'Guests', icon: 'group', }, { label: 'Room and Meal Plan', hint: 'Room and Meal Plan', icon: 'servicebell', }, { label: 'Additional Requests', hint: 'Additional Requests', icon: 'clipboardtasklist', optional: true, }, { label: 'Confirmation', hint: 'Confirmation', icon: 'checkmarkcircle', }, ]; this.initialFormData = { dates: [null, null], adultsCount: 0, childrenCount: 0, petsCount: 0, roomType: undefined, mealPlan: undefined, additionalRequest: '', }; } getInitialSteps(): DxStepperTypes.Item[] { return this.initialSteps.map((item) => ({ ...item })); } getInitialFormData(): BookingFormData { return { ...this.initialFormData, dates: [...this.initialFormData.dates], }; } }
export interface BookingFormData { dates: Array<Date | null>; adultsCount: number; childrenCount: number; petsCount: number; roomType: string | undefined; mealPlan: string | undefined; additionalRequest: string; }
// In real applications, you should not transpile code in the browser. // You can see how to create your own application with Angular and DevExtreme here: // https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Create_a_DevExtreme_Application/ const componentNames = [ 'accordion', 'action-sheet', 'autocomplete', 'bar-gauge', 'box', 'bullet', 'button-group', 'button', 'calendar', 'card-view', 'chart', 'chat', 'check-box', 'circular-gauge', 'color-box', 'context-menu', 'data-grid', 'date-box', 'date-range-box', 'defer-rendering', 'diagram', 'draggable', 'drawer', 'drop-down-box', 'drop-down-button', 'file-manager', 'file-uploader', 'filter-builder', 'form', 'funnel', 'gallery', 'gantt', 'html-editor', 'linear-gauge', 'list', 'load-indicator', 'load-panel', 'lookup', 'map', 'menu', 'multi-view', 'nested', 'number-box', 'pagination', 'pie-chart', 'pivot-grid-field-chooser', 'pivot-grid', 'polar-chart', 'popover', 'popup', 'progress-bar', 'radio-group', 'range-selector', 'range-slider', 'recurrence-editor', 'resizable', 'responsive-box', 'sankey', 'scheduler', 'scroll-view', 'select-box', 'slider', 'sortable', 'sparkline', 'speed-dial-action', 'splitter', 'stepper', 'switch', 'tab-panel', 'tabs', 'tag-box', 'text-area', 'text-box', 'tile-view', 'toast', 'toolbar', 'tooltip', 'tree-list', 'tree-map', 'tree-view', 'validation-group', 'validation-summary', 'validator', 'vector-map', ]; window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, }, meta: { 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, '@angular/platform-browser-dynamic': { 'esModule': true, }, '@angular/platform-browser': { 'esModule': true, }, '@angular/core': { 'esModule': true, }, '@angular/common': { 'esModule': true, }, '@angular/common/http': { 'esModule': true, }, '@angular/animations': { 'esModule': true, }, '@angular/forms': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, map: { 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', /* @angular */ '@angular/compiler': 'bundles:@angular/compiler.umd.js', '@angular/platform-browser-dynamic': 'bundles:@angular/platform-browser-dynamic.umd.js', '@angular/core': 'bundles:@angular/core.umd.js', '@angular/core/primitives/signals': 'bundles:@angular/core.primitives.signals.umd.js', '@angular/common': 'bundles:@angular/common.umd.js', '@angular/common/http': 'bundles:@angular/common-http.umd.js', '@angular/platform-browser': 'bundles:@angular/platform-browser.umd.js', '@angular/platform-browser/animations': 'bundles:@angular/platform-browser.umd.js', '@angular/forms': 'bundles:@angular/forms.umd.js', /* devextreme */ 'devextreme': 'npm:devextreme@25.1.3/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.3/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.19', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.62', /* devextreme-angular umd maps */ 'devextreme-angular': 'bundles:devextreme-angular/devextreme-angular.umd.js', 'devextreme-angular/common/ai-integration': 'bundles:devextreme-angular/devextreme-angular-common-ai-integration.umd.js', 'devextreme-angular/core': 'bundles:devextreme-angular/devextreme-angular-core.umd.js', 'devextreme-angular/common/charts': 'bundles:devextreme-angular/devextreme-angular-common-charts.umd.js', 'devextreme-angular/common/core/animation': 'bundles:devextreme-angular/devextreme-angular-common-core-animation.umd.js', 'devextreme-angular/common/core/environment': 'bundles:devextreme-angular/devextreme-angular-common-core-environment.umd.js', 'devextreme-angular/common/core/events': 'bundles:devextreme-angular/devextreme-angular-common-core-events.umd.js', 'devextreme-angular/common/core/localization': 'bundles:devextreme-angular/devextreme-angular-common-core-localization.umd.js', 'devextreme-angular/common/core': 'bundles:devextreme-angular/devextreme-angular-common-core.umd.js', 'devextreme-angular/common/data/custom-store': 'bundles:devextreme-angular/devextreme-angular-common-data-custom-store.umd.js', 'devextreme-angular/common/data': 'bundles:devextreme-angular/devextreme-angular-common-data.umd.js', 'devextreme-angular/common/export/excel': 'bundles:devextreme-angular/devextreme-angular-common-export-excel.umd.js', 'devextreme-angular/common/export/pdf': 'bundles:devextreme-angular/devextreme-angular-common-export-pdf.umd.js', 'devextreme-angular/common/export': 'bundles:devextreme-angular/devextreme-angular-common-export.umd.js', 'devextreme-angular/common/grids': 'bundles:devextreme-angular/devextreme-angular-common-grids.umd.js', 'devextreme-angular/common': 'bundles:devextreme-angular/devextreme-angular-common.umd.js', 'devextreme-angular/http': 'bundles:devextreme-angular/devextreme-angular-http.umd.js', ...componentNames.reduce((acc, name) => { acc[`devextreme-angular/ui/${name}`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}.umd.js`; acc[`devextreme-angular/ui/${name}/nested`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}-nested.umd.js`; return acc; }, {}), 'tslib': 'npm:tslib/tslib.js', 'rxjs': 'npm:rxjs@7.5.3/dist/bundles/rxjs.umd.js', 'rxjs/operators': 'npm:rxjs@7.5.3/dist/cjs/operators/index.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'app': { main: './app.component.ts', defaultExtension: 'ts', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, 'rxjs': { defaultExtension: 'js', }, 'rxjs/operators': { defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:rxjs@7.5.3/package.json', 'npm:rxjs@7.5.3/operators/package.json', 'npm:devexpress-diagram@2.2.19/package.json', 'npm:devexpress-gantt@4.1.62/package.json', ], }; System.config(window.config); // System.import('@angular/compiler').catch(console.error.bind(console));
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.3/css/dx.light.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/zone.js@0.14.10/bundles/zone.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/reflect-metadata@0.1.13/Reflect.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script src="config.js"></script> <script> System.import("app").catch(console.error.bind(console)); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <demo-app>Loading...</demo-app> </div> </body> </html>
<div> Please let us know if you have any other requests. </div> <dx-form [(formData)]="formData"> <dxi-form-item dataField="additionalRequest" editorType="dxTextArea" [editorOptions]="textAreaOptions" [label]="labelOptions" > </dxi-form-item> </dx-form>
import { Component, Input } from '@angular/core'; import { type DxFormTypes } from 'devextreme-angular/ui/form'; import { type DxTextAreaTypes } from 'devextreme-angular/ui/text-area'; import type { BookingFormData } from '../app.types'; let modulePrefix = ''; // @ts-ignore if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'additional-form', templateUrl: `app/additional-form/additional-form.component.html`, }) export class AdditionalFormComponent { @Input() formData: BookingFormData; textAreaOptions: DxTextAreaTypes.Properties = { height: 160, elementAttr: { id: 'additionalRequest' }, } labelOptions: DxFormTypes.SimpleItem["label"] = { visible: false, }; }
::ng-deep .dx-multiview-item-content:has( .summary-container) { overflow: auto; } ::ng-deep .summary-container { display: flex; flex-direction: column; row-gap: 20px; } ::ng-deep .summary-item { display: flex; flex-direction: column; row-gap: 8px; } ::ng-deep .summary-item-header { font-weight: 600; font-size: var(--dx-font-size-sm); } ::ng-deep .center { text-align: center; } ::ng-deep .summary-item-label { color: var(--dx-color-icon); } ::ng-deep .separator { width: 100%; height: 1px; border-bottom: solid 1px var(--dx-color-border); }
<div class="summary-item-header center" *ngIf="isConfirmed"> Your booking request was submitted. </div> <div class="summary-container" *ngIf="!isConfirmed"> <div class="summary-item"> <div class="summary-item-header">Dates</div> <div class="separator"></div> <div> <span class="summary-item-label">Check-in Date: </span >{{ getCheckInDate() }} </div> <div> <span class="summary-item-label">Check-out Date: </span >{{ getCheckOutDate() }} </div> </div> <div class="summary-item"> <div class="summary-item-header">Guests</div> <div class="separator"></div> <div ><span class="summary-item-label">Adults: </span >{{ formData.adultsCount }}</div > <div ><span class="summary-item-label">Children: </span >{{ formData.childrenCount }}</div > <div ><span class="summary-item-label">Pets: </span >{{ formData.petsCount }}</div > </div> <div class="summary-item"> <div class="summary-item-header">Room and Meals</div> <div class="separator"></div> <div ><span class="summary-item-label">Room Type: </span >{{ formData.roomType }}</div > <div ><span class="summary-item-label">Check-out Date: </span >{{ formData.mealPlan }}</div > </div> <div class="summary-item" *ngIf="!!formData.additionalRequest"> <div class="summary-item-header">Additional Requests</div> <div class="separator"></div> <div>{{ formData.additionalRequest }}</div> </div> </div>
import { Component, Input } from '@angular/core'; import type { BookingFormData } from '../app.types'; let modulePrefix = ''; // @ts-ignore if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'confirmation', templateUrl: `app/confirmation/confirmation.component.html`, styleUrls: [`app/confirmation/confirmation.component.css`], }) export class ConfirmationComponent { @Input() formData: BookingFormData; @Input() isConfirmed: boolean; getCheckInDate() { return new Date(this.formData.dates[0]).toLocaleDateString(); } getCheckOutDate() { return new Date(this.formData.dates[1]).toLocaleDateString(); } }
<p> Select your check-in and check-out dates. If your dates are flexible, include that information in Additional Requests. We will do our best to suggest best pricing options, depending on room availability. </p> <dx-form #formComponent [(formData)]="formData" [validationGroup]="validationGroup" > <dxi-form-item [isRequired]="true" dataField="dates" editorType="dxDateRangeBox" [editorOptions]="dateRangeBoxOptions" [label]="labelOptions" ></dxi-form-item> </dx-form>
import { Component, Input, SimpleChanges, ViewChild } from '@angular/core'; import { DxFormComponent, type DxFormTypes } from 'devextreme-angular/ui/form'; import { type DxDateRangeBoxTypes } from 'devextreme-angular/ui/date-range-box'; import type { BookingFormData } from '../app.types'; let modulePrefix = ''; // @ts-ignore if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'dates-form', templateUrl: `app/dates-form/dates-form.component.html`, }) export class DatesFormComponent { @ViewChild('formComponent', { static: false }) form!: DxFormComponent @Input() formData: BookingFormData; @Input() validationGroup: string; ngOnChanges(changes: SimpleChanges) { if (changes['formData']) { const value = changes['formData'].currentValue; this.form?.instance?.reset(value); } } dateRangeBoxOptions: DxDateRangeBoxTypes.Properties = { startDatePlaceholder: 'Check-in', endDatePlaceholder: 'Check-out', elementAttr: { id: 'datesPicker' }, } labelOptions: DxFormTypes.SimpleItem["label"] = { visible: false, }; }
<p> Enter the number of adults, children, and pets staying in the room. This information help us suggest suitable room types, number of beds, and included amenities. </p> <dx-form #formComponent [(formData)]="formData" [validationGroup]="validationGroup" [colCount]="3" > <dxi-form-item [isRequired]="true" dataField="adultsCount" editorType="dxNumberBox" [editorOptions]="numberBoxOptions" [label]="adultsLabelOptions" > <dxi-form-validation-rule type="range" [min]="1"></dxi-form-validation-rule> </dxi-form-item> <dxi-form-item dataField="childrenCount" editorType="dxNumberBox" [editorOptions]="numberBoxOptions" [label]="childrenLabelOptions" > </dxi-form-item> <dxi-form-item dataField="petsCount" editorType="dxNumberBox" [editorOptions]="numberBoxOptions" [label]="petsLabelOptions" > </dxi-form-item> </dx-form>
import { Component, Input, SimpleChanges, ViewChild } from "@angular/core"; import { DxFormComponent, type DxFormTypes } from 'devextreme-angular/ui/form'; import { type DxNumberBoxTypes } from 'devextreme-angular/ui/number-box'; import type { BookingFormData } from '../app.types'; let modulePrefix = ''; // @ts-ignore if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'guests-form', templateUrl: `app/guests-form/guests-form.component.html`, }) export class GuestsFormComponent { @ViewChild('formComponent', { static: false }) form!: DxFormComponent @Input() formData: BookingFormData; @Input() validationGroup: string; ngOnChanges(changes: SimpleChanges) { if (changes['formData']) { const value = changes['formData'].currentValue; this.form?.instance?.reset(value); } } adultsLabelOptions: DxFormTypes.SimpleItem["label"] = { text: 'Adults', location: 'top', }; childrenLabelOptions: DxFormTypes.SimpleItem["label"] = { text: 'Children', location: 'top', }; petsLabelOptions: DxFormTypes.SimpleItem["label"] = { text: 'Pets', location: 'top', }; numberBoxOptions: DxNumberBoxTypes.Properties = { min: 0, max: 5, showSpinButtons: true, elementAttr: { id: 'adultsCount' } }; }
<p> Review room types that can accommodate your group size and make your selection. You can also choose a meal plan, whether it's breakfast only or full board. </p> <dx-form #formComponent [(formData)]="formData" [validationGroup]="validationGroup" [colCount]="2" > <dxi-form-item [isRequired]="true" dataField="roomType" editorType="dxSelectBox" [editorOptions]="roomSelectBoxOptions" [label]="roomLabelOptions" ></dxi-form-item> <dxi-form-item [isRequired]="true" dataField="mealPlan" editorType="dxSelectBox" [editorOptions]="mealSelectBoxOptions" [label]="mealLabelOptions" ></dxi-form-item> </dx-form>
import { Component, Input, SimpleChanges, ViewChild } from "@angular/core"; import { DxFormComponent, type DxFormTypes } from 'devextreme-angular/ui/form'; import { type DxSelectBoxTypes } from 'devextreme-angular/ui/select-box'; import type { BookingFormData } from '../app.types'; let modulePrefix = ''; // @ts-ignore if (window && window.config?.packageConfigPaths) { modulePrefix = '/app'; } @Component({ selector: 'room-meal-plan-form', templateUrl: `app/room-meal-plan-form/room-meal-plan-form.component.html`, }) export class RoomMealPlanFormComponent { @ViewChild('formComponent', { static: false }) form!: DxFormComponent @Input() formData: BookingFormData; @Input() validationGroup: string; ngOnChanges(changes: SimpleChanges) { if (changes['formData']) { const value = changes['formData'].currentValue; this.form?.instance?.reset(value); } } roomLabelOptions: DxFormTypes.SimpleItem["label"] = { text: 'Room Type', location: 'top', }; mealLabelOptions: DxFormTypes.SimpleItem["label"] = { text: 'Meal Plan', location: 'top', }; roomTypes = ['Single', 'Double', 'Suite']; mealPlans = ['Bed & Breakfast', 'Half Board', 'Full Board', 'All-Inclusive']; mealSelectBoxOptions: DxSelectBoxTypes.Properties = { items: this.mealPlans, elementAttr: { id: 'mealPlan' }, }; roomSelectBoxOptions: DxSelectBoxTypes.Properties = { items: this.roomTypes, elementAttr: { id: 'roomType' }, } }

Validation

Form validation impacts step progression and view switch operations. Attempting to proceed by clicking a step (onSelectionChanging) or pressing "Next" (onClick) triggers the validation process (validateStep) for current step input.

Valid data changes the current step to display a checkmark, selects the next step, and updates the view to the following form. Invalid data prompts error messages, marks the step as invalid, and prevents step progress. Only validated steps allow progression.

To view validation in action, move to step two without "Check-in" and "Check-out" dates. Required fields will fail validation, marking step one as invalid (isValid = false). The icon turns red and displays an exclamation mark. The DateRangeBox component also displays an error icon. DateRangeBox and Stepper both display validation errors since they belong to the same validation group.

The final step is unique. Once the "Additional Requests" step is completed, the request is submitted, and a return to previous steps is not permitted. Click "Reset" to restart booking.