DevExtreme v25.1 is now available.

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

Your search did not match any results.

JavaScript/jQuery Stepper - Form Integration

This demo uses the DevExtreme JavaScript Stepper component to guide users through a multi-page hotel registration form. The JavaScript 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:

  • JavaScript 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
$(() => { const validationGroups = ['dates', 'guests', 'roomAndMealPlan']; let confirmed = false; let formData = getInitialFormData(); const stepper = $('#stepper').dxStepper({ items: steps, onSelectionChanged(e) { const selectedIndex = e.component.option('selectedIndex'); setSelectedIndex(selectedIndex); }, onSelectionChanging(args) { const { component, addedItems, removedItems } = args; 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 && validateStep(removedIndex) === false) { args.cancel = true; } }, }).dxStepper('instance'); const multiViewItems = [ { template: getDatesForm() }, { template: getGuestsForm() }, { template: getRoomAndMealForm() }, { template: getAdditionalRequestsForm() }, { template: getConfirmationTemplate() }, ]; const stepContent = $('#stepContent').dxMultiView({ animationEnabled: false, focusStateEnabled: false, swipeEnabled: false, height: 400, items: multiViewItems, }).dxMultiView('instance'); const prevButton = $('#prevButton').dxButton({ text: 'Back', type: 'normal', width: 100, onClick: () => { const selectedIndex = stepper.option('selectedIndex'); setSelectedIndex(selectedIndex - 1); }, visible: false, }).dxButton('instance'); const nextButton = $('#nextButton').dxButton({ text: 'Next', type: 'default', width: 100, onClick: () => { const selectedIndex = stepper.option('selectedIndex'); if (selectedIndex < steps.length - 1) { if (validateStep(selectedIndex)) { setSelectedIndex(selectedIndex + 1); } } else if (confirmed) { reset(); } else { confirm(); } }, }).dxButton('instance'); function validateStep(index) { const isValid = getValidationResult(index); stepper.option(`items[${index}].isValid`, isValid); return isValid; } function setSelectedIndex(index) { stepper.option('selectedIndex', index); stepContent.option('selectedIndex', index); setCurrentStepCaption(index); updateStepNavigationButtons(index); if (index === steps.length - 1) { stepContent.option('items[4].template', getConfirmationTemplate()); } } function reset() { confirmed = false; resetStepperState(); formData = getInitialFormData(); stepContent.repaint(); setSelectedIndex(0); } function confirm() { confirmed = true; setStepperReadonly(true); validateStep(steps.length - 1); setSelectedIndex(steps.length - 1); } function getValidationResult(index) { if (index >= validationGroups.length) { return true; } return DevExpress.validationEngine.validateGroup(validationGroups[index]).isValid; } function setCurrentStepCaption(index) { if (confirmed) { $('.current-step').empty(); } else if (!$('.current-step').text()) { $('.current-step').append(`Step <span class="selected-index">${index + 1}</span> of ${steps.length}`); } else { $('.selected-index').text(index + 1); } } function updateStepNavigationButtons(index) { const isLastStep = index === steps.length - 1; const lastStepNextButtonText = confirmed ? 'Reset' : 'Confirm'; const nextButtonText = isLastStep ? lastStepNextButtonText : 'Next'; prevButton.option('visible', !!index && !confirmed); nextButton.option('text', nextButtonText); } function setStepperReadonly(readonly) { stepper.option('focusStateEnabled', !readonly); if (readonly) { stepper.option('elementAttr', { class: 'readonly' }); } else { stepper.resetOption('elementAttr'); } } function resetStepperState() { stepper.beginUpdate(); for (let i = 0; i < steps.length; i += 1) { stepper.option(`items[${i}].isValid`, undefined); } setStepperReadonly(false); stepper.endUpdate(); } function getDatesForm() { 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 getGuestsForm() { return () => { 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, 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 getRoomAndMealForm() { return () => { 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, 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 getAdditionalRequestsForm() { 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 () => { 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; }; } });
<!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" /> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> <script>window.jQuery || document.write(decodeURIComponent('%3Cscript src="js/jquery.min.js"%3E%3C/script%3E'))</script> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.3/css/dx.light.css" /> <script src="js/dx.all.debug.js"></script> <script src="data.js"></script> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="index.js"></script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="stepper"></div> <div class="content"> <div id="stepContent"></div> <div class="nav-panel"> <div class="current-step"> Step <span class="selected-index">1</span> of 5</div> <div class="nav-buttons"> <div id="prevButton"></div> <div id="nextButton"></div> </div> </div> </div> </div> </body> </html>
.demo-container { display: flex; flex-direction: column; justify-content: center; row-gap: 20px; height: 580px; min-height: 480px; 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; }
const steps = [ { 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', }, ]; const roomTypes = ['Single', 'Double', 'Suite']; const mealPlans = ['Bed & Breakfast', 'Half Board', 'Full Board', 'All-Inclusive']; const initialFormData = { dates: [null, null], adultsCount: 0, childrenCount: 0, petsCount: 0, roomType: undefined, mealPlan: undefined, additionalRequest: '', }; const getInitialFormData = () => ({ ...initialFormData, dates: [...initialFormData.dates], });

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 JavaScript 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.