@model DevExtreme.MVC.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,
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,
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 System.Web.Mvc;
using DevExtreme.MVC.Demos.Models.SampleData;
using DevExtreme.MVC.Demos.ViewModels;
namespace DevExtreme.MVC.Demos.Controllers {
public class StepperController : Controller {
public ActionResult FormIntegration() {
return View(new StepperFormViewModel {
Steps = SampleData.Steps,
});
}
}
}
using DevExtreme.MVC.Demos.ViewModels;
using System;
namespace DevExtreme.MVC.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 System;
namespace DevExtreme.MVC.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;
}