Browse Source

fix: fix the problem that the display is incorrect when setValue cross-updates arrayField of different levels

pointhalo 2 years ago
parent
commit
a2c8e5c021

+ 124 - 49
cypress/integration/form.spec.js

@@ -524,62 +524,137 @@ describe('Form', () => {
         cy.get('#data-2-rule-2-desc').should('have.value', `new-2-2-desc`);
     });
 
-    // it('2 Nested ArrayField - formApi.setValue level-0 then set level-1 then level-0 again', () => {
-    //     cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/form--basic-nested-demo');
-    //     cy.get('#changeOutSide').click();
-    //     cy.get('#changeInside').click();
-
-    //     cy.get('#data-0-name').should('have.value', `new-0`);
-    //     cy.get('#data-0-rule-0-type').should('have.value', `new-0-0-type-in`);  // cause rewrite by changeInside
-    //     cy.get('#data-0-rule-0-desc').should('have.value', `new-0-0-desc-in`);  // cause rewrite by changeInside
-
-    //     cy.get('#data-0-rule-1-type').should('not.exist'); // cause rewrite by changeInside
-    //     cy.get('#data-0-rule-1-desc').should('not.exist'); // cause rewrite by changeInside
-
-    //     cy.get('#data-1-name').should('have.value', `new-1`);
-    //     cy.get('#data-1-rule-0-type').should('have.value', `new-1-0-type`);
-    //     cy.get('#data-1-rule-0-desc').should('have.value', `new-1-0-desc`);
-
-    //     cy.get('#data-2-name').should('have.value', `new-2`);
-    //     cy.get('#data-2-rule-0-type').should('have.value', `new-2-0-type`);
-    //     cy.get('#data-2-rule-0-desc').should('have.value', `new-2-0-desc`);
-    //     cy.get('#data-2-rule-1-type').should('have.value', `new-2-1-type`);
-    //     cy.get('#data-2-rule-1-desc').should('have.value', `new-2-1-desc`);
-    //     cy.get('#data-2-rule-2-type').should('have.value', `new-2-2-type`);
-    //     cy.get('#data-2-rule-2-desc').should('have.value', `new-2-2-desc`);
-
-    //     cy.get('#changeOutSide').click();
-    // });
-
-
-    // test cache effect when change child\parent
-    // it('2 Nested ArrayField - formApi.setValue level-1 then set level-0', () => {
-    //     cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/form--basic-nested-demo');
-    //     cy.get('#changeInside').click();
-    //     cy.get('#changeOutSide').click();
-    //     cy.get('#changeInside').click();
-    // });
-
-    // it('2 Nested ArrayField - formApi.setValue level-0 then reset', () => {
-    //     cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/form--basic-nested-demo');
-    //     cy.get('#changeInside').click();
-    //     cy.get('#changeOutSide').click();
-    //     cy.get('#changeInside').click();
-    // });
-
-    // it('2 Nested ArrayField - formApi.setValue special row', () => { });
+    it('2 Nested ArrayField - formApi.setValue level-1 then set level-0 then reset', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/form--basic-nested-demo');
+        cy.get('#changeInside').click();
 
-    // it('2 Nested ArrayField - formApi.setValues', () => { });
+        cy.get('#data-0-name').should('have.value', `0`);
+        cy.get('#data-0-rule-0-type').should('have.value', `new-0-0-type-in`);
+        cy.get('#data-0-rule-0-desc').should('have.value', `new-0-0-desc-in`);
+        cy.get('#data-0-rule-1-type').should('not.exist');
+        cy.get('#data-0-rule-1-desc').should('not.exist');
+        cy.get('#data-1-name').should('have.value', `1`);
+        cy.get('#data-1-rule-0-type').should('have.value', `1-0-type`);
+        cy.get('#data-1-rule-0-desc').should('have.value', `1-0-desc`);
 
-    // it('Init - Form Props initValues、ArrayField initValue、Field initValue', () => { 
-    // // 一个 Form 三个 ArrayField
-    // });
+        cy.get('#changeOutSide').click();
+        cy.get('#data-0-name').should('have.value', `new-0`);
+        cy.get('#data-0-rule-0-type').should('have.value', `new-0-0-type`);
+        cy.get('#data-0-rule-0-desc').should('have.value', `new-0-0-desc`);
+        cy.get('#data-0-rule-1-type').should('have.value', `new-0-1-type`);
+        cy.get('#data-0-rule-1-desc').should('have.value', `new-0-1-desc`);
+        cy.get('#data-1-name').should('have.value', `new-1`);
+        cy.get('#data-1-rule-0-type').should('have.value', `new-1-0-type`);
+        cy.get('#data-1-rule-0-desc').should('have.value', `new-1-0-desc`);
+        cy.get('#data-2-name').should('have.value', `new-2`);
+        cy.get('#data-2-rule-0-type').should('have.value', `new-2-0-type`);
+        cy.get('#data-2-rule-0-desc').should('have.value', `new-2-0-desc`);
+        cy.get('#data-2-rule-1-type').should('have.value', `new-2-1-type`);
+        cy.get('#data-2-rule-1-desc').should('have.value', `new-2-1-desc`);
+        cy.get('#data-2-rule-2-type').should('have.value', `new-2-2-type`);
+        cy.get('#data-2-rule-2-desc').should('have.value', `new-2-2-desc`);
 
-    // it('Init - combine', () => {});
+        cy.get('button[type=reset]').click();
+        cy.get('#data-0-name').should('have.value', `0`);
+        cy.get('#data-0-rule-0-type').should('have.value', `0-0-type`);
+        cy.get('#data-0-rule-0-desc').should('have.value', `0-0-desc`);
+        cy.get('#data-0-rule-1-type').should('have.value', `0-1-type`);
+        cy.get('#data-0-rule-1-desc').should('have.value', `0-1-desc`);
+        cy.get('#data-1-name').should('have.value', `1`);
+        cy.get('#data-1-rule-0-type').should('have.value', `1-0-type`);
+        cy.get('#data-1-rule-0-desc').should('have.value', `1-0-desc`);
+    });
 
+    it('2 Nested ArrayField - formApi.setValue level-0 then set level-1 then level-0 then reset', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/form--basic-nested-demo');
+        cy.get('#changeOutSide').click();
+        cy.get('#changeInside').click();
+        cy.get('#changeOutSide').click();
+        cy.get('#data-0-name').should('have.value', `new-0`);
+        cy.get('#data-0-rule-0-type').should('have.value', `new-0-0-type`);
+        cy.get('#data-0-rule-0-desc').should('have.value', `new-0-0-desc`);
+        cy.get('#data-0-rule-1-type').should('have.value', `new-0-1-type`);
+        cy.get('#data-0-rule-1-desc').should('have.value', `new-0-1-desc`);
+        cy.get('#data-1-name').should('have.value', `new-1`);
+        cy.get('#data-1-rule-0-type').should('have.value', `new-1-0-type`);
+        cy.get('#data-1-rule-0-desc').should('have.value', `new-1-0-desc`);
+        cy.get('#data-2-name').should('have.value', `new-2`);
+        cy.get('#data-2-rule-0-type').should('have.value', `new-2-0-type`);
+        cy.get('#data-2-rule-0-desc').should('have.value', `new-2-0-desc`);
+        cy.get('#data-2-rule-1-type').should('have.value', `new-2-1-type`);
+        cy.get('#data-2-rule-1-desc').should('have.value', `new-2-1-desc`);
+        cy.get('#data-2-rule-2-type').should('have.value', `new-2-2-type`);
+        cy.get('#data-2-rule-2-desc').should('have.value', `new-2-2-desc`);
+
+        cy.get('button[type=reset]').click();
+        cy.get('#data-0-name').should('have.value', `0`);
+        cy.get('#data-0-rule-0-type').should('have.value', `0-0-type`);
+        cy.get('#data-0-rule-0-desc').should('have.value', `0-0-desc`);
+        cy.get('#data-0-rule-1-type').should('have.value', `0-1-type`);
+        cy.get('#data-0-rule-1-desc').should('have.value', `0-1-desc`);
+        cy.get('#data-1-name').should('have.value', `1`);
+        cy.get('#data-1-rule-0-type').should('have.value', `1-0-type`);
+        cy.get('#data-1-rule-0-desc').should('have.value', `1-0-desc`);
+    });
+
+    it('2 Nested ArrayField - formApi.setValue special row', () => {
+        cy.visit('http://127.0.0.1:6006/iframe.html?path=/story/form--basic-nested-demo');
+        cy.get('#changeRow').click();
+        cy.get('#data-0-name').should('have.value', `0`);
+        cy.get('#data-0-rule-0-type').should('have.value', `special-row-type`);
+        cy.get('#data-0-rule-0-desc').should('have.value', `special-row-desc`);
+        cy.get('#data-0-rule-1-type').should('have.value', `0-1-type`);
+        cy.get('#data-0-rule-1-desc').should('have.value', `0-1-desc`);
+        cy.get('#data-1-name').should('have.value', `1`);
+        cy.get('#data-1-rule-0-type').should('have.value', `1-0-type`);
+        cy.get('#data-1-rule-0-desc').should('have.value', `1-0-desc`);
+    });
+
+    // it('2 Nested ArrayField - formApi.setValues', () => { });
     // it('Sync setValues - modify', () => { });
     // it('Sync setValues - add', () => { });
     // it('Sync setValues - remove', () => { });
     // it('Async setValues', () => { });
 
+    it('sync setValues - modify value, 2 -> 2', () => {
+    });
+
+    it('sync setValues - add, 2 -> 3, add tail', () => {
+    });
+
+    it('sync setValues - add, 2 -> 3, add first', () => {
+       
+    });
+
+    it('sync setValues - add, 2 -> 3, add middle', () => {
+    
+    });
+
+    it('sync setValues - remove, 3 -> 2, remove first', () => {
+       
+    });
+
+    it('sync setValues - remove, 3 -> 2, remove middle', () => {
+       
+    });
+
+    it('sync setValues - remove, 3 -> 2, remove tail', () => {
+       
+    });
+
+    it('sync setValues - remove, 2 -> 0, remove all', () => {
+       
+    });
+
+
+    it('Init - Form Props initValues、ArrayField initValue、Field initValue', () => { 
+        // 一个 Form 三个 ArrayField
+        
+    });
+
+    // it('Init - combine', () => {});
+
+
+
+
 });

+ 48 - 29
packages/semi-foundation/form/foundation.ts

@@ -57,6 +57,8 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         this.unRegisterArrayField = this.unRegisterArrayField.bind(this);
         this.getArrayField = this.getArrayField.bind(this);
         this.updateArrayField = this.updateArrayField.bind(this);
+        this.getParentArrayField = this.getParentArrayField.bind(this);
+        this.getChildArrayField = this.getChildArrayField.bind(this);
 
         this.getField = this.getField.bind(this);
         this.setValues = this.setValues.bind(this);
@@ -131,15 +133,23 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         this._adapter.forceUpdate();
     }
 
-    registerArrayField(arrayFieldPath: string, { initValue, forceUpdate }: Pick<ArrayFieldStaff, 'initValue' | 'forceUpdate'> ): void {
+    registerArrayField(arrayFieldPath: string, staffObj: Pick<ArrayFieldStaff, 'initValue' | 'forceUpdate'> ): void {
         //  save initValue of arrayField, will be use when calling rest
-        this.registeredArrayField.set(arrayFieldPath, { field: arrayFieldPath, initValue: initValue, forceUpdate });
+        let arrayFieldState = this.registeredArrayField.get(arrayFieldPath);
+        if (arrayFieldState) {
+            this.registeredArrayField.set(arrayFieldPath, { ...arrayFieldState, ...staffObj });
+        }
+        this.registeredArrayField.set(arrayFieldPath, { field: arrayFieldPath, ...staffObj });
     }
-    
 
     unRegisterArrayField(arrayFieldPath: string): void {
+        let childArrayField = this.getChildArrayField(arrayFieldPath);
+        if (childArrayField.size) {
+            childArrayField.forEach(childArray => {
+                this.registeredArrayField.delete(childArray.field);
+            });
+        }
         this.registeredArrayField.delete(arrayFieldPath);
-        console.log('remain', this.registeredArrayField);
     }
 
     getArrayField(arrayFieldPath?: string): ArrayFieldStaff | Map<string, ArrayFieldStaff> {
@@ -149,33 +159,40 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
         return this.registeredArrayField.get(arrayFieldPath);
     }
 
-    // updateArrayField(arrayFieldPath: string, updateStaff?: Omit<ArrayFieldStaff, 'field'>): void {
-    //     const arrayFieldStaff = this.getArrayField(arrayFieldPath);
-    //     const mergeStaff = { ...arrayFieldStaff, ...updateStaff };
-    //     this.registeredArrayField.set(arrayFieldPath, mergeStaff);
-    //     mergeStaff.forceUpdate(mergeStaff?.updateValue);
-    // }
-
     updateArrayField(arrayFieldPath: string, { newValue, oldValue }: any): void {
-        const arrayFieldStaff = this.getArrayField(arrayFieldPath);
+        const arrayFieldStaff = this.getArrayField(arrayFieldPath) as ArrayFieldStaff;
         arrayFieldStaff.forceUpdate({ newValue, oldValue });
-        // // check is nested ArrayField
-        // const nested = this._getNestedField(arrayFieldPath);
-        // const hasParent;
-        // const hasChild;
-        // if (hasParent) {
-        //     // update parent cache value
-        // }
-        // if (hasChild) {
-        //     // update child cache value
-        // }
-        // mergeStaff.forceUpdate(updateDetail);
-    }
-
-    // 使用formApi.setValue等手段,父改子,或者子改父时,需要更新 cache
-    // updateArrayFieldCache(arrayFieldPath: string, ): void {
-    //     const arrayFieldStaff = this.getArrayField(arrayFieldPath);
-    // }
+    }
+
+    getChildArrayField(arrayFieldPath: string) {
+        const allArrayFieldPath = [...this.registeredArrayField.keys()];
+        let childArrayField = new Map();
+        allArrayFieldPath.forEach(item => {
+            const itemPath = toPath(item);
+            const targetPath = toPath(arrayFieldPath);
+            const itemIsTargetChild = targetPath.every((path, i) => (targetPath[i] === itemPath[i])) && (item !== arrayFieldPath);
+            if (itemIsTargetChild) {
+                const realField = this.registeredArrayField.get(item);
+                childArrayField.set(item, realField);
+            }
+        });
+        return childArrayField;
+    }
+
+    getParentArrayField(arrayFieldPath: string) {
+        const allArrayFieldPath = [...this.registeredArrayField.keys()];
+        let parentArrayField = new Map();
+        allArrayFieldPath.forEach(item => {
+            const itemPath = toPath(item);
+            const targetPath = toPath(arrayFieldPath);
+            const itemIsTargetParent = itemPath.every((path, i) => (targetPath[i] === itemPath[i])) && (item !== arrayFieldPath);
+            if (itemIsTargetParent) {
+                const realField = this.registeredArrayField.get(item);
+                parentArrayField.set(item, realField);
+            }
+        });
+        return parentArrayField;
+    }
 
     validate(fieldPaths?: Array<string>): Promise<unknown> {
         const { validateFields } = this.getProps();
@@ -633,6 +650,8 @@ export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
             unRegisterArrayField: this.unRegisterArrayField,
             getArrayField: this.getArrayField,
             updateArrayField: this.updateArrayField,
+            getChildArrayField: this.getChildArrayField,
+            getParentArrayField: this.getParentArrayField,
         };
     }
 

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

@@ -112,14 +112,12 @@ export interface FieldStaff {
 }
 
 interface forceUpdateParams {
-    value?: any;
+    newValue?: any;
     oldValue?: any
 }
 
 export interface ArrayFieldStaff {
     field: string;
-    // updateKey?: string;
-    // updateValue?: any;
     initValue?: any;
     forceUpdate?: (forceUpdateParams: forceUpdateParams) => void
 }
@@ -136,9 +134,11 @@ export interface FormUpdaterContextType {
     getInitValue: (field?: string) => any;
     getFormProps: (keys?: Array<string>) => ComponentProps;
     getField: (field: string) => FieldStaff | undefined;
-    registerArrayField: (arrayFieldPath: string, val: any) => void;
+    registerArrayField: (arrayField: string, val: any) => void;
     unRegisterArrayField: (arrayField: string) => void;
     getArrayField: (arrayField: string) => ArrayFieldStaff;
-    updateArrayField: (arrayField: string, updateValue: any) => void
+    updateArrayField: (arrayField: string, updateValue: any) => void;
+    getParentArrayField: (arrayField: string) => Map<string, ArrayFieldStaff>;
+    getChildArrayField: (arrayField: string) => Map<string, ArrayFieldStaff>
 }
 

+ 2 - 3
packages/semi-ui/form/_story/ArrayField/6-nestedBasicUsage.jsx

@@ -67,8 +67,6 @@ const NestDemo = () => {
         ]);
     };
 
-    // ❌ something still wrong when first click outside and click inside after
-    // ❌ something still wrong when first click outside and click inside after and click outside again
     const setValueChangeInside = () => {
         const formApi = getFormApi();
         formApi.setValue('data[0].rules', [{ desc: 'new-0-0-desc-in', type: 'new-0-0-type-in' }]);
@@ -76,7 +74,7 @@ const NestDemo = () => {
 
     const setValueChangeRow = () => {
         const formApi = getFormApi();
-        formApi.setValue('data[0].rules[0]', { name: 'special-row-name', rules: [{ desc: 'abc', type: 'efg' }] });
+        formApi.setValue('data[0].rules[0]', { type: 'special-row-type', desc: 'special-row-desc' });
     };
     
     return (
@@ -92,6 +90,7 @@ const NestDemo = () => {
                             <Button htmlType='reset' theme='solid' type='secondary' icon={<IconRefresh />}>Reset</Button>
                             <Button id='changeOutSide' theme='solid' type='primary' onClick={() => setValueChangeOutSide()}>ChangeOutSide</Button>
                             <Button id='changeInside' theme='solid' type='tertiary' onClick={() => setValueChangeInside()}>ChangeInsideSide</Button>
+                            <Button id='changeRow' theme='solid' type='tertiary' onClick={() => setValueChangeRow()}>ChangeSpecialRow</Button>
                         </Space>
                         {
                             arrayFields.map(({ field, key, remove }, i) => (

+ 191 - 0
packages/semi-ui/form/_story/ArrayField/7-manualSyncBatchSetUsage.jsx

@@ -0,0 +1,191 @@
+import React, { useState, useRef } from 'react';
+import { Form, Col, Row, Button, ArrayField, Space, useFormState } from '@douyinfe/semi-ui';
+import { IconMinusCircle, IconPlusCircle, IconRefresh } from '@douyinfe/semi-icons';
+import { JSONTree } from 'react-json-tree';
+
+// TODO
+
+//  Async setValues
+//  Sync setValues
+//  Async setValues, override
+//  Sync setValues, override
+
+// 0 -> 3
+// 3 -> 0
+// 3 -> 2
+// 2 -> 3
+
+const FormStateTree = () => {
+    const formState = useFormState();
+    return (
+        <JSONTree
+            shouldExpandNodeInitially={() => true}
+            hideRoot
+            data={formState}
+        />
+    );
+};
+
+const SetValues = () => {
+    const formRef = useRef();
+
+    const getFormApi = () => {
+        const formApi = formRef.current.formApi;  
+        return formApi;
+    };
+
+    const formInitValues = {
+        data: [
+            {
+                name: '0',
+                rules: [
+                    { desc: '0-0-desc', type: '0-0-type' },
+                    { desc: '0-1-desc', type: '0-1-type' },
+                ],
+            },
+            {
+                name: '1',
+                rules: [
+                    { desc: '1-0-desc', type: '1-0-type' }
+                ],
+            }
+        ]
+    };
+
+    const setValueChangeOutSide = () => {
+        const formApi = getFormApi();
+        formApi.setValue('data', [
+            {
+                name: 'new-0',
+                rules: [
+                    { desc: 'new-0-0-desc', type: 'new-0-0-type' },
+                    { desc: 'new-0-1-desc', type: 'new-0-1-type' },
+                ],
+            },
+            {
+                name: 'new-1',
+                rules: [
+                    { desc: 'new-1-0-desc', type: 'new-1-0-type' },
+                ],
+            },
+            {
+                name: 'new-2',
+                rules: [
+                    { desc: 'new-2-0-desc', type: 'new-2-0-type' },
+                    { desc: 'new-2-1-desc', type: 'new-2-1-type' },
+                    { desc: 'new-2-2-desc', type: 'new-2-2-type' },
+                ],
+            }
+        ]);
+    };
+
+    const setValueChangeInside = () => {
+        const formApi = getFormApi();
+        // formApi.setValue('data[0].rules', [{ desc: 'new-0-0-desc-in', type: 'new-0-0-type-in' }]);
+    };
+
+    const setValueChangeRow = () => {
+        const formApi = getFormApi();
+        // formApi.setValue('data[0].rules[0]', { name: 'special-row-name', rules: [{ desc: 'abc', type: 'efg' }] });
+    };
+    
+    return (
+        <Form
+            ref={formRef}
+            initValues={formInitValues}
+            labelPosition='inset'
+        >
+            <ArrayField field='data'>
+                {({ add, arrayFields, addWithInitValue }) => (
+                    <React.Fragment>
+                        <Space>
+                            <Button htmlType='reset' theme='solid' type='secondary' icon={<IconRefresh />}>Reset</Button>
+                            <Button id='changeOutSide' theme='solid' type='primary' onClick={() => setValueChangeOutSide()}>ChangeOutSide</Button>
+                            <Button id='changeInside' theme='solid' type='tertiary' onClick={() => setValueChangeInside()}>ChangeInsideSide</Button>
+                        </Space>
+                        {
+                            arrayFields.map(({ field, key, remove }, i) => (
+                                <div key={key} style={{ width: 1000, display: 'flex', flexWrap: 'wrap' }} id={`data-${i}`} className='line'>
+                                    {key.slice(0, 10)}
+                                    <Space>
+                                        <Form.Input
+                                            field={`${field}.name`}
+                                            label={`${field}.name`}
+                                            style={{ width: 700 }}
+                                            id={`data-${i}-name`}
+                                        >
+                                        </Form.Input>
+                                        <Button
+                                            onClick={() => addWithInitValue({
+                                                name: arrayFields.length,
+                                                rules: [
+                                                    { desc: `${arrayFields.length}-0-desc`, type: `${arrayFields.length}-0-type` }
+                                                ]
+                                            })}
+                                            icon={<IconPlusCircle/>}
+                                            id={`data-${i}-add`} 
+                                            disabled={i !== arrayFields.length - 1}
+                                        />
+                                        <Button
+                                            type='danger'
+                                            onClick={remove}
+                                            id={`data-${i}-remove`} 
+                                            icon={<IconMinusCircle />}
+                                        />
+                                    </Space>
+                           
+                                    <ArrayField field={`${field}.rules`}>
+                                        {({ add: addNested, arrayFields: nestedArrayFields, addWithInitValue: addNestedWithInitValue }, ) => (
+                                            <React.Fragment>
+                                                {
+                                                    nestedArrayFields.map(({ field: f, key: k, remove: r }, n) => (
+                                                        <section className='rules' key={k} style={{ display: 'flex', flexWrap: 'wrap', marginLeft: 36 }}>
+                                                            <Space>
+                                                                {k.slice(0, 10)}
+                                                                <Form.Input
+                                                                    style={{ width: 300, marginRight: 12 }}
+                                                                    field={`${f}.type`}
+                                                                    label={`${f}.type`}
+                                                                    id={`data-${i}-rule-${n}-type`} 
+                                                                />
+                                                                <Form.Input
+                                                                    style={{ width: 300 }}
+                                                                    field={`${f}.desc`}
+                                                                    label={`${f}.desc`}
+                                                                    id={`data-${i}-rule-${n}-desc`} 
+                                                                />
+                                                                <Button
+                                                                    onClick={() => addNestedWithInitValue({ type: `${i}-${n+1}-type`, desc: `${i}-${n+1}-desc` })}
+                                                                    icon={<IconPlusCircle/>}
+                                                                    id={`data-${i}-rule-${n}-add`}
+                                                                    disabled={n !== nestedArrayFields.length - 1}
+                                                                >
+                                                                    add new line
+                                                                </Button>
+                                                                <Button
+                                                                    type='danger'
+                                                                    onClick={r}
+                                                                    id={`data-${i}-rule-${n}-remove`} 
+                                                                    icon={<IconMinusCircle />}
+                                                                />
+                                                            </Space>
+                                                        </section>
+                                                    ))
+                                                }
+                                            </React.Fragment>
+                                        )}
+                                    </ArrayField>
+                                </div>
+                            ))
+                        }
+                    </React.Fragment>
+                )}
+            </ArrayField>
+            <FormStateTree></FormStateTree>
+        </Form>
+    );
+};
+
+SetValues.storyName = 'ArrayField-ManualBatchSet Sync';
+
+export default SetValues;

+ 190 - 0
packages/semi-ui/form/_story/ArrayField/8-manualAsyncBatchSetUsage.jsx

@@ -0,0 +1,190 @@
+import React, { useState, useRef } from 'react';
+import { Form, Col, Row, Button, ArrayField, Space, useFormState } from '@douyinfe/semi-ui';
+import { IconMinusCircle, IconPlusCircle, IconRefresh } from '@douyinfe/semi-icons';
+import { JSONTree } from 'react-json-tree';
+
+// TODO
+//  Async setValues
+//  Sync setValues
+//  Async setValues, override
+//  Sync setValues, override
+
+// 0 -> 3
+// 3 -> 0
+// 3 -> 2
+// 2 -> 3
+
+const FormStateTree = () => {
+    const formState = useFormState();
+    return (
+        <JSONTree
+            shouldExpandNodeInitially={() => true}
+            hideRoot
+            data={formState}
+        />
+    );
+};
+
+const SetValues = () => {
+    const formRef = useRef();
+
+    const getFormApi = () => {
+        const formApi = formRef.current.formApi;  
+        return formApi;
+    };
+
+    const formInitValues = {
+        data: [
+            {
+                name: '0',
+                rules: [
+                    { desc: '0-0-desc', type: '0-0-type' },
+                    { desc: '0-1-desc', type: '0-1-type' },
+                ],
+            },
+            {
+                name: '1',
+                rules: [
+                    { desc: '1-0-desc', type: '1-0-type' }
+                ],
+            }
+        ]
+    };
+
+    const setValueChangeOutSide = () => {
+        const formApi = getFormApi();
+        formApi.setValue('data', [
+            {
+                name: 'new-0',
+                rules: [
+                    { desc: 'new-0-0-desc', type: 'new-0-0-type' },
+                    { desc: 'new-0-1-desc', type: 'new-0-1-type' },
+                ],
+            },
+            {
+                name: 'new-1',
+                rules: [
+                    { desc: 'new-1-0-desc', type: 'new-1-0-type' },
+                ],
+            },
+            {
+                name: 'new-2',
+                rules: [
+                    { desc: 'new-2-0-desc', type: 'new-2-0-type' },
+                    { desc: 'new-2-1-desc', type: 'new-2-1-type' },
+                    { desc: 'new-2-2-desc', type: 'new-2-2-type' },
+                ],
+            }
+        ]);
+    };
+
+    const setValueChangeInside = () => {
+        const formApi = getFormApi();
+        formApi.setValue('data[0].rules', [{ desc: 'new-0-0-desc-in', type: 'new-0-0-type-in' }]);
+    };
+
+    const setValueChangeRow = () => {
+        const formApi = getFormApi();
+        formApi.setValue('data[0].rules[0]', { name: 'special-row-name', rules: [{ desc: 'abc', type: 'efg' }] });
+    };
+    
+    return (
+        <Form
+            ref={formRef}
+            initValues={formInitValues}
+            labelPosition='inset'
+        >
+            <ArrayField field='data'>
+                {({ add, arrayFields, addWithInitValue }) => (
+                    <React.Fragment>
+                        <Space>
+                            <Button htmlType='reset' theme='solid' type='secondary' icon={<IconRefresh />}>Reset</Button>
+                            <Button id='changeOutSide' theme='solid' type='primary' onClick={() => setValueChangeOutSide()}>ChangeOutSide</Button>
+                            <Button id='changeInside' theme='solid' type='tertiary' onClick={() => setValueChangeInside()}>ChangeInsideSide</Button>
+                        </Space>
+                        {
+                            arrayFields.map(({ field, key, remove }, i) => (
+                                <div key={key} style={{ width: 1000, display: 'flex', flexWrap: 'wrap' }} id={`data-${i}`} className='line'>
+                                    {key.slice(0, 10)}
+                                    <Space>
+                                        <Form.Input
+                                            field={`${field}.name`}
+                                            label={`${field}.name`}
+                                            style={{ width: 700 }}
+                                            id={`data-${i}-name`}
+                                        >
+                                        </Form.Input>
+                                        <Button
+                                            onClick={() => addWithInitValue({
+                                                name: arrayFields.length,
+                                                rules: [
+                                                    { desc: `${arrayFields.length}-0-desc`, type: `${arrayFields.length}-0-type` }
+                                                ]
+                                            })}
+                                            icon={<IconPlusCircle/>}
+                                            id={`data-${i}-add`} 
+                                            disabled={i !== arrayFields.length - 1}
+                                        />
+                                        <Button
+                                            type='danger'
+                                            onClick={remove}
+                                            id={`data-${i}-remove`} 
+                                            icon={<IconMinusCircle />}
+                                        />
+                                    </Space>
+                           
+                                    <ArrayField field={`${field}.rules`}>
+                                        {({ add: addNested, arrayFields: nestedArrayFields, addWithInitValue: addNestedWithInitValue }, ) => (
+                                            <React.Fragment>
+                                                {
+                                                    nestedArrayFields.map(({ field: f, key: k, remove: r }, n) => (
+                                                        <section className='rules' key={k} style={{ display: 'flex', flexWrap: 'wrap', marginLeft: 36 }}>
+                                                            <Space>
+                                                                {k.slice(0, 10)}
+                                                                <Form.Input
+                                                                    style={{ width: 300, marginRight: 12 }}
+                                                                    field={`${f}.type`}
+                                                                    label={`${f}.type`}
+                                                                    id={`data-${i}-rule-${n}-type`} 
+                                                                />
+                                                                <Form.Input
+                                                                    style={{ width: 300 }}
+                                                                    field={`${f}.desc`}
+                                                                    label={`${f}.desc`}
+                                                                    id={`data-${i}-rule-${n}-desc`} 
+                                                                />
+                                                                <Button
+                                                                    onClick={() => addNestedWithInitValue({ type: `${i}-${n+1}-type`, desc: `${i}-${n+1}-desc` })}
+                                                                    icon={<IconPlusCircle/>}
+                                                                    id={`data-${i}-rule-${n}-add`}
+                                                                    disabled={n !== nestedArrayFields.length - 1}
+                                                                >
+                                                                    add new line
+                                                                </Button>
+                                                                <Button
+                                                                    type='danger'
+                                                                    onClick={r}
+                                                                    id={`data-${i}-rule-${n}-remove`} 
+                                                                    icon={<IconMinusCircle />}
+                                                                />
+                                                            </Space>
+                                                        </section>
+                                                    ))
+                                                }
+                                            </React.Fragment>
+                                        )}
+                                    </ArrayField>
+                                </div>
+                            ))
+                        }
+                    </React.Fragment>
+                )}
+            </ArrayField>
+            <FormStateTree></FormStateTree>
+        </Form>
+    );
+};
+
+SetValues.storyName = 'ArrayField-ManualBatchSet Async';
+
+export default SetValues;

+ 158 - 0
packages/semi-ui/form/_story/ArrayField/9-initUsage.jsx

@@ -0,0 +1,158 @@
+import React, { useState, useRef } from 'react';
+import { Form, Col, Row, Button, ArrayField, Space, useFormState } from '@douyinfe/semi-ui';
+import { IconMinusCircle, IconPlusCircle } from '@douyinfe/semi-icons';
+
+import { JSONTree } from 'react-json-tree';
+
+const FormStateTree = () => {
+    const formState = useFormState();
+    return (
+        <JSONTree
+            shouldExpandNodeInitially={() => true}
+            hideRoot
+            data={formState}
+        />
+    );
+};
+
+function InitDemo() {
+    const formRef = useRef();
+    const formInitValues = {
+        data: [
+            { name: 'Semi D2C', role: 'Engineer' },
+            { name: 'Semi C2D', role: 'Designer' },
+            // { name: 'Semi DSM', role: 'Designer' },
+        ]
+    };
+
+    const arrayFieldInitValue = [
+        { name: 'Semi D2C', role: 'Engineer' },
+        // { name: 'Semi C2D', role: 'Designer' },
+    ];
+
+    return (
+        <Form ref={formRef} initValues={formInitValues}>
+            {/* <ArrayField field="data">
+                {({ add, addWithInitValue, arrayFields }) => (
+                    <React.Fragment>
+                        <Space>
+                            <Button htmlType='reset'>Reset</Button>
+                            <Button id='add' onClick={add} icon={<IconPlusCircle />} type="primary">Add</Button>
+                            <Button
+                                id='addWithInit'
+                                onClick={() => addWithInitValue({ name: `Semi New-${arrayFields.length + 1}`, role: 'Designer' })}
+                                icon={<IconPlusCircle />}
+                                type="primary">
+                                Add with row initValue
+                            </Button>
+                        </Space>
+                        {
+                            arrayFields.map(({ field, key, remove }, i) => (
+                                <div key={key} style={{ display: 'flex', width: 600 }} id={`data-${i}`} className='line'>
+                                    <Space>
+                                        <Form.Input
+                                            id={`data-${i}-name`}
+                                            field={`${field}[name]`}
+                                            label={`${field}.name`}
+                                            style={{ width: 200 }}
+                                        />
+                                        <Form.Input
+                                            id={`data-${i}-role`}
+                                            field={`${field}[role]`}
+                                            label={`${field}.role`}
+                                            style={{ width: 200 }}
+                                        />
+                                    </Space>
+                                    <Button style={{ margin: "36px 0 0 12px" }} type="danger" icon={<IconMinusCircle/>} onClick={remove}>remove this line</Button>
+                                </div>
+                            ))
+                        }
+                        <Button htmlType='reset'>Reset</Button>
+                    </React.Fragment>
+                )}
+            </ArrayField> */}
+            <ArrayField field="dataB" initValue={arrayFieldInitValue}>
+                {({ add, addWithInitValue, arrayFields }) => (
+                    <React.Fragment>
+                        <Space>
+                            <Button id='add' onClick={add} icon={<IconPlusCircle />} type="primary">Add</Button>
+                            <Button
+                                id='addWithInit'
+                                onClick={() => addWithInitValue({ name: `Semi New-${arrayFields.length + 1}`, role: 'Designer' })}
+                                icon={<IconPlusCircle />}
+                                type="primary">
+                                Add with row initValue
+                            </Button>
+                        </Space>
+                        {
+                            arrayFields.map(({ field, key, remove }, i) => (
+                                <div key={key} style={{ display: 'flex', width: 600 }} id={`dataB-${i}`} className='line'>
+                                    <Space>
+                                        <Form.Input
+                                            id={`dataB-${i}-name`}
+                                            field={`${field}[name]`}
+                                            label={`${field}.name`}
+                                            style={{ width: 200 }}
+                                        />
+                                        <Form.Input
+                                            id={`dataB-${i}-role`}
+                                            field={`${field}[role]`}
+                                            label={`${field}.role`}
+                                            style={{ width: 200 }}
+                                        />
+                                    </Space>
+                                    <Button style={{ margin: "36px 0 0 12px" }} type="danger" icon={<IconMinusCircle/>} onClick={remove}>remove this line</Button>
+                                </div>
+                            ))
+                        }
+                    </React.Fragment>
+                )}
+            </ArrayField>
+
+            {/* <ArrayField field="dataC">
+                {({ add, addWithInitValue, arrayFields }) => (
+                    <React.Fragment>
+                        <Space>
+                            <Button id='add' onClick={add} icon={<IconPlusCircle />} type="primary">Add</Button>
+                            <Button
+                                id='addWithInit'
+                                onClick={() => addWithInitValue({ name: `Semi New-${arrayFields.length + 1}`, role: 'Designer' })}
+                                icon={<IconPlusCircle />}
+                                type="primary">
+                                Add with row initValue
+                            </Button>
+                        </Space>
+                        {
+                            arrayFields.map(({ field, key, remove }, i) => (
+                                <div key={key} style={{ display: 'flex', width: 600 }} id={`dataC-${i}`} className='line'>
+                                    <Space>
+                                        <Form.Input
+                                            id={`dataC-${i}-name`}
+                                            field={`${field}[name]`}
+                                            label={`${field}.name`}
+                                            style={{ width: 200 }}
+                                            initValue={'Semi DSM'}
+                                        />
+                                        <Form.Input
+                                            id={`dataC-${i}-role`}
+                                            field={`${field}[role]`}
+                                            label={`${field}.role`}
+                                            style={{ width: 200 }}
+                                            initValue={'Designer'}
+                                        />
+                                    </Space>
+                                    <Button style={{ margin: "36px 0 0 12px" }} type="danger" icon={<IconMinusCircle/>} onClick={remove}>remove this line</Button>
+                                </div>
+                            ))
+                        }
+                    </React.Fragment>
+                )}
+            </ArrayField>       */}
+            <FormStateTree></FormStateTree>
+        </Form>
+    );
+}
+
+InitDemo.storyName = 'ArrayField-init';
+
+export default InitDemo;

+ 3 - 0
packages/semi-ui/form/_story/ArrayField/index.js

@@ -4,3 +4,6 @@ export { default as ResetDemo } from './3-resetUsage.jsx';
 export { default as ManualSetDemo } from './4-manualSyncSetUsage.jsx';
 export { default as ManualSetAsyncDemo } from './5-manualAsyncSetUsage.jsx';
 export { default as BasicNestedDemo } from './6-nestedBasicUsage.jsx';
+export { default as ManualBatchSetDemo } from './7-manualSyncBatchSetUsage.jsx';
+export { default as ManualBatchSetAsyncDemo } from './8-manualAsyncBatchSetUsage.jsx';
+export { default as InitDemo } from './9-initUsage.jsx';

+ 0 - 0
packages/semi-ui/form/_story/ArrayField/initUsage.jsx


+ 16 - 29
packages/semi-ui/form/arrayField.tsx

@@ -50,22 +50,14 @@ const initValueAdapter = (initValue: any) => {
  * @param {any[]} cacheValue
  * @returns string[]
  * 
- * 2 -> 3,理想情况,看加的哪个,复用2个
- * 3 -> 2, 理想情况,看删的哪个,减少,
- * 2 -> 2, 看值变不变
- *   嵌套场景,外部变不变无所谓,里面尽量不变,否则会走卸载。除非判断出嵌套时候
- *   外部 setValue、setValues 变更,不易判断改的是哪行
- *    如果只有自己一层,那都变问题也不大。如果有两层以上,变就会导致里面一层卸载再挂载。
  */
 
 const generateKeys = (value: any[] = [], oldKeys?: string[], cacheValues: any[] = []) => {
     const val = initValueAdapter(value);
     const newKeys = getUuidByArray(val);
     // const keys = newKeys.map((key, i) => (oldKeys && oldKeys[i] ? oldKeys[i] : key));
-
     const keys = [];
 
-    // Keys 的复用机制需要
     value.forEach((newRow, i) => {
         const cacheRow = get(cacheValues, i);
         if (!isEqual(newRow, cacheRow)) {
@@ -106,10 +98,10 @@ class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
         // // whether the fields inside arrayField should use props.initValue in current render process
 
         // Separate the arrays that reset and the usual add and remove modify, otherwise they will affect each other
-        const initValueCopyForFormState = cloneDeep(initValue);
-        const initValueCopyForReset = cloneDeep(initValue);
-        context.registerArrayField(field, { initValue: initValueCopyForReset, forceUpdate: this.forceUpdate });
-        context.updateStateValue(field, initValueCopyForFormState, { notNotify: true, notUpdate: true });
+        // const initValueCopyForFormState = cloneDeep(initValue);
+        // const initValueCopyForReset = cloneDeep(initValue);
+        // context.registerArrayField(field, { initValue: initValueCopyForReset, forceUpdate: this.forceUpdate });
+        // context.updateStateValue(field, initValueCopyForFormState, { notNotify: true, notUpdate: true });
     }
 
     componentDidMount() {
@@ -127,27 +119,27 @@ class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
         const initValueCopyForFormState = cloneDeep(initValue);
         const initValueCopyForReset = cloneDeep(initValue);
 
-        // 如果首次挂载,应该使用初始值,如果不是首次挂载,例如嵌套场景下,level 1 keys变更导致的 level 2子级重新挂载,那应该直接使用formState 中的值 , 且无需注册 initValue(否则会影响保存的 initValue)
+        // 如果首次挂载,应该使用初始值,如果不是首次挂载,例如嵌套场景下,level 1 keys变更导致的 level 2子级重新挂载,那应该直接使用formState 中的值, 且无需注册 initValue
         if (!this.hasMounted) {
             updater.registerArrayField(field, { initValue: initValueCopyForReset, forceUpdate: this.forceKeysUpdate });
             updater.updateStateValue(field, initValueCopyForFormState, { notNotify: true, notUpdate: true });
         } else {
-            if (field === 'data[0].rules') {
-                const a = updater.getArrayField();
-                console.log('data[0].rules didmount');
-            }
             updater.registerArrayField(field, { forceUpdate: this.forceKeysUpdate });
         }
-        console.log('didMount', this.props.field);
     }
 
     componentWillUnmount() {
         const updater = this.context;
         const { field } = this.props;
-        // 卸载时,不卸载 initValue?
-        // 嵌套的不做卸载?
-        updater.unRegisterArrayField(field);
-        console.log('unmount', this.props.field);
+        const hasParentArrayField = updater.getParentArrayField(field);
+        const size = updater.getParentArrayField(field).size;
+        if (Boolean(size)) {
+            // if is parant arraField, need to unregister nested arrayField here
+            // 嵌套的ArrayField在父级仍存在时,不需要走卸载, 统一在父级ArrayField做卸载
+            // console.log('嵌套的ArrayField在父级仍存在时,不需要自己走卸载, 统一在父级ArrayField做卸载', field);
+        } else {
+            updater.unRegisterArrayField(field);
+        }
     }
 
     forceKeysUpdate = ({ newValue, oldValue }): void => {
@@ -157,13 +149,9 @@ class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
 
         const fieldValues = newValue ? newValue : updater.getValue(field);
         const newKeys = generateKeys(fieldValues, keys, oldValue);
-        // const newKeys = generateKeys(fieldValues, keys, this.cacheFieldValues);
 
         // eslint-disable-next-line
-        this.setState({ keys: newKeys });
-        // A: A1、A2   ->  A: A1-new  -> A: A1、A2
-        // 如果 forecUpdate child,改变了 child 的length,但是这个无法更新父级的 cacheFieldValue。会有问题, cache不是一个好的设计,要做完善还得在 setValue时检测潜在的对父子 cacheValue的影响
-        // this.cacheFieldValues = cloneDeep(value);
+        this.setState({ keys: newKeys })
     }
 
     add() {
@@ -172,6 +160,7 @@ class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
         // this.shouldUseInitValue = true;
         // TODO allowEmpty 为 false 的情况下
         this.setState({ keys });
+
     }
 
     addWithInitValue(lineObject: Record<string, any>) {
@@ -208,7 +197,6 @@ class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
             updater.updateStateValue(field, newArrayFieldValue);
         }
         this.setState({ keys: newKeys });
-        // this.cacheFieldValues = cloneDeep(newArrayFieldValue);
     }
 
     render() {
@@ -223,7 +211,6 @@ class ArrayFieldComponent extends Component<ArrayFieldProps, ArrayFieldState> {
         const { addWithInitValue } = this;
         const contextVal = {
             isInArrayField: true,
-            // shouldUseInitValue: this.shouldUseInitValue,
         };
         return (
             <ArrayFieldContext.Provider value={contextVal}>

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

@@ -109,10 +109,11 @@ function withField<
         try {
             arrayFieldState = useArrayFieldState();
             if (arrayFieldState) {
-                initVal =
-                    arrayFieldState.shouldUseInitValue && typeof initValue !== 'undefined'
-                        ? initValue
-                        : initValueInFormOpts;
+                // initVal =
+                //     arrayFieldState.shouldUseInitValue && typeof initValue !== 'undefined'
+                //         ? initValue
+                //         : initValueInFormOpts;
+                initVal = typeof initValue !== 'undefined' ? initValue : initValueInFormOpts;
             }
         } catch (err) {}