DevExtreme v25.1 is now available.

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

Your search did not match any results.

React 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
import React, { useState, useCallback, useMemo } from 'react'; import { Stepper, Item, type StepperTypes } from 'devextreme-react/stepper' import { Button } from 'devextreme-react/button'; import { MultiView } from 'devextreme-react/multi-view'; import validationEngine from 'devextreme/ui/validation_engine'; import DatesForm from './DatesForm.tsx'; import GuestsForm from './GuestsForm.tsx'; import RoomMealPlanForm from './RoomMealPlanForm.tsx'; import AdditionalForm from './AdditionalForm.tsx'; import Confirmation from './Confirmation.tsx'; import { initialSteps, getInitialFormData } from './data.ts'; import { BookingFormData } from './types.ts'; const validationGroups = ['dates', 'guests', 'roomAndMealPlan']; export default function App () { const [selectedIndex, setSelectedIndex] = useState(0); const [steps, setSteps] = useState<StepperTypes.Item[]>(initialSteps); const [formData, setFormData] = useState<BookingFormData>(getInitialFormData); const [isConfirmed, setIsConfirmed] = useState(false); const [isStepperReadonly, setIsStepperReadonly] = useState(false); const getValidationResult = useCallback((index: number) => { if (index >= validationGroups.length) { return true; } return validationEngine.validateGroup(validationGroups[index]).isValid; }, []); const setStepValidationResult = useCallback((index: number, isValid: boolean | undefined) => { setSteps((prev) => prev.map((step, i) => { if (i === index) { return { ...step, isValid, } } return step; })); }, []); const onPrevButtonClick = useCallback(() => { setSelectedIndex((prev) => prev - 1); }, []); const moveNext = useCallback(() => { const isValid = getValidationResult(selectedIndex); setStepValidationResult(selectedIndex, isValid); if (isValid){ setSelectedIndex(selectedIndex + 1); } }, [getValidationResult, selectedIndex, setStepValidationResult]); const onConfirm = useCallback(() => { setIsConfirmed(true); setStepValidationResult(initialSteps.length - 1, true); setIsStepperReadonly(true); }, [setStepValidationResult]); const onReset = useCallback(() => { setIsConfirmed(false); setSteps(initialSteps); setSelectedIndex(0); setFormData(getInitialFormData); setIsStepperReadonly(false); }, []); const onNextButtonClick = useCallback(() => { if (selectedIndex < initialSteps.length -1) { moveNext(); } else if (isConfirmed) { onReset(); } else { onConfirm(); } }, [selectedIndex, isConfirmed, onConfirm, onReset, moveNext]); const nextButtonText = useMemo(() => { if (selectedIndex < steps.length - 1) { return 'Next'; } if (isConfirmed) { return 'Reset'; } return 'Confirm'; }, [selectedIndex, isConfirmed, steps.length]); const onSelectionChanging = useCallback((args: StepperTypes.SelectionChangingEvent) => { const { component, addedItems, removedItems } = args; const { items = [] } = component.option(); const addedIndex = items.findIndex((item: StepperTypes.Item) => item === addedItems[0]); const removedIndex = items.findIndex((item: StepperTypes.Item) => item === removedItems[0]); const isMoveForward = addedIndex > removedIndex; if (isMoveForward) { const isValid = getValidationResult(removedIndex); setStepValidationResult(removedIndex, isValid); if (isValid === false) { args.cancel = true; } } }, [setStepValidationResult, getValidationResult]); const onSelectionChanged = useCallback(({ component }: StepperTypes.SelectionChangedEvent) => { setSelectedIndex(component.option('selectedIndex') ?? 0); }, []); const renderDatesForm = useCallback(() => { return <DatesForm formData={formData} validationGroup={validationGroups[0]} />; }, [formData]); const renderGuestsForm = useCallback(() => { return <GuestsForm formData={formData} validationGroup={validationGroups[1]} />; }, [formData]); const renderRoomMealPlanForm = useCallback(() => { return <RoomMealPlanForm formData={formData} validationGroup={validationGroups[2]} />; }, [formData]); const renderAdditionalForm = useCallback(() => { return <AdditionalForm formData={formData} />; }, [formData]); const renderConfirmation = useCallback(() => { return <Confirmation formData={formData} isConfirmed={isConfirmed} />; }, [formData, isConfirmed]); return ( <> <Stepper className={ isStepperReadonly ? 'readonly' : ''} focusStateEnabled={!isStepperReadonly} selectedIndex={selectedIndex} onSelectionChanged={onSelectionChanged} onSelectionChanging={onSelectionChanging} > {steps.map((step) => <Item key={step.label} {...step} />)} </Stepper> <div className="content"> <MultiView selectedIndex={selectedIndex} focusStateEnabled={false} animationEnabled={false} swipeEnabled={false} height={400} > <Item render={renderDatesForm} /> <Item render={renderGuestsForm} /> <Item render={renderRoomMealPlanForm} /> <Item render={renderAdditionalForm} /> <Item render={renderConfirmation} /> </MultiView> <div className="nav-panel"> <div className="current-step"> {!isConfirmed && ( <> Step <span className="selected-index">{selectedIndex + 1}</span> {' of '} {steps.length} </> )} </div> <div className="nav-buttons"> <Button id="prevButton" text="Back" type="normal" onClick={onPrevButtonClick} visible={selectedIndex !== 0 && !isConfirmed} width={100} /> <Button id="nextButton" text={nextButtonText} type="default" onClick={onNextButtonClick} width={100} /> </div> </div> </div> </> ); }
import React, { useState, useCallback, useMemo } from 'react'; import { Stepper, Item } from 'devextreme-react/stepper'; import { Button } from 'devextreme-react/button'; import { MultiView } from 'devextreme-react/multi-view'; import validationEngine from 'devextreme/ui/validation_engine'; import DatesForm from './DatesForm.js'; import GuestsForm from './GuestsForm.js'; import RoomMealPlanForm from './RoomMealPlanForm.js'; import AdditionalForm from './AdditionalForm.js'; import Confirmation from './Confirmation.js'; import { initialSteps, getInitialFormData } from './data.js'; const validationGroups = ['dates', 'guests', 'roomAndMealPlan']; export default function App() { const [selectedIndex, setSelectedIndex] = useState(0); const [steps, setSteps] = useState(initialSteps); const [formData, setFormData] = useState(getInitialFormData); const [isConfirmed, setIsConfirmed] = useState(false); const [isStepperReadonly, setIsStepperReadonly] = useState(false); const getValidationResult = useCallback((index) => { if (index >= validationGroups.length) { return true; } return validationEngine.validateGroup(validationGroups[index]).isValid; }, []); const setStepValidationResult = useCallback((index, isValid) => { setSteps((prev) => prev.map((step, i) => { if (i === index) { return { ...step, isValid, }; } return step; })); }, []); const onPrevButtonClick = useCallback(() => { setSelectedIndex((prev) => prev - 1); }, []); const moveNext = useCallback(() => { const isValid = getValidationResult(selectedIndex); setStepValidationResult(selectedIndex, isValid); if (isValid) { setSelectedIndex(selectedIndex + 1); } }, [getValidationResult, selectedIndex, setStepValidationResult]); const onConfirm = useCallback(() => { setIsConfirmed(true); setStepValidationResult(initialSteps.length - 1, true); setIsStepperReadonly(true); }, [setStepValidationResult]); const onReset = useCallback(() => { setIsConfirmed(false); setSteps(initialSteps); setSelectedIndex(0); setFormData(getInitialFormData); setIsStepperReadonly(false); }, []); const onNextButtonClick = useCallback(() => { if (selectedIndex < initialSteps.length - 1) { moveNext(); } else if (isConfirmed) { onReset(); } else { onConfirm(); } }, [selectedIndex, isConfirmed, onConfirm, onReset, moveNext]); const nextButtonText = useMemo(() => { if (selectedIndex < steps.length - 1) { return 'Next'; } if (isConfirmed) { return 'Reset'; } return 'Confirm'; }, [selectedIndex, isConfirmed, steps.length]); const onSelectionChanging = useCallback( (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) { const isValid = getValidationResult(removedIndex); setStepValidationResult(removedIndex, isValid); if (isValid === false) { args.cancel = true; } } }, [setStepValidationResult, getValidationResult], ); const onSelectionChanged = useCallback(({ component }) => { setSelectedIndex(component.option('selectedIndex') ?? 0); }, []); const renderDatesForm = useCallback(() => ( <DatesForm formData={formData} validationGroup={validationGroups[0]} /> ), [formData]); const renderGuestsForm = useCallback(() => ( <GuestsForm formData={formData} validationGroup={validationGroups[1]} /> ), [formData]); const renderRoomMealPlanForm = useCallback(() => ( <RoomMealPlanForm formData={formData} validationGroup={validationGroups[2]} /> ), [formData]); const renderAdditionalForm = useCallback(() => <AdditionalForm formData={formData} />, [formData]); const renderConfirmation = useCallback(() => ( <Confirmation formData={formData} isConfirmed={isConfirmed} /> ), [formData, isConfirmed]); return ( <React.Fragment> <Stepper className={isStepperReadonly ? 'readonly' : ''} focusStateEnabled={!isStepperReadonly} selectedIndex={selectedIndex} onSelectionChanged={onSelectionChanged} onSelectionChanging={onSelectionChanging} > {steps.map((step) => ( <Item key={step.label} {...step} /> ))} </Stepper> <div className="content"> <MultiView selectedIndex={selectedIndex} focusStateEnabled={false} animationEnabled={false} swipeEnabled={false} height={400} > <Item render={renderDatesForm} /> <Item render={renderGuestsForm} /> <Item render={renderRoomMealPlanForm} /> <Item render={renderAdditionalForm} /> <Item render={renderConfirmation} /> </MultiView> <div className="nav-panel"> <div className="current-step"> {!isConfirmed && ( <React.Fragment> Step <span className="selected-index">{selectedIndex + 1}</span> {' of '} {steps.length} </React.Fragment> )} </div> <div className="nav-buttons"> <Button id="prevButton" text="Back" type="normal" onClick={onPrevButtonClick} visible={selectedIndex !== 0 && !isConfirmed} width={100} /> <Button id="nextButton" text={nextButtonText} type="default" onClick={onNextButtonClick} width={100} /> </div> </div> </div> </React.Fragment> ); }
import React, { FC, memo } from 'react'; import 'devextreme-react/text-area'; import { Form, SimpleItem } from 'devextreme-react/form'; import { FormProps } from './types.ts'; const AdditionalForm: FC<FormProps> = memo(({ formData }) => ( <> <div> Please let us know if you have any other requests. </div> <Form formData={formData} > <SimpleItem dataField='additionalRequest' editorType='dxTextArea' editorOptions={{ height: 160, elementAttr: { id: 'additionalRequest' }, }} label={{ visible: false }} ></SimpleItem> </Form> </> )); AdditionalForm.displayName = 'AdditionalForm'; export default AdditionalForm;
import React, { FC, memo } from 'react'; import 'devextreme-react/date-range-box'; import { BookingFormData } from './types.ts'; interface ConfirmationProps { formData: BookingFormData; isConfirmed: boolean; } const Confirmation: FC<ConfirmationProps> = ({ formData, isConfirmed }) => { if (isConfirmed) { return ( <div className="summary-item-header center"> Your booking request was submitted. </div> ); } return ( <div className="summary-container"> <div className="summary-item"> <div className="summary-item-header">Dates</div> <div className="separator"></div> <div> <span className="summary-item-label">Check-in Date: </span>{new Date(formData.dates[0]).toLocaleDateString()} </div> <div> <span className="summary-item-label">Check-out Date: </span>{new Date(formData.dates[1]).toLocaleDateString()} </div> </div> <div className="summary-item"> <div className="summary-item-header">Guests</div> <div className="separator"></div> <div><span className="summary-item-label">Adults: </span>{formData.adultsCount}</div> <div><span className="summary-item-label">Children: </span>{formData.childrenCount}</div> <div><span className="summary-item-label">Pets: </span>{formData.petsCount}</div> </div> <div className="summary-item"> <div className="summary-item-header">Room and Meals</div> <div className="separator"></div> <div><span className="summary-item-label">Room Type: </span>{formData.roomType}</div> <div><span className="summary-item-label">Check-out Date: </span>{formData.mealPlan}</div> </div> {!!formData.additionalRequest && ( <div className="summary-item"> <div className="summary-item-header">Additional Requests</div> <div className="separator"></div> <div>{formData.additionalRequest}</div> </div> )} </div> ); }; Confirmation.displayName = 'Confirmation'; export default Confirmation;
import React, { FC, memo } from 'react'; import 'devextreme-react/date-range-box'; import { Form, SimpleItem } from 'devextreme-react/form'; import { FormProps } from './types.ts'; const DatesForm: FC<FormProps> = memo(({ formData, validationGroup }) => ( <> <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> <Form formData={formData} validationGroup={validationGroup}> <SimpleItem isRequired dataField='dates' editorType='dxDateRangeBox' editorOptions={{ startDatePlaceholder: 'Check-in', endDatePlaceholder: 'Check-out', elementAttr: { id: 'datesPicker' }, }} label={{ visible: false }} /> </Form> </> )); DatesForm.displayName = 'DatesForm'; export default DatesForm;
import React, { FC, memo } from 'react'; import { Form, RangeRule, SimpleItem } from 'devextreme-react/form'; import 'devextreme-react/number-box'; import { FormProps } from './types.ts'; const GuestsForm: FC<FormProps> = memo(({ formData, validationGroup }) => ( <> <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> <Form formData={formData} validationGroup={validationGroup} colCount={3}> <SimpleItem isRequired dataField='adultsCount' editorType='dxNumberBox' editorOptions={{ elementAttr: { id: 'adultsCount' }, showSpinButtons: true, min: 0, max: 5 }} label={{ text: 'Adults', location: 'top' }} > <RangeRule min={1} /> </SimpleItem> <SimpleItem dataField='childrenCount' editorType='dxNumberBox' editorOptions={{ showSpinButtons: true, min: 0, max: 5 }} label={{ text: 'Children', location: 'top' }} /> <SimpleItem dataField='petsCount' editorType='dxNumberBox' editorOptions={{ showSpinButtons: true, min: 0, max: 5 }} label={{ text: 'Pets', location: 'top' }} /> </Form> </> )); GuestsForm.displayName = 'GuestsForm'; export default GuestsForm;
import React, { FC, memo } from 'react'; import 'devextreme-react/select-box'; import { Form, SimpleItem } from 'devextreme-react/form'; import { FormProps } from './types.ts'; import { mealPlans, roomTypes } from './data.ts'; const RoomMealPlanForm: FC<FormProps> = memo(({ formData, validationGroup }) => ( <> <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> <Form formData={formData} validationGroup={validationGroup} colCount={2}> <SimpleItem dataField='roomType' isRequired editorType='dxSelectBox' label={{ text: 'Room Type', location: 'top' }} editorOptions={{ items: roomTypes, elementAttr: { id: 'roomType' }, }} /> <SimpleItem dataField='mealPlan' isRequired editorType='dxSelectBox' label={{ text: 'Meal Plan', location: 'top' }} editorOptions={{ items: mealPlans, elementAttr: { id: 'mealPlan' }, }} /> </Form> </> )); RoomMealPlanForm.displayName = 'RoomMealPlanForm'; export default RoomMealPlanForm;
import React from 'react'; import ReactDOM from 'react-dom'; import themes from 'devextreme/ui/themes'; import App from './App.tsx'; themes.initialized(() => { ReactDOM.render( <App />, document.getElementById('app'), ); });
import { type StepperTypes } from 'devextreme-react/stepper' import type { BookingFormData } from './types'; export const initialSteps: StepperTypes.Item[] = [ { 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', }, ]; export const roomTypes = ['Single', 'Double', 'Suite']; export const mealPlans = ['Bed & Breakfast', 'Half Board', 'Full Board', 'All-Inclusive']; export const initialFormData: BookingFormData = { dates: [null, null], adultsCount: 0, childrenCount: 0, petsCount: 0, roomType: undefined, mealPlan: undefined, additionalRequest: '', }; export const getInitialFormData = () => ({ ...initialFormData, dates: [...initialFormData.dates], });
export interface BookingFormData { dates: Array<Date | null>; adultsCount: number; childrenCount: number; petsCount: number; roomType: string | undefined; mealPlan: string | undefined; additionalRequest: string; } export interface FormProps { formData: BookingFormData; validationGroup?: string; }
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, defaultExtension: 'js', 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', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types/prop-types.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', 'devextreme': 'npm:devextreme@25.1.3/cjs', 'devextreme-react': 'npm:devextreme-react@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/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.62/dist/dx-gantt.js', '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', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme-react/common': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: 'json', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: true, }, }; System.config(window.config);
import React, { memo } from 'react'; import 'devextreme-react/text-area'; import { Form, SimpleItem } from 'devextreme-react/form'; const AdditionalForm = memo(({ formData }) => ( <React.Fragment> <div>Please let us know if you have any other requests.</div> <Form formData={formData}> <SimpleItem dataField="additionalRequest" editorType="dxTextArea" editorOptions={{ height: 160, elementAttr: { id: 'additionalRequest' }, }} label={{ visible: false }} ></SimpleItem> </Form> </React.Fragment> )); AdditionalForm.displayName = 'AdditionalForm'; export default AdditionalForm;
import React from 'react'; import 'devextreme-react/date-range-box'; const Confirmation = ({ formData, isConfirmed }) => { if (isConfirmed) { return <div className="summary-item-header center">Your booking request was submitted.</div>; } return ( <div className="summary-container"> <div className="summary-item"> <div className="summary-item-header">Dates</div> <div className="separator"></div> <div> <span className="summary-item-label">Check-in Date: </span> {new Date(formData.dates[0]).toLocaleDateString()} </div> <div> <span className="summary-item-label">Check-out Date: </span> {new Date(formData.dates[1]).toLocaleDateString()} </div> </div> <div className="summary-item"> <div className="summary-item-header">Guests</div> <div className="separator"></div> <div> <span className="summary-item-label">Adults: </span> {formData.adultsCount} </div> <div> <span className="summary-item-label">Children: </span> {formData.childrenCount} </div> <div> <span className="summary-item-label">Pets: </span> {formData.petsCount} </div> </div> <div className="summary-item"> <div className="summary-item-header">Room and Meals</div> <div className="separator"></div> <div> <span className="summary-item-label">Room Type: </span> {formData.roomType} </div> <div> <span className="summary-item-label">Check-out Date: </span> {formData.mealPlan} </div> </div> {!!formData.additionalRequest && ( <div className="summary-item"> <div className="summary-item-header">Additional Requests</div> <div className="separator"></div> <div>{formData.additionalRequest}</div> </div> )} </div> ); }; Confirmation.displayName = 'Confirmation'; export default Confirmation;
import React, { memo } from 'react'; import 'devextreme-react/date-range-box'; import { Form, SimpleItem } from 'devextreme-react/form'; const DatesForm = memo(({ formData, validationGroup }) => ( <React.Fragment> <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> <Form formData={formData} validationGroup={validationGroup} > <SimpleItem isRequired dataField="dates" editorType="dxDateRangeBox" editorOptions={{ startDatePlaceholder: 'Check-in', endDatePlaceholder: 'Check-out', elementAttr: { id: 'datesPicker' }, }} label={{ visible: false }} /> </Form> </React.Fragment> )); DatesForm.displayName = 'DatesForm'; export default DatesForm;
import React, { memo } from 'react'; import { Form, RangeRule, SimpleItem } from 'devextreme-react/form'; import 'devextreme-react/number-box'; const GuestsForm = memo(({ formData, validationGroup }) => ( <React.Fragment> <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> <Form formData={formData} validationGroup={validationGroup} colCount={3} > <SimpleItem isRequired dataField="adultsCount" editorType="dxNumberBox" editorOptions={{ elementAttr: { id: 'adultsCount' }, showSpinButtons: true, min: 0, max: 5, }} label={{ text: 'Adults', location: 'top' }} > <RangeRule min={1} /> </SimpleItem> <SimpleItem dataField="childrenCount" editorType="dxNumberBox" editorOptions={{ showSpinButtons: true, min: 0, max: 5 }} label={{ text: 'Children', location: 'top' }} /> <SimpleItem dataField="petsCount" editorType="dxNumberBox" editorOptions={{ showSpinButtons: true, min: 0, max: 5 }} label={{ text: 'Pets', location: 'top' }} /> </Form> </React.Fragment> )); GuestsForm.displayName = 'GuestsForm'; export default GuestsForm;
import React, { memo } from 'react'; import 'devextreme-react/select-box'; import { Form, SimpleItem } from 'devextreme-react/form'; import { mealPlans, roomTypes } from './data.js'; const RoomMealPlanForm = memo(({ formData, validationGroup }) => ( <React.Fragment> <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> <Form formData={formData} validationGroup={validationGroup} colCount={2} > <SimpleItem dataField="roomType" isRequired editorType="dxSelectBox" label={{ text: 'Room Type', location: 'top' }} editorOptions={{ items: roomTypes, elementAttr: { id: 'roomType' }, }} /> <SimpleItem dataField="mealPlan" isRequired editorType="dxSelectBox" label={{ text: 'Meal Plan', location: 'top' }} editorOptions={{ items: mealPlans, elementAttr: { id: 'mealPlan' }, }} /> </Form> </React.Fragment> )); RoomMealPlanForm.displayName = 'RoomMealPlanForm'; export default RoomMealPlanForm;
export {};
import React from 'react'; import ReactDOM from 'react-dom'; import themes from 'devextreme/ui/themes'; import App from './App.js'; themes.initialized(() => { ReactDOM.render(<App />, document.getElementById('app')); });
export const 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', }, ]; export const roomTypes = ['Single', 'Double', 'Suite']; export const mealPlans = ['Bed & Breakfast', 'Half Board', 'Full Board', 'All-Inclusive']; export const initialFormData = { dates: [null, null], adultsCount: 0, childrenCount: 0, petsCount: 0, roomType: undefined, mealPlan: undefined, additionalRequest: '', }; export const getInitialFormData = () => ({ ...initialFormData, dates: [...initialFormData.dates], });
<!DOCTYPE html> <html 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" /> <link rel="stylesheet" type="text/css" href="styles.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/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
.demo-container { min-height: 580px; } #app { 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.