Skip to content
This repository was archived by the owner on Aug 23, 2022. It is now read-only.

Commit 5ce4457

Browse files
committed
Merge branch 'master' of github.com:davidkpiano/react-redux-form
2 parents b008e0e + 8ef4b17 commit 5ce4457

File tree

3 files changed

+184
-8
lines changed

3 files changed

+184
-8
lines changed

react-redux-form.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,15 @@ interface ErrorsComponentMessages {
4848
interface FormValidators {
4949
[key: string]: Validators;
5050
}
51+
interface FormValidatorsFn {
52+
(val: any): FormValidationErrors
53+
}
5154
interface ValidationErrors {
5255
[key: string]: any;
5356
}
57+
interface FormValidationErrors {
58+
[key: string]: ValidationErrors
59+
}
5460
/**
5561
* Internal interface
5662
*/
@@ -270,7 +276,7 @@ interface BaseFormProps {
270276
* * Specifying validators on the form is usually sufficient - you don't need to put validators on the <Field> for most use cases.
271277
* * If you need validators to run on submit, this is the place to put them.
272278
*/
273-
validators?: Validators | FormValidators;
279+
validators?: Validators | FormValidators | FormValidatorsFn;
274280

275281
/**
276282
* An object representing the error validators for the fields inside the form, where:

src/components/form-component.js

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import invariant from 'invariant';
2020

2121
const propTypes = {
2222
component: PropTypes.any,
23-
validators: PropTypes.object,
23+
validators: PropTypes.oneOfType([
24+
PropTypes.object,
25+
PropTypes.func,
26+
]),
2427
errors: PropTypes.object,
2528
validateOn: PropTypes.oneOf([
2629
'change',
@@ -157,12 +160,8 @@ function createFormClass(s = defaultStrategy) {
157160
const validatorsChanged = validators !== this.props.validators
158161
|| errors !== this.props.errors;
159162

160-
const errorValidators = validators
161-
? merge(invertValidators(validators), errors)
162-
: errors;
163-
164-
let validityChanged = false;
165163
const fieldsErrors = {};
164+
let validityChanged = false;
166165

167166
// this is (internally) mutative for performance reasons.
168167
const validateField = (errorValidator, field) => {
@@ -197,12 +196,64 @@ function createFormClass(s = defaultStrategy) {
197196
validityChanged = true;
198197
}
199198

199+
// Changed the below for a test that errors and validations
200+
// get merged correctly, but it appears this wasn't actually
201+
// supported for the same field? Also could have the side
202+
// effect that errors wouldn't get cleared?
203+
// fieldsErrors[field] = merge(fieldsErrors[field] || {}, fieldErrors);
204+
200205
fieldsErrors[field] = fieldErrors;
201206
}
202207
}
203208
};
204209

205-
mapValues(errorValidators, validateField);
210+
// Run errors first, validations should take precendence.
211+
// When run below will replace the contents of the fieldErrors[].
212+
mapValues(errors, validateField);
213+
214+
if (typeof validators === 'function') {
215+
const field = '';
216+
217+
const nextValue = field
218+
? s.get(nextProps.modelValue, field)
219+
: nextProps.modelValue;
220+
221+
const currentValue = field
222+
? s.get(modelValue, field)
223+
: modelValue;
224+
225+
// If the validators didn't change, the validity didn't change.
226+
if ((!initial && !validatorsChanged) && (nextValue === currentValue)) {
227+
// TODO this will only set the errors on form when using the function.
228+
// How handle? Safe to assume will be no dispatch?
229+
// fieldsErrors[field] = getField(formValue, field).errors;
230+
} else {
231+
const multiFieldErrors = getValidity(validators, nextValue);
232+
233+
if (multiFieldErrors) {
234+
Object.keys(multiFieldErrors).forEach((key) => {
235+
// key will be the model value to apply errors to.
236+
const fieldErrors = multiFieldErrors[key];
237+
const currentErrors = getField(formValue, key).errors;
238+
239+
// Invert validators
240+
Object.keys(fieldErrors).forEach((validationName) => {
241+
fieldErrors[validationName] = !fieldErrors[validationName];
242+
});
243+
244+
if (!validityChanged && !shallowEqual(fieldErrors, currentErrors)) {
245+
validityChanged = true;
246+
}
247+
248+
fieldsErrors[key] = fieldErrors;
249+
});
250+
}
251+
}
252+
} else if (validators) {
253+
const errorValidators = invertValidators(validators);
254+
255+
mapValues(errorValidators, validateField);
256+
}
206257

207258
// Compute form-level validity
208259
if (!fieldsErrors.hasOwnProperty('')) {

test/form-component-spec.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,6 +1766,125 @@ Object.keys(testContexts).forEach((testKey) => {
17661766
});
17671767
});
17681768

1769+
describe('form validation as function', () => {
1770+
const initialState = getInitialState({
1771+
items: [
1772+
{ name: 'one' },
1773+
{ name: 'two' },
1774+
{ name: 'three' },
1775+
],
1776+
});
1777+
const store = testCreateStore({
1778+
testForm: formReducer('test', initialState),
1779+
test: modelReducer('test', initialState),
1780+
});
1781+
1782+
const form = TestUtils.renderIntoDocument(
1783+
<Provider store={store}>
1784+
<Form
1785+
model="test"
1786+
validators={(model) => {
1787+
const field1 = 'items[0].name';
1788+
const field2 = 'items[1].name';
1789+
const hasValue = (value) => value && value.length;
1790+
1791+
const field1Value = get(model, field1);
1792+
const field2Value = get(model, field2);
1793+
1794+
const notRequired = () => Boolean(hasValue(field1Value) || hasValue(field2Value));
1795+
const containsOne = field1Value.includes('one');
1796+
const validations = {};
1797+
1798+
validations[field1] = {
1799+
required: notRequired(),
1800+
needsFoo: containsOne,
1801+
};
1802+
validations[field2] = {
1803+
required: notRequired(),
1804+
};
1805+
return validations;
1806+
}}
1807+
errors={{
1808+
'items[2].name': (value) => { // eslint-disable-line arrow-body-style
1809+
return value.includes('three') ? false : { invalidThree: 'invalid three' };
1810+
},
1811+
}}
1812+
>
1813+
{get(initialState, 'items').map((item, i) =>
1814+
<Control model={`test.items[${i}].name`} />
1815+
)}
1816+
</Form>
1817+
</Provider>
1818+
);
1819+
1820+
const [input1, input2, input3] = TestUtils
1821+
.scryRenderedDOMComponentsWithTag(form, 'input');
1822+
1823+
it('should initially validate each item', () => {
1824+
const { $form, items } = store.getState().testForm;
1825+
assert.isTrue(items[0].name.valid);
1826+
assert.isTrue(items[1].name.valid);
1827+
assert.isTrue($form.valid);
1828+
});
1829+
1830+
it('should check validity of each item on change', () => {
1831+
input2.value = '';
1832+
TestUtils.Simulate.change(input2);
1833+
const { $form, items } = store.getState().testForm;
1834+
1835+
assert.isTrue(items[0].name.valid);
1836+
assert.isTrue(items[1].name.valid);
1837+
assert.isTrue($form.valid);
1838+
1839+
input1.value = '';
1840+
TestUtils.Simulate.change(input1);
1841+
1842+
const { $form: $form1, items: items1 } = store.getState().testForm;
1843+
1844+
assert.isFalse(items1[0].name.valid);
1845+
assert.isFalse(items1[1].name.valid);
1846+
assert.isFalse($form1.valid);
1847+
});
1848+
1849+
it('should set validation type on change', () => {
1850+
input2.value = '';
1851+
TestUtils.Simulate.change(input2);
1852+
input1.value = '';
1853+
TestUtils.Simulate.change(input1);
1854+
const { $form, items } = store.getState().testForm;
1855+
1856+
assert.isFalse(items[0].name.validity.required);
1857+
assert.isTrue(items[0].name.errors.required);
1858+
assert.isFalse(items[1].name.validity.required);
1859+
assert.isTrue(items[1].name.errors.required);
1860+
assert.isFalse($form.valid);
1861+
});
1862+
1863+
it('should aggregate errors and validations.', () => {
1864+
input1.value = '';
1865+
TestUtils.Simulate.change(input1);
1866+
input2.value = '';
1867+
TestUtils.Simulate.change(input2);
1868+
input3.value = 'foo';
1869+
TestUtils.Simulate.change(input3);
1870+
const { $form, items } = store.getState().testForm;
1871+
1872+
assert.isFalse(items[0].name.validity.required);
1873+
assert.isFalse(items[0].name.validity.needsFoo);
1874+
1875+
assert.isTrue(items[0].name.errors.required);
1876+
assert.isTrue(items[0].name.errors.needsFoo);
1877+
1878+
assert.isFalse(items[1].name.validity.required);
1879+
assert.isTrue(items[1].name.errors.required);
1880+
1881+
assert.isFalse(items[2].name.validity.invalidThree);
1882+
assert.equal(items[2].name.errors.invalidThree, 'invalid three');
1883+
1884+
assert.isFalse($form.valid);
1885+
});
1886+
});
1887+
17691888
describe('submit valid form no validators', () => {
17701889
const initialState = getInitialState({ foo: '' });
17711890

0 commit comments

Comments
 (0)