Skip to content

Commit 7d7b9e6

Browse files
committed
further testing and implementation details
1 parent cbb0293 commit 7d7b9e6

17 files changed

+214
-107
lines changed

README.md

Lines changed: 72 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#redux-form
1+
# redux-form
22
---
33
[<img src="http://npm.packagequality.com/badge/redux-form.png" align="right"/>](http://packagequality.com/#?package=redux-form)
44

@@ -103,17 +103,17 @@ const reducer = combineReducers(reducers);
103103
const store = createStore(reducer);
104104
```
105105

106-
*NOTE*If you are not [doing the `connect()`ing yourself](#doing-the-connecting-yourself) (and it is recommended that
107-
you do not, unless you have an advanced use case that requires it), _you **must** mount the reducer at `form`_.
106+
*NOTE*You really should mount your `redux-form` reducer at `form`, but if you absolutely must mount it somewhere
107+
else, you may specify a `reduxMountPoint` parameter to the `reduxForm()` decorator. See below.
108108

109-
__STEP 2:__ Wrap your form component with `connectReduxForm()`. `connectReduxForm()` wraps your form component in a
110-
Higher Order Component that connects to the Redux store and provides functions, as props to your component, for your
111-
form elements to use for sending `onChange` and `onBlur` events, as well as a function to handle synchronous
112-
validation `onSubmit`. Let's look at a simple example.
109+
__STEP 2:__ Wrap your form component with `reduxForm()`. `reduxForm()` wraps your form component in a Higher Order
110+
Component that connects to the Redux store and provides functions, as props to your component, for your form elements
111+
to use for sending `onChange` and `onBlur` events, as well as a function to handle synchronous
112+
validation and form submission. Let's look at a simple example.
113113

114114
### A Simple Form Component
115115

116-
You will need to wrap your form component with `redux-form`'s `connectReduxForm()` function.
116+
You will need to wrap your form component with `redux-form`'s `reduxForm()` function.
117117

118118
```javascript
119119
import React, {Component, PropTypes} from 'react';
@@ -148,8 +148,8 @@ class ContactForm extends Component {
148148
}
149149
}
150150

151-
// apply connectReduxForm() and include synchronous validation
152-
ContactForm = connectReduxForm({
151+
// apply reduxForm() and include synchronous validation
152+
ContactForm = reduxForm({
153153
form: 'contact', // the name of your form and the key to
154154
// where your form's state will be mounted
155155
fields: ['name', 'address', 'phone'], // a list of all your fields in your form
@@ -161,22 +161,41 @@ export default ContactForm;
161161
```
162162

163163
Notice that we're just using vanilla `<input>` elements there is no state in the `ContactForm` component.
164-
`handleSubmit` will call the function passed into `ContactForm`'s [`onSubmit` prop](#onsubmit-function-optional) **after** validation (both synchronous and asynchronous) completes successfully.
164+
`handleSubmit` will call the function passed into `ContactForm`'s [`onSubmit` prop](#onsubmit-function-optional)
165+
**after** validation (both synchronous and asynchronous) completes successfully.
165166

166167
See [Submitting Your Form](#submitting-your-form).
167168

168169
### ES7 Decorator Sugar
169170

170171
Using [ES7 decorator proposal](https://github.com/wycats/javascript-decorators), the example above
171-
could be written as:
172+
could be written different. Rather than...
172173

173174
```javascript
174-
@connectReduxForm({
175+
class ContactForm extends Component {
176+
// ...
177+
}
178+
179+
ContactForm reduxForm({
180+
form: 'contact',
181+
fields: ['name', 'address', 'phone'],
182+
validate: validateContact
183+
})(ContactForm);
184+
185+
export default ContactForm;
186+
```
187+
188+
...it could just be...
189+
190+
```javascript
191+
@reduxForm({
175192
form: 'contact',
176193
fields: ['name', 'address', 'phone'],
177194
validate: validateContact
178195
})
179196
export default class ContactForm extends Component {
197+
// ...
198+
}
180199
```
181200

182201
Much nicer, don't you think?
@@ -215,45 +234,71 @@ function validateContact(data, props) {
215234
```
216235
You get the idea.
217236

218-
### Asynchronous Validation
237+
This example validation function is purely for simplistic demonstration value. In your application, you will want to
238+
build some type of reusable system of validators. [Here is a simple
239+
example](https://github .com/erikras/react-redux-universal-hot-example/blob/master/src/utils/validation.js).
240+
241+
242+
The recommended way to do server-side validation with `redux-form` is to return a rejected promise from the `onSubmit`
243+
function. There are two ways to give `redux-form` a function to run when your form is submitted:
244+
245+
* Pass it as an `onSubmit` prop to your decorated component. In which case, you would use
246+
`onClick={this.props.handleSubmit}` inside your decorated component to cause it to fire when the submit button is
247+
clicked.
248+
* Pass it as a parameter to the `this.props.handleSubmit` function _from inside your decorated component_. In which
249+
case, you would use `onClick={this.props.handleSubmit(mySubmit)}` inside your decorated component to cause it to fire
250+
when the submit button is clicked.
251+
252+
The errors are displayed in the exact same way as validation errors created by
253+
[Synchronous Validation](#/synchronous-validation).
254+
255+
256+
### Asynchronous Blur Validation
219257

220258
Async validation can be achieved by passing in an asynchronous function that returns a Promise that will resolve
221259
to validation errors of the format that the synchronous [validation function](#synchronous-validation)
222260
generates. So this...
223261

262+
Ideally, you will specify if you want asynchronous validation to be triggered when one or more of your form
263+
fields is blurred, you may specify those fields as `asyncBlurFields`. Like so:
264+
224265
```javascript
225-
// apply connectReduxForm() and include synchronous validation
226-
ContactForm = connectReduxForm({
266+
// apply reduxForm() and include synchronous validation
267+
ContactForm = reduxForm({
227268
form: 'contact',
228269
fields: ['name', 'address', 'phone'],
229270
validate: validateContact
230271
})(ContactForm);
231272
```
232273
...changes to this:
274+
233275
```javascript
234276
function validateContactAsync(data, dispatch) {
235277
return new Promise((resolve, reject) => {
236278
const errors = {};
237-
// do async validation
238-
resolve(errors);
279+
let valid = true;
280+
// do async validation, which sets values in errors and flips valid flag
281+
if(valid) {
282+
resolve();
283+
} else {
284+
reject(errors);
285+
}
239286
});
240287
}
241288

242-
// apply connectReduxForm() and include synchronous AND asynchronous validation
243-
ContactForm = connectReduxForm({
289+
// apply reduxForm() and include synchronous AND asynchronous validation
290+
ContactForm = reduxForm({
244291
form: 'contact',
245292
fields: ['name', 'address', 'phone'],
246293
validate: validateContact,
247-
asyncValidate: validateContactAsync
294+
asyncValidate: validateContactAsync,
295+
asyncBlurFields: ['name', 'phone']
248296
})(ContactForm);
249297
```
250298

251-
Optionally, if you want asynchronous validation to be triggered when one or more of your form
252-
fields is blurred, you may specify those fields as `asyncBlurFields`. Like so:
253-
254299
```javascript
255300
// will only run async validation when 'name' or 'phone' is blurred
256-
ContactForm = connectReduxForm({
301+
ContactForm = reduxForm({
257302
form: 'contact',
258303
fields: ['name', 'address', 'phone'],
259304
validate: validateContact,
@@ -528,49 +573,9 @@ class DynamicForm extends Component {
528573
529574
## Advanced Usage
530575
531-
#### Doing the `connect()`ing Yourself
532-
533-
If, for some reason, you cannot mount the `redux-form` reducer at `form` in Redux, you may mount it anywhere else and
534-
do the `connect()` call yourself. Rather than wrap your form component with `redux-form`'s `connectReduxForm()`, you
535-
will need to wrap your form component *both* with
536-
[React Redux](https://github.com/gaearon/react-redux)'s `connect()` function *and* with `redux-form`'s
537-
`reduxForm()` function.
538-
539-
```javascript
540-
import React, {Component, PropTypes} from 'react';
541-
import {connect} from 'react-redux';
542-
import {reduxForm} from 'redux-form';
543-
import validateContact from './validateContact';
544-
545-
class ContactForm extends Component {
546-
//...
547-
}
548-
549-
// apply reduxForm() and include synchronous validation
550-
// note: we're using reduxForm, not connectReduxForm
551-
ContactForm = reduxForm({
552-
form: 'contact',
553-
fields: ['name', 'address', 'phone'],
554-
validate: validateContact
555-
})(ContactForm);
556-
557-
// ------- HERE'S THE IMPORTANT BIT -------
558-
function mapStateToProps(state, ownProps) {
559-
// this is React Redux API: https://github.com/rackt/react-redux
560-
// for example, you may use ownProps here to refer to the props passed from parent.
561-
return {
562-
form: state.placeWhereYouMountedFormReducer[ownProps.something]
563-
};
564-
}
565-
566-
// apply connect() to bind it to Redux state
567-
ContactForm = connect(mapStateToProps)(ContactForm);
568-
569-
// export the wrapped component
570-
export default ContactForm;
571-
```
576+
#### Specifying a different reducer mount point
572577
573-
As you can see, `connectReduxForm()` is a tiny wrapper over `reduxForm()` that applies `connect()` for you.
578+
If, for some reason, you cannot mount the `redux-form` reducer at `form` in Redux, you may mount it elsewhere by specifying a `reducerMountPoint` configuration option.
574579
575580
##### Binding Action Creators
576581

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"clean": "rimraf dist lib",
1616
"lint": "eslint src",
1717
"prepublish": "npm run lint && npm run test && npm run clean && npm run build",
18-
"test": "mocha --compilers js:babel/register --recursive src/**/__tests__/* --require src/__tests__/setup.js"
18+
"test": "mocha --compilers js:babel/register --recursive 'src/**/__tests__/*' --require src/__tests__/setup.js"
1919
},
2020
"keywords": [
2121
"react",

src/__tests__/asyncValidation.spec.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe('asyncValidation', () => {
3333
});
3434
});
3535

36-
it('should call start, fn, and stop on promise reject', () => {
36+
it('should throw when promise rejected with no errors', () => {
3737
const fn = createSpy().andReturn(Promise.reject());
3838
const start = createSpy();
3939
const stop = createSpy();
@@ -46,4 +46,21 @@ describe('asyncValidation', () => {
4646
expect(stop).toHaveBeenCalled();
4747
});
4848
});
49+
50+
it('should call start, fn, and stop on promise reject', () => {
51+
const errors = {foo: 'error'};
52+
const fn = createSpy().andReturn(Promise.reject(errors));
53+
const start = createSpy();
54+
const stop = createSpy();
55+
const promise = asyncValidation(fn, start, stop);
56+
expect(fn).toHaveBeenCalled();
57+
expect(start).toHaveBeenCalled();
58+
return promise.then(() => {
59+
expect(false).toBe(true); // should not get into resolve branch
60+
}, () => {
61+
expect(stop)
62+
.toHaveBeenCalled()
63+
.toHaveBeenCalledWith(errors);
64+
});
65+
});
4966
});

src/__tests__/createReduxForm.spec.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ describe('createReduxForm', () => {
3131
if (readonly) {
3232
expect(field.onBlur).toNotExist();
3333
expect(field.onChange).toNotExist();
34-
expect(field.onDrag).toNotExist();
34+
expect(field.onDragStart).toNotExist();
3535
expect(field.onDrop).toNotExist();
3636
expect(field.onFocus).toNotExist();
3737
expect(field.onUpdate).toNotExist();
3838
} else {
3939
expect(field.onBlur).toBeA('function');
4040
expect(field.onChange).toBeA('function');
41-
expect(field.onDrag).toBeA('function');
41+
expect(field.onDragStart).toBeA('function');
4242
expect(field.onDrop).toBeA('function');
4343
expect(field.onFocus).toBeA('function');
4444
expect(field.onUpdate).toBeA('function');
@@ -321,8 +321,12 @@ describe('createReduxForm', () => {
321321
);
322322
const stub = TestUtils.findRenderedComponentWithType(dom, Form);
323323

324+
expect(stub.props.active).toBe(undefined);
325+
324326
stub.props.fields.foo.onFocus();
325327

328+
expect(stub.props.active).toBe('foo');
329+
326330
expect(stub.props.fields).toBeA('object');
327331
expectField({
328332
field: stub.props.fields.foo,

src/__tests__/handleSubmit.spec.js

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('handleSubmit', () => {
5050
.toHaveBeenCalledWith(values, props);
5151
expect(asyncValidate)
5252
.toHaveBeenCalled()
53-
.toHaveBeenCalledWith(values);
53+
.toHaveBeenCalledWith();
5454
expect(submit)
5555
.toHaveBeenCalled()
5656
.toHaveBeenCalledWith(values, dispatch);
@@ -70,6 +70,38 @@ describe('handleSubmit', () => {
7070
const validate = createSpy().andReturn({});
7171
const props = {dispatch, fields, startSubmit, stopSubmit, touch, validate};
7272

73+
return handleSubmit(submit, values, props, asyncValidate)
74+
.then(result => {
75+
expect(result).toBe(undefined);
76+
expect(touch)
77+
.toHaveBeenCalled()
78+
.toHaveBeenCalledWith(...fields);
79+
expect(validate)
80+
.toHaveBeenCalled()
81+
.toHaveBeenCalledWith(values, props);
82+
expect(asyncValidate)
83+
.toHaveBeenCalled()
84+
.toHaveBeenCalledWith();
85+
expect(submit).toNotHaveBeenCalled();
86+
expect(startSubmit).toNotHaveBeenCalled();
87+
expect(stopSubmit).toNotHaveBeenCalled();
88+
}, () => {
89+
expect(false).toBe(true); // should not get into reject branch
90+
});
91+
});
92+
93+
it('should not submit if async validation fails and return rejected promise', () => {
94+
const values = {foo: 'bar', baz: 42};
95+
const fields = ['foo', 'baz'];
96+
const submit = createSpy().andReturn(69);
97+
const dispatch = () => null;
98+
const touch = createSpy();
99+
const startSubmit = createSpy();
100+
const stopSubmit = createSpy();
101+
const asyncValidate = createSpy().andReturn(Promise.reject());
102+
const validate = createSpy().andReturn({});
103+
const props = {dispatch, fields, startSubmit, stopSubmit, touch, validate, returnRejectedSubmitPromise: true};
104+
73105
return handleSubmit(submit, values, props, asyncValidate)
74106
.then(() => {
75107
expect(false).toBe(true); // should not get into reject branch
@@ -83,7 +115,7 @@ describe('handleSubmit', () => {
83115
.toHaveBeenCalledWith(values, props);
84116
expect(asyncValidate)
85117
.toHaveBeenCalled()
86-
.toHaveBeenCalledWith(values);
118+
.toHaveBeenCalledWith();
87119
expect(submit).toNotHaveBeenCalled();
88120
expect(startSubmit).toNotHaveBeenCalled();
89121
expect(stopSubmit).toNotHaveBeenCalled();
@@ -113,7 +145,7 @@ describe('handleSubmit', () => {
113145
.toHaveBeenCalledWith(values, props);
114146
expect(asyncValidate)
115147
.toHaveBeenCalled()
116-
.toHaveBeenCalledWith(values);
148+
.toHaveBeenCalledWith();
117149
expect(submit)
118150
.toHaveBeenCalled()
119151
.toHaveBeenCalledWith(values, dispatch);
@@ -147,7 +179,7 @@ describe('handleSubmit', () => {
147179
.toHaveBeenCalledWith(values, props);
148180
expect(asyncValidate)
149181
.toHaveBeenCalled()
150-
.toHaveBeenCalledWith(values);
182+
.toHaveBeenCalledWith();
151183
expect(submit)
152184
.toHaveBeenCalled()
153185
.toHaveBeenCalledWith(values, dispatch);
@@ -184,7 +216,7 @@ describe('handleSubmit', () => {
184216
.toHaveBeenCalledWith(values, props);
185217
expect(asyncValidate)
186218
.toHaveBeenCalled()
187-
.toHaveBeenCalledWith(values);
219+
.toHaveBeenCalledWith();
188220
expect(submit)
189221
.toHaveBeenCalled()
190222
.toHaveBeenCalledWith(values, dispatch);
@@ -223,7 +255,7 @@ describe('handleSubmit', () => {
223255
.toHaveBeenCalledWith(values, props);
224256
expect(asyncValidate)
225257
.toHaveBeenCalled()
226-
.toHaveBeenCalledWith(values);
258+
.toHaveBeenCalledWith();
227259
expect(submit)
228260
.toHaveBeenCalled()
229261
.toHaveBeenCalledWith(values, dispatch);

0 commit comments

Comments
 (0)