Repository: DevExpress/devextreme-react
Pull request #572 (2021-06-15 14:12:56)

Fix regression with object option changed
T1002464
T1006531 


Patch:
diff --git a/packages/devextreme-react/src/core/configuration/tree.ts b/packages/devextreme-react/src/core/configuration/tree.ts
index a5b61b9..2661b21 100644
--- a/packages/devextreme-react/src/core/configuration/tree.ts
+++ b/packages/devextreme-react/src/core/configuration/tree.ts
@@ -84,17 +84,9 @@ enum ValueType {
 function findValueInObject(obj: unknown, path: string[]): undefined | IValueDescriptor {
   const key = path.shift();
   if (!key) {
-    let type: ValueType;
-    if (obj instanceof Array) {
-      type = ValueType.Array;
-    } else if (obj instanceof Object) {
-      type = ValueType.Complex;
-    } else {
-      type = ValueType.Simple;
-    }
     return {
       value: obj,
-      type,
+      type: ValueType.Simple,
     };
   }
 
diff --git a/packages/devextreme-react/src/core/configuration/utils.ts b/packages/devextreme-react/src/core/configuration/utils.ts
index 71d7b55..fffa3e0 100644
--- a/packages/devextreme-react/src/core/configuration/utils.ts
+++ b/packages/devextreme-react/src/core/configuration/utils.ts
@@ -37,3 +37,14 @@ export const isIE = (): boolean => {
 
   return (msie > 0 || trident > 0);
 };
+
+export const shallowEquals = (
+  first: Record<string, unknown>,
+  second: Record<string, unknown>,
+): boolean => {
+  if (Object.keys(first).length !== Object.keys(second).length) {
+    return false;
+  }
+
+  return Object.keys(first).every((key) => first[key] === second[key]);
+};
diff --git a/packages/devextreme-react/src/core/options-manager.ts b/packages/devextreme-react/src/core/options-manager.ts
index cfe2d34..049fa7b 100644
--- a/packages/devextreme-react/src/core/options-manager.ts
+++ b/packages/devextreme-react/src/core/options-manager.ts
@@ -3,7 +3,7 @@ import TemplatesManager from './templates-manager';
 import { getChanges } from './configuration/comparer';
 import { IConfigNode } from './configuration/config-node';
 import { buildConfig, findValue, ValueType } from './configuration/tree';
-import { mergeNameParts } from './configuration/utils';
+import { mergeNameParts, shallowEquals } from './configuration/utils';
 import { capitalizeFirstLetter } from './helpers';
 
 class OptionsManager {
@@ -128,9 +128,16 @@ class OptionsManager {
         this.setGuard(mergeNameParts(e.fullName, key), value[key]);
       });
     } else {
-      if (value === e.value) {
+      const valuesAreEqual = value === e.value;
+      const valuesAreEqualObjects = !valuesAreEqual
+        && value instanceof Object
+        && e.value instanceof Object
+        && shallowEquals(value as Record<string, unknown>, e.value as Record<string, unknown>);
+
+      if (valuesAreEqual || valuesAreEqualObjects) {
         return;
       }
+
       this.setGuard(e.fullName, value);
     }
   }
diff --git a/packages/sandbox/rerendered-chart-example.tsx b/packages/sandbox/rerendered-chart-example.tsx
index d843626..40b7a67 100644
--- a/packages/sandbox/rerendered-chart-example.tsx
+++ b/packages/sandbox/rerendered-chart-example.tsx
@@ -77,13 +77,11 @@ let handleClick = (): void => {
     const dataSource = chartDataSource;
 
     setTimeout(() => {
-      const dataFrame = data.map((item) => {
-        return {
-          date: new Date(item["date"]),
-          minTemp: item["minTemp"],
-          maxTemp: item["maxTemp"]
-        };
-      });
+      const dataFrame = data.map((item) => ({
+        date: new Date(item.date),
+        minTemp: item.minTemp,
+        maxTemp: item.maxTemp,
+      }));
 
       const componentStorage = dataSource.store();
       dataFrame.forEach((item) => componentStorage.insert(item));
@@ -105,7 +103,7 @@ class NewChartExample extends React.Component {
         <Chart id="chart" dataSource={chartDataSource}>
           <Series argumentField="date" valueField="minTemp" />
           <ArgumentAxis argumentType="datetime">
-            <Label render={LabelTemplate}></Label>
+            <Label render={LabelTemplate} />
           </ArgumentAxis>
         </Chart>
         <button onClick={handleClick}>Click Me</button>


Test patch:
diff --git a/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx b/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx
index 468ece1..8f22464 100644
--- a/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx
+++ b/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx
@@ -258,9 +258,8 @@ describe('option control', () => {
     fireOptionChange('complexOption', {});
     jest.runAllTimers();
 
-    expect(Widget.option.mock.calls.length).toBe(2);
-    expect(Widget.option.mock.calls[0]).toEqual(['complexOption.a', 123]);
-    expect(Widget.option.mock.calls[1]).toEqual(['complexOption.b', 234]);
+    expect(Widget.option.mock.calls.length).toBe(1);
+    expect(Widget.option.mock.calls[0]).toEqual(['complexOption', { a: 123, b: 234 }]);
   });
 
   it('rolls back complex option controlled field', () => {
@@ -274,6 +273,17 @@ describe('option control', () => {
     expect(Widget.option.mock.calls[0]).toEqual(['complexOption.a', 123]);
   });
 
+  it('should not rolls back complex option if shallow equals', () => {
+    render(
+      <ControlledComponent complexOption={{ a: 123, b: 234 }} />,
+    );
+
+    fireOptionChange('complexOption', { a: 123, b: 234 });
+    jest.runAllTimers();
+
+    expect(Widget.option.mock.calls.length).toBe(0);
+  });
+
   it('rolls back one simple option and updates other', () => {
     const { rerender } = render(
       <ControlledComponent everyOption={123} anotherOption="const" />,
@@ -448,9 +458,20 @@ describe('cfg-component option control', () => {
 
     fireOptionChange('nestedOption.complexValue', {});
     jest.runAllTimers();
-    expect(Widget.option.mock.calls.length).toBe(2);
-    expect(Widget.option.mock.calls[0]).toEqual(['nestedOption.complexValue.a', 123]);
-    expect(Widget.option.mock.calls[1]).toEqual(['nestedOption.complexValue.b', 234]);
+    expect(Widget.option.mock.calls.length).toBe(1);
+    expect(Widget.option.mock.calls[0]).toEqual(['nestedOption.complexValue', { a: 123, b: 234 }]);
+  });
+
+  it('should not rolls cfg-component option complex value if shallow equals', () => {
+    render(
+      <ControlledComponent>
+        <NestedComponent complexValue={{ a: 123, b: 234 }} />
+      </ControlledComponent>,
+    );
+
+    fireOptionChange('nestedOption.complexValue', { a: 123, b: 234 });
+    jest.runAllTimers();
+    expect(Widget.option.mock.calls.length).toBe(0);
   });
 
   it('rolls cfg-component option value if parent object changes another field', () => {
diff --git a/packages/devextreme-react/src/core/__tests__/tagbox.test.tsx b/packages/devextreme-react/src/core/__tests__/tagbox.test.tsx
deleted file mode 100644
index 6473680..0000000
--- a/packages/devextreme-react/src/core/__tests__/tagbox.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { render, cleanup } from '@testing-library/react';
-import * as React from 'react';
-import { TagBox } from '../../tag-box';
-
-jest.useFakeTimers();
-
-describe('templates', () => {
-  afterEach(() => {
-    jest.clearAllMocks();
-    cleanup();
-  });
-
-  it('should change value without an error', () => {
-    const ref = React.createRef() as React.RefObject<TagBox>;
-    const { container } = render(<TagBox
-      ref={ref}
-      dataSource={['1', '2', '3']}
-      showClearButton
-      value={['1']}
-      tagRender={() => <div>test</div>}
-    />);
-    const instance = ref?.current?.instance;
-
-    instance?.option('value', ['1', '2', '3']);
-    jest.runAllTimers();
-    expect(container.children[0].getElementsByClassName('dx-tag').length).toBe(3);
-  });
-});
diff --git a/packages/devextreme-react/src/core/configuration/__tests__/tree.test.ts b/packages/devextreme-react/src/core/configuration/__tests__/tree.test.ts
index fb59d80..cc30573 100644
--- a/packages/devextreme-react/src/core/configuration/__tests__/tree.test.ts
+++ b/packages/devextreme-react/src/core/configuration/__tests__/tree.test.ts
@@ -1,5 +1,5 @@
 /* eslint-disable max-classes-per-file */
-import { findValueInObject, ValueType } from '../tree';
+import { findValueInObject } from '../tree';
 
 describe('check findValueInObject', () => {
   it('get underfined by string', () => {
@@ -10,15 +10,6 @@ describe('check findValueInObject', () => {
     expect(valueInObject).toEqual(undefined);
   });
 
-  it('should return different ValueType', () => {
-    const valueInArray = findValueInObject(['ArrayValue'], []);
-    expect(valueInArray?.type).toEqual(ValueType.Array);
-    const valueInObject = findValueInObject({ value: 'ObjectValue' }, []);
-    expect(valueInObject?.type).toEqual(ValueType.Complex);
-    const valueInSimple = findValueInObject('SimpleValue', []);
-    expect(valueInSimple?.type).toEqual(ValueType.Simple);
-  });
-
   it('get value by object', () => {
     const obj = {
       sortOrder: 'desc',