Browse Source

fix: InputNumber min value notifyChange bug #812 (#815)

走鹃 3 years ago
parent
commit
44280802d2

+ 22 - 6
packages/semi-foundation/inputNumber/foundation.ts

@@ -4,7 +4,7 @@
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import keyCode from '../utils/keyCode';
 import { numbers } from './constants';
-import { toNumber, toString, get } from 'lodash';
+import { toNumber, toString, get, isString } from 'lodash';
 import { minus as numberMinus } from '../utils/number';
 
 export interface InputNumberAdapter extends DefaultAdapter {
@@ -26,6 +26,14 @@ export interface InputNumberAdapter extends DefaultAdapter {
     restoreCursor: (str?: string) => boolean;
     fixCaret: (start: number, end: number) => void;
     setClickUpOrDown: (clicked: boolean) => void;
+    updateStates: (states: BaseInputNumberState, callback?: () => void) => void;
+}
+
+export interface BaseInputNumberState {
+    value?: number | string;
+    number?: number | null;
+    focusing?: boolean;
+    hovering?: boolean;
 }
 
 class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
@@ -360,17 +368,21 @@ class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
         const { defaultValue, value } = this.getProps();
 
         const propsValue = this._isControlledComponent('value') ? value : defaultValue;
-        const tmpNumer = this.doParse(toString(propsValue), false, true, true);
+        const tmpNumber = this.doParse(toString(propsValue), false, true, true);
 
         let number = null;
-        if (typeof tmpNumer === 'number' && !isNaN(tmpNumer)) {
-            number = tmpNumer;
+        if (typeof tmpNumber === 'number' && !isNaN(tmpNumber)) {
+            number = tmpNumber;
         }
 
-        const formatedValue = typeof number === 'number' ? this.doFormat(number, true) : '';
+        const formattedValue = typeof number === 'number' ? this.doFormat(number, true) : '';
 
         this._adapter.setNumber(number);
-        this._adapter.setValue(formatedValue);
+        this._adapter.setValue(formattedValue);
+
+        if (isString(formattedValue) && formattedValue !== String(propsValue)) {
+            this.notifyChange(formattedValue, null);
+        }
     }
 
     add(step?: number, event?: any): string {
@@ -616,6 +628,10 @@ class InputNumberFoundation extends BaseFoundation<InputNumberAdapter> {
             this._adapter.notifyNumberChange(value, e);
         }
     }
+
+    updateStates(states: BaseInputNumberState, callback?: () => void) {
+        this._adapter.updateStates(states, callback);
+    }
 }
 
 export default InputNumberFoundation;

+ 17 - 5
packages/semi-ui/form/hoc/withField.tsx

@@ -379,12 +379,24 @@ function withField<
                 return () => {};
             }
             // log('register: ' + field);
-            updater.register(field, fieldState, {
+
+            // field value may change after field component mounted, we use ref value here to get changed value
+            const refValue = getVal();
+            updater.register(
                 field,
-                fieldApi,
-                keepState,
-                allowEmpty: allowEmpty || allowEmptyString,
-            });
+                {
+                    value: refValue,
+                    error,
+                    touched,
+                    status,
+                },
+                {
+                    field,
+                    fieldApi,
+                    keepState,
+                    allowEmpty: allowEmpty || allowEmptyString,
+                }
+            );
             // return unRegister cb
             return () => {
                 updater.unRegister(field);

+ 40 - 3
packages/semi-ui/inputNumber/__test__/inputNumber.test.js

@@ -6,7 +6,7 @@ import keyCode from '@douyinfe/semi-foundation/utils/keyCode';
 import * as _ from 'lodash';
 import { BASE_CLASS_PREFIX } from '../../../semi-foundation/base/constants';
 import { numbers } from '@douyinfe/semi-foundation/inputNumber/constants';
-import { Form, withField } from '../../index';
+import { Form, withField, useFormApi } from '../../index';
 
 const log = (...args) => console.log(...args);
 const times = (n = 0, fn) => {
@@ -182,8 +182,9 @@ describe(`InputNumber`, () => {
         const inputElem = inputNumber.find('input');
 
         inputElem.simulate('change', event);
-        expect(onChange.calledOnce).toBe(true);
-        expect(onChange.calledWithMatch(Number(newValue.toFixed(precision)))).toBe(true);
+        expect(onChange.calledTwice).toBe(true);
+        expect(onChange.getCall(1).args[0]).toEqual(Number(newValue.toFixed(precision)));
+        // expect(onChange.calledWithMatch(Number(newValue.toFixed(precision)))).toBe(true);
         expect(inputElem.instance().value).toBe(formatter(newValue));
 
         inputElem.simulate('blur');
@@ -395,4 +396,40 @@ describe(`InputNumber`, () => {
         expect(onUpClick.called).toBe(false);
         expect(onDownClick.called).toBe(false);
     });
+
+    it('fix controlled min value didMount', () => {
+        const spyChange = sinon.spy();
+        const inputNumber = mount(
+            <InputNumber min={1} value={0} onChange={spyChange} />
+        );
+        expect(spyChange.calledOnce).toBe(true);
+    });
+
+    it('fix controlled min value didUpdate', () => {
+        const spyChange = sinon.spy();
+        const value = undefined;
+        const inputNumber = mount(
+            <InputNumber min={1} value={value} onChange={spyChange} />
+        );
+        inputNumber.setProps({ value: 0 });
+        expect(spyChange.calledTwice).toBe(true);
+        expect(spyChange.getCall(0).args[0]).toEqual('');
+        expect(spyChange.getCall(1).args[0]).toEqual(1);
+    });
+
+    it('fix controlled min value form field', () => {
+        const spyChange = sinon.spy();
+        let formApi = null;
+        let getFormApi = api => {
+            formApi = api;
+        };
+        const inputNumber = mount(
+            <Form initValues={{ minControlled: 0 }} getFormApi={getFormApi}>
+                <Form.InputNumber field="minControlled" min={1} onChange={spyChange} />
+            </Form>
+        );
+        expect(spyChange.calledOnce).toBe(true);
+        expect(spyChange.getCall(0).args[0]).toEqual(1);
+        expect(formApi.getValue('minControlled')).toBe(1);
+    });
 });

+ 43 - 1
packages/semi-ui/inputNumber/_story/inputNumber.stories.js

@@ -4,6 +4,7 @@ import './inputNumber.scss';
 import InputNumber from '../index';
 import Button from '../../button/index';
 import { withField, Form } from '../../index';
+import { useFormApi } from '../../form';
 
 export default {
   title: 'InputNumber',
@@ -656,4 +657,45 @@ export const FixPrecision = () => {
         <InputNumber keepFocus onBlur={() => console.log('blur')} onChange={v => setValue2(v)} value={value2} style={{ width: 190 }} precision={2} />
     </div>
   );
-}
+}
+
+/**
+ * 受控传超出 min value 的值,需要触发 onChange
+ * 不然在 Form 中使用可能会导致 Form State 与 InputNumber 展示的值不同问题
+ */
+export const FixMinValue = () => {
+  const [value, setValue] = useState();
+  const formRef = useFormApi();
+  return (
+      <div style={{ width: 280 }}>
+          <Button onClick={() => setValue(0)}>min=1, setValue=0</Button>
+          <InputNumber
+            min={1}
+            value={value} 
+            onChange={(v, e) => {
+              console.log('inputNumber1 change', `'${v}'`, e);
+              setValue(v);
+            }} 
+          />
+          <InputNumber
+            min={1}
+            value={0} 
+            onChange={(v, e) => {
+              console.log('inputNumber2 change', v, e);
+            }}
+          />
+          <Form initValues={{ minControlled: 0 }}>
+            <Form.InputNumber
+              field='minControlled'
+              min={1}
+              onChange={(v, e) => {
+                console.log('form inputNumber change', v, e);
+              }}
+            />
+          </Form>
+          <Button onClick={() => formRef.current.setValue('minControlled', 0) }>set form value</Button>
+          <Button onClick={() => { console.log('form value', JSON.stringify(formRef.current.getValues()))}}>get form values</Button>
+      </div>
+  );
+}
+FixMinValue.storyName = 'fix min value';

+ 22 - 14
packages/semi-ui/inputNumber/index.tsx

@@ -10,13 +10,13 @@ import Input, { InputProps } from '../input';
 import { forwardStatics } from '@douyinfe/semi-foundation/utils/object';
 import isNullOrUndefined from '@douyinfe/semi-foundation/utils/isNullOrUndefined';
 import isBothNaN from '@douyinfe/semi-foundation/utils/isBothNaN';
-import InputNumberFoundation, { InputNumberAdapter } from '@douyinfe/semi-foundation/inputNumber/foundation';
+import InputNumberFoundation, { BaseInputNumberState, InputNumberAdapter } from '@douyinfe/semi-foundation/inputNumber/foundation';
 import BaseComponent from '../_base/baseComponent';
 import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/inputNumber/constants';
 import { IconChevronUp, IconChevronDown } from '@douyinfe/semi-icons';
 
 import '@douyinfe/semi-foundation/inputNumber/inputNumber.scss';
-import { isNaN, noop } from 'lodash';
+import { isNaN, isString, noop } from 'lodash';
 import { ArrayElement } from '../_base/base';
 
 export interface InputNumberProps extends InputProps {
@@ -54,12 +54,7 @@ export interface InputNumberProps extends InputProps {
     onUpClick?: (value: string, e: React.MouseEvent<HTMLButtonElement>) => void;
 }
 
-export interface InputNumberState {
-    value?: number | string;
-    number?: number | null; // Current parsed numbers
-    focusing?: boolean;
-    hovering?: boolean;
-}
+export interface InputNumberState extends BaseInputNumberState {}
 
 class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
     static propTypes = {
@@ -222,6 +217,9 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
             },
             setClickUpOrDown: value => {
                 this.clickUpOrDown = value;
+            },
+            updateStates: (states, callback) => {
+                this.setState(states, callback);
             }
         };
     }
@@ -250,13 +248,15 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
     componentDidUpdate(prevProps: InputNumberProps) {
         const { value } = this.props;
         const { focusing } = this.state;
+        let newValue;
         /**
          * To determine whether the front and back are equal
          * NaN need to check whether both are NaN
          */
         if (value !== prevProps.value && !isBothNaN(value, prevProps.value)) {
             if (isNullOrUndefined(value) || value === '') {
-                this.setState({ value: '', number: null });
+                newValue = '';
+                this.foundation.updateStates({ value: newValue, number: null });
             } else {
                 let valueStr = value;
                 if (typeof value === 'number') {
@@ -306,22 +306,30 @@ class InputNumber extends BaseComponent<InputNumberProps, InputNumberState> {
                          */
                         if (this.clickUpOrDown) {
                             obj.value = this.foundation.doFormat(valueStr, true);
+                            newValue = obj.value;
                         }
-                        this.setState(obj, () => this.adapter.restoreCursor());
+                        this.foundation.updateStates(obj, () => this.adapter.restoreCursor());
                     } else if (!isNaN(toNum)) {
                         // Update input content when controlled input is illegal and not NaN
-                        this.setState({ value: this.foundation.doFormat(toNum, false) });
+                        newValue = this.foundation.doFormat(toNum, false);
+                        this.foundation.updateStates({ value: newValue });
                     } else {
                         // Update input content when controlled input NaN
-                        this.setState({ value: this.foundation.doFormat(valueStr, false) });
+                        newValue = this.foundation.doFormat(valueStr, false);
+                        this.foundation.updateStates({ value: newValue });
                     }
                 } else if (this.foundation.isValidNumber(parsedNum)) {
-                    this.setState({ number: parsedNum, value: this.foundation.doFormat(parsedNum) });
+                    newValue = this.foundation.doFormat(parsedNum);
+                    this.foundation.updateStates({ number: parsedNum, value: newValue });
                 } else {
                     // Invalid digital analog blurring effect instead of controlled failure
-                    this.setState({ number: null, value: '' });
+                    newValue = '';
+                    this.foundation.updateStates({ number: null, value: newValue });
                 }
             }
+            if (isString(newValue) && newValue !== String(this.props.value)) {
+                this.foundation.notifyChange(newValue, null);
+            }
         }
 
         if (!this.clickUpOrDown) {