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;
}