Repository: erikras/redux-form
Pull request #617 (2016-02-04 02:41:52)

Potential fix for #468
Potential fix for #468 

It forces the user to specify the shape of the value being passed into `addField`. But at least it allows me to use redux-form on my project!


Patch:
diff --git a/src/actions.js b/src/actions.js
index e85c60d0..e6f8e09f 100644
--- a/src/actions.js
+++ b/src/actions.js
@@ -1,8 +1,8 @@
 import { ADD_ARRAY_VALUE, BLUR, CHANGE, DESTROY, FOCUS, INITIALIZE, REMOVE_ARRAY_VALUE, RESET, START_ASYNC_VALIDATION,
   START_SUBMIT, STOP_ASYNC_VALIDATION, STOP_SUBMIT, SUBMIT_FAILED, SWAP_ARRAY_VALUES, TOUCH, UNTOUCH } from './actionTypes';
 
-export const addArrayValue = (path, value, index) =>
-  ({type: ADD_ARRAY_VALUE, path, value, index});
+export const addArrayValue = (path, value, index, fields) =>
+  ({type: ADD_ARRAY_VALUE, path, value, index, fields});
 
 export const blur = (field, value) =>
   ({type: BLUR, field, value});
diff --git a/src/readField.js b/src/readField.js
index f47eedab..47fe1293 100644
--- a/src/readField.js
+++ b/src/readField.js
@@ -7,7 +7,15 @@ import silencePromise from './silencePromise';
 import read from './read';
 import updateField from './updateField';
 
-const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncValidate, isReactNative, props, callback = () => null) => {
+function getSuffix(input, closeIndex) {
+  let suffix = input.substring(closeIndex + 1);
+  if (suffix[0] === '.') {
+    suffix = suffix.substring(1);
+  }
+  return suffix;
+}
+
+const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncValidate, isReactNative, props, callback = () => null, prefix = '') => {
   const {asyncBlurFields, blur, change, focus, form, initialValues, readonly, addArrayValue,
     removeArrayValue, swapArrayValues} = props;
   const dotIndex = fieldName.indexOf('.');
@@ -19,15 +27,21 @@ const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncV
   if (openIndex > 0 && (dotIndex < 0 || openIndex < dotIndex)) {
     // array field
     const key = fieldName.substring(0, openIndex);
-    let rest = fieldName.substring(closeIndex + 1);
-    if (rest[0] === '.') {
-      rest = rest.substring(1);
-    }
+    const rest = getSuffix(fieldName, closeIndex);
     const stateArray = state && state[key] || [];
+    const fullPrefix = prefix + fieldName.substring(0, closeIndex + 1);
+    const subfields = props.fields
+      .reduce((accumulator, field) => {
+        if (field.indexOf(fullPrefix) === 0) {
+          accumulator.push(field);
+        }
+        return accumulator;
+      }, [])
+      .map(field => getSuffix(field, prefix.length + closeIndex));
     if (!fields[key]) {
       fields[key] = [];
       Object.defineProperty(fields[key], 'addField', {
-        value: (value, index) => addArrayValue(pathToHere + key, value, index)
+        value: (value, index) => addArrayValue(pathToHere + key, value, index, subfields)
       });
       Object.defineProperty(fields[key], 'removeField', {
         value: index => removeArrayValue(pathToHere + key, index)
@@ -42,8 +56,10 @@ const readField = (state, fieldName, pathToHere = '', fields, syncErrors, asyncV
         fieldArray[index] = {};
       }
       const dest = rest ? fieldArray[index] : {};
-      const result = readField(fieldState, rest, `${pathToHere}${key}[${index}]${rest ? '.' : ''}`, dest, syncErrors,
-        asyncValidate, isReactNative, props, callback);
+      const nextPath = `${pathToHere}${key}[${index}]${rest ? '.' : ''}`;
+      const nextPrefix = `${prefix}${key}[]${rest ? '.' : ''}`;
+      const result = readField(fieldState, rest, nextPath, dest, syncErrors,
+        asyncValidate, isReactNative, props, callback, nextPrefix);
       if (!rest) { // if nothing after [] in field name, assign directly to array
         fieldArray[index] = result;
       }
diff --git a/src/reducer.js b/src/reducer.js
index b3f56799..b13f584b 100644
--- a/src/reducer.js
+++ b/src/reducer.js
@@ -21,12 +21,12 @@ export const initialState = {
 };
 
 const behaviors = {
-  [ADD_ARRAY_VALUE](state, {path, index, value}) {
+  [ADD_ARRAY_VALUE](state, {path, index, value, fields}) {
     const array = read(path, state);
     const stateCopy = {...state};
     const arrayCopy = array ? [...array] : [];
     const newValue = value !== null && typeof value === 'object' ?
-      initializeState(value, Object.keys(value)) : makeFieldValue({value});
+      initializeState(value, fields || Object.keys(value)) : makeFieldValue({value});
     if (index === undefined) {
       arrayCopy.push(newValue);
     } else {


Test patch:
diff --git a/src/__tests__/actions.spec.js b/src/__tests__/actions.spec.js
index 1b7e6baa..960485ee 100644
--- a/src/__tests__/actions.spec.js
+++ b/src/__tests__/actions.spec.js
@@ -10,19 +10,29 @@ describe('actions', () => {
       type: ADD_ARRAY_VALUE,
       path: 'foo',
       index: 1,
-      value: undefined
+      value: undefined,
+      fields: undefined
     });
     expect(addArrayValue('bar.baz')).toEqual({
       type: ADD_ARRAY_VALUE,
       path: 'bar.baz',
       index: undefined,
-      value: undefined
+      value: undefined,
+      fields: undefined
     });
     expect(addArrayValue('bar.baz', 'foo', 2)).toEqual({
       type: ADD_ARRAY_VALUE,
       path: 'bar.baz',
       index: 2,
-      value: 'foo'
+      value: 'foo',
+      fields: undefined
+    });
+    expect(addArrayValue('bar.baz', 'foo', 2, ['x', 'y'])).toEqual({
+      type: ADD_ARRAY_VALUE,
+      path: 'bar.baz',
+      index: 2,
+      value: 'foo',
+      fields: [ 'x', 'y' ]
     });
   });
 
diff --git a/src/__tests__/createReduxForm.spec.js b/src/__tests__/createReduxForm.spec.js
index a4fae301..6e7cf613 100644
--- a/src/__tests__/createReduxForm.spec.js
+++ b/src/__tests__/createReduxForm.spec.js
@@ -1235,114 +1235,153 @@ describe('createReduxForm', () => {
   });
 
   // Test to demonstrate bug: https://github.com/erikras/redux-form/issues/468
-  //it('should add array values with DEEP defaults', () => {
-  //  const store = makeStore();
-  //  const form = 'testForm';
-  //  const Decorated = reduxForm({
-  //    form,
-  //    fields: [
-  //      'proposals[].arrival',
-  //      'proposals[].departure',
-  //      'proposals[].note',
-  //      'proposals[].rooms[].name',
-  //      'proposals[].rooms[].adults',
-  //      'proposals[].rooms[].children'
-  //    ]
-  //  })(Form);
-  //  const dom = TestUtils.renderIntoDocument(
-  //    <Provider store={store}>
-  //      <Decorated/>
-  //    </Provider>
-  //  );
-  //  const stub = TestUtils.findRenderedComponentWithType(dom, Form);
-  //
-  //  expect(stub.props.fields.proposals).toBeA('array');
-  //  expect(stub.props.fields.proposals.length).toBe(0);
-  //  expect(stub.props.fields.proposals.addField).toBeA('function');
-  //
-  //  // add field
-  //  const today = new Date();
-  //  stub.props.fields.proposals.addField({
-  //    arrival: today,
-  //    departure: today,
-  //    note: '',
-  //    rooms: [{
-  //      name: 'Room 1',
-  //      adults: 2,
-  //      children: 0
-  //    }]
-  //  });
-  //
-  //  // check field
-  //  expect(stub.props.fields.proposals.length).toBe(1);
-  //  expect(stub.props.fields.proposals[0]).toBeA('object');
-  //  expectField({
-  //    field: stub.props.fields.proposals[0].arrival,
-  //    name: 'proposals[0].arrival',
-  //    value: today,
-  //    initial: today,
-  //    valid: true,
-  //    dirty: false,
-  //    error: undefined,
-  //    touched: false,
-  //    visited: false
-  //  });
-  //  expectField({
-  //    field: stub.props.fields.proposals[0].departure,
-  //    name: 'proposals[0].departure',
-  //    value: today,
-  //    initial: today,
-  //    valid: true,
-  //    dirty: false,
-  //    error: undefined,
-  //    touched: false,
-  //    visited: false
-  //  });
-  //  expectField({
-  //    field: stub.props.fields.proposals[0].note,
-  //    name: 'proposals[0].note',
-  //    value: '',
-  //    initial: '',
-  //    valid: true,
-  //    dirty: false,
-  //    error: undefined,
-  //    touched: false,
-  //    visited: false
-  //  });
-  //  expectField({
-  //    field: stub.props.fields.proposals[0].rooms[0].name,
-  //    name: 'proposals[0].rooms[0].name',
-  //    value: 'Room 1',
-  //    initial: 'Room 1',
-  //    valid: true,
-  //    dirty: false,
-  //    error: undefined,
-  //    touched: false,
-  //    visited: false
-  //  });
-  //  expectField({
-  //    field: stub.props.fields.proposals[0].rooms[0].adults,
-  //    name: 'proposals[0].rooms[0].adults',
-  //    value: 2,
-  //    initial: 2,
-  //    valid: true,
-  //    dirty: false,
-  //    error: undefined,
-  //    touched: false,
-  //    visited: false
-  //  });
-  //  expectField({
-  //    field: stub.props.fields.proposals[0].rooms[0].children,
-  //    name: 'proposals[0].rooms[0].children',
-  //    value: 0,
-  //    initial: 0,
-  //    valid: true,
-  //    dirty: false,
-  //    error: undefined,
-  //    touched: false,
-  //    visited: false
-  //  });
-  //});
+  it('should add array values with DEEP defaults', () => {
+    const store = makeStore();
+    const form = 'testForm';
+    const Decorated = reduxForm({
+      form,
+      fields: [
+        'proposals[].arrival',
+        'proposals[].departure',
+        'proposals[].note',
+        'proposals[].rooms[].name',
+        'proposals[].rooms[].adults',
+        'proposals[].rooms[].children'
+      ]
+    })(Form);
+    const dom = TestUtils.renderIntoDocument(
+      <Provider store={store}>
+        <Decorated/>
+      </Provider>
+    );
+    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
+
+    expect(stub.props.fields.proposals).toBeA('array');
+    expect(stub.props.fields.proposals.length).toBe(0);
+    expect(stub.props.fields.proposals.addField).toBeA('function');
+
+    // add field
+    const today = new Date();
+    stub.props.fields.proposals.addField({
+      arrival: today,
+      departure: today,
+      note: '',
+      rooms: [{
+        name: 'Room 1',
+        adults: 2,
+        children: 0
+      }]
+    });
+
+    stub.props.fields.proposals[0].rooms.addField({
+      name: 'Room 2',
+      adults: 0,
+      children: 2
+    });
+
+    // check field
+    expect(stub.props.fields.proposals.length).toBe(1);
+    expect(stub.props.fields.proposals[0]).toBeA('object');
+    expectField({
+      field: stub.props.fields.proposals[0].arrival,
+      name: 'proposals[0].arrival',
+      value: today,
+      initial: today,
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].departure,
+      name: 'proposals[0].departure',
+      value: today,
+      initial: today,
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].note,
+      name: 'proposals[0].note',
+      value: '',
+      initial: '',
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].rooms[0].name,
+      name: 'proposals[0].rooms[0].name',
+      value: 'Room 1',
+      initial: 'Room 1',
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].rooms[0].adults,
+      name: 'proposals[0].rooms[0].adults',
+      value: 2,
+      initial: 2,
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].rooms[0].children,
+      name: 'proposals[0].rooms[0].children',
+      value: 0,
+      initial: 0,
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].rooms[1].name,
+      name: 'proposals[0].rooms[1].name',
+      value: 'Room 2',
+      initial: 'Room 2',
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].rooms[1].adults,
+      name: 'proposals[0].rooms[1].adults',
+      value: 0,
+      initial: 0,
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+    expectField({
+      field: stub.props.fields.proposals[0].rooms[1].children,
+      name: 'proposals[0].rooms[1].children',
+      value: 2,
+      initial: 2,
+      valid: true,
+      dirty: false,
+      error: undefined,
+      touched: false,
+      visited: false
+    });
+  });
 
   it('should initialize an array field, blowing away existing value', () => {
     const store = makeStore();
diff --git a/src/__tests__/readField.spec.js b/src/__tests__/readField.spec.js
index 57f2ccb5..c28be06c 100644
--- a/src/__tests__/readField.spec.js
+++ b/src/__tests__/readField.spec.js
@@ -21,7 +21,8 @@ describe('readField', () => {
     initialValues: {},
     readonly: false,
     addArrayValue: noop,
-    removeArrayValue: noop
+    removeArrayValue: noop,
+    fields: []
   };
 
   const expectField = ({field, name, value, dirty, touched, visited, error, initialValue, readonly, checked}) => {
@@ -591,7 +592,38 @@ describe('readField', () => {
     fields.foo.addField('rabbit');
     expect(spy)
       .toHaveBeenCalled()
-      .toHaveBeenCalledWith('foo', 'rabbit', undefined);
+      .toHaveBeenCalledWith('foo', 'rabbit', undefined, []);
+  });
+
+  it('should allow an array field to add a deeply nested value', () => {
+    const spy = createSpy();
+    const fields = {};
+    readField({
+      foo: [
+        {
+          bar: [
+            { baz: 'foo[0].bar[0].baz' },
+            { baz: 'foo[0].bar[1].baz' }
+          ]
+        },
+        {
+          bar: [
+            { baz: 'foo[1].bar[0].baz' },
+            { baz: 'foo[1].bar[1].baz' }
+          ]
+        }
+      ]
+    }, 'foo[]', undefined, fields, {}, undefined, false, {
+      ...defaultProps,
+      addArrayValue: spy,
+      fields: [
+        'foo[].bar[].baz'
+      ]
+    });
+    fields.foo.addField('rabbit');
+    expect(spy)
+      .toHaveBeenCalled()
+      .toHaveBeenCalledWith('foo', 'rabbit', undefined, ['bar[].baz']);
   });
 
   it('should allow an array field to remove a value', () => {