Repository: erikras/redux-form (js)

Issue:
asyncValidate not called when field empty (even when field has been touched)
I'm not sure if this behaviour is by design or not, but I have found that if I have a form field which is async validated, the validation function is not called when the field is empty - even if it has previously been touched/edited. This means that I can't perform "required string" validation async, as far as I can tell.

Steps to reproduce:
1. Create a redux-form with one field, with an `asyncValidate` function and the field in `asyncFields`
2. Enter some text in the field and blur it - the validation function will be called
3. Clear the text in the field and blur it - the validation function will not be called

I would expect the function to be called in 3 as I have previously touched the field, and I might for example want to check if a required value is present.

A tiny bit of debugging shows that at 3, `allPristine` in https://github.com/erikras/redux-form/blob/a53f22b816d72dca726d7a220b0e1428dbe522f8/src/createHigherOrderComponent.js#L72 is `true` - perhaps I've misunderstood what pristine refers to in this context, but I think perhaps `allPristine` should be false, as this field has been touched.

Any thoughts? At the minute this prevents me from using async validation for the form I am working on (trying to use https://github.com/jquense/yup to validate against a schema, but it only works with callbacks/promises, not synchronously)

Thanks!
Tom

Pull request description:
suppress async validation when pristine and initialized. fixes #614
Won't call async validation _on blur_ if your form is pristine and initialized. #614

Patch:
diff --git a/src/createHigherOrderComponent.js b/src/createHigherOrderComponent.js
index 33e78384..91bb4612 100644
--- a/src/createHigherOrderComponent.js
+++ b/src/createHigherOrderComponent.js
@@ -44,7 +44,7 @@ const createHigherOrderComponent = (config,
       }
 
       componentWillReceiveProps(nextProps) {
-        if (!deepEqual(this.props.fields, nextProps.fields) || !deepEqual(this.props.form, nextProps.form, { strict: true })) {
+        if (!deepEqual(this.props.fields, nextProps.fields) || !deepEqual(this.props.form, nextProps.form, {strict: true})) {
           this.fields = readFields(nextProps, this.props, this.fields, this.asyncValidate, isReactNative);
         }
         if (!deepEqual(this.props.initialValues, nextProps.initialValues)) {
@@ -60,16 +60,20 @@ const createHigherOrderComponent = (config,
 
       asyncValidate(name, value) {
         const {asyncValidate, dispatch, fields, form, startAsyncValidation, stopAsyncValidation, validate} = this.props;
+        const isSubmitting = !name;
         if (asyncValidate) {
           const values = getValues(fields, form);
           if (name) {
             values[name] = value;
           }
           const syncErrors = validate(values, this.props);
-          const { allPristine } = this.fields._meta;
+          const {allPristine} = this.fields._meta;
+          const initialized = form._initialized;
 
-          // if blur validating, only run async validate if the form is dirty and sync validation passes
-          if (!allPristine && (!name || isValid(syncErrors[name]))) {
+          // if blur validating, only run async validate if sync validation passes
+          // and submitting (not blur validation) or form is dirty or form was never initialized
+          const syncValidationPasses = isSubmitting || isValid(syncErrors[name]);
+          if (syncValidationPasses && (isSubmitting || !allPristine || !initialized)) {
             return asyncValidation(() =>
               asyncValidate(values, dispatch, this.props), startAsyncValidation, stopAsyncValidation, name);
           }
diff --git a/src/initializeState.js b/src/initializeState.js
index 14657e8b..7d56bbe7 100644
--- a/src/initializeState.js
+++ b/src/initializeState.js
@@ -21,7 +21,7 @@ const initializeState = (values, fields, state = {}) => {
     }
     const openIndex = path.indexOf('[');
     const closeIndex = path.indexOf(']');
-    const result = {...dest} || {};
+    const result = dest || {};
     if (dotIndex >= 0 && (openIndex < 0 || dotIndex < openIndex)) {
       // is dot notation
       const key = path.substring(0, dotIndex);


Test patch:
diff --git a/src/__tests__/createReduxForm.spec.js b/src/__tests__/createReduxForm.spec.js
index e133aed7..b2bbe492 100644
--- a/src/__tests__/createReduxForm.spec.js
+++ b/src/__tests__/createReduxForm.spec.js
@@ -970,6 +970,120 @@ describe('createReduxForm', () => {
     TestUtils.Simulate.submit(button);
   });
 
+  it('should NOT call async validation if form is pristine and initialized', () => {
+    const store = makeStore();
+    const form = 'testForm';
+    const errorValue = {foo: 'no bears allowed'};
+    const asyncValidate = createSpy().andReturn(Promise.reject(errorValue));
+    const Decorated = reduxForm({
+      form,
+      fields: ['foo', 'bar'],
+      asyncValidate,
+      asyncBlurFields: ['foo'],
+      initialValues: {
+        foo: 'dog',
+        bar: 'cat'
+      }
+    })(Form);
+    const dom = TestUtils.renderIntoDocument(
+      <Provider store={store}>
+        <Decorated/>
+      </Provider>
+    );
+    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
+
+    stub.props.fields.foo.onBlur('dog');
+    expect(asyncValidate).toNotHaveBeenCalled();
+  });
+
+  it('should call async validation if form is dirty and initialized', () => {
+    const store = makeStore();
+    const form = 'testForm';
+    const errorValue = {foo: 'no bears allowed'};
+    const asyncValidate = createSpy().andReturn(Promise.reject(errorValue));
+    const Decorated = reduxForm({
+      form,
+      fields: ['foo', 'bar'],
+      asyncValidate,
+      asyncBlurFields: ['foo'],
+      initialValues: {
+        foo: 'dog',
+        bar: 'cat'
+      }
+    })(Form);
+    const dom = TestUtils.renderIntoDocument(
+      <Provider store={store}>
+        <Decorated/>
+      </Provider>
+    );
+    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
+
+    stub.props.fields.foo.onBlur('bear');
+    expect(asyncValidate).toHaveBeenCalled();
+  });
+
+  it('should call async validation if form is pristine and NOT initialized', () => {
+    const store = makeStore();
+    const form = 'testForm';
+    const errorValue = {foo: 'no bears allowed'};
+    const asyncValidate = createSpy().andReturn(Promise.reject(errorValue));
+    const Decorated = reduxForm({
+      form,
+      fields: ['foo', 'bar'],
+      asyncValidate,
+      asyncBlurFields: ['foo']
+    })(Form);
+    const dom = TestUtils.renderIntoDocument(
+      <Provider store={store}>
+        <Decorated/>
+      </Provider>
+    );
+    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
+
+    stub.props.fields.foo.onBlur();
+    expect(asyncValidate).toHaveBeenCalled();
+  });
+
+  it('should call async validation on submit even if pristine and initialized', () => {
+    const submit = createSpy();
+    class FormComponent extends Component {
+      render() {
+        return (
+          <form onSubmit={this.props.handleSubmit(submit)}/>
+        );
+      }
+    }
+    FormComponent.propTypes = {
+      handleSubmit: PropTypes.func.isRequired
+    };
+
+    const store = makeStore();
+    const form = 'testForm';
+    const errorValue = {foo: 'no dogs allowed'};
+    const asyncValidate = createSpy().andReturn(Promise.reject(errorValue));
+    const Decorated = reduxForm({
+      form,
+      fields: ['foo', 'bar'],
+      asyncValidate,
+      asyncBlurFields: ['foo'],
+      initialValues: {
+        foo: 'dog',
+        bar: 'cat'
+      }
+    })(FormComponent);
+    const dom = TestUtils.renderIntoDocument(
+      <Provider store={store}>
+        <Decorated/>
+      </Provider>
+    );
+    const button = TestUtils.findRenderedDOMComponentWithTag(dom, 'form');
+
+    TestUtils.Simulate.submit(button);
+
+    expect(asyncValidate).toHaveBeenCalled();
+    expect(submit).toNotHaveBeenCalled();
+  });
+
   it('should call submit function passed to handleSubmit', (done) => {
     const submit = (values) => {
       expect(values).toEqual({
@@ -1685,40 +1799,6 @@ describe('createReduxForm', () => {
     });
   });
 
-  it('should disallow async validation when form is pristine', () => {
-    const store = makeStore();
-    const form = 'testForm';
-    const asyncValidate = createSpy(() => Promise.resolve());
-    const Decorated = reduxForm({
-      form,
-      fields: ['name'],
-      initialValues: {name: 'Tom'},
-      asyncValidate
-    })(Form);
-    const dom = TestUtils.renderIntoDocument(
-      <Provider store={store}>
-        <Decorated/>
-      </Provider>
-    );
-    const stub = TestUtils.findRenderedComponentWithType(dom, Form);
-
-    // check field before validation
-    expectField({
-      field: stub.props.fields.name,
-      name: 'name',
-      value: 'Tom',
-      initial: 'Tom',
-      valid: true,
-      dirty: false,
-      error: undefined,
-      touched: false,
-      visited: false
-    });
-
-    stub.props.asyncValidate();
-    expect(asyncValidate).toNotHaveBeenCalled();
-  });
-
   it('should allow deep async validation error values', () => {
     const store = makeStore();
     const form = 'testForm';