Repository: DevExpress/devextreme-react
Pull request #464 (2020-10-20 13:51:14)

new block of rules implement
react/no-unescaped-entities
react/no-access-state-in-setstate
react/no-children-prop
react/prefer-stateless-function
react/require-default-props
react/static-property-placement
react/no-unused-state


Patch:
diff --git a/.eslintrc b/.eslintrc
index a61b4b9..1d94747 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -50,13 +50,6 @@
         "prefer-template": "off",
         "prefer-destructuring": "off",
         "react/forbid-prop-types": "off",
-        "react/no-unescaped-entities": "off",
-        "react/no-access-state-in-setstate": "off",
-        "react/no-children-prop": "off",
-        "react/no-unused-state": "off",
-        "react/prefer-stateless-function": "off",
-        "react/require-default-props": "off",
-        "react/static-property-placement": "off",
         "space-unary-ops": "off",
         "spaced-comment": "off",
         "@typescript-eslint/no-explicit-any": "off",
diff --git a/example/accordion-example.tsx b/example/accordion-example.tsx
index fab568b..0412e8c 100644
--- a/example/accordion-example.tsx
+++ b/example/accordion-example.tsx
@@ -17,23 +17,21 @@ const items: IAccordionItemProps[] = [
 
 const ItemTitle = (data: IAccordionItemProps) => <div style={{height: 100}}>{data.title}</div>;
 
-export default class extends React.Component<any, any> {
-  public render() {
-    return (
-      <Example title="DxAccordion">
-        <hr />
-        <h4>Simple Accordion</h4>
-        <Accordion
-          items={items}
-        />
-        <hr />
-        <h4>Accordion with itemTitleRender</h4>
-        <Accordion
-          collapsible
-          items={items}
-          itemTitleRender={ItemTitle}
-        />
-      </Example>
-    );
-  }
-}
+export default () => {
+  return (
+    <Example title="DxAccordion">
+      <hr />
+      <h4>Simple Accordion</h4>
+      <Accordion
+        items={items}
+      />
+      <hr />
+      <h4>Accordion with itemTitleRender</h4>
+      <Accordion
+        collapsible
+        items={items}
+        itemTitleRender={ItemTitle}
+      />
+    </Example>
+  );
+};
diff --git a/example/chart-example.tsx b/example/chart-example.tsx
index 5901df4..964833e 100644
--- a/example/chart-example.tsx
+++ b/example/chart-example.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/no-unused-state */
 import * as React from 'react';
 
 import { orangesByDay } from './data';
diff --git a/example/example-block.tsx b/example/example-block.tsx
index b298e62..ca981c1 100644
--- a/example/example-block.tsx
+++ b/example/example-block.tsx
@@ -24,4 +24,8 @@ const Example = (props: IProps) => {
   );
 };
 
+Example.defaultProps = {
+  state: {},
+};
+
 export default Example;
diff --git a/example/map-example.tsx b/example/map-example.tsx
index 976d149..2c62925 100644
--- a/example/map-example.tsx
+++ b/example/map-example.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/no-unused-state */
 import * as React from 'react';
 import Example from './example-block';
 
diff --git a/example/popup-example.tsx b/example/popup-example.tsx
index 4266804..81a8db7 100644
--- a/example/popup-example.tsx
+++ b/example/popup-example.tsx
@@ -49,7 +49,7 @@ export default class extends React.Component<any, { visible: boolean; text: stri
           width={600}
           height={400}
         >
-          Enter valid text (it should be 'good'):
+          Enter valid text (it should be `&apos;`good`&apos;`):
           <br />
           <TextBox value={text} onValueChanged={this.handleTextUpdate} valueChangeEvent="input" />
           <br />
diff --git a/example/scheduler-example.tsx b/example/scheduler-example.tsx
index e81b512..de30cc0 100644
--- a/example/scheduler-example.tsx
+++ b/example/scheduler-example.tsx
@@ -20,19 +20,17 @@ class DateCell extends React.PureComponent<any> {
   }
 }
 
-export default class extends React.Component<any, any> {
-  public render() {
-    return (
-      <Example title="DxScheduler">
-        <Scheduler
-          dateCellComponent={DateCell}
-          dataSource={appointments}
-          height={400}
-          startDayHour={9}
-          defaultCurrentView="week"
-          defaultCurrentDate={new Date(2017, 4, 25)}
-        />
-      </Example>
-    );
-  }
-}
+export default () => {
+  return (
+    <Example title="DxScheduler">
+      <Scheduler
+        dateCellComponent={DateCell}
+        dataSource={appointments}
+        height={400}
+        startDayHour={9}
+        defaultCurrentView="week"
+        defaultCurrentDate={new Date(2017, 4, 25)}
+      />
+    </Example>
+  );
+};
diff --git a/example/selectbox-example.tsx b/example/selectbox-example.tsx
index 3f382d8..e865c9a 100644
--- a/example/selectbox-example.tsx
+++ b/example/selectbox-example.tsx
@@ -5,25 +5,20 @@ const SelectBoxItem = (option: any) => {
   return <span>{option.data.value}</span>;
 };
 
-class SelectBoxEditor extends React.Component {
-
-  public render() {
-    return (
-      <SelectBox
-        searchEnabled
-        showClearButton
-        searchTimeout={0}
-        noDataText="No results found"
-        displayExpr="label"
-        valueExpr="value"
-        items={items}
-        itemComponent={SelectBoxItem}
-      />
-    );
-  }
-}
-
-export default SelectBoxEditor;
+export default () => {
+  return (
+    <SelectBox
+      searchEnabled
+      showClearButton
+      searchTimeout={0}
+      noDataText="No results found"
+      displayExpr="label"
+      valueExpr="value"
+      items={items}
+      itemComponent={SelectBoxItem}
+    />
+  );
+};
 
 const items = [
   {
diff --git a/example/text-box-example.tsx b/example/text-box-example.tsx
index 39f9195..0697061 100644
--- a/example/text-box-example.tsx
+++ b/example/text-box-example.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/no-unused-state */
 import * as React from 'react';
 import Example from './example-block';
 
diff --git a/src/core/options-manager.ts b/src/core/options-manager.ts
index 1825598..2dd999a 100644
--- a/src/core/options-manager.ts
+++ b/src/core/options-manager.ts
@@ -9,6 +9,8 @@ import { capitalizeFirstLetter } from './helpers';
 class OptionsManager {
   private readonly _guards: Record<string, number> = {};
 
+  private readonly _updatedOptions: Set<string> = new Set();
+
   private _templatesManager: TemplatesManager;
 
   private _instance: any;
@@ -79,6 +81,7 @@ class OptionsManager {
     }
 
     for (const key of Object.keys(changes.options)) {
+      this._updatedOptions.add(key);
       this._setValue(key, changes.options[key]);
     }
 
@@ -93,7 +96,11 @@ class OptionsManager {
       return;
     }
 
-    this._callOptionChangeHandler(e.fullName, e.value);
+    if (this._updatedOptions.has(e.fullName)) {
+      this._updatedOptions.delete(e.fullName);
+    } else {
+      this._callOptionChangeHandler(e.fullName, e.value);
+    }
     const valueDescriptor = findValue(this._currentConfig, e.fullName.split('.'));
     if (!valueDescriptor) {
       return;


Test patch:
diff --git a/src/core/__tests__/nested-option.test.tsx b/src/core/__tests__/nested-option.test.tsx
index f479e97..7ec219a 100644
--- a/src/core/__tests__/nested-option.test.tsx
+++ b/src/core/__tests__/nested-option.test.tsx
@@ -288,7 +288,7 @@ describe('nested option', () => {
       { c: 123, d: 'abc', key: 1 },
       { c: 456, d: 'def', key: 2 },
     ];
-    mount(<TestContainer children={children} />)
+    mount(<TestContainer>{children}</TestContainer>)
       .setProps({
         children: [
           { c: 123, d: 'abc', key: 1 },
@@ -320,7 +320,7 @@ describe('nested option', () => {
       { c: 123, d: 'abc', key: 1 },
       { c: 456, d: 'def', key: 2 },
     ];
-    mount(<TestContainer children={children} />)
+    mount(<TestContainer>{children}</TestContainer>)
       .setProps({
         children: [
           { c: 123, d: 'abc', key: 1 },
diff --git a/src/core/__tests__/props-updating.test.tsx b/src/core/__tests__/props-updating.test.tsx
index cbda7a7..836f3ce 100644
--- a/src/core/__tests__/props-updating.test.tsx
+++ b/src/core/__tests__/props-updating.test.tsx
@@ -642,7 +642,7 @@ describe('mutation detection', () => {
 });
 
 describe('onXXXChange', () => {
-  it('is called on component changes option', () => {
+  it('is called on component changes controlled option', () => {
     const onPropChange = jest.fn();
     const component = mount(
       <TestComponent
@@ -654,7 +654,7 @@ describe('onXXXChange', () => {
 
     const sampleProps = { text: '1' };
     component.setProps(sampleProps);
-
+    fireOptionChange('text', '1'); // controlled property calls the "onOptionChanged"
     expect(onPropChange).not.toBeCalled();
 
     fireOptionChange('text', '2');
@@ -662,6 +662,25 @@ describe('onXXXChange', () => {
     expect(onPropChange).toBeCalledWith('2');
   });
 
+  it('is not called on component changes controlled option using "onXXXChange"', () => {
+    const onPropChange = jest.fn();
+    const component = mount(
+            <TestComponent
+                text="0"
+                onTextChange={onPropChange}
+            />,
+    );
+    onPropChange.mockImplementation((value) => {
+      component.setProps({ text: `X${value}`});
+      fireOptionChange('text', `X${value}`);
+    });
+    expect(onPropChange).not.toBeCalled();
+
+    fireOptionChange('text', '2');
+    expect(onPropChange).toHaveBeenCalledTimes(1);
+    expect(component.prop('text')).toBe('X2');
+  });
+
   it('is called on component changes complex option', () => {
     const onPropChange = jest.fn();
     mount(
diff --git a/src/core/__tests__/template.test.tsx b/src/core/__tests__/template.test.tsx
index f4e733c..468691d 100644
--- a/src/core/__tests__/template.test.tsx
+++ b/src/core/__tests__/template.test.tsx
@@ -34,8 +34,7 @@ function testTemplateOption(testedOption: string) {
 
   if (testedOption === 'itemComponent') {
     prepareTemplate = (render) => {
-      class ItemComponent extends React.PureComponent {
-        public props: { data: any, index?: number };
+      class ItemComponent extends React.PureComponent<{data: any, index?: number}> {
 
         public render() {
           const {data, index} = this.props;