Your search did not match any results.

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
@model DevExtreme.NETCore.Demos.ViewModels.StepperFormViewModel @(Html.DevExtreme().Stepper() .ID("stepper") .OnInitialized("onStepperInitialized") .OnSelectionChanged("onSelectionChanged") .OnSelectionChanging("onSelectionChanging") .Items(items => { foreach (var step in Model.Steps) { items.Add() .Label(step.Label) .Hint(step.Hint) .Icon(step.Icon) .Optional(step.Optional); } }) ) <div class="content"> @(Html.DevExtreme().MultiView() .ID("stepContent") .Height(400) .Loop(false) .AnimationEnabled(false) .FocusStateEnabled(false) .OnInitialized("onMultiViewInitialized") .Items(items => { items.Add() .Template(new JS("getDatesTemplate()")); items.Add() .Template(new JS("getGuestsTemplate()")); items.Add() .Template(new JS("getRoomAndMealTemplate()")); items.Add() .Template(new JS("getAdditionalRequestsTemplate()")); items.Add() .Template(new JS("getConfirmationTemplate()")); }) ) <div class="nav-panel"> <div class="current-step">Step <span class="selected-index">1</span> of <span class="step-count">@Model.Steps.Length</span></div> <div class="nav-buttons"> @(Html.DevExtreme().Button() .ID("prevButton") .Type(ButtonType.Normal) .Text("Back") .Visible(false) .Width(100) .OnInitialized("onPrevButtonInitialized") .OnClick("onPrevButtonClick") ) @(Html.DevExtreme().Button() .ID("nextButton") .Type(ButtonType.Default) .Text("Next") .Width(100) .OnInitialized("onNextButtonInitialized") .OnClick("onNextButtonClick") ) </div> </div> </div> <script> let stepper, prevButton, nextButton stepContent; const stepsCount = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Steps.Length)); let confirmed = false; const roomTypes = ['Single', 'Double', 'Suite']; const mealPlans = ['Bed & Breakfast', 'Half Board', 'Full Board', 'All-Inclusive']; const validationGroups = ['dates', 'guests', 'roomAndMealPlan']; function getInitialFormData() { const initialFormData = { dates: [null, null], adultsCount: 0, childrenCount: 0, petsCount: 0, roomType: undefined, mealPlan: undefined, additionalRequest: '', }; return { ...initialFormData, dates: [...initialFormData.dates], }; } let formData = getInitialFormData(); function onStepperInitialized(e) { stepper = e.component; } function onMultiViewInitialized(e) { stepContent = e.component; } function onNextButtonInitialized(e) { nextButton = e.component; } function onPrevButtonInitialized(e) { prevButton = e.component; } function setSelectedIndex(index) { stepper.option('selectedIndex', index); } function getValidationResult(index) { if (index >= validationGroups.length) { return true; } return DevExpress.validationEngine.validateGroup(validationGroups[index]).isValid; } function setStepValidationResult(index, isValid) { stepper.option(`items[${index}].isValid`, isValid); } function onPrevButtonClick(e) { const selectedIndex = stepper.option('selectedIndex'); setSelectedIndex(selectedIndex - 1); } function moveNext(selectedIndex) { const isValid = getValidationResult(selectedIndex); setStepValidationResult(selectedIndex, isValid); if (isValid) { setSelectedIndex(selectedIndex + 1); } } function setStepperReadonly(readonly) { stepper.option('focusStateEnabled', !readonly); if (readonly) { stepper.option('elementAttr', { class: 'readonly' }); } else { stepper.resetOption('elementAttr'); } } function confirm() { confirmed = true; prevButton.option('visible', false); nextButton.option('text', 'Reset'); setStepValidationResult(stepsCount - 1, true); setStepperReadonly(true); $('.current-step').text(''); } function resetStepperState() { stepper.beginUpdate(); stepper.option('selectedIndex', 0); setStepperReadonly(false); for (let i = 0; i < stepsCount; i += 1) { setStepValidationResult(i, undefined); } stepper.endUpdate(); } function reset() { confirmed = false; resetStepperState(); formData = getInitialFormData(); stepContent.repaint(); $('.current-step').append(`Step <span class="selected-index">1</span> of <span class="step-count">${stepsCount}</span>`); }; function onNextButtonClick(e) { const selectedIndex = stepper.option('selectedIndex'); if (selectedIndex < stepsCount - 1) { moveNext(selectedIndex); } else if (confirmed) { reset(); } else { confirm(); } if (stepper.option('selectedIndex') === stepsCount - 1) { stepContent.option('items[4].template', getConfirmationTemplate()); } } function onSelectionChanged(e) { const selectedIndex = e.component.option('selectedIndex'); const isLastStep = selectedIndex === stepsCount - 1; prevButton.option('visible', !!selectedIndex); nextButton.option('text', isLastStep ? 'Confirm' : 'Next'); stepContent.option('selectedIndex', selectedIndex); $('.selected-index').text(selectedIndex + 1); } function onSelectionChanging(e) { const { component, addedItems, removedItems } = e; const { items = [] } = component.option(); const addedIndex = items.findIndex((item) => item === addedItems[0]); const removedIndex = items.findIndex((item) => item === removedItems[0]); const isMoveForward = addedIndex > removedIndex; if (isMoveForward) { const isValid = getValidationResult(removedIndex); setStepValidationResult(removedIndex, isValid); if (isValid === false) { e.cancel = true; } } } function getDatesTemplate() { return function () { return $('<div>').append( $('<p>').text(`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.`), $('<div>').dxForm({ formData, validationGroup: validationGroups[0], items: [{ dataField: 'dates', editorType: 'dxDateRangeBox', editorOptions: { elementAttr: { id: 'datesPicker' }, startDatePlaceholder: 'Check-in', endDatePlaceholder: 'Check-out', }, isRequired: true, label: { visible: false }, }], }), ); } } function getGuestsTemplate() { return function () { const getNumberBoxOptions = (options) => ({ editorType: 'dxNumberBox', ...options, editorOptions: { showSpinButtons: true, min: 0, max: 5, ...options.editorOptions, }, label: { location: 'top', ...options.label, }, }); return $('<div>').append( $('<p>').text(`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.`), $('<div>').dxForm({ formData, validationGroup: validationGroups[1], colCount: 3, colCountByScreen: { xs: 3 }, items: [ getNumberBoxOptions({ dataField: 'adultsCount', isRequired: true, label: { text: 'Adults' }, editorOptions: { elementAttr: { id: 'adultsCount' }, }, validationRules: [{ type: 'range', min: 1, }], }), getNumberBoxOptions({ dataField: 'childrenCount', label: { text: 'Children' }, }), getNumberBoxOptions({ dataField: 'petsCount', label: { text: 'Pets' }, }), ], }), ); } } function getRoomAndMealTemplate() { return function () { const getSelectBoxOptions = (options) => ({ editorType: 'dxSelectBox', isRequired: true, ...options, label: { location: 'top', ...options.label, }, }); return $('<div>').append( $('<p>').text(`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.`), $('<div>').dxForm({ formData, validationGroup: validationGroups[2], colCount: 2, colCountByScreen: { xs: 2 }, items: [ getSelectBoxOptions({ dataField: 'roomType', editorOptions: { items: roomTypes, elementAttr: { id: 'roomType' }, }, label: { text: 'Room Type' }, }), getSelectBoxOptions({ dataField: 'mealPlan', editorOptions: { items: mealPlans, elementAttr: { id: 'mealPlan' }, }, label: { text: 'Meal Plan' }, }), ], }), ); } } function getAdditionalRequestsTemplate() { return function () { return $('<div>').append( $('<div>').text('Please let us know if you have any other requests.'), $('<div>').dxForm({ formData, items: [ { dataField: 'additionalRequest', editorType: 'dxTextArea', editorOptions: { height: 160, elementAttr: { id: 'additionalRequest' }, }, label: { visible: false }, }, ], }), ); } } function getConfirmationTemplate() { return function () { if (confirmed) { return '<div class="summary-item-header center">Your booking request was submitted.</div>'; } const summaryContainer = $('<div class="summary-container">'); const datesData = $(` <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>${new Date(formData.dates[0]).toLocaleDateString()}</div> <div><span class="summary-item-label">Check-out Date: </span>${new Date(formData.dates[1]).toLocaleDateString()}</div> </div> `); const guestsData = $(` <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> `); const roomAndMealData = $(` <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> `); summaryContainer.append(datesData, guestsData, roomAndMealData); if (formData.additionalRequest) { const additionalRequestsData = $(` <div class="summary-item"> <div class="summary-item-header">Additional Requests</div> <div class="separator"></div> <div>${formData.additionalRequest}</div> </div> `); summaryContainer.append(additionalRequestsData); } return summaryContainer; } } </script>
using DevExtreme.NETCore.Demos.Models.SampleData; using DevExtreme.NETCore.Demos.ViewModels; using Microsoft.AspNetCore.Mvc; namespace DevExtreme.NETCore.Demos.Controllers { public class StepperController : Controller { public ActionResult FormIntegration() { return View(new StepperFormViewModel { Steps = SampleData.Steps, }); } } }
using DevExtreme.NETCore.Demos.ViewModels; using System; namespace DevExtreme.NETCore.Demos.Models.SampleData { public partial class SampleData { public static StepViewModel[] Steps = new [] { new StepViewModel { Label = "Dates", Hint = "Dates", Icon = "daterangepicker" }, new StepViewModel { Label = "Guests", Hint = "Guests", Icon = "group" }, new StepViewModel { Label = "Room and Meal Plan", Hint = "Room and Meal Plan", Icon = "servicebell" }, new StepViewModel { Label = "Additional Requests", Hint = "Additional Requests", Icon = "clipboardtasklist", Optional = true }, new StepViewModel { Label = "Confirmation", Hint = "Confirmation", Icon = "checkmarkcircle" }, }; } }
using DevExtreme.NETCore.Demos.Models; using System; namespace DevExtreme.NETCore.Demos.ViewModels { public class StepViewModel { public string Label { get; set; } public string Hint { get; set; } public string Icon { get; set; } public bool Optional { get; set; } } public class StepperFormViewModel { public StepViewModel[] Steps { get; set; } } }
.demo-wrapper .simulator .demo-device .demo-container { min-height: 580px; } .demo-container { display: flex; flex-direction: column; justify-content: center; row-gap: 20px; height: 580px; min-width: 620px; } .content { padding-inline: 40px; flex: 1; display: flex; flex-direction: column; row-gap: 20px; } .dx-multiview-item-content:has(> .summary-container) { overflow: auto; } .summary-container { display: flex; flex-direction: column; row-gap: 20px; } .summary-item { display: flex; flex-direction: column; row-gap: 8px; } .summary-item-header { font-weight: 600; font-size: var(--dx-font-size-sm); } .center { text-align: center; } .summary-item-label { color: var(--dx-color-icon); } .separator { width: 100%; height: 1px; border-bottom: solid 1px var(--dx-color-border); } .nav-panel { display: flex; align-items: center; justify-content: space-between; } .current-step { color: var(--dx-color-icon); } .nav-buttons { display: flex; gap: 8px; } .readonly { pointer-events: none; }

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.