Browse Source

fix: [form] setValue remove middle line in field of array type, value not working as expected (#605)

pointhalo 3 years ago
parent
commit
2f0d625874

+ 24 - 0
cypress/integration/form.spec.js

@@ -0,0 +1,24 @@
+// form.spec.js created with Cypress
+//
+// Start writing your Cypress tests below!
+// If you're unfamiliar with how Cypress works,
+// check out the link below and learn how to write your first test:
+// https://on.cypress.io/writing-first-test
+
+/**
+ * why use `.then`?
+ * @see https://docs.cypress.io/guides/core-concepts/variables-and-aliases#Return-Values
+ */
+describe('Form', () => {
+    it('formApi-setValue with array field path, 3 -> 2, remove middle line field', () => {
+        cy.visit('http://127.0.0.1:6009/iframe.html?path=/story/form--use-form-api-set-value-update-array');
+        cy.get(':nth-child(3) > .semi-button').click();
+        // line 1
+        cy.get('[x-field-id="effects[0].name"] > .semi-form-field-main > .semi-input-wrapper > input').should('have.value', '1-name');
+        cy.get('[x-field-id="effects[0].type"] > .semi-form-field-main > .semi-input-wrapper > input').should('have.value', '1-type');
+        // line 2
+        cy.get('[x-field-id="effects[1].name"] > .semi-form-field-main > .semi-input-wrapper > input').should('have.value', '3-name');
+        cy.get('[x-field-id="effects[1].type"] > .semi-form-field-main > .semi-input-wrapper > input').should('have.value', '3-type');
+        // cy.get('body').find('.semi-popover .semi-datepicker').should('have.length', 0);
+    });
+});

+ 40 - 36
packages/semi-foundation/form/foundation.ts

@@ -410,7 +410,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
     }
 
     // update formState value
-    updateStateValue(field: string, value: any, opts: CallOpts): void {
+    updateStateValue(field: string, value: any, opts: CallOpts, callback?: () => void): void {
         const notNotify = opts && opts.notNotify;
         const notUpdate = opts && opts.notUpdate;
         const fieldAllowEmpty = opts && opts.fieldAllowEmpty;
@@ -442,7 +442,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         }
 
         if (!notUpdate) {
-            this._adapter.forceUpdate();
+            this._adapter.forceUpdate(callback);
         }
     }
 
@@ -455,7 +455,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
     }
 
     // update formState touched
-    updateStateTouched(field: string, isTouched: boolean, opts?: CallOpts): void {
+    updateStateTouched(field: string, isTouched: boolean, opts?: CallOpts, callback?: () => void): void {
         const notNotify = opts && opts.notNotify;
         const notUpdate = opts && opts.notUpdate;
         ObjectUtil.set(this.data.touched, field, isTouched);
@@ -464,7 +464,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
             this._adapter.notifyChange(this.data);
         }
         if (!notUpdate) {
-            this._adapter.forceUpdate();
+            this._adapter.forceUpdate(callback);
         }
     }
 
@@ -477,7 +477,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
     }
 
     // update formState error
-    updateStateError(field: string, error: any, opts: CallOpts): void {
+    updateStateError(field: string, error: any, opts: CallOpts, callback?: () => void): void {
         const notNotify = opts && opts.notNotify;
         const notUpdate = opts && opts.notUpdate;
         ObjectUtil.set(this.data.errors, field, error);
@@ -488,7 +488,7 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         }
 
         if (!notUpdate) {
-            this._adapter.forceUpdate();
+            this._adapter.forceUpdate(callback);
         }
     }
 
@@ -506,16 +506,18 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
                 // At this time, first modify formState directly, then find out the subordinate fields and drive them to update
                 // Eg: peoples: [0, 2, 3]. Each value of the peoples array corresponds to an Input Field
                 // When the user directly calls formA pi.set Value ('peoples', [2,3])
-                this.updateStateValue(field, newValue, opts);
-                let nestedFields = this._getNestedField(field);
-                if (nestedFields.size) {
-                    nestedFields.forEach(fieldStaff => {
-                        let fieldPath = fieldStaff.field;
-                        let newFieldVal = ObjectUtil.get(this.data.values, fieldPath);
-                        let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
-                        fieldStaff.fieldApi.setValue(newFieldVal, nestedBatchUpdateOpts);
-                    });
-                }
+                this.updateStateValue(field, newValue, opts, () => {
+                    let nestedFields = this._getNestedField(field);
+                    if (nestedFields.size) {
+                        nestedFields.forEach(fieldStaff => {
+                            let fieldPath = fieldStaff.field;
+                            let newFieldVal = ObjectUtil.get(this.data.values, fieldPath);
+                            let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
+                            fieldStaff.fieldApi.setValue(newFieldVal, nestedBatchUpdateOpts);
+                        });
+                    }
+                });
+
                 // If the reset happens to be, then update the updateKey corresponding to ArrayField to render it again
                 if (this.getArrayField(field)) {
                     this.updateArrayField(field, { updateKey: new Date().valueOf() });
@@ -528,16 +530,17 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
             if (fieldApi) {
                 fieldApi.setError(newError, opts);
             } else {
-                this.updateStateError(field, newError, opts);
-                let nestedFields = this._getNestedField(field);
-                if (nestedFields.size) {
-                    nestedFields.forEach(fieldStaff => {
-                        let fieldPath = fieldStaff.field;
-                        let newFieldError = ObjectUtil.get(this.data.errors, fieldPath);
-                        let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
-                        fieldStaff.fieldApi.setError(newFieldError, nestedBatchUpdateOpts);
-                    });
-                }
+                this.updateStateError(field, newError, opts, () => {
+                    let nestedFields = this._getNestedField(field);
+                    if (nestedFields.size) {
+                        nestedFields.forEach(fieldStaff => {
+                            let fieldPath = fieldStaff.field;
+                            let newFieldError = ObjectUtil.get(this.data.errors, fieldPath);
+                            let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
+                            fieldStaff.fieldApi.setError(newFieldError, nestedBatchUpdateOpts);
+                        });
+                    }
+                });
                 if (this.getArrayField(field)) {
                     this.updateArrayField(field, { updateKey: new Date().valueOf() });
                 }
@@ -549,16 +552,17 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
             if (fieldApi) {
                 fieldApi.setTouched(isTouched, opts);
             } else {
-                this.updateStateTouched(field, isTouched, opts);
-                let nestedFields = this._getNestedField(field);
-                if (nestedFields.size) {
-                    nestedFields.forEach(fieldStaff => {
-                        let fieldPath = fieldStaff.field;
-                        let newFieldTouch = ObjectUtil.get(this.data.touched, fieldPath);
-                        let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
-                        fieldStaff.fieldApi.setTouched(newFieldTouch, nestedBatchUpdateOpts);
-                    });
-                }
+                this.updateStateTouched(field, isTouched, opts, () => {
+                    let nestedFields = this._getNestedField(field);
+                    if (nestedFields.size) {
+                        nestedFields.forEach(fieldStaff => {
+                            let fieldPath = fieldStaff.field;
+                            let newFieldTouch = ObjectUtil.get(this.data.touched, fieldPath);
+                            let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
+                            fieldStaff.fieldApi.setTouched(newFieldTouch, nestedBatchUpdateOpts);
+                        });
+                    }
+                });
                 if (this.getArrayField(field)) {
                     this.updateArrayField(field, { updateKey: new Date().valueOf() });
                 }

+ 1 - 1
packages/semi-foundation/form/interface.ts

@@ -12,7 +12,7 @@ export interface BaseFormAdapter<P = Record<string, any>, S = Record<string, any
     cloneDeep: (val: any, ...rest: any[]) => any;
     notifySubmit: (values: any) => void;
     notifySubmitFail: (errors: Record<string, any>, values: any) => void;
-    forceUpdate: () => void;
+    forceUpdate: (callback?: () => void) => void;
     notifyChange: (formState: FormState) => void;
     notifyValueChange: (values: any, changedValues: any) => void;
     notifyReset: () => void;

+ 182 - 0
packages/semi-ui/form/__test__/formApi.test.js

@@ -30,6 +30,11 @@ const fields = (
     </>
 );
 
+const getDomValue = (field, form) => {
+    let inputDOM = form.find(`[x-field-id="${field}"] input`).getDOMNode();
+    return inputDOM.getAttribute("value");
+};
+
 describe('Form-formApi', () => {
     beforeEach(() => {
         document.body.innerHTML = '';
@@ -416,6 +421,183 @@ describe('Form-formApi', () => {
         expect(formApi.getFormState().touched).toEqual({ a: { b: true, c: true }});
     })
 
+        it('formApi-setValue, field path precise', () => {
+        // case like:
+        // Exist 3 Field: a.b、a.c、a.d
+        // formApi.setValue('a.b', '123');
+        let formApi = null;
+        const fields = (
+            <>
+                <Form.Input field='a.b' />
+                <Form.Input field='a.c' />
+                <Form.Input field='a.d' />
+            </>
+        );
+        const props = {
+            children: fields,
+            getFormApi: api => {
+                formApi = api;
+            },
+        };
+        const form = getForm(props);
+        formApi.setValue('a.c', 'semi');
+        // check formState.values
+        let val = formApi.getValue('a.c');
+        expect(val).toEqual('semi');
+        form.update();
+        // check dom render
+        expect(getDomValue('a.c', form)).toEqual('semi');
+    });
+
+    it('formApi-setValue, field path belongs to parent aggregate', () => {
+        // case like:
+        // Exist 3 Field: a.b、a.c、a.d
+        // formApi.setValue('a', { b: 'semi', c: 'design' });
+        let formApi = null;
+        const fields = (
+            <>
+                <Form.Input field='a.b' />
+                <Form.Input field='a.c' />
+                <Form.Input field='a.d' />
+            </>
+        );
+        const props = {
+            children: fields,
+            getFormApi: api => {
+                formApi = api;
+            },
+        };
+        const form = getForm(props);
+        formApi.setValue('a', { b: 'semi', c: 'design' });
+        let acVal = formApi.getValue('a.c');
+        let abVal = formApi.getValue('a.b');
+        expect(abVal).toEqual('semi');
+        expect(acVal).toEqual('design');
+        form.update();
+
+        // check dom render
+        expect(getDomValue('a.b', form)).toEqual('semi');
+        expect(getDomValue('a.c', form)).toEqual('design');
+    });
+
+    it('formApi-setValue with array field path, 0 -> 3', () => {
+
+        const fields = ({ formState, values }) => {
+
+            return values.a && values.a.map((effect, i) => (
+                <div key={effect.key}>
+                    <Form.Input field={`a[${i}].name`} />
+                    <Form.Input field={`a[${i}].type`}  />
+                </div>
+            ));
+        };
+        let formApi = null;
+        const props = {
+            children: fields,
+            getFormApi: api => {
+                formApi = api;
+            },
+        };
+        const form = getForm(props);
+        let targetValue = [
+            { name: '0-name', type: '0-type' },
+            { name: '1-name', type: '1-type' },
+            { name: '2-name', type: '2-type' },
+        ];
+        formApi.setValue('a', targetValue);
+        let formStateValues = formApi.getValue();
+        form.update();
+        // check dom render
+        expect(getDomValue('a[0].name', form)).toEqual('0-name');
+        expect(getDomValue('a[0].type', form)).toEqual('0-type');
+        expect(getDomValue('a[1].name', form)).toEqual('1-name');
+        expect(getDomValue('a[1].type', form)).toEqual('1-type');
+        expect(getDomValue('a[2].name', form)).toEqual('2-name');
+        expect(getDomValue('a[2].type', form)).toEqual('2-type');
+    });
+
+    // // this case result was different in cypress / jest, jest result is wrong
+    // it('formApi-setValue with array field path, 3 -> 2, delete some field', done => {
+    //     const fields = ({ formState, values }) => {
+    //         return values.a && values.a.map((item, i) => (
+    //             <div key={item.key} style={{ width: 300 }}>
+    //                 <Form.Input field={`a[${i}].name`} />
+    //                 <Form.Input field={`a[${i}].type`} />
+    //             </div>
+    //         ));
+    //     };
+    //     let formApi = null;
+    //     const props = {
+    //         children: fields,
+    //         initValues: {
+    //             a: [
+    //                 { name: '0-name', type: '0-type', key: 0 },
+    //                 { name: '1-name', type: '1-type', key: 1 },
+    //                 { name: '2-name', type: '2-type', key: 2 },
+    //             ]
+    //         },
+    //         getFormApi: api => {
+    //             formApi = api;
+    //         },
+    //     };
+    //     let form = getForm(props);
+    //     // remove middle one
+    //     formApi.setValue('a', [
+    //         { name: '0-name', type: '0-type', key: 0 },
+    //         { name: '2-name', type: '2-type', key: 2 },
+    //     ]);
+    //     let formStateValues = formApi.getValue();
+    //     form.update();
+
+    //     setTimeout(() => {
+    //         // check dom render
+    //         expect(getDomValue('a[0].name', form)).toEqual('0-name');
+    //         expect(getDomValue('a[0].type', form)).toEqual('0-type');
+    //         expect(getDomValue('a[1].name', form)).toEqual('2-name');
+    //         expect(getDomValue('a[1].type', form)).toEqual('2-type');
+
+    //         expect(form.exists(`[x-field-id="a[2].name"] input`)).toEqual(false);
+    //         expect(form.exists(`[x-field-id="a[2].type"] input`)).toEqual(false);
+    //         done();
+    //     }, 5000);
+    // });
+
+    it('formApi-setValue with array field path, 1 -> 3, add some field', () => {
+        const fields = ({ formState, values }) => {
+            return values.a && values.a.map((effect, i) => (
+                <div key={effect.key}>
+                    <Form.Input field={`a[${i}].name`} />
+                    <Form.Input field={`a[${i}].type`} />
+                </div>
+            ));
+        };
+        let formApi = null;
+        const props = {
+            children: fields,
+            initValues: {
+                a: [{ name: 'semi', type: 'design' }]
+            },
+            getFormApi: api => {
+                formApi = api;
+            },
+        };
+        let form = getForm(props);
+        formApi.setValue('a', [
+            { name: '0-name', type: '0-type' },
+            { name: '1-name', type: '1-type' },
+            { name: '2-name', type: '2-type' },
+        ]);
+        let formStateValues = formApi.getValue();
+        form.update();
+        // check dom render
+        expect(getDomValue('a[0].name', form)).toEqual('0-name');
+        expect(getDomValue('a[0].type', form)).toEqual('0-type');
+        expect(getDomValue('a[1].name', form)).toEqual('1-name');
+        expect(getDomValue('a[1].type', form)).toEqual('1-type');
+        expect(getDomValue('a[2].name', form)).toEqual('2-name');
+        expect(getDomValue('a[2].type', form)).toEqual('2-type');
+    });
+
     // it('formApi-submitForm', () => {
     //     // submit should call validate first
     // });

+ 4 - 7
packages/semi-ui/form/_story/FormApi/arrayDemo.jsx

@@ -24,9 +24,9 @@ class ArrayDemo extends React.Component {
         this.state = {
             initValues: {
                 effects: [
-                    { name: '脸部贴纸', type: '2D', key: 0 },
-                    { name: '美妆', type: '2D', key: 1 },
-                    { name: '前景贴纸', type: '3D', key: 2 },
+                    { name: '1-name', type: '1-type', key: 0 },
+                    { name: '2-name', type: '2-type', key: 1 },
+                    { name: '3-name', type: '3-type', key: 2 },
                 ]
             }
         };
@@ -59,10 +59,7 @@ class ArrayDemo extends React.Component {
         return values.effects && values.effects.map((effect, i) => (
             <div key={effect.key} style={{ width: 1000, display: 'flex' }}>
                 <Form.Input field={`effects[${i}].name`} style={{ width: 200, marginRight: 16 }} />
-                <Form.Select field={`effects[${i}].type`} style={{ width: 90 }}>
-                    <Form.Select.Option value="2D">2D</Form.Select.Option>
-                    <Form.Select.Option value="3D">3D</Form.Select.Option>
-                </Form.Select>
+                <Form.Input field={`effects[${i}].type`} style={{ width: 90 }} />
                 <Button type="danger" onClick={() => this.remove(effect.key)} style={{ margin: 16 }}>Remove</Button>
             </div>
         ));

+ 2 - 2
packages/semi-ui/form/_story/Layout/slotDemo.jsx

@@ -98,7 +98,7 @@ class AssistComponent extends React.Component {
                             我是Semi Form SlotB, 我的Label Align、Width与众不同
                         </div>
                     </Form.Slot>
-                </Form>,
+                </Form>
                 <Form.Slot
                     label={{
                         text: 'SlotB',
@@ -117,7 +117,7 @@ class AssistComponent extends React.Component {
                     >
                         我是Slot,我并没有被Form包裹,我是单独使用的
                     </div>
-                </Form.Slot>,
+                </Form.Slot>
             </>
         );
     }

+ 6 - 6
packages/semi-ui/form/_story/form.stories.js

@@ -158,6 +158,12 @@ FormApiSetValueUsingFieldParentPath.story = {
   name: 'formApi-setValue using field parent path',
 };
 
+export const UseFormApiSetValueUpdateArray = () => <ArrayDemo />;
+
+UseFormApiSetValueUpdateArray.story = {
+  name: 'formApi-setValue set array',
+};
+
 export const DynamicAddRemoveField = () => (
   <Form>
     {({ formState }) => (
@@ -209,12 +215,6 @@ _ArrayFieldCollapseDemo.story = {
   name: 'ArrayField-CollapseDemo',
 };
 
-export const ArrayFieldDynamicUpdate = () => <ArrayDemo />;
-
-ArrayFieldDynamicUpdate.story = {
-  name: 'ArrayField-dynamic update',
-};
-
 export const LinkField = () => <LinkFieldForm />;
 
 LinkField.story = {

+ 2 - 2
packages/semi-ui/form/baseForm.tsx

@@ -160,8 +160,8 @@ class Form extends BaseComponent<BaseFormProps, BaseFormState> {
             notifySubmitFail: (errors: ErrorMsg, values: any) => {
                 this.props.onSubmitFail(errors, values);
             },
-            forceUpdate: () => {
-                this.forceUpdate();
+            forceUpdate: (callback?: () => void) => {
+                this.forceUpdate(callback);
             },
             notifyChange: (formState: FormState) => {
                 this.props.onChange(formState);