Skip to content

Commit 0101771

Browse files
pharret31dmlvr
andauthored
Form: implement relayouting instead of full rerender on dimension change (#32728)
Co-authored-by: dmlvr <[email protected]>
1 parent 546573a commit 0101771

File tree

9 files changed

+456
-60
lines changed

9 files changed

+456
-60
lines changed
9.57 KB
Loading

e2e/testcafe-devextreme/tests/navigation/form/layout.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createScreenshotsComparer } from 'devextreme-screenshot-comparer';
22
import { ClientFunction } from 'testcafe';
3+
import Form from 'devextreme-testcafe-models/form/form';
34
import url from '../../../helpers/getPageUrl';
45
import { createWidget } from '../../../helpers/createWidget';
56
import { testScreenshot } from '../../../helpers/themeUtils';
@@ -228,3 +229,59 @@ test('SimpleItem: item1_cSpan_2', async (t) => {
228229
],
229230
}));
230231
});
232+
233+
test('Validation errors persist after resize', async (t) => {
234+
const { takeScreenshot, compareResults } = createScreenshotsComparer(t);
235+
236+
const form = new Form('#container');
237+
238+
await waitFont();
239+
await form.validate();
240+
241+
await t.resizeWindow(400, 800);
242+
243+
await testScreenshot(t, takeScreenshot, 'form_validation_errors_after_resize.png', { element: '#container' });
244+
245+
await t
246+
.expect(compareResults.isValid())
247+
.ok(compareResults.errorMessages());
248+
}).before(async () => createWidget('dxForm', {
249+
colCountByScreen: {
250+
xs: 1,
251+
sm: 2,
252+
md: 2,
253+
lg: 2,
254+
},
255+
items: [
256+
{
257+
dataField: 'name',
258+
editorType: 'dxTextBox',
259+
validationRules: [{ type: 'required' }],
260+
},
261+
{
262+
dataField: 'birthDate',
263+
editorType: 'dxDateBox',
264+
validationRules: [{ type: 'required' }],
265+
},
266+
{
267+
dataField: 'role',
268+
editorType: 'dxSelectBox',
269+
editorOptions: {
270+
dataSource: ['Dev', 'QA', 'PM'],
271+
},
272+
validationRules: [{ type: 'required' }],
273+
},
274+
{
275+
dataField: 'agree',
276+
editorType: 'dxCheckBox',
277+
editorOptions: {
278+
text: 'I agree',
279+
},
280+
validationRules: [{
281+
type: 'custom',
282+
validationCallback: () => false,
283+
message: 'Required',
284+
}],
285+
},
286+
],
287+
}));

packages/devextreme/js/__internal/ui/form/form.layout_manager.ts

Lines changed: 144 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { renderEmptyItem } from '@ts/ui/form/components/empty_item';
3737
import { renderFieldItem } from '@ts/ui/form/components/field_item';
3838
import {
3939
FIELD_ITEM_CLASS,
40+
FORM_FIELD_ITEM_COL_CLASS,
4041
FORM_LAYOUT_MANAGER_CLASS,
4142
LAYOUT_MANAGER_ONE_COLUMN,
4243
ROOT_SIMPLE_ITEM_CLASS,
@@ -54,10 +55,10 @@ import ResponsiveBox from '@ts/ui/responsive_box';
5455

5556
const FORM_EDITOR_BY_DEFAULT = 'dxTextBox';
5657

57-
const LAYOUT_MANAGER_FIRST_ROW_CLASS = 'dx-first-row';
58-
const LAYOUT_MANAGER_LAST_ROW_CLASS = 'dx-last-row';
59-
const LAYOUT_MANAGER_FIRST_COL_CLASS = 'dx-first-col';
60-
const LAYOUT_MANAGER_LAST_COL_CLASS = 'dx-last-col';
58+
export const LAYOUT_MANAGER_FIRST_ROW_CLASS = 'dx-first-row';
59+
export const LAYOUT_MANAGER_LAST_ROW_CLASS = 'dx-last-row';
60+
export const LAYOUT_MANAGER_FIRST_COL_CLASS = 'dx-first-col';
61+
export const LAYOUT_MANAGER_LAST_COL_CLASS = 'dx-last-col';
6162

6263
const MIN_COLUMN_WIDTH = 200;
6364

@@ -72,6 +73,15 @@ type ExtendedItem = Item & {
7273
allowIndeterminateState?: boolean;
7374
};
7475

76+
type Location = Required<Omit<LocationItem, 'screen'>>;
77+
78+
interface LocationBoundaryFlags {
79+
isFirstCol: boolean;
80+
isLastCol: boolean;
81+
isFirstRow: boolean;
82+
isLastRow: boolean;
83+
}
84+
7585
export interface TemplatesInfo {
7686
itemType?: string;
7787
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -130,7 +140,7 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
130140

131141
_labelTemplateRenderedCallCount?: number;
132142

133-
_cashedColCount?: number;
143+
_cachedColCount?: number | null;
134144

135145
_getDefaultOptions(): ExtendedLayoutManagerProperties {
136146
return {
@@ -500,40 +510,21 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
500510
}
501511
const $itemElement = $(itemElement);
502512

503-
const itemRenderedCountInPreviousRows = location.row * colCount;
504-
505-
const item = that._items?.[location.col + itemRenderedCountInPreviousRows];
513+
const item = that._getLayoutManagerItemByLocation(location);
506514
if (!item) {
507515
return;
508516
}
509517

510-
const itemCssClassList: string[] = [item.cssClass ?? ''];
511-
512-
$itemElement.toggleClass(SINGLE_COLUMN_ITEM_CONTENT, that.isSingleColumnMode(this));
513-
514-
if (location.row === 0) {
515-
itemCssClassList.push(LAYOUT_MANAGER_FIRST_ROW_CLASS);
516-
}
517-
if (location.col === 0) {
518-
itemCssClassList.push(LAYOUT_MANAGER_FIRST_COL_CLASS);
519-
}
520-
521518
const { isRoot } = that.option();
522519

523520
if (item.itemType === SIMPLE_ITEM_TYPE && isRoot) {
524521
$itemElement.addClass(ROOT_SIMPLE_ITEM_CLASS);
525522
}
526-
const isLastColumn = (location.col === colCount - 1)
527-
|| (location.col + location.colspan === colCount);
528-
const rowsCount = that._getRowsCount();
529-
const isLastRow = location.row === rowsCount - 1;
530523

531-
if (isLastColumn) {
532-
itemCssClassList.push(LAYOUT_MANAGER_LAST_COL_CLASS);
533-
}
534-
if (isLastRow) {
535-
itemCssClassList.push(LAYOUT_MANAGER_LAST_ROW_CLASS);
536-
}
524+
$itemElement.toggleClass(SINGLE_COLUMN_ITEM_CONTENT, that.isSingleColumnMode(this));
525+
526+
const itemCssClassList: string[] = [item.cssClass ?? ''];
527+
itemCssClassList.push(...that._getLocationCssClasses(location));
537528

538529
if (item.itemType !== 'empty') {
539530
itemCssClassList.push(FIELD_ITEM_CLASS);
@@ -543,7 +534,7 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
543534
itemCssClassList.push(cssItemClass);
544535

545536
if (isDefined(item.col)) {
546-
itemCssClassList.push(`dx-col-${item.col}`);
537+
itemCssClassList.push(`${FORM_FIELD_ITEM_COL_CLASS}${item.col}`);
547538
}
548539
}
549540

@@ -578,12 +569,12 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
578569
}
579570

580571
if (colCount === 'auto') {
581-
if (this._cashedColCount) {
582-
return this._cashedColCount;
572+
if (this._cachedColCount) {
573+
return this._cachedColCount;
583574
}
584575

585576
colCount = this._getMaxColCount();
586-
this._cashedColCount = colCount;
577+
this._cachedColCount = colCount;
587578
}
588579
// @ts-expect-error ts-error
589580
return colCount < 1 ? 1 : colCount;
@@ -607,7 +598,7 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
607598
}
608599

609600
isCachedColCountObsolete(): boolean {
610-
return !!this._cashedColCount && this._getMaxColCount() !== this._cashedColCount;
601+
return !!this._cachedColCount && this._getMaxColCount() !== this._cachedColCount;
611602
}
612603

613604
_prepareItemsWithMerging(colCount: number): void {
@@ -644,8 +635,7 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
644635

645636
_setItems(items: ExtendedItem[]): void {
646637
this._items = items;
647-
// @ts-expect-error ts-error
648-
this._cashedColCount = null; // T923489
638+
this._cachedColCount = null; // T923489
649639
}
650640

651641
_generateLayoutItems(): ResponsiveBoxItem[] {
@@ -1128,11 +1118,127 @@ class LayoutManager extends Widget<LayoutManagerProperties> {
11281118
}
11291119

11301120
_resetColCount(): void {
1131-
// @ts-expect-error ts-error
1132-
this._cashedColCount = null;
1121+
this._cachedColCount = null;
11331122
this._invalidate();
11341123
}
11351124

1125+
updateResponsiveBoxLayout(): void {
1126+
if (!this._responsiveBox) {
1127+
return;
1128+
}
1129+
1130+
this._cachedColCount = null;
1131+
1132+
this._items = (this._items ?? []).filter((item) => !item.merged);
1133+
1134+
const colCount = this._getColCount();
1135+
this._prepareItemsWithMerging(colCount);
1136+
1137+
const newLayoutItems = this._generateLayoutItems();
1138+
1139+
const { items: responsiveBoxItems } = this._responsiveBox.option();
1140+
const existingItems: ResponsiveBoxItem[] = responsiveBoxItems ?? [];
1141+
1142+
for (let i = 0; i < existingItems.length && i < newLayoutItems.length; i += 1) {
1143+
existingItems[i].location = newLayoutItems[i].location;
1144+
}
1145+
1146+
const newCols = this._generateRatio(colCount);
1147+
const newRows = this._generateRatio(this._getRowsCount(), true);
1148+
1149+
this._responsiveBox._options.silent({
1150+
cols: newCols,
1151+
rows: newRows,
1152+
});
1153+
1154+
this._responsiveBox.repaint();
1155+
this._updateItemsCssClasses();
1156+
}
1157+
1158+
_getLocationBoundaryFlags(location: Required<Omit<LocationItem, 'screen'>>): LocationBoundaryFlags {
1159+
const colCount = this._getColCount();
1160+
const rowsCount = this._getRowsCount();
1161+
1162+
return {
1163+
isFirstCol: location.col === 0,
1164+
isLastCol: (location.col === colCount - 1)
1165+
|| (location.col + location.colspan === colCount),
1166+
isFirstRow: location.row === 0,
1167+
isLastRow: location.row === rowsCount - 1,
1168+
};
1169+
}
1170+
1171+
_getLocationCssClasses(location: Location): string[] {
1172+
const cssClasses: string[] = [];
1173+
const locationFlags = this._getLocationBoundaryFlags(location);
1174+
1175+
if (locationFlags.isFirstRow) {
1176+
cssClasses.push(LAYOUT_MANAGER_FIRST_ROW_CLASS);
1177+
}
1178+
if (locationFlags.isFirstCol) {
1179+
cssClasses.push(LAYOUT_MANAGER_FIRST_COL_CLASS);
1180+
}
1181+
if (locationFlags.isLastCol) {
1182+
cssClasses.push(LAYOUT_MANAGER_LAST_COL_CLASS);
1183+
}
1184+
if (locationFlags.isLastRow) {
1185+
cssClasses.push(LAYOUT_MANAGER_LAST_ROW_CLASS);
1186+
}
1187+
1188+
return cssClasses;
1189+
}
1190+
1191+
_getLayoutManagerItemByLocation(location: Location): ExtendedItem | undefined {
1192+
const colCount = this._getColCount();
1193+
const index = location.row * colCount + location.col;
1194+
return this._items?.[index];
1195+
}
1196+
1197+
_updateItemsCssClasses(): void {
1198+
const { items: responsiveBoxItems } = this._responsiveBox.option();
1199+
responsiveBoxItems?.forEach((
1200+
responsiveBoxItem: ResponsiveBoxItem,
1201+
): void => {
1202+
const { location } = responsiveBoxItem;
1203+
if (!location || Array.isArray(location)) {
1204+
return;
1205+
}
1206+
1207+
const typedLocation = location as Location;
1208+
1209+
const {
1210+
isFirstCol,
1211+
isLastCol,
1212+
isFirstRow,
1213+
isLastRow,
1214+
} = this._getLocationBoundaryFlags(typedLocation);
1215+
1216+
const item = this._getLayoutManagerItemByLocation(typedLocation);
1217+
if (!item || item.itemType === 'empty') {
1218+
return;
1219+
}
1220+
1221+
const $itemContainer = this._itemsRunTimeInfo.findItemContainerByItem(item);
1222+
$itemContainer.parent().toggleClass(SINGLE_COLUMN_ITEM_CONTENT, this.isSingleColumnMode());
1223+
$itemContainer
1224+
.toggleClass(LAYOUT_MANAGER_FIRST_COL_CLASS, isFirstCol)
1225+
.toggleClass(LAYOUT_MANAGER_LAST_COL_CLASS, isLastCol)
1226+
.toggleClass(LAYOUT_MANAGER_FIRST_ROW_CLASS, isFirstRow)
1227+
.toggleClass(LAYOUT_MANAGER_LAST_ROW_CLASS, isLastRow);
1228+
1229+
const element = $itemContainer.get(0);
1230+
if (element) {
1231+
element.className = [...element.classList]
1232+
.filter((name: string): boolean => !name.startsWith(FORM_FIELD_ITEM_COL_CLASS))
1233+
.join(' ');
1234+
}
1235+
1236+
if (isDefined(typedLocation.col)) {
1237+
$itemContainer.addClass(`${FORM_FIELD_ITEM_COL_CLASS}${typedLocation.col}`);
1238+
}
1239+
});
1240+
}
1241+
11361242
linkEditorToDataField(editorInstance: Editor, dataField: string): void {
11371243
this.on('optionChanged', (args: OptionChanged<FormProperties>): void => {
11381244
if (args.fullName === `layoutData.${dataField}`) {

0 commit comments

Comments
 (0)