Skip to content

Commit

Permalink
Support async validation on change (#3531)
Browse files Browse the repository at this point in the history
* Support async validation on change

* Fix flow issues
  • Loading branch information
ericbiewener authored and erikras committed Nov 20, 2017
1 parent 40c58c5 commit 81169ce
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 10 deletions.
7 changes: 6 additions & 1 deletion src/ConnectedField.js
Expand Up @@ -127,6 +127,11 @@ const createConnectedField = (structure: Structure<*, *>) => {
if (!defaultPrevented) {
// dispatch change action
dispatch(_reduxForm.change(name, newValue))

// call post-change callback
if (_reduxForm.asyncValidate) {
_reduxForm.asyncValidate(name, newValue, 'change')
}
}
}

Expand Down Expand Up @@ -197,7 +202,7 @@ const createConnectedField = (structure: Structure<*, *>) => {

// call post-blur callback
if (_reduxForm.asyncValidate) {
_reduxForm.asyncValidate(name, newValue)
_reduxForm.asyncValidate(name, newValue, 'blur')
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/ConnectedFields.js
Expand Up @@ -97,6 +97,11 @@ const createConnectedFields = (structure: Structure<*, *>) => {
const value = onChangeValue(event, { name, parse })

dispatch(_reduxForm.change(name, value))

// call post-change callback
if (_reduxForm.asyncValidate) {
_reduxForm.asyncValidate(name, value, 'change')
}
}

handleFocus = (name: string): void => {
Expand All @@ -113,7 +118,7 @@ const createConnectedFields = (structure: Structure<*, *>) => {

// call post-blur callback
if (_reduxForm.asyncValidate) {
_reduxForm.asyncValidate(name, value)
_reduxForm.asyncValidate(name, value, 'blur')
}
}

Expand Down
88 changes: 87 additions & 1 deletion src/__tests__/reduxForm.spec.js
Expand Up @@ -3502,7 +3502,8 @@ const describeReduxForm = (name, structure, combineReducers, setup) => {
const Decorated = reduxForm({
form: 'testForm',
asyncValidate,
asyncBlurFields: ['deep.foo']
asyncBlurFields: ['deep.foo'],
asyncChangeFields: [],
})(Form)

const dom = TestUtils.renderIntoDocument(
Expand Down Expand Up @@ -3604,6 +3605,90 @@ const describeReduxForm = (name, structure, combineReducers, setup) => {
})
})

it('should call async on change of async change field', () => {
const store = makeStore({})
const inputRender = jest.fn(props => <input {...props.input} />)
const formRender = jest.fn()
const asyncErrors = {
deep: {
foo: 'async error'
}
}
const asyncValidate = jest
.fn()
.mockImplementation(() => Promise.reject(asyncErrors))

class Form extends Component {
render() {
formRender(this.props)
return (
<form>
<Field name="deep.foo" component={inputRender} type="text" />
</form>
)
}
}
const Decorated = reduxForm({
form: 'testForm',
asyncValidate,
asyncBlurFields: [],
asyncChangeFields: ['deep.foo'],
})(Form)

const dom = TestUtils.renderIntoDocument(
<Provider store={store}>
<Decorated />
</Provider>
)

const inputElement = TestUtils.findRenderedDOMComponentWithTag(
dom,
'input'
)

TestUtils.Simulate.change(inputElement, { target: { value: 'bar' } })

setTimeout(() => {
expect(store.getState()).toEqualMap({
form: {
testForm: {
anyTouched: true,
values: {
deep: {
foo: 'bar'
}
},
fields: {
deep: {
foo: {
touched: true
}
}
},
registeredFields: {
'deep.foo': { name: 'deep.foo', type: 'Field', count: 1 }
},
asyncErrors
}
}
})
// rerender form twice because of async validation start and again for valid -> invalid
expect(formRender.calls.length).toBe(4)

expect(asyncValidate).toHaveBeenCalled()
expect(propsAtNthRender(asyncValidate, 0)).toEqualMap({
deep: { foo: 'bar' }
})

// input rerendered twice, at start and end of async validation
expect(inputRender.calls.length).toBe(4)
expect(propsAtNthRender(inputRender, 3).meta.pristine).toBe(false)
expect(propsAtNthRender(inputRender, 3).input.value).toBe('bar')
expect(propsAtNthRender(inputRender, 3).meta.valid).toBe(false)
expect(propsAtNthRender(inputRender, 3).meta.error).toBe('async error')
})
})

it('should call form-level onChange when values change', () => {
const store = makeStore({})
const renderFoo = jest.fn(props => <input {...props.input} />)
Expand Down Expand Up @@ -4280,6 +4365,7 @@ const describeReduxForm = (name, structure, combineReducers, setup) => {
form: 'testForm',
asyncValidate,
asyncBlurFields: ['foo'],
asyncChangeFields: [],
shouldAsyncValidate
})(Form)

Expand Down
17 changes: 11 additions & 6 deletions src/createReduxForm.js
Expand Up @@ -204,6 +204,7 @@ export type Props = {
arraySwap: ArraySwapAction,
arrayUnshift: ArrayUnshiftAction,
asyncBlurFields?: string[],
asyncChangeFields?: string[],
asyncErrors?: any,
asyncValidate: AsyncValidateFunction,
asyncValidating: boolean,
Expand Down Expand Up @@ -648,9 +649,10 @@ const createReduxForm = (structure: Structure<*, *>) => {
: undefined
}

asyncValidate = (name: string, value: any) => {
asyncValidate = (name: string, value: any, trigger: 'blur' | 'change') => {
const {
asyncBlurFields,
asyncChangeFields,
asyncErrors,
asyncValidate,
dispatch,
Expand All @@ -668,16 +670,19 @@ const createReduxForm = (structure: Structure<*, *>) => {
? values
: setIn(values, name, value)
const syncValidationPasses = submitting || !getIn(syncErrors, name)
const isBlurredField =
const fieldNeedsValidation =
!submitting &&
(!asyncBlurFields ||
~asyncBlurFields.indexOf(name.replace(/\[[0-9]+\]/g, '[]')))
trigger === 'blur'
? (!asyncBlurFields ||
~asyncBlurFields.indexOf(name.replace(/\[[0-9]+\]/g, '[]')))
: (!asyncChangeFields ||
~asyncChangeFields.indexOf(name.replace(/\[[0-9]+\]/g, '[]')))
if (
(isBlurredField || submitting) &&
(fieldNeedsValidation || submitting) &&
shouldAsyncValidate({
asyncErrors,
initialized,
trigger: submitting ? 'submit' : 'blur',
trigger: submitting ? 'submit' : trigger,
blurredField: name,
pristine,
syncValidationPasses
Expand Down
1 change: 1 addition & 0 deletions src/defaultShouldAsyncValidate.js
Expand Up @@ -19,6 +19,7 @@ const defaultShouldAsyncValidate = ({
}
switch (trigger) {
case 'blur':
case 'change':
// blurring
return true
case 'submit':
Expand Down
2 changes: 1 addition & 1 deletion src/types.js.flow
Expand Up @@ -65,7 +65,7 @@ export type Event = {
export type Context = {
form: string,
getFormState: GetFormState,
asyncValidate: { (name: ?string, value: ?any): Promise<*> },
asyncValidate: { (name: ?string, value: ?any, trigger: 'blur' | 'change'): Promise<*> },
getValues: { (): Object },
sectionPrefix?: string,
register: (
Expand Down

0 comments on commit 81169ce

Please sign in to comment.