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

Commit 19f67e5

Browse files
committed
Adding deep model validation for <Form> validators prop. Fixes #510
1 parent 484f213 commit 19f67e5

File tree

3 files changed

+126
-21
lines changed

3 files changed

+126
-21
lines changed

docs/guides/validation.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,47 @@ import { Form } from 'react-redux-form';
235235
</Form>
236236
```
237237
238+
## Deep Model Validation in `<Form>`
239+
240+
As of RRF version 1.2.4, you can have deep validators in the `<Form validators={{...}}>` prop. Here's what it looks like:
241+
242+
```jsx
243+
// Suppose you have a store with this 'user' model:
244+
// {
245+
// name: 'Bob',
246+
// phones: [
247+
// { type: 'home', number: '5551231234' },
248+
// { type: 'cell', number: '5550980987' },
249+
// ],
250+
// }
251+
252+
// You can validate each individual phone number like so:
253+
<Form
254+
model="user"
255+
validators={{
256+
'phones[].number': (value) => value && value.length === 10,
257+
}}
258+
>
259+
{/* etc. */}
260+
</Form>
261+
````
262+
263+
The empty brackets in the validator key `'phones[].number'` tell RRF to validate the `.number` property for each `phone` in the `user.phones[]` array.
264+
265+
Alternatively, you can just set this validator directly on each control; e.g.:
266+
267+
```jsx
268+
{user.phones.map((phone, i) =>
269+
<Control
270+
model={`phones[${i}].number`}
271+
validators={{
272+
validNumber: (value) => value && value.length === 10,
273+
}}
274+
/>
275+
)}
276+
```
277+
278+
238279
## Custom Error Messages
239280

240281
Similar to how the `validators` prop and `setValidity()` action works, you can use the `errors` prop and `setErrors()` action to indicate errors. Keep in mind, these should express the _inverse_ validity state of the model. This means that anything _truthy_ indicates an error.

src/components/form-component.js

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -153,30 +153,47 @@ function createFormClass(s = defaultStrategy) {
153153
: errors;
154154

155155
let validityChanged = false;
156+
const fieldsErrors = {};
157+
158+
// this is (internally) mutative for performance reasons.
159+
const validateField = (errorValidator, field) => {
160+
if (!!~field.indexOf('[]')) {
161+
const [parentModel, childModel] = field.split('[]');
162+
163+
const nextValue = parentModel
164+
? s.get(nextProps.modelValue, parentModel)
165+
: nextProps.modelValue;
166+
167+
nextValue.forEach((subValue, index) => {
168+
validateField(errorValidator, `${parentModel}[${index}]${childModel}`);
169+
});
170+
} else {
171+
const nextValue = field
172+
? s.get(nextProps.modelValue, field)
173+
: nextProps.modelValue;
174+
175+
const currentValue = field
176+
? s.get(modelValue, field)
177+
: modelValue;
178+
179+
const currentErrors = getField(formValue, field).errors;
180+
181+
// If the validators didn't change, the validity didn't change.
182+
if ((!initial && !validatorsChanged) && (nextValue === currentValue)) {
183+
fieldsErrors[field] = getField(formValue, field).errors;
184+
} else {
185+
const fieldErrors = getValidity(errorValidator, nextValue);
186+
187+
if (!validityChanged && !shallowEqual(fieldErrors, currentErrors)) {
188+
validityChanged = true;
189+
}
156190

157-
const fieldsErrors = mapValues(errorValidators, (errorValidator, field) => {
158-
const nextValue = field
159-
? s.get(nextProps.modelValue, field)
160-
: nextProps.modelValue;
161-
162-
const currentValue = field
163-
? s.get(modelValue, field)
164-
: modelValue;
165-
166-
const currentErrors = getField(formValue, field).errors;
167-
168-
if ((!initial && !validatorsChanged) && (nextValue === currentValue)) {
169-
return getField(formValue, field).errors;
170-
}
171-
172-
const fieldErrors = getValidity(errorValidator, nextValue);
173-
174-
if (!shallowEqual(fieldErrors, currentErrors)) {
175-
validityChanged = true;
191+
fieldsErrors[field] = fieldErrors;
192+
}
176193
}
194+
};
177195

178-
return fieldErrors;
179-
});
196+
mapValues(errorValidators, validateField);
180197

181198
// Compute form-level validity
182199
if (!fieldsErrors.hasOwnProperty('')) {

test/form-component-spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,53 @@ Object.keys(testContexts).forEach((testKey) => {
16161616
});
16171617
});
16181618

1619+
describe('validation of nested/deep model values', () => {
1620+
const initialState = getInitialState({
1621+
items: [
1622+
{ name: 'one' },
1623+
{ name: 'two' },
1624+
],
1625+
});
1626+
const store = testCreateStore({
1627+
testForm: formReducer('test', initialState),
1628+
test: modelReducer('test', initialState),
1629+
});
1630+
const form = TestUtils.renderIntoDocument(
1631+
<Provider store={store}>
1632+
<Form
1633+
model="test"
1634+
validators={{
1635+
'items[].name': (name) => name && name.length,
1636+
}}
1637+
>
1638+
{get(initialState, 'items').map((item, i) =>
1639+
<Control model={`test.items[${i}].name`} />
1640+
)}
1641+
</Form>
1642+
</Provider>
1643+
);
1644+
1645+
const [_, input2] = TestUtils
1646+
.scryRenderedDOMComponentsWithTag(form, 'input');
1647+
1648+
it('should initially validate each item', () => {
1649+
const { $form, items } = store.getState().testForm;
1650+
assert.isTrue(items[0].name.valid);
1651+
assert.isTrue(items[1].name.valid);
1652+
assert.isTrue($form.valid);
1653+
});
1654+
1655+
it('should check validity of each item on change', () => {
1656+
input2.value = '';
1657+
TestUtils.Simulate.change(input2);
1658+
const { $form, items } = store.getState().testForm;
1659+
1660+
assert.isTrue(items[0].name.valid);
1661+
assert.isFalse(items[1].name.valid);
1662+
assert.isFalse($form.valid);
1663+
});
1664+
});
1665+
16191666
describe('submit after field invalid', () => {
16201667
const initialState = getInitialState({ username: '' });
16211668
const store = testCreateStore({

0 commit comments

Comments
 (0)