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

Commit 6f54d39

Browse files
committed
2 parents c76c440 + 3389ada commit 6f54d39

File tree

9 files changed

+140
-13
lines changed

9 files changed

+140
-13
lines changed

docs/api/Form.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,28 @@ _(Function)_ Called every time a `submit` event is emitted from the form.
235235

236236
### Notes
237237
- This is most useful for use with `<LocalForm>`, and is primarily intended for `<LocalForm>`.
238+
239+
## `hideNativeErrors={true}`
240+
_(Boolean)_ Indicates whether native HTML5 constraint validation error messages should be shown. This does not preclude the form from failing to submit if native validation fails.
241+
242+
(since: 1.14.0)
243+
244+
```jsx
245+
// native errors will NOT show
246+
// <Errors /> will show when touched (or submit button clicked) as expected
247+
<Form model="user" hideNativeErrors>
248+
<Control.text
249+
model=".email"
250+
type="email"
251+
/>
252+
<Errors
253+
model=".email"
254+
messages={{
255+
valueMissing: 'Hey, where is your email?',
256+
typeMismatch: 'Not a valid email!'
257+
}}
258+
show="touched"
259+
/>
260+
<button>Submit!</button>
261+
</Form>
262+
```

docs/guides/faqs.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,29 @@ You will also get the native HTML5 constraint validation with these, as if you w
174174

175175
You might think that `noValidate` will solve this issue, but according to the [W3 spec for `noValidate`](https://dev.w3.org/html5/spec-preview/form-submission.html#attr-fs-novalidate), it does _not_ prevent a form from being submitted if the form is invalid due to HTML5 constraint validity. RRF follows the spec closely with regard to this behavior.
176176

177-
Instead, use the native `onInvalid` handler to prevent the native HTML5 validation message from displaying:
177+
To solve this, since version 0.14.0, the `hideNativeErrors` prop can be used to indicate that you do not want native HTML5 constraint validation messages appearing. This will still retain the behavior that if a control is invalid due to HTML5 validation, the form will fail to submit:
178+
179+
```jsx
180+
// native errors will NOT show
181+
// <Errors /> will show when touched (or submit button clicked) as expected
182+
<Form model="user" hideNativeErrors>
183+
<Control.text
184+
model=".email"
185+
type="email"
186+
/>
187+
<Errors
188+
model=".email"
189+
messages={{
190+
valueMissing: 'Hey, where is your email?',
191+
typeMismatch: 'Not a valid email!'
192+
}}
193+
show="touched"
194+
/>
195+
<button>Submit!</button>
196+
</Form>
197+
```
198+
199+
Using `hideNativeErrors` is the recommended way to solve this. However, if you want to prevent HTML5 validation messages from showing on individual controls, use the native `onInvalid` handler to prevent the native HTML5 validation message from displaying:
178200

179201
```jsx
180202
<Control.text

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-redux-form",
3-
"version": "1.13.0",
3+
"version": "1.14.0",
44
"description": "Create Forms Easily with React and Redux",
55
"main": "lib/index.js",
66
"typings": "react-redux-form.d.ts",
@@ -97,8 +97,8 @@
9797
"shallow-compare": "1.2.1"
9898
},
9999
"peerDependencies": {
100-
"react": "^0.14.0 || ^15.0.0",
101-
"react-dom": "^0.14.7 || ^15.0.0",
100+
"react": "^0.14.0 || ^15.0.0 || ^16.0.0",
101+
"react-dom": "^0.14.7 || ^15.0.0 || ^16.0.0",
102102
"react-redux": "^4.0.0 || ^5.0.3",
103103
"redux": "^3.0.0"
104104
}

src/components/control-component.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,13 @@ function createControlClass(s = defaultStrategy) {
400400
return;
401401
}
402402
case 'validate':
403+
case 'reset':
404+
if (intent.type === 'reset') {
405+
this.setViewValue(modelValue);
406+
if (this.handleUpdate.cancel) {
407+
this.handleUpdate.cancel();
408+
}
409+
}
403410
if (containsEvent(validateOn, 'change')) {
404411
this.validate({ clearIntents: intent });
405412
}
@@ -568,23 +575,27 @@ function createControlClass(s = defaultStrategy) {
568575
dispatch,
569576
} = this.props;
570577

571-
if (!validators && !errorValidators) return modelValue;
572-
if (!fieldValue) return modelValue;
578+
if ((!validators && !errorValidators && !this.willValidate) || !fieldValue) {
579+
return;
580+
}
573581

574582
const fieldValidity = getValidity(validators, modelValue);
575583
const fieldErrors = getValidity(errorValidators, modelValue);
584+
const nodeErrors = this.getNodeErrors();
576585

577-
const errors = validators
586+
let errors = validators
578587
? merge(invertValidity(fieldValidity), fieldErrors)
579588
: fieldErrors;
580589

590+
if (this.willValidate) {
591+
errors = merge(errors, nodeErrors);
592+
}
593+
581594
if (!shallowEqual(errors, fieldValue.errors)) {
582595
dispatch(mergeOrSetErrors(model, errors, options));
583596
} else if (options.clearIntents) {
584597
dispatch(actions.clearIntents(model, options.clearIntents));
585598
}
586-
587-
return modelValue;
588599
}
589600

590601
render() {

src/components/form-component.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const propTypes = {
5050
getRef: PropTypes.func,
5151
getDispatch: PropTypes.func,
5252
onBeforeSubmit: PropTypes.func,
53+
hideNativeErrors: PropTypes.bool,
5354

5455
// standard HTML attributes
5556
action: PropTypes.string,
@@ -367,6 +368,8 @@ function createFormClass(s = defaultStrategy) {
367368
component,
368369
children,
369370
formValue,
371+
hideNativeErrors,
372+
noValidate,
370373
} = this.props;
371374

372375
const allowedProps = omit(this.props, disallowedPropTypeKeys);
@@ -380,6 +383,7 @@ function createFormClass(s = defaultStrategy) {
380383
onSubmit: this.handleSubmit,
381384
onReset: this.handleReset,
382385
ref: this.attachNode,
386+
noValidate: hideNativeErrors || noValidate,
383387
}, renderableChildren);
384388
}
385389
}

src/reducers/form-actions-reducer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import getFormValue from '../utils/get-form-value';
2222
const resetFieldState = (field, customInitialFieldState) => {
2323
if (!isPlainObject(field)) return field;
2424

25-
const intents = [{ type: 'validate' }];
25+
const intents = [{ type: 'reset' }];
2626
let resetValue = getMeta(field, 'initialValue');
2727
const loadedValue = getMeta(field, 'loadedValue');
2828

src/utils/debounce.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,10 @@ export default function debounce(func, delay) {
1818
if (laterFunc) laterFunc();
1919
};
2020

21+
debouncedFunc.cancel = () => {
22+
clearTimeout(timeout);
23+
laterFunc = undefined;
24+
};
25+
2126
return debouncedFunc;
2227
}

test/control-component-spec.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,6 +2144,66 @@ Object.keys(testContexts).forEach((testKey) => {
21442144
assert.equal(get(store.getState().test, 'foo'), 'debounced');
21452145
assert.equal(input.value, 'debounced');
21462146
});
2147+
2148+
it('should cancel debounced changes when control is reset', (done) => {
2149+
const initialState = getInitialState({ foo: 'bar' });
2150+
const store = testCreateStore({
2151+
test: modelReducer('test', initialState),
2152+
testForm: formReducer('test', initialState),
2153+
});
2154+
2155+
const control = testRender(
2156+
<Control.text
2157+
model="test.foo"
2158+
debounce={10}
2159+
/>, store);
2160+
2161+
const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
2162+
input.value = 'debounced';
2163+
2164+
TestUtils.Simulate.change(input);
2165+
2166+
store.dispatch(actions.reset('test'));
2167+
2168+
setTimeout(() => {
2169+
assert.equal(get(store.getState().test, 'foo'), 'bar');
2170+
assert.equal(input.value, 'bar');
2171+
done();
2172+
}, 20);
2173+
});
2174+
2175+
it('should cancel debounced changes when control is reset then unmounted', (done) => {
2176+
const initialState = getInitialState({ foo: 'bar' });
2177+
const store = testCreateStore({
2178+
test: modelReducer('test', initialState),
2179+
testForm: formReducer('test', initialState),
2180+
});
2181+
2182+
const container = document.createElement('div');
2183+
2184+
const control = ReactDOM.render(
2185+
<Provider store={store}>
2186+
<Control.text
2187+
model="test.foo"
2188+
debounce={10}
2189+
/>
2190+
</Provider>,
2191+
container);
2192+
2193+
const input = TestUtils.findRenderedDOMComponentWithTag(control, 'input');
2194+
input.value = 'debounced';
2195+
2196+
TestUtils.Simulate.change(input);
2197+
2198+
store.dispatch(actions.reset('test'));
2199+
ReactDOM.unmountComponentAtNode(container);
2200+
2201+
setTimeout(() => {
2202+
assert.equal(get(store.getState().test, 'foo'), 'bar');
2203+
assert.equal(input.value, 'bar');
2204+
done();
2205+
}, 20);
2206+
});
21472207
});
21482208

21492209
describe('persist prop', () => {

test/field-actions-spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ Object.keys(testContexts).forEach((testKey) => {
180180

181181
const resetState = reducer(undefined, actions.reset('test'));
182182

183-
assert.include(resetState.$form.intents, { type: 'validate' });
183+
assert.include(resetState.$form.intents, { type: 'reset' });
184184

185-
assert.include(resetState.button.$form.intents, { type: 'validate' },
186-
'should intend to revalidate subfields');
185+
assert.include(resetState.button.$form.intents, { type: 'reset' },
186+
'should intend to revalidate subfields (handled with reset)');
187187
});
188188
});
189189

0 commit comments

Comments
 (0)