Skip to content

Commit facba66

Browse files
authored
React: Fix nested components double render (T1299644) (DevExpress#30720)
1 parent 49b7a64 commit facba66

File tree

10 files changed

+337
-8
lines changed

10 files changed

+337
-8
lines changed

e2e/wrappers/builders/angular19/src/utils/componentFinder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export const COMPONENTS = [
99
name: 'InputsListInForm',
1010
component: import('@external/inputs-list-in-form/angular19/inputs-list-in-form.component').then((m) => m.InputsListInFormComponent),
1111
},
12+
{
13+
path: 'select-box-nested-validator',
14+
name: 'SelectBoxNestedValidator',
15+
component: import('@external/select-box-nested-validator/angular19/select-box-nested-validator.component').then((m) => m.SelectBoxNestedValidatorComponent),
16+
},
1217
{
1318
path: 'text-box-dynamic-styles',
1419
name: 'TextBoxDynamicStyles',

e2e/wrappers/builders/react19/src/utils/componentFinder.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const COMPONENTS = [
99
name: 'InputsListInForm',
1010
component: () => import('@examples/inputs-list-in-form/react19/index.jsx')
1111
},
12+
{
13+
path: 'select-box-nested-validator',
14+
name: 'SelectBoxNestedValidator',
15+
component: () => import('@examples/select-box-nested-validator/react19/index.jsx')
16+
},
1217
{
1318
path: 'text-box-dynamic-styles',
1419
name: 'TextBoxDynamicStyles',

e2e/wrappers/builders/vue3/src/utils/componentFinder.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const COMPONENTS = [
99
name: 'InputsListInForm',
1010
component: () => import('@examples/inputs-list-in-form/vue3/index.vue')
1111
},
12+
{
13+
path: 'select-box-nested-validator',
14+
name: 'SelectBoxNestedValidator',
15+
component: () => import('@examples/select-box-nested-validator/vue3/index.vue')
16+
},
1217
{
1318
path: 'text-box-dynamic-styles',
1419
name: 'TextBoxDynamicStyles',
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { SelectBoxNestedValidatorComponent } from './select-box-nested-validator.component';
2+
3+
export default {
4+
component: SelectBoxNestedValidatorComponent,
5+
path: 'select-box-nested-validator'
6+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Component } from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
import { DxSelectBoxModule, DxFormModule, DxButtonModule, DxValidatorModule, DxValidationSummaryModule } from 'devextreme-angular';
5+
6+
@Component({
7+
selector: 'app-select-box-nested-validator',
8+
standalone: true,
9+
imports: [CommonModule, DxSelectBoxModule, DxFormModule, DxButtonModule, DxValidatorModule, DxValidationSummaryModule],
10+
template: `
11+
<dx-form
12+
[validationGroup]="groupName"
13+
[formData]="formData">
14+
<dxi-item>
15+
<dx-select-box
16+
[value]="formData.type"
17+
(onValueChanged)="valueChanged($event)"
18+
[items]="items"
19+
[showClearButton]="true"
20+
valueExpr="id"
21+
displayExpr="description">
22+
<dx-validator [validationGroup]="groupName">
23+
<dxi-validation-rule
24+
type="required"
25+
message="Type is required">
26+
</dxi-validation-rule>
27+
</dx-validator>
28+
</dx-select-box>
29+
</dxi-item>
30+
</dx-form>
31+
<dx-validation-summary [validationGroup]="groupName"></dx-validation-summary>
32+
<dx-button
33+
[validationGroup]="groupName"
34+
text="Validate"
35+
(onClick)="onClick($event)">
36+
</dx-button>
37+
`
38+
})
39+
export class SelectBoxNestedValidatorComponent {
40+
groupName = "sharedGroup";
41+
42+
formData = { code: null, type: null };
43+
44+
items = [
45+
{
46+
id: 1,
47+
description: "One",
48+
},
49+
];
50+
51+
valueChanged = (e: any) => {
52+
this.formData = {
53+
...this.formData,
54+
type: e.value,
55+
};
56+
};
57+
58+
onClick = (e: any) => {
59+
return e.validationGroup?.validate();
60+
};
61+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from "react";
2+
3+
import {
4+
Form,
5+
SimpleItem,
6+
RequiredRule,
7+
} from "devextreme-react/form";
8+
import { Button } from "devextreme-react/button";
9+
import { SelectBox } from "devextreme-react/select-box";
10+
import { Validator } from "devextreme-react/validator";
11+
import { ValidationSummary } from "devextreme-react/validation-summary";
12+
13+
const groupName = "sharedGroup";
14+
15+
const onClick = (e) => {
16+
return e.validationGroup?.validate();
17+
};
18+
19+
const items = [
20+
{
21+
id: 1,
22+
description: "One",
23+
},
24+
];
25+
26+
const SelectBoxWithValidator = () => {
27+
const [formData, setFormData] = React.useState({ code: null, type: null });
28+
29+
const valueChanged = React.useCallback((e) => {
30+
setFormData((prevFormData) => {
31+
return {
32+
...prevFormData,
33+
type: e.value,
34+
};
35+
});
36+
}, []);
37+
38+
return (
39+
<>
40+
<Form validationGroup={groupName} formData={formData}>
41+
<SimpleItem>
42+
<SelectBox
43+
value={formData.type}
44+
onValueChanged={valueChanged}
45+
items={items}
46+
showClearButton
47+
valueExpr={'id'}
48+
displayExpr={'description'}
49+
>
50+
<Validator validationGroup={groupName}>
51+
<RequiredRule message='Type is required' />
52+
</Validator>
53+
</SelectBox>
54+
</SimpleItem>
55+
</Form>
56+
<ValidationSummary validationGroup={groupName} />
57+
<Button validationGroup={groupName} text='Validate' onClick={onClick} />
58+
</>
59+
);
60+
}
61+
62+
63+
64+
export default SelectBoxWithValidator;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<template>
2+
<div>
3+
<DxForm
4+
:validation-group="groupName"
5+
:form-data="formData">
6+
<DxSimpleItem>
7+
<DxSelectBox
8+
:value="formData.type"
9+
@value-changed="valueChanged"
10+
:items="items"
11+
:show-clear-button="true"
12+
value-expr="id"
13+
display-expr="description">
14+
<DxValidator :validation-group="groupName">
15+
<DxRequiredRule message="Type is required" />
16+
</DxValidator>
17+
</DxSelectBox>
18+
</DxSimpleItem>
19+
</DxForm>
20+
<DxValidationSummary :validation-group="groupName" />
21+
<DxButton
22+
:validation-group="groupName"
23+
text="Validate"
24+
@click="onClick">
25+
</DxButton>
26+
</div>
27+
</template>
28+
29+
<script>
30+
import { reactive } from 'vue';
31+
import { DxSelectBox } from 'devextreme-vue/select-box';
32+
import { DxForm, DxSimpleItem } from 'devextreme-vue/form';
33+
import DxButton from 'devextreme-vue/button';
34+
import { DxValidator, DxRequiredRule } from 'devextreme-vue/validator';
35+
import DxValidationSummary from 'devextreme-vue/validation-summary';
36+
37+
const groupName = "sharedGroup";
38+
39+
const items = [
40+
{
41+
id: 1,
42+
description: "One",
43+
},
44+
];
45+
46+
export default {
47+
name: 'SelectBoxNestedValidator',
48+
components: {
49+
DxSelectBox,
50+
DxForm,
51+
DxSimpleItem,
52+
DxButton,
53+
DxValidator,
54+
DxRequiredRule,
55+
DxValidationSummary
56+
},
57+
setup() {
58+
const formData = reactive({ code: null, type: null });
59+
60+
const valueChanged = (e) => {
61+
formData.type = e.value;
62+
};
63+
64+
const onClick = (e) => {
65+
return e.validationGroup?.validate();
66+
};
67+
68+
return {
69+
groupName,
70+
formData,
71+
items,
72+
valueChanged,
73+
onClick
74+
};
75+
}
76+
};
77+
</script>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Selector } from 'testcafe';
2+
import { testInFramework } from '../test-helpers';
3+
4+
testInFramework('SelectBox nested validator scenarios', 'select-box-nested-validator', [
5+
'SelectBox with nested Validator component should not render double errors',
6+
async (t) => {
7+
const validateButton = Selector('.dx-button-text').withText('Validate');
8+
const validationSummary = Selector('.dx-validationsummary');
9+
10+
await t
11+
.expect(validateButton.exists).ok('Validate button should exist')
12+
.expect(validationSummary.exists).ok('Validation summary should exist');
13+
14+
await t.click(validateButton);
15+
16+
const updatedValidationSummaryItems = validationSummary.find('.dx-validationsummary-item');
17+
await t.expect(updatedValidationSummaryItems.count).eql(1, 'Should have exactly one validation error in summary');
18+
const errorMessage = await updatedValidationSummaryItems.nth(0).innerText;
19+
await t.expect(errorMessage).eql('Type is required', 'Error message should be "Type is required"');
20+
},
21+
22+
'SelectBox validation should pass when value is selected',
23+
async (t) => {
24+
const validateButton = Selector('.dx-button-text').withText('Validate');
25+
const validationSummary = Selector('.dx-validationsummary');
26+
const selectBox = Selector('.dx-selectbox');
27+
const selectBoxArrow = selectBox.find('.dx-dropdowneditor-button');
28+
29+
await t.click(selectBoxArrow);
30+
const firstItem = Selector('.dx-item').withText('One');
31+
32+
await t
33+
.expect(firstItem.exists).ok('First item should exist in dropdown')
34+
.click(firstItem);
35+
36+
await t.click(validateButton);
37+
38+
const updatedValidationSummaryItems = validationSummary.find('.dx-validationsummary-item');
39+
await t.expect(updatedValidationSummaryItems.count).eql(0, 'Should have no validation errors when value is selected');
40+
}
41+
]);

packages/devextreme-react/src/core/__tests__/integration/integration.test.tsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import { useState, useEffect } from 'react';
66
import SelectBox from '../../../select-box';
77
import TextBox from '../../../text-box';
88
import DataGrid from '../../../data-grid';
9+
import {
10+
Form,
11+
RequiredRule,
12+
SimpleItem,
13+
} from '../../../form';
14+
import Validator from '../../../validator';
15+
import ValidationSummary from '../../../validation-summary';
916
import { ContextMenu, Item as ContextMenuItem } from '../../../context-menu';
17+
import Button from '../../../button';
1018

1119
jest.useFakeTimers();
1220

@@ -18,6 +26,67 @@ describe('integration tests', () => {
1826
testingLib.cleanup();
1927
});
2028

29+
it('renders selectbox with nested Validator component without double error rendering', async () => {
30+
const user = userEvent.setup({ delay: null });
31+
const groupName = "sharedGroup";
32+
const onClick = (e: any) => {
33+
return e.validationGroup?.validate();
34+
};
35+
36+
const items = [
37+
{
38+
id: 1,
39+
description: "One",
40+
},
41+
];
42+
43+
const SelectBoxWithValidator = () => {
44+
const [formData, setFormData] = React.useState({ code: null, type: null });
45+
46+
const valueChanged = React.useCallback((e) => {
47+
setFormData((prevFormData) => {
48+
return {
49+
...prevFormData,
50+
type: e.value,
51+
};
52+
});
53+
}, []);
54+
55+
return (
56+
<>
57+
<Form validationGroup={groupName} formData={formData}>
58+
<SimpleItem>
59+
<SelectBox
60+
value={formData.type}
61+
onValueChanged={valueChanged}
62+
items={items}
63+
showClearButton
64+
valueExpr={'id'}
65+
displayExpr={'description'}
66+
>
67+
<Validator validationGroup={groupName}>
68+
<RequiredRule message='Type is required' />
69+
</Validator>
70+
</SelectBox>
71+
</SimpleItem>
72+
</Form>
73+
<ValidationSummary validationGroup={groupName} />
74+
<Button validationGroup={groupName} text='Validate' onClick={onClick} />
75+
</>
76+
);
77+
}
78+
79+
testingLib.render(
80+
<React.Fragment>
81+
<SelectBoxWithValidator />
82+
</React.Fragment>
83+
);
84+
85+
await user.click(testingLib.screen.getByText('Validate'));
86+
const summaryElement = document.querySelector('.dx-validationsummary');
87+
expect(summaryElement?.children.length).toBe(1);
88+
})
89+
2190
it('renders selectbox in strict mode when data source is specified dynamically without errors', () => {
2291
const Field = (data: any) => {
2392
return (<TextBox defaultValue={data && data.Name} />);

0 commit comments

Comments
 (0)