Skip to content

Commit 986e7e1

Browse files
committed
Stepper: add form integration vue demo
1 parent d13db65 commit 986e7e1

File tree

11 files changed

+627
-13
lines changed

11 files changed

+627
-13
lines changed

apps/demos/Demos/Stepper/FormIntegration/React/App.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ export default function App () {
3333
setSelectedIndex((prev) => prev - 1);
3434
}, []);
3535

36-
const onConfirm = useCallback(() => {
37-
setIsConfirmed(true);
38-
setStepValidationResult(initialSteps.length - 1, true);
39-
}, []);
40-
41-
const onReset = useCallback(() => {
42-
setIsConfirmed(false);
43-
setSteps(initialSteps);
44-
setSelectedIndex(0);
45-
formRef.current.instance().updateData(cloneFormData());
46-
validationEngine.resetGroup(validationGroups[0])
47-
}, []);
36+
const onConfirm = useCallback(() => {
37+
setIsConfirmed(true);
38+
setStepValidationResult(initialSteps.length - 1, true);
39+
}, []);
40+
41+
const onReset = useCallback(() => {
42+
setIsConfirmed(false);
43+
setSteps(initialSteps);
44+
setSelectedIndex(0);
45+
formRef.current.instance().updateData(cloneFormData());
46+
validationEngine.resetGroup(validationGroups[0])
47+
}, []);
4848

4949
const onNextButtonClick = useCallback(() => {
5050
if (selectedIndex < initialSteps.length -1) {
@@ -160,7 +160,7 @@ export default function App () {
160160
text="Back"
161161
type="normal"
162162
onClick={onPrevButtonClick}
163-
visible={selectedIndex !== 0}
163+
visible={selectedIndex !== 0 && !isConfirmed}
164164
/>
165165

166166
<Button
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<div>
3+
Please let us know if you have any other requests.
4+
</div>
5+
<DxForm :form-data="formData">
6+
<DxSimpleItem
7+
data-field="additionalRequest"
8+
editor-type="dxTextArea"
9+
:editor-options="textAreaOptions"
10+
:label="labelOptions"
11+
/>
12+
</DxForm>
13+
</template>
14+
15+
<script setup lang="ts">
16+
import DxForm, { DxSimpleItem } from 'devextreme-vue/form';
17+
import 'devextreme/ui/text_area';
18+
import { BookingFormData } from './types.ts';
19+
20+
const props = withDefaults(defineProps<{
21+
formData: BookingFormData;
22+
}>(), {
23+
formData: () => ({}),
24+
});
25+
26+
const labelOptions = {
27+
visible: false,
28+
};
29+
30+
const textAreaOptions = {
31+
height: 160,
32+
};
33+
</script>
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<template>
2+
<DxStepper
3+
:items="items"
4+
v-model:selected-index="selectedIndex"
5+
@selection-changing="onSelectionChanging"
6+
/>
7+
<div class="content">
8+
<DxMultiView
9+
v-model:selected-index="selectedIndex"
10+
:animation-enabled="false"
11+
:swipe-enabled="false"
12+
:height="300"
13+
>
14+
<DxItem>
15+
<template #default>
16+
<DatesTemplate
17+
:form-data="formData"
18+
:validation-group="validationGroups[0]"
19+
/>
20+
</template>
21+
</DxItem>
22+
<DxItem>
23+
<template #default>
24+
<GuestsTemplate
25+
:form-data="formData"
26+
:validation-group="validationGroups[1]"
27+
/>
28+
</template>
29+
</DxItem>
30+
<DxItem>
31+
<template #default>
32+
<RoomMealPlanTemplate
33+
:form-data="formData"
34+
:validation-group="validationGroups[2]"
35+
/>
36+
</template>
37+
</DxItem>
38+
<DxItem>
39+
<template #default>
40+
<AdditionalTemplate :form-data="formData"/>
41+
</template>
42+
</DxItem>
43+
<DxItem>
44+
<template #default>
45+
<ConfirmationTemplate
46+
:form-data="formData"
47+
v-model:is-confirmed="isConfirmed"
48+
/>
49+
</template>
50+
</DxItem>
51+
</DxMultiView>
52+
<div class="nav-panel">
53+
<div class="current-step">
54+
<span v-if="!isConfirmed">
55+
Step <span class="selected-index">{{ selectedIndex + 1 }}</span> of <span class="step-count">{{ items.length }}</span>
56+
</span>
57+
</div>
58+
<div class="nav-buttons">
59+
<DxButton
60+
:visible="selectedIndex !== 0 && !isConfirmed"
61+
text="Back"
62+
type="normal"
63+
@click="onPrevButtonClick"
64+
/>
65+
<DxButton
66+
:text="nextButtonText"
67+
type="default"
68+
@click="onNextButtonClick"
69+
/>
70+
</div>
71+
</div>
72+
</div>
73+
</template>
74+
75+
<script setup lang="ts">
76+
import { ref, computed } from 'vue';
77+
import DxButton from 'devextreme-vue/button';
78+
import DxMultiView, { DxItem } from 'devextreme-vue/multi-view';
79+
import DxStepper from 'devextreme-vue/stepper';
80+
import type { IItemProps } from 'devextreme-react/cjs/stepper';
81+
import type { SelectionChangingEvent } from 'devextreme/ui/stepper';
82+
import validationEngine from 'devextreme/ui/validation_engine';
83+
import DatesTemplate from './DatesTemplate.vue';
84+
import GuestsTemplate from './GuestsTemplate.vue';
85+
import RoomMealPlanTemplate from './RoomMealPlanTemplate.vue';
86+
import AdditionalTemplate from './AdditionalTemplate.vue';
87+
import ConfirmationTemplate from './ConfirmationTemplate.vue';
88+
import { initialSteps, initialFormData } from './data.ts';
89+
import { BookingFormData } from './types';
90+
91+
const cloneItems = () => initialSteps.map((item) => ({ ...item }));
92+
const cloneFormData = () => ({
93+
...initialFormData,
94+
dates: [...initialFormData.dates],
95+
});
96+
97+
const selectedIndex = ref(0);
98+
const isConfirmed = ref(false);
99+
const items = ref<IItemProps[]>(cloneItems());
100+
const formData = ref<BookingFormData>(cloneFormData());
101+
102+
const validationGroups = ['dates', 'guests', 'roomAndMealPlan'];
103+
104+
const nextButtonText = computed(() => {
105+
if (selectedIndex.value < items.value.length - 1) {
106+
return 'Next';
107+
}
108+
109+
return isConfirmed.value ? 'Reset' : 'Confirm';
110+
});
111+
112+
const getValidationResult = (index: number) => {
113+
if (index >= validationGroups.length) {
114+
return true;
115+
}
116+
117+
return validationEngine.validateGroup(validationGroups[index]).isValid;
118+
};
119+
120+
const setStepValidationResult = (index: number, isValid: boolean | undefined) => {
121+
const prev = items.value;
122+
123+
items.value = prev.map((item, i) => {
124+
if (i === index) {
125+
return {
126+
...item,
127+
isValid,
128+
};
129+
}
130+
131+
return item;
132+
});
133+
};
134+
135+
function onSelectionChanging(e: SelectionChangingEvent) {
136+
if (isConfirmed.value) {
137+
e.cancel = true;
138+
139+
return;
140+
}
141+
142+
const { component, addedItems, removedItems } = e;
143+
const { items = [] } = component.option();
144+
145+
const addedIndex = items.findIndex((item: IItemProps) => item === addedItems[0]);
146+
const removedIndex = items.findIndex((item: IItemProps) => item === removedItems[0]);
147+
const isMoveForward = addedIndex > removedIndex;
148+
149+
if (isMoveForward) {
150+
const isValid = getValidationResult(removedIndex);
151+
152+
setStepValidationResult(removedIndex, isValid);
153+
154+
if (isValid === false) {
155+
e.cancel = true;
156+
}
157+
}
158+
}
159+
160+
function onPrevButtonClick() {
161+
selectedIndex.value -= 1;
162+
}
163+
164+
const moveNext = () => {
165+
const isValid = getValidationResult(selectedIndex.value);
166+
167+
setStepValidationResult(selectedIndex.value, isValid);
168+
169+
if (isValid) {
170+
selectedIndex.value += 1;
171+
}
172+
};
173+
174+
const reset = () => {
175+
isConfirmed.value = false;
176+
selectedIndex.value = 0;
177+
formData.value = cloneFormData();
178+
validationEngine.resetGroup(validationGroups[0]);
179+
items.value = cloneItems();
180+
};
181+
182+
const confirm = () => {
183+
isConfirmed.value = true;
184+
setStepValidationResult(items.value.length - 1, true);
185+
};
186+
187+
function onNextButtonClick() {
188+
if (selectedIndex.value < items.value.length - 1) {
189+
moveNext();
190+
} else if (isConfirmed.value) {
191+
reset();
192+
} else {
193+
confirm();
194+
}
195+
}
196+
</script>
197+
198+
<style scoped>
199+
.demo-container {
200+
display: flex;
201+
flex-direction: column;
202+
justify-content: center;
203+
row-gap: 20px;
204+
height: 480px;
205+
min-width: 420px;
206+
padding: 40px 20px;
207+
}
208+
209+
.content {
210+
padding-inline: 40px;
211+
flex: 1;
212+
display: flex;
213+
flex-direction: column;
214+
row-gap: 20px;
215+
}
216+
217+
.dx-multiview-item-content:has(> .summary-container) {
218+
overflow: auto;
219+
}
220+
221+
.summary-container {
222+
display: flex;
223+
flex-direction: column;
224+
row-gap: 20px;
225+
}
226+
227+
.summary-item {
228+
display: flex;
229+
flex-direction: column;
230+
row-gap: 8px;
231+
}
232+
233+
.summary-item-header {
234+
font-weight: 600;
235+
font-size: var(--dx-font-size-sm);
236+
}
237+
238+
.center {
239+
text-align: center;
240+
}
241+
242+
.summary-item-label {
243+
color: var(--dx-color-icon);
244+
}
245+
246+
.separator {
247+
width: 100%;
248+
height: 1px;
249+
border-bottom: solid 1px var(--dx-color-border);
250+
}
251+
252+
.nav-panel {
253+
display: flex;
254+
align-items: center;
255+
justify-content: space-between;
256+
}
257+
258+
.current-step {
259+
color: var(--dx-color-icon);
260+
}
261+
262+
.nav-buttons > *:not(:last-child) {
263+
margin-right: 8px;
264+
}
265+
</style>

0 commit comments

Comments
 (0)