From 1533cfb762f7f2fc439bae0fdbcab6175ac2cad9 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 14:11:32 +0800 Subject: [PATCH 01/41] Reformat initial state in tests in preparation for expansion --- packages/uniforms/__tests__/BaseField.js | 9 ++++++++- packages/uniforms/__tests__/connectField.js | 9 ++++++++- packages/uniforms/__tests__/injectName.js | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/uniforms/__tests__/BaseField.js b/packages/uniforms/__tests__/BaseField.js index 7623e3fa5..4990fd1d3 100644 --- a/packages/uniforms/__tests__/BaseField.js +++ b/packages/uniforms/__tests__/BaseField.js @@ -30,7 +30,14 @@ describe('BaseField', () => { const model = {a: {b: {c: 'example'}}}; const onChange = jest.fn(); const randomId = randomIds(); - const state = {changed: !1, changedMap: {}, label: !0, disabled: !1, placeholder: !0, showInlineError: !0}; + const state = { + changed: false, + changedMap: {}, + label: true, + disabled: false, + placeholder: true, + showInlineError: true + }; const schema = createSchemaBridge({ getDefinition (name) { // Simulate SimpleSchema. diff --git a/packages/uniforms/__tests__/connectField.js b/packages/uniforms/__tests__/connectField.js index 2deea6232..00f6eaa3c 100644 --- a/packages/uniforms/__tests__/connectField.js +++ b/packages/uniforms/__tests__/connectField.js @@ -13,7 +13,14 @@ describe('connectField', () => { const error = new Error(); const onChange = jest.fn(); const randomId = randomIds(); - const state = {changed: !1, changedMap: {}, label: !0, disabled: !1, placeholder: !1, showInlineError: !0}; + const state = { + changed: false, + changedMap: {}, + label: true, + disabled: false, + placeholder: false, + showInlineError: true + }; const schema = createSchemaBridge({ getDefinition (name) { return { diff --git a/packages/uniforms/__tests__/injectName.js b/packages/uniforms/__tests__/injectName.js index 42130c50b..8bd90be8a 100644 --- a/packages/uniforms/__tests__/injectName.js +++ b/packages/uniforms/__tests__/injectName.js @@ -14,7 +14,14 @@ describe('injectName', () => { const error = new Error(); const onChange = () => {}; const randomId = randomIds(); - const state = {changed: !1, changedMap: {}, label: !0, disabled: !1, placeholder: !1, showInlineError: !0}; + const state = { + changed: false, + changedMap: {}, + label: true, + disabled: false, + placeholder: false, + showInlineError: true + }; const schema = createSchemaBridge({ getDefinition (name) { return { From 407efefa4b88b22c98f7a815e5a37f4e12720157 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 11:31:37 +0800 Subject: [PATCH 02/41] Add additional checks to existing tests to increase coverage - The successs and failure callback behaviour was not being tested when submitting without `onSubmit`. - The value passed to the `onSubmit` callbacks was not tested. --- packages/uniforms/__tests__/BaseForm.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/uniforms/__tests__/BaseForm.js b/packages/uniforms/__tests__/BaseForm.js index 414037f71..eadcdb8d7 100644 --- a/packages/uniforms/__tests__/BaseForm.js +++ b/packages/uniforms/__tests__/BaseForm.js @@ -30,6 +30,8 @@ describe('BaseForm', () => { afterEach(() => { onChange.mockReset(); onSubmit.mockReset(); + onSubmitSuccess.mockReset(); + onSubmitFailure.mockReset(); }); describe('child context', () => { @@ -254,7 +256,7 @@ describe('BaseForm', () => { expect(onSubmit).toHaveBeenLastCalledWith(model); }); - it('calls `onSubmit` with correct model (`modelTransform`)', () => { + it('calls `onSubmit` with the correctly `modelTransform`ed model', () => { wrapper.setProps({ modelTransform (mode, model) { if (mode === 'submit') { @@ -272,15 +274,19 @@ describe('BaseForm', () => { wrapper.setProps({modelTransform: undefined}); }); - it('does nothing without `onSubmit`', () => { - wrapper.setProps({onSubmit: undefined}); + it('without `onSubmit` calls only `onSubmitSuccess`', async () => { + wrapper.setProps({onSubmit: undefined, onSubmitSuccess, onSubmitFailure}); wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); expect(onSubmit).not.toBeCalled(); + expect(onSubmitSuccess).toBeCalledTimes(1); + expect(onSubmitFailure).not.toBeCalled(); }); - it('calls `onSubmitSuccess` when `onSubmit` resolves', async () => { - onSubmit.mockReturnValueOnce(Promise.resolve()); + it('calls `onSubmitSuccess` with the returned value when `onSubmit` resolves', async () => { + const onSubmitValue = 'value'; + onSubmit.mockReturnValueOnce(Promise.resolve(onSubmitValue)); const wrapper = mount( @@ -291,10 +297,12 @@ describe('BaseForm', () => { await new Promise(resolve => process.nextTick(resolve)); expect(onSubmitSuccess).toHaveBeenCalledTimes(1); + expect(onSubmitSuccess).toHaveBeenLastCalledWith(onSubmitValue); }); - it('calls `onSubmitFailure` when `onSubmit` rejects', async () => { - onSubmit.mockReturnValueOnce(Promise.reject()); + it('calls `onSubmitFailure` with the thrown error when `onSubmit` rejects', async () => { + const onSubmitError = 'error'; + onSubmit.mockReturnValueOnce(Promise.reject(onSubmitError)); const wrapper = mount( @@ -305,6 +313,7 @@ describe('BaseForm', () => { await new Promise(resolve => process.nextTick(resolve)); expect(onSubmitFailure).toHaveBeenCalledTimes(1); + expect(onSubmitFailure).toHaveBeenLastCalledWith(onSubmitError); }); }); }); From 099ca9aff9d3d571b26bbeb42816d466745dd432 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 11:42:46 +0800 Subject: [PATCH 03/41] Add straightforward tracking of `submitting` state when submitting A new state property `submitting` is introduced, which is `true` while an onSubmit promise is pending and `false` otherwise. --- .../uniforms-antd/__tests__/_createContext.js | 1 + .../__tests__/_createContext.js | 1 + .../__tests__/_createContext.js | 1 + .../__tests__/_createContext.js | 1 + .../__tests__/_createContext.js | 1 + .../__tests__/_createContext.js | 1 + packages/uniforms/__tests__/BaseField.js | 1 + packages/uniforms/__tests__/BaseForm.js | 20 +++++++++++++ packages/uniforms/__tests__/connectField.js | 1 + packages/uniforms/__tests__/injectName.js | 1 + packages/uniforms/src/BaseForm.js | 28 +++++++++++++++---- packages/uniforms/src/filterDOMProps.js | 1 + 12 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/uniforms-antd/__tests__/_createContext.js b/packages/uniforms-antd/__tests__/_createContext.js index b6c4a5f5e..7c44b7d8e 100644 --- a/packages/uniforms-antd/__tests__/_createContext.js +++ b/packages/uniforms-antd/__tests__/_createContext.js @@ -20,6 +20,7 @@ const createContext = (schema, context) => ({ changedMap: {}, changed: false, + submitting: false, disabled: false, label: false, placeholder: false, diff --git a/packages/uniforms-bootstrap3/__tests__/_createContext.js b/packages/uniforms-bootstrap3/__tests__/_createContext.js index b6c4a5f5e..7c44b7d8e 100644 --- a/packages/uniforms-bootstrap3/__tests__/_createContext.js +++ b/packages/uniforms-bootstrap3/__tests__/_createContext.js @@ -20,6 +20,7 @@ const createContext = (schema, context) => ({ changedMap: {}, changed: false, + submitting: false, disabled: false, label: false, placeholder: false, diff --git a/packages/uniforms-bootstrap4/__tests__/_createContext.js b/packages/uniforms-bootstrap4/__tests__/_createContext.js index b6c4a5f5e..7c44b7d8e 100644 --- a/packages/uniforms-bootstrap4/__tests__/_createContext.js +++ b/packages/uniforms-bootstrap4/__tests__/_createContext.js @@ -20,6 +20,7 @@ const createContext = (schema, context) => ({ changedMap: {}, changed: false, + submitting: false, disabled: false, label: false, placeholder: false, diff --git a/packages/uniforms-material/__tests__/_createContext.js b/packages/uniforms-material/__tests__/_createContext.js index d0eec02e5..3b6726839 100644 --- a/packages/uniforms-material/__tests__/_createContext.js +++ b/packages/uniforms-material/__tests__/_createContext.js @@ -23,6 +23,7 @@ const createContext = (schema, context) => ({ changed: false, disabled: false, + submitting: false, label: false, placeholder: false, showInlineError: false, diff --git a/packages/uniforms-semantic/__tests__/_createContext.js b/packages/uniforms-semantic/__tests__/_createContext.js index b6c4a5f5e..7c44b7d8e 100644 --- a/packages/uniforms-semantic/__tests__/_createContext.js +++ b/packages/uniforms-semantic/__tests__/_createContext.js @@ -20,6 +20,7 @@ const createContext = (schema, context) => ({ changedMap: {}, changed: false, + submitting: false, disabled: false, label: false, placeholder: false, diff --git a/packages/uniforms-unstyled/__tests__/_createContext.js b/packages/uniforms-unstyled/__tests__/_createContext.js index b6c4a5f5e..7c44b7d8e 100644 --- a/packages/uniforms-unstyled/__tests__/_createContext.js +++ b/packages/uniforms-unstyled/__tests__/_createContext.js @@ -20,6 +20,7 @@ const createContext = (schema, context) => ({ changedMap: {}, changed: false, + submitting: false, disabled: false, label: false, placeholder: false, diff --git a/packages/uniforms/__tests__/BaseField.js b/packages/uniforms/__tests__/BaseField.js index 4990fd1d3..21279fe6a 100644 --- a/packages/uniforms/__tests__/BaseField.js +++ b/packages/uniforms/__tests__/BaseField.js @@ -33,6 +33,7 @@ describe('BaseField', () => { const state = { changed: false, changedMap: {}, + submitting: false, label: true, disabled: false, placeholder: true, diff --git a/packages/uniforms/__tests__/BaseForm.js b/packages/uniforms/__tests__/BaseForm.js index eadcdb8d7..8609e8364 100644 --- a/packages/uniforms/__tests__/BaseForm.js +++ b/packages/uniforms/__tests__/BaseForm.js @@ -284,6 +284,26 @@ describe('BaseForm', () => { expect(onSubmitFailure).not.toBeCalled(); }); + it('sets `submitting` state', async () => { + let resolveSubmit = null; + wrapper.setProps({onSubmit: () => new Promise(resolve => resolveSubmit = resolve)}); + + const context1 = wrapper.instance().getChildContext().uniforms.state; + expect(context1).toHaveProperty('submitting', false); + + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + const context2 = wrapper.instance().getChildContext().uniforms.state; + expect(context2).toHaveProperty('submitting', true); + + resolveSubmit(); + await new Promise(resolve => process.nextTick(resolve)); + + const context3 = wrapper.instance().getChildContext().uniforms.state; + expect(context3).toHaveProperty('submitting', false); + }); + it('calls `onSubmitSuccess` with the returned value when `onSubmit` resolves', async () => { const onSubmitValue = 'value'; onSubmit.mockReturnValueOnce(Promise.resolve(onSubmitValue)); diff --git a/packages/uniforms/__tests__/connectField.js b/packages/uniforms/__tests__/connectField.js index 00f6eaa3c..05f392af8 100644 --- a/packages/uniforms/__tests__/connectField.js +++ b/packages/uniforms/__tests__/connectField.js @@ -16,6 +16,7 @@ describe('connectField', () => { const state = { changed: false, changedMap: {}, + submitting: false, label: true, disabled: false, placeholder: false, diff --git a/packages/uniforms/__tests__/injectName.js b/packages/uniforms/__tests__/injectName.js index 8bd90be8a..b14153512 100644 --- a/packages/uniforms/__tests__/injectName.js +++ b/packages/uniforms/__tests__/injectName.js @@ -17,6 +17,7 @@ describe('injectName', () => { const state = { changed: false, changedMap: {}, + submitting: false, label: true, disabled: false, placeholder: false, diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index e6b50fdce..c28a57786 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -65,6 +65,7 @@ export default class BaseForm extends Component { state: PropTypes.shape({ changed: PropTypes.bool.isRequired, changedMap: PropTypes.object.isRequired, + submitting: PropTypes.bool.isRequired, label: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired, @@ -80,7 +81,13 @@ export default class BaseForm extends Component { constructor () { super(...arguments); - this.state = {bridge: createSchemaBridge(this.props.schema), changed: null, changedMap: {}, resetCount: 0}; + this.state = { + bridge: createSchemaBridge(this.props.schema), + changed: null, + changedMap: {}, + submitting: null, + resetCount: 0 + }; this.delayId = false; this.randomId = randomIds(this.props.id); @@ -126,8 +133,9 @@ export default class BaseForm extends Component { getChildContextState () { return { - changed: !!this.state.changed, - changedMap: this.state.changedMap, + changed: !!this.state.changed, + changedMap: this.state.changedMap, + submitting: !!this.state.submitting, label: !!this.props.label, disabled: !!this.props.disabled, @@ -149,7 +157,7 @@ export default class BaseForm extends Component { } componentWillMount () { - this.setState(() => ({}), () => this.setState(() => ({changed: false, changedMap: {}}))); + this.setState(() => ({}), () => this.setState(() => ({changed: false, changedMap: {}, submitting: false}))); } componentWillReceiveProps ({schema}) { @@ -225,7 +233,7 @@ export default class BaseForm extends Component { } __reset (state) { - return {changed: false, changedMap: {}, resetCount: state.resetCount + 1}; + return {changed: false, changedMap: {}, submitting: false, resetCount: state.resetCount + 1}; } onReset () { @@ -238,9 +246,19 @@ export default class BaseForm extends Component { event.stopPropagation(); } + this.setState({submitting: true}); return Promise.resolve( this.props.onSubmit && this.props.onSubmit(this.getModel('submit')) + ).then( + val => { + this.setState({submitting: false}); + return val; + }, + err => { + this.setState({submitting: false}); + throw err; + } ).then( this.props.onSubmitSuccess, this.props.onSubmitFailure diff --git a/packages/uniforms/src/filterDOMProps.js b/packages/uniforms/src/filterDOMProps.js index 255387ab0..1848c1801 100644 --- a/packages/uniforms/src/filterDOMProps.js +++ b/packages/uniforms/src/filterDOMProps.js @@ -4,6 +4,7 @@ const unwantedProps = [ // These props are provided by BaseField 'changed', 'changedMap', + 'submitting', 'disabled', 'error', 'errorMessage', From 1b4e727de17aa878002ec959ad6e2601f39ff1ce Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 14:13:27 +0800 Subject: [PATCH 04/41] Skip setting `submitting` state when `onSubmit` is not async. --- packages/uniforms/src/BaseForm.js | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index c28a57786..b1a6187e9 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -3,6 +3,7 @@ import React from 'react'; import cloneDeep from 'lodash/cloneDeep'; import get from 'lodash/get'; import set from 'lodash/set'; +import isFunction from 'lodash/isFunction'; import {Component} from 'react'; import changedKeys from './changedKeys'; @@ -246,20 +247,25 @@ export default class BaseForm extends Component { event.stopPropagation(); } - this.setState({submitting: true}); - return Promise.resolve( - this.props.onSubmit && - this.props.onSubmit(this.getModel('submit')) - ).then( - val => { - this.setState({submitting: false}); - return val; - }, - err => { - this.setState({submitting: false}); - throw err; - } - ).then( + const res = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); + let submitting = Promise.resolve(res); + + // Do not change the `submitting` state if onSubmit is not async + if (res && isFunction(res.then)) { + this.setState({submitting: true}); + submitting = submitting.then( + val => { + this.setState({submitting: false}); + return val; + }, + err => { + this.setState({submitting: false}); + throw err; + } + ); + } + + return submitting.then( this.props.onSubmitSuccess, this.props.onSubmitFailure ); From 53a969021894545843c43425b56240940c11fd72 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 14:41:46 +0800 Subject: [PATCH 05/41] Catch errors (synchronously) thrown by `onSubmit` The presence of `onSubmitFailure` makes it look like it might be ok to let `onSubmit` throw an error (as well as rejecting the promise it returns). If `onSubmit` does throw an error, the form itself could enter an invalid state and the uncaught error message would be mixed up with uniforms implementation, so it would be hard for the end user to debug. --- packages/uniforms/src/BaseForm.js | 37 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index b1a6187e9..c75329a8f 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -247,22 +247,27 @@ export default class BaseForm extends Component { event.stopPropagation(); } - const res = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); - let submitting = Promise.resolve(res); - - // Do not change the `submitting` state if onSubmit is not async - if (res && isFunction(res.then)) { - this.setState({submitting: true}); - submitting = submitting.then( - val => { - this.setState({submitting: false}); - return val; - }, - err => { - this.setState({submitting: false}); - throw err; - } - ); + let submitting; + try { + const res = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); + submitting = Promise.resolve(res); + + // Do not change the `submitting` state if onSubmit is not async + if (res && isFunction(res.then)) { + this.setState({submitting: true}); + submitting = submitting.then( + val => { + this.setState({submitting: false}); + return val; + }, + err => { + this.setState({submitting: false}); + throw err; + } + ); + } + } catch (error) { + submitting = Promise.reject(error); } return submitting.then( From b7df3e0ab6bfa9ff293cc3363f50c5ddca02a637 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 14:49:07 +0800 Subject: [PATCH 06/41] Add `submitting` state to docs --- INTRODUCTION.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/INTRODUCTION.md b/INTRODUCTION.md index 9ae6f325f..a85149d6a 100644 --- a/INTRODUCTION.md +++ b/INTRODUCTION.md @@ -884,6 +884,7 @@ MyComponentUsingUniformsContext.contextTypes = { state: PropTypes.shape({ changed: PropTypes.bool.isRequired, changedMap: PropTypes.object.isRequired, + submitting: PropTypes.bool.isRequired, label: PropTypes.bool.isRequired, disabled: PropTypes.bool.isRequired, @@ -948,8 +949,8 @@ import filterDOMProps from 'uniforms/filterDOMProps'; // This field works as follows: render standard submit field and disable it, when // the form is invalid. It's a simplified version of a default SubmitField from // uniforms-unstyled. -const SubmitField = (props, {uniforms: {error, state: {disabled}}}) => - +const SubmitField = (props, {uniforms: {error, state: {disabled, submitting}}}) => + ; SubmitField.contextTypes = BaseField.contextTypes; From bed378d2dcb5f5fd3f6c3dcf68b7cc720eb09bb5 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 17 Jul 2018 16:23:57 +0800 Subject: [PATCH 07/41] Add test case for `onSubmit` throwing --- packages/uniforms/__tests__/BaseForm.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/uniforms/__tests__/BaseForm.js b/packages/uniforms/__tests__/BaseForm.js index 8609e8364..817c972fc 100644 --- a/packages/uniforms/__tests__/BaseForm.js +++ b/packages/uniforms/__tests__/BaseForm.js @@ -66,6 +66,7 @@ describe('BaseForm', () => { expect(context.uniforms).toHaveProperty('state', expect.any(Object)); expect(context.uniforms.state).toHaveProperty('changed', false); expect(context.uniforms.state).toHaveProperty('changedMap', {}); + expect(context.uniforms.state).toHaveProperty('submitting', false); expect(context.uniforms.state).toHaveProperty('label', true); expect(context.uniforms.state).toHaveProperty('disabled', false); expect(context.uniforms.state).toHaveProperty('placeholder', false); @@ -313,14 +314,13 @@ describe('BaseForm', () => { ); wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); expect(onSubmitSuccess).toHaveBeenCalledTimes(1); expect(onSubmitSuccess).toHaveBeenLastCalledWith(onSubmitValue); }); - it('calls `onSubmitFailure` with the thrown error when `onSubmit` rejects', async () => { + it('calls `onSubmitFailure` with the error when `onSubmit` rejects', async () => { const onSubmitError = 'error'; onSubmit.mockReturnValueOnce(Promise.reject(onSubmitError)); @@ -329,7 +329,21 @@ describe('BaseForm', () => { ); wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + expect(onSubmitFailure).toHaveBeenCalledTimes(1); + expect(onSubmitFailure).toHaveBeenLastCalledWith(onSubmitError); + }); + it('calls `onSubmitFailure` with the error when `onSubmit` throws', async () => { + const onSubmitError = 'error'; + wrapper.setProps({ + onSubmit () { + throw onSubmitError; + } + }); + + wrapper.find('form').simulate('submit'); await new Promise(resolve => process.nextTick(resolve)); expect(onSubmitFailure).toHaveBeenCalledTimes(1); From b65eff02c40e2e69b155663343800347f1915d8b Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Wed, 18 Jul 2018 13:55:04 +0800 Subject: [PATCH 08/41] Fix ordering of new `filterDOMProps` `unwantedProp` --- packages/uniforms/src/filterDOMProps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uniforms/src/filterDOMProps.js b/packages/uniforms/src/filterDOMProps.js index 1848c1801..ab3be467e 100644 --- a/packages/uniforms/src/filterDOMProps.js +++ b/packages/uniforms/src/filterDOMProps.js @@ -4,7 +4,6 @@ const unwantedProps = [ // These props are provided by BaseField 'changed', 'changedMap', - 'submitting', 'disabled', 'error', 'errorMessage', @@ -21,6 +20,7 @@ const unwantedProps = [ 'parent', 'placeholder', 'showInlineError', + 'submitting', 'transform', 'value', From cb10d5e2530370d9a3a6cd35aadf5823a412bf88 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Wed, 18 Jul 2018 13:58:28 +0800 Subject: [PATCH 09/41] Initialise `submitting` to false from the beginning instead of following the pattern used with `changed` --- packages/uniforms/src/BaseForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index c75329a8f..a331a674e 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -86,7 +86,7 @@ export default class BaseForm extends Component { bridge: createSchemaBridge(this.props.schema), changed: null, changedMap: {}, - submitting: null, + submitting: false, resetCount: 0 }; @@ -158,7 +158,7 @@ export default class BaseForm extends Component { } componentWillMount () { - this.setState(() => ({}), () => this.setState(() => ({changed: false, changedMap: {}, submitting: false}))); + this.setState(() => ({}), () => this.setState(() => ({changed: false, changedMap: {}}))); } componentWillReceiveProps ({schema}) { From 70cc1e8cb26a8d9f30b650ba1c3aa59d8b78d077 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Wed, 18 Jul 2018 14:02:30 +0800 Subject: [PATCH 10/41] Use promise `finally` instead of `then` It turns out the pollyfil that we are using supports it. --- packages/uniforms/src/BaseForm.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index a331a674e..9b91f9237 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -255,16 +255,7 @@ export default class BaseForm extends Component { // Do not change the `submitting` state if onSubmit is not async if (res && isFunction(res.then)) { this.setState({submitting: true}); - submitting = submitting.then( - val => { - this.setState({submitting: false}); - return val; - }, - err => { - this.setState({submitting: false}); - throw err; - } - ); + submitting = submitting.finally(() => this.setState({submitting: false})); } } catch (error) { submitting = Promise.reject(error); From 89668b8bf65855bad41df1fd0d917d54f0be6fed Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Wed, 18 Jul 2018 14:02:54 +0800 Subject: [PATCH 11/41] Rename `res` to `result` --- packages/uniforms/src/BaseForm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index 9b91f9237..100559803 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -249,11 +249,11 @@ export default class BaseForm extends Component { let submitting; try { - const res = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); - submitting = Promise.resolve(res); + const result = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); + submitting = Promise.resolve(result); // Do not change the `submitting` state if onSubmit is not async - if (res && isFunction(res.then)) { + if (result && isFunction(result.then)) { this.setState({submitting: true}); submitting = submitting.finally(() => this.setState({submitting: false})); } From a81fbb22f5980db57550cd2bd742361be41fe3b8 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Thu, 19 Jul 2018 13:57:30 +0800 Subject: [PATCH 12/41] Revert "Catch errors (synchronously) thrown by `onSubmit`" This reverts commit 53a969021894545843c43425b56240940c11fd72. --- packages/uniforms/src/BaseForm.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index 100559803..eb017874c 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -247,18 +247,13 @@ export default class BaseForm extends Component { event.stopPropagation(); } - let submitting; - try { - const result = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); - submitting = Promise.resolve(result); - - // Do not change the `submitting` state if onSubmit is not async - if (result && isFunction(result.then)) { - this.setState({submitting: true}); - submitting = submitting.finally(() => this.setState({submitting: false})); - } - } catch (error) { - submitting = Promise.reject(error); + const result = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); + let submitting = Promise.resolve(result); + + // Do not change the `submitting` state if onSubmit is not async + if (result && isFunction(result.then)) { + this.setState({submitting: true}); + submitting = submitting.finally(() => this.setState({submitting: false})); } return submitting.then( From be04d7945c36d87a3158d60cb851b0daccea2b27 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Thu, 19 Jul 2018 13:57:59 +0800 Subject: [PATCH 13/41] Revert "Add test case for `onSubmit` throwing" This reverts commit bed378d2dcb5f5fd3f6c3dcf68b7cc720eb09bb5. --- packages/uniforms/__tests__/BaseForm.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/uniforms/__tests__/BaseForm.js b/packages/uniforms/__tests__/BaseForm.js index 817c972fc..8609e8364 100644 --- a/packages/uniforms/__tests__/BaseForm.js +++ b/packages/uniforms/__tests__/BaseForm.js @@ -66,7 +66,6 @@ describe('BaseForm', () => { expect(context.uniforms).toHaveProperty('state', expect.any(Object)); expect(context.uniforms.state).toHaveProperty('changed', false); expect(context.uniforms.state).toHaveProperty('changedMap', {}); - expect(context.uniforms.state).toHaveProperty('submitting', false); expect(context.uniforms.state).toHaveProperty('label', true); expect(context.uniforms.state).toHaveProperty('disabled', false); expect(context.uniforms.state).toHaveProperty('placeholder', false); @@ -314,13 +313,14 @@ describe('BaseForm', () => { ); wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); expect(onSubmitSuccess).toHaveBeenCalledTimes(1); expect(onSubmitSuccess).toHaveBeenLastCalledWith(onSubmitValue); }); - it('calls `onSubmitFailure` with the error when `onSubmit` rejects', async () => { + it('calls `onSubmitFailure` with the thrown error when `onSubmit` rejects', async () => { const onSubmitError = 'error'; onSubmit.mockReturnValueOnce(Promise.reject(onSubmitError)); @@ -329,21 +329,7 @@ describe('BaseForm', () => { ); wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmitFailure).toHaveBeenCalledTimes(1); - expect(onSubmitFailure).toHaveBeenLastCalledWith(onSubmitError); - }); - it('calls `onSubmitFailure` with the error when `onSubmit` throws', async () => { - const onSubmitError = 'error'; - wrapper.setProps({ - onSubmit () { - throw onSubmitError; - } - }); - - wrapper.find('form').simulate('submit'); await new Promise(resolve => process.nextTick(resolve)); expect(onSubmitFailure).toHaveBeenCalledTimes(1); From 5bfc0604300b3d8c4b0962ad3588874ffb1d83ce Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Thu, 19 Jul 2018 14:01:02 +0800 Subject: [PATCH 14/41] Include reason in comment about conditionally setting `submitting` state --- packages/uniforms/src/BaseForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index eb017874c..6744c4159 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -250,7 +250,7 @@ export default class BaseForm extends Component { const result = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); let submitting = Promise.resolve(result); - // Do not change the `submitting` state if onSubmit is not async + // Do not change the `submitting` state if onSubmit is not async so we don't cause an unnecessary re-render if (result && isFunction(result.then)) { this.setState({submitting: true}); submitting = submitting.finally(() => this.setState({submitting: false})); From 6b839a9360d08af12c51613a0ed71dc5a5557a3c Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Fri, 20 Jul 2018 14:09:37 +0800 Subject: [PATCH 15/41] Re-add check for `submitting` in the default context test --- packages/uniforms/__tests__/BaseForm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/uniforms/__tests__/BaseForm.js b/packages/uniforms/__tests__/BaseForm.js index 8609e8364..2f02a0329 100644 --- a/packages/uniforms/__tests__/BaseForm.js +++ b/packages/uniforms/__tests__/BaseForm.js @@ -66,6 +66,7 @@ describe('BaseForm', () => { expect(context.uniforms).toHaveProperty('state', expect.any(Object)); expect(context.uniforms.state).toHaveProperty('changed', false); expect(context.uniforms.state).toHaveProperty('changedMap', {}); + expect(context.uniforms.state).toHaveProperty('submitting', false); expect(context.uniforms.state).toHaveProperty('label', true); expect(context.uniforms.state).toHaveProperty('disabled', false); expect(context.uniforms.state).toHaveProperty('placeholder', false); From 4eee44a65223489a74abd3691fd04b5e6419408c Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Fri, 20 Jul 2018 16:13:31 +0800 Subject: [PATCH 16/41] Rewrite tests to do with changing props The existing collection of these tests was almost impossible to follow. --- packages/uniforms/__tests__/ValidatedForm.js | 152 +++++++++---------- packages/uniforms/src/ValidatedForm.js | 8 +- 2 files changed, 82 insertions(+), 78 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 3513fe3a7..5eb1ca297 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -75,67 +75,6 @@ describe('ValidatedForm', () => { expect(onSubmit).not.toBeCalled(); }); - - it('revalidates with new model only if required', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model}); - - expect(validator).not.toBeCalled(); - }); - - it('revalidates with new model', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model}); - - expect(validator).toHaveBeenCalledTimes(1); - }); - - it('revalidates with new model only when changed', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model}); - - expect(validator).not.toBeCalled(); - }); - - it('revalidates with new validator only if required', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model, validator: {}}); - - expect(validator).toHaveBeenCalledTimes(1); - }); - - it('revalidates with new validator', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model, validator: {}}); - - expect(validator).not.toBeCalled(); - }); - it('validates (onChange)', () => { const wrapper = mount( @@ -347,26 +286,85 @@ describe('ValidatedForm', () => { }); }); - describe('when props changed', () => { - it('calls correct validator', () => { - const wrapper = mount( - - ); + describe('when props are changed', () => { + const anotherModel = {x: 2}; - const alternativeValidator = jest.fn(); - const alternativeSchema = { - getDefinition () {}, - messageForError () {}, - objectKeys () {}, - validator: () => alternativeValidator - }; + describe('in "onChange" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount( + + ); + }); - wrapper.setProps({schema: alternativeSchema}); + it('does not revalidate arbitrarily', () => { + console.log('arb', wrapper); + wrapper.setProps({anything: 'anything'}); - wrapper.find('form').simulate('submit'); + expect(validator).not.toBeCalled(); + }); + + it('revalidates if `model` changes', () => { + wrapper.setProps({model: anotherModel}); + + expect(validator).toHaveBeenCalledTimes(1); + }); + + it('revalidates if `validator` changes', () => { + wrapper.setProps({validator: {}}); - expect(validator).toHaveBeenCalledTimes(0); - expect(alternativeValidator).toHaveBeenCalledTimes(1); + expect(validator).toHaveBeenCalledTimes(1); + }); }); + + describe('in "onSubmit" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount( + + ); + }); + + it('does not revalidate', () => { + wrapper.setProps({model: anotherModel, validator: {}}); + + expect(validator).not.toBeCalled(); + }); + }); + + describe('in any mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount( + + ); + }); + + // it('does not get a new validator arbitrarily', () => { + // ... + // }); + + // it('gets a new validator if `validator` changes', () => { + // ... + // }); + + it('calls the new validator if `schema` changes', () => { + const alternativeValidator = jest.fn(); + const alternativeSchema = { + getDefinition () {}, + messageForError () {}, + objectKeys () {}, + validator: () => alternativeValidator + }; + + wrapper.setProps({schema: alternativeSchema}); + + wrapper.find('form').simulate('submit'); + + expect(validator).not.toBeCalled(); + expect(alternativeValidator).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 21f2db79b..57b57a462 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -41,6 +41,7 @@ const Validated = parent => class extends parent { error: null, validate: false, + validating: false, validator: this .getChildContextSchema() .getValidator(this.props.validator) @@ -59,6 +60,7 @@ const Validated = parent => class extends parent { onValidate, // eslint-disable-line no-unused-vars validator, // eslint-disable-line no-unused-vars validate, // eslint-disable-line no-unused-vars + // validating, // eslist-disable-line no-unused-vars ...props } = super.getNativeFormProps(); @@ -150,10 +152,14 @@ const Validated = parent => class extends parent { catched = error; } + this.setState({validating: true}); return new Promise((resolve, reject) => { this.props.onValidate(model, catched, (error = catched) => { // Do not copy error from props to state. - this.setState(() => ({error: error === this.props.error ? null : error}), () => { + this.setState(() => ({ + error: error === this.props.error ? null : error, + validating: false + }), () => { if (error) { reject(error); } else { From edefb1d47ab5c31f3612fc8f8b4b0c4a007dadd5 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Wed, 25 Jul 2018 14:34:45 +0800 Subject: [PATCH 17/41] Re-organise and -write tests, grouping them by event --- .gitignore | 4 + packages/uniforms/__tests__/ValidatedForm.js | 278 ++++++------------- 2 files changed, 93 insertions(+), 189 deletions(-) diff --git a/.gitignore b/.gitignore index fe165e5fa..4ac473524 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ lerna-debug.log packages/*/*.js packages/*/node_modules + + +.idea/* +*package-lock.json diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 5eb1ca297..6eb52bf81 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -1,3 +1,5 @@ +/* eslint "no-console": 1 */ + import React from 'react'; import {mount} from 'enzyme'; @@ -34,235 +36,136 @@ describe('ValidatedForm', () => { }); describe('when submitted', () => { - it('calls `onSubmit` when valid', async () => { - const wrapper = mount( + let wrapper; + beforeEach(() => { + wrapper = mount( ); + }); + it('calls `onSubmit` when validation succeeds', async () => { wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); expect(onSubmit).toHaveBeenCalledTimes(1); }); - it('calls `onSubmit` with correct model', async () => { - const wrapper = mount( - - ); - + it('skips `onSubmit` when validation fails', async () => { + validator.mockImplementation(() => { + throw error; + }); wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - expect(onSubmit).toHaveBeenLastCalledWith(model); + expect(onSubmit).not.toBeCalled(); }); - it('skips `onSubmit` when invalid', async () => { - const wrapper = mount( - - ); - + it('updates error state with async errors from `onSubmit`', async () => { + onSubmit.mockImplementationOnce(() => Promise.reject(error)); wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - expect(onSubmit).not.toBeCalled(); + expect(onSubmit).toHaveBeenCalled(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); - it('validates (onChange)', () => { - const wrapper = mount( - - ); + }); - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + describe('on change', () => { + describe('in "onChange" mode', () => { + it('validates', () => { + const wrapper = mount( + + ); - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenLastCalledWith('key', 'value'); - }); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - it('validates (onChangeAfterSubmit)', async () => { - validator.mockImplementationOnce(() => { - throw new Error(); + expect(validator).toHaveBeenCalledTimes(1); }); + }); - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).not.toBeCalled(); - expect(onSubmit).not.toBeCalled(); - - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(validator).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenLastCalledWith('key', 'value'); - - wrapper.find('form').simulate('submit'); + describe('in "onSubmit" mode', () => { + it('does not validate', () => { + const wrapper = mount( + + ); - await new Promise(resolve => process.nextTick(resolve)); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - expect(validator).toHaveBeenCalledTimes(3); - expect(onSubmit).toHaveBeenCalledTimes(1); - expect(onSubmit).toHaveBeenLastCalledWith(model); + expect(validator).not.toHaveBeenCalled(); + }); }); - it('validates (onSubmit)', async () => { - validator.mockImplementationOnce(() => { - throw new Error(); + describe('in "onChangeAfterSubmit" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount( + + ); }); - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).not.toBeCalled(); - expect(onSubmit).not.toBeCalled(); - - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenLastCalledWith('key', 'value'); - - wrapper.find('form').simulate('submit'); + it('does not validates before submit', () => { + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + expect(validator).not.toHaveBeenCalled(); + }); - await new Promise(resolve => process.nextTick(resolve)); + it('validates after submit', async () => { + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalledTimes(2); - expect(onSubmit).toHaveBeenCalledTimes(1); - expect(onSubmit).toHaveBeenLastCalledWith(model); + validator.mockReset(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + expect(validator).toHaveBeenCalledTimes(1); + }); }); }); - describe('when validated', () => { - it('calls `onValidate`', () => { - const wrapper = mount( - - ); + describe('on validation', () => { + let wrapper, form; - wrapper.instance().getChildContext().uniforms.onChange('a', 2); + beforeEach(async () => { + wrapper = mount( + + ); + form = wrapper.instance(); + }); + it('validates (when `.validate` is called)', () => { + form.validate(); expect(validator).toHaveBeenCalledTimes(1); + }); + + it('correctly calls `onValidate` when validation succeeds', () => { + form.validate(); expect(onValidate).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenLastCalledWith({a: 2}, null, expect.any(Function)); + expect(onValidate).toHaveBeenLastCalledWith(model, null, expect.any(Function)); }); - it('calls `onValidate` (error)', () => { + it('correctly calls `onValidate` when validation fails ', () => { validator.mockImplementation(() => { throw error; }); + form.validate(); - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('a', 2); - - expect(validator).toHaveBeenCalledTimes(1); expect(onValidate).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenLastCalledWith({a: 2}, error, expect.any(Function)); + expect(onValidate).toHaveBeenLastCalledWith(model, error, expect.any(Function)); }); - it('calls `onValidate` (`modelTransform`)', () => { - const modelTransform = (mode, model) => { - if (mode === 'validate') { - return {...model, b: 1}; - } - - return model; - }; - - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('a', 2); - - expect(validator).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenLastCalledWith({a: 2, b: 1}, null, expect.any(Function)); - }); - - it('works with async errors from `onSubmit`', async () => { - onSubmit.mockImplementationOnce(() => Promise.reject(new Error())); - - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); - }); - - it('works with async errors from `onValidate`', () => { - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('a', 2); - - const callArgs = onValidate.mock.calls[0]; - callArgs[2](error); + it('updates error state with async errors from `onValidate`', () => { + onValidate.mockImplementationOnce((a, b, next) => { + next(error); + }); + form.validate(); expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); - it('works with no errors from `onValidate`', () => { - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('a', 2); - - const callArgs = onValidate.mock.calls[0]; - callArgs[2](); - - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', null); + it('uses `modelTransform`s `validate` mode', () => { + const transformedModel = {b: 1}; + const modelTransform = (mode, model) => mode === 'validate' ? transformedModel : model; + wrapper.setProps({modelTransform}); + form.validate(); + expect(onValidate).toHaveBeenLastCalledWith(transformedModel, null, expect.any(Function)); + expect(validator).toHaveBeenLastCalledWith(transformedModel); }); }); @@ -277,11 +180,9 @@ describe('ValidatedForm', () => { ); wrapper.find('form').simulate('submit'); - expect(wrapper.instance().getChildContext().uniforms.error).toBeTruthy(); wrapper.instance().reset(); - expect(wrapper.instance().getChildContext().uniforms.error).toBeNull(); }); }); @@ -340,15 +241,15 @@ describe('ValidatedForm', () => { ); }); - // it('does not get a new validator arbitrarily', () => { - // ... - // }); + it.skip('Reuses the validator between validations', () => { + // ... + }); - // it('gets a new validator if `validator` changes', () => { - // ... - // }); + it.skip('uses the new validator settings if `validator` changes', () => { + // ... + }); - it('calls the new validator if `schema` changes', () => { + it('uses the new validator if `schema` changes', () => { const alternativeValidator = jest.fn(); const alternativeSchema = { getDefinition () {}, @@ -356,9 +257,7 @@ describe('ValidatedForm', () => { objectKeys () {}, validator: () => alternativeValidator }; - wrapper.setProps({schema: alternativeSchema}); - wrapper.find('form').simulate('submit'); expect(validator).not.toBeCalled(); @@ -367,4 +266,5 @@ describe('ValidatedForm', () => { }); }); + }); From 2d38fc59010828ac02dceff88225aacb79e7a198 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Wed, 25 Jul 2018 15:04:22 +0800 Subject: [PATCH 18/41] Add tests around interactions with schema validator --- packages/uniforms/__tests__/ValidatedForm.js | 41 ++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 6eb52bf81..a8a8802b0 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -11,11 +11,7 @@ jest.mock('meteor/check'); describe('ValidatedForm', () => { const onChange = jest.fn(); const onSubmit = jest.fn(); - const onValidate = jest.fn((model, error, next) => { - if (error) return; - - next(); - }); + const onValidate = jest.fn((model, error, next) => next()); const validator = jest.fn(); const error = new Error(); @@ -134,6 +130,22 @@ describe('ValidatedForm', () => { expect(validator).toHaveBeenCalledTimes(1); }); + it('correctly calls `validator`', () => { + form.validate(); + expect(validator).toHaveBeenCalledTimes(1); + expect(validator).toHaveBeenLastCalledWith(model); + }); + + it('updates error state with errors from `validator`', async () => { + validator.mockImplementationOnce(() => { + throw error; + }); + form.validate(); + await new Promise(resolve => process.nextTick(resolve)); + + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); + }); + it('correctly calls `onValidate` when validation succeeds', () => { form.validate(); expect(onValidate).toHaveBeenCalledTimes(1); @@ -150,22 +162,37 @@ describe('ValidatedForm', () => { expect(onValidate).toHaveBeenLastCalledWith(model, error, expect.any(Function)); }); - it('updates error state with async errors from `onValidate`', () => { + it('lets `onValidate` suppress `validator` errors', async () => { + validator.mockImplementationOnce(() => { + throw error; + }); + onValidate.mockImplementationOnce((a, b, next) => { + next(null); + }); + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('updates error state with async errors from `onValidate`', async () => { onValidate.mockImplementationOnce((a, b, next) => { next(error); }); form.validate(); + await new Promise(resolve => process.nextTick(resolve)); expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); + it('uses `modelTransform`s `validate` mode', () => { const transformedModel = {b: 1}; const modelTransform = (mode, model) => mode === 'validate' ? transformedModel : model; wrapper.setProps({modelTransform}); form.validate(); - expect(onValidate).toHaveBeenLastCalledWith(transformedModel, null, expect.any(Function)); expect(validator).toHaveBeenLastCalledWith(transformedModel); + expect(onValidate).toHaveBeenLastCalledWith(transformedModel, null, expect.any(Function)); }); }); From 43b0fa17114e39cbd9ab5b2c9d867bd2cace2586 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Sat, 8 Sep 2018 09:19:07 +0800 Subject: [PATCH 19/41] Reformat and reorder --- packages/uniforms/__tests__/ValidatedForm.js | 192 ++++++++----------- 1 file changed, 84 insertions(+), 108 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index a8a8802b0..4b00a1c7c 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -31,97 +31,11 @@ describe('ValidatedForm', () => { validator.mockReset(); }); - describe('when submitted', () => { - let wrapper; - beforeEach(() => { - wrapper = mount( - - ); - }); - - it('calls `onSubmit` when validation succeeds', async () => { - wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmit).toHaveBeenCalledTimes(1); - }); - - it('skips `onSubmit` when validation fails', async () => { - validator.mockImplementation(() => { - throw error; - }); - wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmit).not.toBeCalled(); - }); - - it('updates error state with async errors from `onSubmit`', async () => { - onSubmit.mockImplementationOnce(() => Promise.reject(error)); - wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmit).toHaveBeenCalled(); - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); - }); - }); - - describe('on change', () => { - describe('in "onChange" mode', () => { - it('validates', () => { - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - - expect(validator).toHaveBeenCalledTimes(1); - }); - }); - - describe('in "onSubmit" mode', () => { - it('does not validate', () => { - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - - expect(validator).not.toHaveBeenCalled(); - }); - }); - - describe('in "onChangeAfterSubmit" mode', () => { - let wrapper; - beforeEach(() => { - wrapper = mount( - - ); - }); - - it('does not validates before submit', () => { - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - expect(validator).not.toHaveBeenCalled(); - }); - - it('validates after submit', async () => { - wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - - validator.mockReset(); - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - expect(validator).toHaveBeenCalledTimes(1); - }); - }); - }); - describe('on validation', () => { let wrapper, form; beforeEach(async () => { - wrapper = mount( - - ); + wrapper = mount(); form = wrapper.instance(); }); @@ -140,7 +54,7 @@ describe('ValidatedForm', () => { validator.mockImplementationOnce(() => { throw error; }); - form.validate(); + form.validate().catch(() => {}); await new Promise(resolve => process.nextTick(resolve)); expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); @@ -166,7 +80,7 @@ describe('ValidatedForm', () => { validator.mockImplementationOnce(() => { throw error; }); - onValidate.mockImplementationOnce((a, b, next) => { + onValidate.mockImplementationOnce((m, e, next) => { next(null); }); wrapper.find('form').simulate('submit'); @@ -176,7 +90,7 @@ describe('ValidatedForm', () => { }); it('updates error state with async errors from `onValidate`', async () => { - onValidate.mockImplementationOnce((a, b, next) => { + onValidate.mockImplementationOnce((m, e, next) => { next(error); }); form.validate(); @@ -196,16 +110,87 @@ describe('ValidatedForm', () => { }); }); - describe('when reset', () => { + describe('when submitted', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); + + it('calls `onSubmit` when validation succeeds', async () => { + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + expect(onSubmit).toHaveBeenCalledTimes(1); + }); + + it('skips `onSubmit` when validation fails', async () => { + validator.mockImplementation(() => { + throw error; + }); + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + expect(onSubmit).not.toBeCalled(); + }); + + it('updates error state with async errors from `onSubmit`', async () => { + onSubmit.mockImplementationOnce(() => Promise.reject(error)); + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + expect(onSubmit).toHaveBeenCalled(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); + }); + }); + + describe('on change', () => { + describe('in "onChange" mode', () => { + it('validates', () => { + const wrapper = mount(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + + expect(validator).toHaveBeenCalledTimes(1); + }); + }); + + describe('in "onSubmit" mode', () => { + it('does not validate', () => { + const wrapper = mount(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + + expect(validator).not.toHaveBeenCalled(); + }); + }); + + describe('in "onChangeAfterSubmit" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); + + it('does not validates before submit', () => { + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + expect(validator).not.toHaveBeenCalled(); + }); + + it('validates after submit', async () => { + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + validator.mockReset(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + expect(validator).toHaveBeenCalledTimes(1); + }); + }); + }); + + + describe('on reset', () => { it('removes `error`', () => { + const wrapper = mount(); validator.mockImplementationOnce(() => { throw new Error(); }); - - const wrapper = mount( - - ); - wrapper.find('form').simulate('submit'); expect(wrapper.instance().getChildContext().uniforms.error).toBeTruthy(); @@ -220,21 +205,16 @@ describe('ValidatedForm', () => { describe('in "onChange" mode', () => { let wrapper; beforeEach(() => { - wrapper = mount( - - ); + wrapper = mount(); }); it('does not revalidate arbitrarily', () => { - console.log('arb', wrapper); wrapper.setProps({anything: 'anything'}); - expect(validator).not.toBeCalled(); }); it('revalidates if `model` changes', () => { wrapper.setProps({model: anotherModel}); - expect(validator).toHaveBeenCalledTimes(1); }); @@ -248,9 +228,7 @@ describe('ValidatedForm', () => { describe('in "onSubmit" mode', () => { let wrapper; beforeEach(() => { - wrapper = mount( - - ); + wrapper = mount(); }); it('does not revalidate', () => { @@ -263,9 +241,7 @@ describe('ValidatedForm', () => { describe('in any mode', () => { let wrapper; beforeEach(() => { - wrapper = mount( - - ); + wrapper = mount(); }); it.skip('Reuses the validator between validations', () => { From 86690f232937059d10722be6d268666694c87eb3 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Sat, 8 Sep 2018 09:19:31 +0800 Subject: [PATCH 20/41] Expand prop changing tests --- packages/uniforms/__tests__/ValidatedForm.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 4b00a1c7c..428eee53f 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -220,7 +220,11 @@ describe('ValidatedForm', () => { it('revalidates if `validator` changes', () => { wrapper.setProps({validator: {}}); + expect(validator).toHaveBeenCalledTimes(1); + }); + it('revalidate if `schema` changes', () => { + wrapper.setProps({schema: {...schema}}); expect(validator).toHaveBeenCalledTimes(1); }); }); @@ -231,9 +235,18 @@ describe('ValidatedForm', () => { wrapper = mount(); }); - it('does not revalidate', () => { - wrapper.setProps({model: anotherModel, validator: {}}); + it('does not revalidate when `model` changes', () => { + wrapper.setProps({model: {}}); + expect(validator).not.toBeCalled(); + }); + + it('does not revalidate when validator `options` change', () => { + wrapper.setProps({validator: {}}); + expect(validator).not.toBeCalled(); + }); + it('does not revalidate when `schema` changes', () => { + wrapper.setProps({schema: {...schema}}); expect(validator).not.toBeCalled(); }); }); From fa4dc6bd97b6b6c8f821b4290e625e7ff0049d67 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Sat, 8 Sep 2018 09:31:50 +0800 Subject: [PATCH 21/41] Fix test "lets `onValidate` suppress `validator` errors" (I think) This inverts the logic of the test, causing it to fail, but I can't see how the test impl differs from the description in /API.md#validatedform (line 1189). --- packages/uniforms/__tests__/ValidatedForm.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 428eee53f..64c788c71 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -83,10 +83,11 @@ describe('ValidatedForm', () => { onValidate.mockImplementationOnce((m, e, next) => { next(null); }); - wrapper.find('form').simulate('submit'); await new Promise(resolve => process.nextTick(resolve)); - expect(onSubmit).not.toHaveBeenCalled(); + expect(validator).toHaveBeenCalled(); + expect(onValidate).toHaveBeenCalled(); + expect(onSubmit).toHaveBeenCalled(); }); it('updates error state with async errors from `onValidate`', async () => { From b789830ee10fbc4a9f74da8647435306dda1654d Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Sat, 8 Sep 2018 09:44:11 +0800 Subject: [PATCH 22/41] Add missing line --- packages/uniforms/__tests__/ValidatedForm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 64c788c71..83290ff33 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -83,6 +83,7 @@ describe('ValidatedForm', () => { onValidate.mockImplementationOnce((m, e, next) => { next(null); }); + form.validate(); await new Promise(resolve => process.nextTick(resolve)); expect(validator).toHaveBeenCalled(); From 106a18d2b2b8aa25f5dce637d2b934e54bb89e8e Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Sat, 8 Sep 2018 10:01:43 +0800 Subject: [PATCH 23/41] Rewrite ValidatedForm test suite --- packages/uniforms/__tests__/ValidatedForm.js | 440 ++++++++----------- 1 file changed, 177 insertions(+), 263 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 3513fe3a7..7fc33cb0e 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -9,11 +9,7 @@ jest.mock('meteor/check'); describe('ValidatedForm', () => { const onChange = jest.fn(); const onSubmit = jest.fn(); - const onValidate = jest.fn((model, error, next) => { - if (error) return; - - next(); - }); + const onValidate = jest.fn((model, error, next) => next()); const validator = jest.fn(); const error = new Error(); @@ -33,340 +29,258 @@ describe('ValidatedForm', () => { validator.mockReset(); }); - describe('when submitted', () => { - it('calls `onSubmit` when valid', async () => { - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmit).toHaveBeenCalledTimes(1); - }); - - it('calls `onSubmit` with correct model', async () => { - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmit).toHaveBeenLastCalledWith(model); - }); - - it('skips `onSubmit` when invalid', async () => { - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(onSubmit).not.toBeCalled(); - }); - - it('revalidates with new model only if required', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); + describe('on validation', () => { + let wrapper, form; - wrapper.setProps({model}); - - expect(validator).not.toBeCalled(); + beforeEach(async () => { + wrapper = mount(); + form = wrapper.instance(); }); - it('revalidates with new model', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model}); - + it('validates (when `.validate` is called)', () => { + form.validate(); expect(validator).toHaveBeenCalledTimes(1); }); - it('revalidates with new model only when changed', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model}); - - expect(validator).not.toBeCalled(); - }); - - it('revalidates with new validator only if required', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model, validator: {}}); - + it('correctly calls `validator`', () => { + form.validate(); expect(validator).toHaveBeenCalledTimes(1); + expect(validator).toHaveBeenLastCalledWith(model); }); - it('revalidates with new validator', () => { - const wrapper = mount( - - ); - - expect(validator).not.toBeCalled(); - - wrapper.setProps({model, validator: {}}); + it('updates error state with errors from `validator`', async () => { + validator.mockImplementationOnce(() => { + throw error; + }); + form.validate().catch(() => {}); + await new Promise(resolve => process.nextTick(resolve)); - expect(validator).not.toBeCalled(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); - it('validates (onChange)', () => { - const wrapper = mount( - - ); + it('correctly calls `onValidate` when validation succeeds', () => { + form.validate(); + expect(onValidate).toHaveBeenCalledTimes(1); + expect(onValidate).toHaveBeenLastCalledWith(model, null, expect.any(Function)); + }); - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + it('correctly calls `onValidate` when validation fails ', () => { + validator.mockImplementation(() => { + throw error; + }); + form.validate(); - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenLastCalledWith('key', 'value'); + expect(onValidate).toHaveBeenCalledTimes(1); + expect(onValidate).toHaveBeenLastCalledWith(model, error, expect.any(Function)); }); - it('validates (onChangeAfterSubmit)', async () => { + it('lets `onValidate` suppress `validator` errors', async () => { validator.mockImplementationOnce(() => { - throw new Error(); + throw error; }); + onValidate.mockImplementationOnce((m, e, next) => { + next(null); + }); + form.validate(); + await new Promise(resolve => process.nextTick(resolve)); - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); + expect(validator).toHaveBeenCalled(); + expect(onValidate).toHaveBeenCalled(); + expect(onSubmit).toHaveBeenCalled(); + }); + it('updates error state with async errors from `onValidate`', async () => { + onValidate.mockImplementationOnce((m, e, next) => { + next(error); + }); + form.validate(); await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).not.toBeCalled(); - expect(onSubmit).not.toBeCalled(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); + }); - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - await new Promise(resolve => process.nextTick(resolve)); + it('uses `modelTransform`s `validate` mode', () => { + const transformedModel = {b: 1}; + const modelTransform = (mode, model) => mode === 'validate' ? transformedModel : model; + wrapper.setProps({modelTransform}); + form.validate(); + expect(validator).toHaveBeenLastCalledWith(transformedModel); + expect(onValidate).toHaveBeenLastCalledWith(transformedModel, null, expect.any(Function)); + }); + }); - expect(validator).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenLastCalledWith('key', 'value'); + describe('when submitted', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); + it('calls `onSubmit` when validation succeeds', async () => { wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalledTimes(3); expect(onSubmit).toHaveBeenCalledTimes(1); - expect(onSubmit).toHaveBeenLastCalledWith(model); }); - it('validates (onSubmit)', async () => { - validator.mockImplementationOnce(() => { - throw new Error(); + it('skips `onSubmit` when validation fails', async () => { + validator.mockImplementation(() => { + throw error; }); - - const wrapper = mount( - - ); - wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).not.toBeCalled(); expect(onSubmit).not.toBeCalled(); + }); - wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - - await new Promise(resolve => process.nextTick(resolve)); - - expect(validator).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenLastCalledWith('key', 'value'); - + it('updates error state with async errors from `onSubmit`', async () => { + onSubmit.mockImplementationOnce(() => Promise.reject(error)); wrapper.find('form').simulate('submit'); - await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalledTimes(2); - expect(onSubmit).toHaveBeenCalledTimes(1); - expect(onSubmit).toHaveBeenLastCalledWith(model); + expect(onSubmit).toHaveBeenCalled(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); }); - describe('when validated', () => { - it('calls `onValidate`', () => { - const wrapper = mount( - - ); + describe('on change', () => { + describe('in "onChange" mode', () => { + it('validates', () => { + const wrapper = mount(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - wrapper.instance().getChildContext().uniforms.onChange('a', 2); - - expect(validator).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenLastCalledWith({a: 2}, null, expect.any(Function)); - }); - - it('calls `onValidate` (error)', () => { - validator.mockImplementation(() => { - throw error; + expect(validator).toHaveBeenCalledTimes(1); }); + }); - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('a', 2); + describe('in "onSubmit" mode', () => { + it('does not validate', () => { + const wrapper = mount(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); - expect(validator).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenLastCalledWith({a: 2}, error, expect.any(Function)); + expect(validator).not.toHaveBeenCalled(); + }); }); - it('calls `onValidate` (`modelTransform`)', () => { - const modelTransform = (mode, model) => { - if (mode === 'validate') { - return {...model, b: 1}; - } - - return model; - }; + describe('in "onChangeAfterSubmit" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); - const wrapper = mount( - - ); + it('does not validates before submit', () => { + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + expect(validator).not.toHaveBeenCalled(); + }); - wrapper.instance().getChildContext().uniforms.onChange('a', 2); + it('validates after submit', async () => { + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenCalledTimes(1); - expect(onValidate).toHaveBeenLastCalledWith({a: 2, b: 1}, null, expect.any(Function)); + validator.mockReset(); + wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); + expect(validator).toHaveBeenCalledTimes(1); + }); }); + }); - it('works with async errors from `onSubmit`', async () => { - onSubmit.mockImplementationOnce(() => Promise.reject(new Error())); - - const wrapper = mount( - - ); + describe('on reset', () => { + it('removes `error`', () => { + const wrapper = mount(); + validator.mockImplementationOnce(() => { + throw new Error(); + }); wrapper.find('form').simulate('submit'); + expect(wrapper.instance().getChildContext().uniforms.error).toBeTruthy(); - await new Promise(resolve => process.nextTick(resolve)); - - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); + wrapper.instance().reset(); + expect(wrapper.instance().getChildContext().uniforms.error).toBeNull(); }); + }); - it('works with async errors from `onValidate`', () => { - const wrapper = mount( - - ); - - wrapper.instance().getChildContext().uniforms.onChange('a', 2); - - const callArgs = onValidate.mock.calls[0]; - callArgs[2](error); + describe('when props are changed', () => { + const anotherModel = {x: 2}; - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); - }); + describe('in "onChange" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); - it('works with no errors from `onValidate`', () => { - const wrapper = mount( - - ); + it('does not revalidate arbitrarily', () => { + wrapper.setProps({anything: 'anything'}); + expect(validator).not.toBeCalled(); + }); - wrapper.instance().getChildContext().uniforms.onChange('a', 2); + it('revalidates if `model` changes', () => { + wrapper.setProps({model: anotherModel}); + expect(validator).toHaveBeenCalledTimes(1); + }); - const callArgs = onValidate.mock.calls[0]; - callArgs[2](); + it('revalidates if `validator` changes', () => { + wrapper.setProps({validator: {}}); + expect(validator).toHaveBeenCalledTimes(1); + }); - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', null); + it('revalidate if `schema` changes', () => { + wrapper.setProps({schema: {...schema}}); + expect(validator).toHaveBeenCalledTimes(1); + }); }); - }); - describe('when reset', () => { - it('removes `error`', () => { - validator.mockImplementationOnce(() => { - throw new Error(); + describe('in "onSubmit" mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); }); - const wrapper = mount( - - ); - - wrapper.find('form').simulate('submit'); - - expect(wrapper.instance().getChildContext().uniforms.error).toBeTruthy(); + it('does not revalidate when `model` changes', () => { + wrapper.setProps({model: {}}); + expect(validator).not.toBeCalled(); + }); - wrapper.instance().reset(); + it('does not revalidate when validator `options` change', () => { + wrapper.setProps({validator: {}}); + expect(validator).not.toBeCalled(); + }); - expect(wrapper.instance().getChildContext().uniforms.error).toBeNull(); + it('does not revalidate when `schema` changes', () => { + wrapper.setProps({schema: {...schema}}); + expect(validator).not.toBeCalled(); + }); }); - }); - - describe('when props changed', () => { - it('calls correct validator', () => { - const wrapper = mount( - - ); - const alternativeValidator = jest.fn(); - const alternativeSchema = { - getDefinition () {}, - messageForError () {}, - objectKeys () {}, - validator: () => alternativeValidator - }; + describe('in any mode', () => { + let wrapper; + beforeEach(() => { + wrapper = mount(); + }); - wrapper.setProps({schema: alternativeSchema}); + it.skip('Reuses the validator between validations', () => { + // ... + }); - wrapper.find('form').simulate('submit'); + it.skip('uses the new validator settings if `validator` changes', () => { + // ... + }); - expect(validator).toHaveBeenCalledTimes(0); - expect(alternativeValidator).toHaveBeenCalledTimes(1); + it('uses the new validator if `schema` changes', () => { + const alternativeValidator = jest.fn(); + const alternativeSchema = { + getDefinition () {}, + messageForError () {}, + objectKeys () {}, + validator: () => alternativeValidator + }; + wrapper.setProps({schema: alternativeSchema}); + wrapper.find('form').simulate('submit'); + + expect(validator).not.toBeCalled(); + expect(alternativeValidator).toHaveBeenCalledTimes(1); + }); }); + }); + }); From b4dfccb32a8f5fdc2c807e7088a6885fa86b32a2 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Mon, 10 Sep 2018 14:04:13 +0800 Subject: [PATCH 24/41] Fix code style errors --- packages/uniforms/__tests__/ValidatedForm.js | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 7fc33cb0e..49bfce34f 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -30,10 +30,13 @@ describe('ValidatedForm', () => { }); describe('on validation', () => { - let wrapper, form; + let wrapper; + let form; - beforeEach(async () => { - wrapper = mount(); + beforeEach(() => { + wrapper = mount( + + ); form = wrapper.instance(); }); @@ -78,7 +81,7 @@ describe('ValidatedForm', () => { validator.mockImplementationOnce(() => { throw error; }); - onValidate.mockImplementationOnce((m, e, next) => { + onValidate.mockImplementationOnce((model, existingError, next) => { next(null); }); form.validate(); @@ -90,7 +93,7 @@ describe('ValidatedForm', () => { }); it('updates error state with async errors from `onValidate`', async () => { - onValidate.mockImplementationOnce((m, e, next) => { + onValidate.mockImplementationOnce((model, existingError, next) => { next(error); }); form.validate(); @@ -144,7 +147,7 @@ describe('ValidatedForm', () => { }); describe('on change', () => { - describe('in "onChange" mode', () => { + describe('in `onChange` mode', () => { it('validates', () => { const wrapper = mount(); wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); @@ -153,7 +156,7 @@ describe('ValidatedForm', () => { }); }); - describe('in "onSubmit" mode', () => { + describe('in `onSubmit` mode', () => { it('does not validate', () => { const wrapper = mount(); wrapper.instance().getChildContext().uniforms.onChange('key', 'value'); @@ -162,7 +165,7 @@ describe('ValidatedForm', () => { }); }); - describe('in "onChangeAfterSubmit" mode', () => { + describe('in `onChangeAfterSubmit` mode', () => { let wrapper; beforeEach(() => { wrapper = mount(); @@ -202,7 +205,7 @@ describe('ValidatedForm', () => { describe('when props are changed', () => { const anotherModel = {x: 2}; - describe('in "onChange" mode', () => { + describe('in `onChange` mode', () => { let wrapper; beforeEach(() => { wrapper = mount(); @@ -229,7 +232,7 @@ describe('ValidatedForm', () => { }); }); - describe('in "onSubmit" mode', () => { + describe('in `onSubmit` mode', () => { let wrapper; beforeEach(() => { wrapper = mount(); From a9d7566abec6f81c1e82d968c335cb240ee7b810 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Mon, 10 Sep 2018 14:14:20 +0800 Subject: [PATCH 25/41] Refactor broken test in terms of validation instead of submission The test 'lets `onValidate` suppress `validator` errors' is really about testing `onValidate`, not the submit flow, so now the test is 'leaves error state alone when `onValidate` suppress `validator` errors' (like 'updates error state with async errors from `onValidate`'). --- packages/uniforms/__tests__/ValidatedForm.js | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index 49bfce34f..ea0b1ef85 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -8,7 +8,7 @@ jest.mock('meteor/check'); describe('ValidatedForm', () => { const onChange = jest.fn(); - const onSubmit = jest.fn(); + const onSubmit = jest.fn(async () => {}); const onValidate = jest.fn((model, error, next) => next()); const validator = jest.fn(); @@ -77,29 +77,27 @@ describe('ValidatedForm', () => { expect(onValidate).toHaveBeenLastCalledWith(model, error, expect.any(Function)); }); - it('lets `onValidate` suppress `validator` errors', async () => { - validator.mockImplementationOnce(() => { - throw error; - }); + it('updates error state with async errors from `onValidate`', async () => { onValidate.mockImplementationOnce((model, existingError, next) => { - next(null); + next(error); }); form.validate(); - await new Promise(resolve => process.nextTick(resolve)); - expect(validator).toHaveBeenCalled(); - expect(onValidate).toHaveBeenCalled(); - expect(onSubmit).toHaveBeenCalled(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); - it('updates error state with async errors from `onValidate`', async () => { + it('leaves error state alone when `onValidate` suppress `validator` errors', async () => { + validator.mockImplementationOnce(() => { + throw error; + }); onValidate.mockImplementationOnce((model, existingError, next) => { - next(error); + next(null); }); form.validate(); - await new Promise(resolve => process.nextTick(resolve)); - expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); + expect(validator).toHaveBeenCalled(); + expect(onValidate).toHaveBeenCalled(); + expect(wrapper.instance().getChildContext()).not.toHaveProperty('uniforms.error', error); }); From 18fc7ac76b17f152525891c7429dda6f8fdf924b Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Mon, 10 Sep 2018 14:17:35 +0800 Subject: [PATCH 26/41] There is no need to pass `onSubmit` after all --- packages/uniforms/__tests__/ValidatedForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index ea0b1ef85..c788ae129 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -35,7 +35,7 @@ describe('ValidatedForm', () => { beforeEach(() => { wrapper = mount( - + ); form = wrapper.instance(); }); From e092d049df58a2daced00a154bb290cef3728934 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 17:10:41 +0800 Subject: [PATCH 27/41] Add option for async onValidate and onSubmit to demo Simulating async validation and submission is a useful demonstration of the API --- demo/imports/components/ApplicationPreviewField.js | 10 ++++++++-- demo/imports/components/ApplicationPropsField.js | 2 ++ demo/imports/lib/schema.js | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/demo/imports/components/ApplicationPreviewField.js b/demo/imports/components/ApplicationPreviewField.js index dd8324a9d..3ae1d2812 100644 --- a/demo/imports/components/ApplicationPreviewField.js +++ b/demo/imports/components/ApplicationPreviewField.js @@ -32,8 +32,14 @@ class ApplicationPreview extends Component { const Form = themes[this.props.theme].AutoForm; const link = styles[this.props.theme]; - const props = {...this.props.value}; + const {asyncOnSubmit, asyncOnValidate, ...props} = {...this.props.value}; props.schema = this._schema; + if (asyncOnSubmit) { + props.onSubmit = () => new Promise(resolve => setTimeout(resolve, 1000)); + } + if (asyncOnValidate) { + props.onValidate = (model, error, next) => setTimeout(() => next(), 1000); + } return (
@@ -42,7 +48,7 @@ class ApplicationPreview extends Component { {this.props.errorMessage ? ( ) : ( -
+ )} {this.state.model !== undefined &&
} diff --git a/demo/imports/components/ApplicationPropsField.js b/demo/imports/components/ApplicationPropsField.js index 2d1e3b4b6..36ffbeefe 100644 --- a/demo/imports/components/ApplicationPropsField.js +++ b/demo/imports/components/ApplicationPropsField.js @@ -31,6 +31,8 @@ const ApplicationProps = ({onChange, schema, theme, value}) => { + + diff --git a/demo/imports/lib/schema.js b/demo/imports/lib/schema.js index 39ba96284..633d9cc9e 100644 --- a/demo/imports/lib/schema.js +++ b/demo/imports/lib/schema.js @@ -49,7 +49,9 @@ const schema = new SimpleSchema2({ label: true, placeholder: false, schema: presets[Object.keys(presets)[0]], - showInlineError: false + showInlineError: false, + asyncOnSubmit: false, + asyncOnValidate: false }, uniforms: { schema: new SimpleSchema2({ @@ -59,6 +61,8 @@ const schema = new SimpleSchema2({ label: {optional: true, type: Boolean}, placeholder: {optional: true, type: Boolean}, showInlineError: {optional: true, type: Boolean}, + asyncOnSubmit: {optional: true, type: Boolean, label: 'Async onSubmit (1 sec)'}, + asyncOnValidate: {optional: true, type: Boolean, label: 'Async onValidate (1 sec)'}, schema: { optional: true, From 3515b43fbddabbd0d04babd0c8fbc88fc652742f Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 17:33:15 +0800 Subject: [PATCH 28/41] Add tests for `validating` context attribute --- packages/uniforms/__tests__/ValidatedForm.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index c788ae129..d40c4b7b1 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -100,6 +100,20 @@ describe('ValidatedForm', () => { expect(wrapper.instance().getChildContext()).not.toHaveProperty('uniforms.error', error); }); + it('has `validating` context variable, default `false`', () => { + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.state.validating', false); + }); + + it('sets `validating` `true` while validating', async () => { + onValidate.mockImplementationOnce(() => {}); + form.validate(); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.state.validating', true); + + // Resolve the async validation by calling the third argument of the first call to onValidate. + expect(onValidate).toHaveBeenCalledTimes(1); + onValidate.mock.calls[0][2](); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.state.validating', false); + }); it('uses `modelTransform`s `validate` mode', () => { const transformedModel = {b: 1}; From 033c96369df8d5c1aa330613a50ca371e40d2871 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 17:33:44 +0800 Subject: [PATCH 29/41] Include `validating` state in ValidatedForm childContext --- packages/uniforms/src/ValidatedForm.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 57b57a462..6f6b57a5e 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -55,6 +55,14 @@ const Validated = parent => class extends parent { return super.getChildContextError() || this.state.error; } + getChildContextState () { + return { + ...super.getChildContextState(), + + validating: this.state.validating + }; + } + getNativeFormProps () { const { onValidate, // eslint-disable-line no-unused-vars From 9e9aa6924f411c921a9d17dc09c661e1f10c85fa Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 17:47:21 +0800 Subject: [PATCH 30/41] Revert .gitignore changes --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 4ac473524..fe165e5fa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,3 @@ lerna-debug.log packages/*/*.js packages/*/node_modules - - -.idea/* -*package-lock.json From 551f05325f19a9017ec5bb7dd0c6c475bc88a497 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 17:48:47 +0800 Subject: [PATCH 31/41] Add validating state to the example `SubmitField` impl --- INTRODUCTION.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/INTRODUCTION.md b/INTRODUCTION.md index a85149d6a..35509c5df 100644 --- a/INTRODUCTION.md +++ b/INTRODUCTION.md @@ -949,8 +949,8 @@ import filterDOMProps from 'uniforms/filterDOMProps'; // This field works as follows: render standard submit field and disable it, when // the form is invalid. It's a simplified version of a default SubmitField from // uniforms-unstyled. -const SubmitField = (props, {uniforms: {error, state: {disabled, submitting}}}) => - +const SubmitField = (props, {uniforms: {error, state: {disabled, submitting, validating}}}) => + ; SubmitField.contextTypes = BaseField.contextTypes; From 0d529d1616352cec3f6a0359541bd90becccd633 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 18:10:35 +0800 Subject: [PATCH 32/41] Add `validating` state to `__reset` --- packages/uniforms/src/ValidatedForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 6f6b57a5e..2880e1fb7 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -109,7 +109,7 @@ const Validated = parent => class extends parent { } __reset (state) { - return {...super.__reset(state), error: null, validate: false}; + return {...super.__reset(state), error: null, validate: false, validating: false}; } onSubmit (event) { From 50ff7d50db2992dbe88c86a410e48f30aceed198 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 18:11:03 +0800 Subject: [PATCH 33/41] Add TODO about `childContextTypes` --- packages/uniforms/src/ValidatedForm.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 2880e1fb7..0edc99ce9 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -33,6 +33,8 @@ const Validated = parent => class extends parent { ]).isRequired }; + // TODO add `uniforms.state.validating` to `childContextTypes`. + constructor () { super(...arguments); From 639ea96f9fb8394284eff8e31900d89cbebd2ebd Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 18:12:33 +0800 Subject: [PATCH 34/41] Add `validating` to filterDOMProps.js --- packages/uniforms/src/filterDOMProps.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/uniforms/src/filterDOMProps.js b/packages/uniforms/src/filterDOMProps.js index ab3be467e..96e8f96ba 100644 --- a/packages/uniforms/src/filterDOMProps.js +++ b/packages/uniforms/src/filterDOMProps.js @@ -22,6 +22,7 @@ const unwantedProps = [ 'showInlineError', 'submitting', 'transform', + 'validating', 'value', // These are used by AutoField From 83d6ba21a9898186dfd44d6005ca615606006ea8 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 18:17:22 +0800 Subject: [PATCH 35/41] Save on a `Promise` construction in the async case --- packages/uniforms/src/BaseForm.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index 6744c4159..6353338a0 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -248,12 +248,14 @@ export default class BaseForm extends Component { } const result = this.props.onSubmit && this.props.onSubmit(this.getModel('submit')); - let submitting = Promise.resolve(result); - // Do not change the `submitting` state if onSubmit is not async so we don't cause an unnecessary re-render + // Set the `submitting` state only if onSubmit is async so we don't cause an unnecessary re-render + let submitting; if (result && isFunction(result.then)) { this.setState({submitting: true}); - submitting = submitting.finally(() => this.setState({submitting: false})); + submitting = result.finally(() => this.setState({submitting: false})); + } else { + submitting = Promise.resolve(result); } return submitting.then( From ae584c94263ef6d6ef9541de28031a06857c45a9 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 11 Sep 2018 18:19:49 +0800 Subject: [PATCH 36/41] Remove mistaken line in `getNativeFormProps` --- packages/uniforms/src/ValidatedForm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 0edc99ce9..0cb1d2438 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -70,7 +70,6 @@ const Validated = parent => class extends parent { onValidate, // eslint-disable-line no-unused-vars validator, // eslint-disable-line no-unused-vars validate, // eslint-disable-line no-unused-vars - // validating, // eslist-disable-line no-unused-vars ...props } = super.getNativeFormProps(); From 29f8ed848287194b19e660f6ded7217e558b113c Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Tue, 13 Nov 2018 10:33:06 +0800 Subject: [PATCH 37/41] Make onValidate#onSubmit set `submitting` true (then false) --- packages/uniforms/__tests__/BaseForm.js | 2 +- packages/uniforms/__tests__/ValidatedForm.js | 25 +++++++++++++++++++- packages/uniforms/src/ValidatedForm.js | 12 +++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/uniforms/__tests__/BaseForm.js b/packages/uniforms/__tests__/BaseForm.js index 2f02a0329..94174e9a2 100644 --- a/packages/uniforms/__tests__/BaseForm.js +++ b/packages/uniforms/__tests__/BaseForm.js @@ -285,7 +285,7 @@ describe('BaseForm', () => { expect(onSubmitFailure).not.toBeCalled(); }); - it('sets `submitting` state', async () => { + it('sets `submitting` state while submitting', async () => { let resolveSubmit = null; wrapper.setProps({onSubmit: () => new Promise(resolve => resolveSubmit = resolve)}); diff --git a/packages/uniforms/__tests__/ValidatedForm.js b/packages/uniforms/__tests__/ValidatedForm.js index d40c4b7b1..f0878da7e 100644 --- a/packages/uniforms/__tests__/ValidatedForm.js +++ b/packages/uniforms/__tests__/ValidatedForm.js @@ -128,7 +128,9 @@ describe('ValidatedForm', () => { describe('when submitted', () => { let wrapper; beforeEach(() => { - wrapper = mount(); + wrapper = mount( + + ); }); it('calls `onSubmit` when validation succeeds', async () => { @@ -156,6 +158,27 @@ describe('ValidatedForm', () => { expect(onSubmit).toHaveBeenCalled(); expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.error', error); }); + + it('sets `submitting` `true` while validating, before `BaseForm#onSubmit`', async () => { + onValidate.mockImplementationOnce(() => {}); + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.state.submitting', true); + }); + + it('sets `submitting` back to `false` after sync `onSubmit`', async () => { + onValidate.mockImplementationOnce(() => {}); + onSubmit.mockImplementationOnce(() => {}); + wrapper.find('form').simulate('submit'); + await new Promise(resolve => process.nextTick(resolve)); + + expect(onValidate).toHaveBeenCalledTimes(1); + // Resolve the async validation by calling the third argument of the first call to onValidate. + onValidate.mock.calls[0][2](); + + await new Promise(resolve => process.nextTick(resolve)); + expect(wrapper.instance().getChildContext()).toHaveProperty('uniforms.state.submitting', false); + }); }); describe('on change', () => { diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 0cb1d2438..a0f01b940 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -120,7 +120,7 @@ const Validated = parent => class extends parent { } const promise = new Promise((resolve, reject) => { - this.setState(() => ({validate: true}), () => { + this.setState(() => ({validate: true, submitting: true}), () => { this.onValidate().then( () => { super.onSubmit().then( @@ -136,8 +136,14 @@ const Validated = parent => class extends parent { }); }); - // NOTE: It's okay for this Promise to reject. - promise.catch(() => {}); + promise + .catch(() => { + // `onSubmit` should never reject, so we ignore this rejection. + }) + .finally(() => { + // If validation fails, or `super.onSubmit` doesn't touch `submitting`, we need to reset it. + this.setState(state => state.submitting ? {submitting: false} : null); + }); return promise; } From d2dd32a50fd409b5b66c420a3fa0935694387650 Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Thu, 15 Nov 2018 15:13:00 +0800 Subject: [PATCH 38/41] Change `.catch().finally()` to `.catch().then()` --- packages/uniforms/src/ValidatedForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index a0f01b940..92ea8ec48 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -140,7 +140,7 @@ const Validated = parent => class extends parent { .catch(() => { // `onSubmit` should never reject, so we ignore this rejection. }) - .finally(() => { + .then(() => { // If validation fails, or `super.onSubmit` doesn't touch `submitting`, we need to reset it. this.setState(state => state.submitting ? {submitting: false} : null); }); From 3640cde9fab54f5fcc3c5378c86895fceddae2be Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Mon, 19 Nov 2018 15:28:27 +0800 Subject: [PATCH 39/41] Add `static plainChildContextTypes` to Forms, for use when extending --- packages/uniforms/src/BaseForm.js | 26 ++- packages/uniforms/src/ValidatedForm.js | 295 +++++++++++++------------ 2 files changed, 175 insertions(+), 146 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index 6353338a0..ae52b8052 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -1,15 +1,21 @@ import PropTypes from 'prop-types'; import React from 'react'; import cloneDeep from 'lodash/cloneDeep'; +import mapValues from 'lodash/mapValues'; import get from 'lodash/get'; import set from 'lodash/set'; import isFunction from 'lodash/isFunction'; +import isPlainObject from 'lodash/isPlainObject'; import {Component} from 'react'; import changedKeys from './changedKeys'; import createSchemaBridge from './createSchemaBridge'; import randomIds from './randomIds'; +function shapify (typeOrObject) { + return isPlainObject(typeOrObject) ? PropTypes.shape(mapValues(typeOrObject, shapify)).isRequired : typeOrObject; +} + export default class BaseForm extends Component { static displayName = 'Form'; @@ -44,14 +50,18 @@ export default class BaseForm extends Component { autosaveDelay: PropTypes.number }; - static childContextTypes = { - uniforms: PropTypes.shape({ + static shapifyPropTypes (propTypes) { + return mapValues(propTypes, shapify); + } + + static plainChildContextTypes = { + uniforms: { name: PropTypes.arrayOf(PropTypes.string).isRequired, error: PropTypes.object, model: PropTypes.object.isRequired, - schema: PropTypes.shape({ + schema: { getError: PropTypes.func.isRequired, getErrorMessage: PropTypes.func.isRequired, getErrorMessages: PropTypes.func.isRequired, @@ -61,9 +71,9 @@ export default class BaseForm extends Component { getSubfields: PropTypes.func.isRequired, getType: PropTypes.func.isRequired, getValidator: PropTypes.func.isRequired - }).isRequired, + }, - state: PropTypes.shape({ + state: { changed: PropTypes.bool.isRequired, changedMap: PropTypes.object.isRequired, submitting: PropTypes.bool.isRequired, @@ -72,13 +82,15 @@ export default class BaseForm extends Component { disabled: PropTypes.bool.isRequired, placeholder: PropTypes.bool.isRequired, showInlineError: PropTypes.bool.isRequired - }).isRequired, + }, onChange: PropTypes.func.isRequired, randomId: PropTypes.func.isRequired - }).isRequired + } }; + static childContextTypes = BaseForm.shapifyPropTypes(BaseForm.plainChildContextTypes); + constructor () { super(...arguments); diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 92ea8ec48..38da37412 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -1,189 +1,206 @@ import PropTypes from 'prop-types'; import cloneDeep from 'lodash/cloneDeep'; +import merge from 'lodash/merge'; import isEqual from 'lodash/isEqual'; import set from 'lodash/set'; import BaseForm from './BaseForm'; -const Validated = parent => class extends parent { - static Validated = Validated; +const Validated = parent => { - static displayName = `Validated${parent.displayName}`; - - static defaultProps = { - ...parent.defaultProps, - - onValidate (model, error, callback) { - callback(); + const plainChildContextTypes = merge( + { + uniforms: { + state: { + validating: PropTypes.bool + } + } }, + parent.plainChildContextTypes + ); - validate: 'onChangeAfterSubmit' - }; + return class ValidatedForm extends parent { + static Validated = Validated; - static propTypes = { - ...parent.propTypes, + static displayName = `Validated${parent.displayName}`; - onValidate: PropTypes.func.isRequired, + static defaultProps = { + ...parent.defaultProps, - validator: PropTypes.any, - validate: PropTypes.oneOf([ - 'onChange', - 'onChangeAfterSubmit', - 'onSubmit' - ]).isRequired - }; + onValidate (model, error, callback) { + callback(); + }, - // TODO add `uniforms.state.validating` to `childContextTypes`. + validate: 'onChangeAfterSubmit' + }; - constructor () { - super(...arguments); + static propTypes = { + ...parent.propTypes, - this.state = { - ...this.state, + onValidate: PropTypes.func.isRequired, - error: null, - validate: false, - validating: false, - validator: this - .getChildContextSchema() - .getValidator(this.props.validator) + validator: PropTypes.any, + validate: PropTypes.oneOf([ + 'onChange', + 'onChangeAfterSubmit', + 'onSubmit' + ]).isRequired }; - this.onValidate = this.validate = this.onValidate.bind(this); - this.onValidateModel = this.validateModel = this.onValidateModel.bind(this); - } - getChildContextError () { - return super.getChildContextError() || this.state.error; - } + static plainChildContextTypes = plainChildContextTypes; + static childContextTypes = parent.shapifyPropTypes(plainChildContextTypes); - getChildContextState () { - return { - ...super.getChildContextState(), + constructor () { + super(...arguments); - validating: this.state.validating - }; - } + this.state = { + ...this.state, + + error: null, + validate: false, + validating: false, + validator: this + .getChildContextSchema() + .getValidator(this.props.validator) + }; + + this.onValidate = this.validate = this.onValidate.bind(this); + this.onValidateModel = this.validateModel = this.onValidateModel.bind(this); + } + + getChildContextError () { + return super.getChildContextError() || this.state.error; + } + + getChildContextState () { + return { + ...super.getChildContextState(), + + validating: this.state.validating + }; + } - getNativeFormProps () { - const { - onValidate, // eslint-disable-line no-unused-vars - validator, // eslint-disable-line no-unused-vars - validate, // eslint-disable-line no-unused-vars + getNativeFormProps () { + const { + onValidate, // eslint-disable-line no-unused-vars + validator, // eslint-disable-line no-unused-vars + validate, // eslint-disable-line no-unused-vars - ...props - } = super.getNativeFormProps(); + ...props + } = super.getNativeFormProps(); - return props; - } + return props; + } - componentWillReceiveProps ({model, schema, validate, validator}) { - super.componentWillReceiveProps(...arguments); + componentWillReceiveProps ({model, schema, validate, validator}) { + super.componentWillReceiveProps(...arguments); - if (this.props.schema !== schema || this.props.validator !== validator) { - this.setState(({bridge}) => ({ - validator: bridge.getValidator(validator) - }), () => { + if (this.props.schema !== schema || this.props.validator !== validator) { + this.setState(({bridge}) => ({ + validator: bridge.getValidator(validator) + }), () => { + if (validate === 'onChange' || validate === 'onChangeAfterSubmit' && this.state.validate) { + this.onValidate(); + } + }); + } else if (!isEqual(this.props.model, model)) { if (validate === 'onChange' || validate === 'onChangeAfterSubmit' && this.state.validate) { - this.onValidate(); + this.onValidateModel(model); } - }); - } else if (!isEqual(this.props.model, model)) { - if (validate === 'onChange' || validate === 'onChangeAfterSubmit' && this.state.validate) { - this.onValidateModel(model); } } - } - - onChange (key, value) { - // eslint-disable-next-line max-len - if (this.props.validate === 'onChange' || this.props.validate === 'onChangeAfterSubmit' && this.state.validate) { - this.onValidate(key, value).catch(() => {}); - } - // FIXME: https://github.com/vazco/uniforms/issues/293 - // if (this.props.validate === 'onSubmit' && this.state.validate) { - // this.setState(() => ({error: null})); - // } + onChange (key, value) { + // eslint-disable-next-line max-len + if (this.props.validate === 'onChange' || this.props.validate === 'onChangeAfterSubmit' && this.state.validate) { + this.onValidate(key, value).catch(() => {}); + } - super.onChange(...arguments); - } + // FIXME: https://github.com/vazco/uniforms/issues/293 + // if (this.props.validate === 'onSubmit' && this.state.validate) { + // this.setState(() => ({error: null})); + // } - __reset (state) { - return {...super.__reset(state), error: null, validate: false, validating: false}; - } + super.onChange(...arguments); + } - onSubmit (event) { - if (event) { - event.preventDefault(); - event.stopPropagation(); + __reset (state) { + return {...super.__reset(state), error: null, validate: false, validating: false}; } - const promise = new Promise((resolve, reject) => { - this.setState(() => ({validate: true, submitting: true}), () => { - this.onValidate().then( - () => { - super.onSubmit().then( - resolve, - error => { - this.setState({error}); - reject(error); - } - ); - }, - reject - ); - }); - }); - - promise - .catch(() => { - // `onSubmit` should never reject, so we ignore this rejection. - }) - .then(() => { - // If validation fails, or `super.onSubmit` doesn't touch `submitting`, we need to reset it. - this.setState(state => state.submitting ? {submitting: false} : null); + onSubmit (event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + const promise = new Promise((resolve, reject) => { + this.setState(() => ({validate: true, submitting: true}), () => { + this.onValidate().then( + () => { + super.onSubmit().then( + resolve, + error => { + this.setState({error}); + reject(error); + } + ); + }, + reject + ); + }); }); - return promise; - } + promise + .catch(() => { + // `onSubmit` should never reject, so we ignore this rejection. + }) + .then(() => { + // If validation fails, or `super.onSubmit` doesn't touch `submitting`, we need to reset it. + this.setState(state => state.submitting ? {submitting: false} : null); + }); - onValidate (key, value) { - let model = this.getChildContextModel(); - if (model && key) { - model = set(cloneDeep(model), key, cloneDeep(value)); + return promise; } - return this.onValidateModel(model); - } - - onValidateModel (model) { - model = this.getModel('validate', model); + onValidate (key, value) { + let model = this.getChildContextModel(); + if (model && key) { + model = set(cloneDeep(model), key, cloneDeep(value)); + } - let catched = this.props.error || null; - try { - this.state.validator(model); - } catch (error) { - catched = error; + return this.onValidateModel(model); } - this.setState({validating: true}); - return new Promise((resolve, reject) => { - this.props.onValidate(model, catched, (error = catched) => { - // Do not copy error from props to state. - this.setState(() => ({ - error: error === this.props.error ? null : error, - validating: false - }), () => { - if (error) { - reject(error); - } else { - resolve(); - } + onValidateModel (model) { + model = this.getModel('validate', model); + + let catched = this.props.error || null; + try { + this.state.validator(model); + } catch (error) { + catched = error; + } + + this.setState({validating: true}); + return new Promise((resolve, reject) => { + this.props.onValidate(model, catched, (error = catched) => { + // Do not copy error from props to state. + this.setState(() => ({ + error: error === this.props.error ? null : error, + validating: false + }), () => { + if (error) { + reject(error); + } else { + resolve(); + } + }); }); }); - }); - } + } + }; }; export default Validated(BaseForm); From 0e668892221fd271be0ff06f63bd25a68b75b7fe Mon Sep 17 00:00:00 2001 From: Ben Ritter Date: Mon, 19 Nov 2018 15:32:55 +0800 Subject: [PATCH 40/41] Remove unused name and fix indentation --- packages/uniforms/src/ValidatedForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 38da37412..81c7e20a0 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -19,7 +19,7 @@ const Validated = parent => { parent.plainChildContextTypes ); - return class ValidatedForm extends parent { + return class extends parent { static Validated = Validated; static displayName = `Validated${parent.displayName}`; @@ -48,7 +48,7 @@ const Validated = parent => { }; - static plainChildContextTypes = plainChildContextTypes; + static plainChildContextTypes = plainChildContextTypes; static childContextTypes = parent.shapifyPropTypes(plainChildContextTypes); constructor () { From 056f0841ff087e3d3ec6eeacddbd0f060c20a06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Miernik?= Date: Sun, 25 Nov 2018 16:54:57 +0100 Subject: [PATCH 41/41] Reorganized context types. --- packages/uniforms/src/BaseForm.js | 91 ++++---- packages/uniforms/src/ValidatedForm.js | 301 ++++++++++++------------- 2 files changed, 190 insertions(+), 202 deletions(-) diff --git a/packages/uniforms/src/BaseForm.js b/packages/uniforms/src/BaseForm.js index ae52b8052..11f0b7153 100644 --- a/packages/uniforms/src/BaseForm.js +++ b/packages/uniforms/src/BaseForm.js @@ -12,9 +12,43 @@ import changedKeys from './changedKeys'; import createSchemaBridge from './createSchemaBridge'; import randomIds from './randomIds'; -function shapify (typeOrObject) { - return isPlainObject(typeOrObject) ? PropTypes.shape(mapValues(typeOrObject, shapify)).isRequired : typeOrObject; -} +export const __childContextTypes = { + name: PropTypes.arrayOf(PropTypes.string).isRequired, + + error: PropTypes.object, + model: PropTypes.object.isRequired, + + schema: { + getError: PropTypes.func.isRequired, + getErrorMessage: PropTypes.func.isRequired, + getErrorMessages: PropTypes.func.isRequired, + getField: PropTypes.func.isRequired, + getInitialValue: PropTypes.func.isRequired, + getProps: PropTypes.func.isRequired, + getSubfields: PropTypes.func.isRequired, + getType: PropTypes.func.isRequired, + getValidator: PropTypes.func.isRequired + }, + + state: { + changed: PropTypes.bool.isRequired, + changedMap: PropTypes.object.isRequired, + submitting: PropTypes.bool.isRequired, + + label: PropTypes.bool.isRequired, + disabled: PropTypes.bool.isRequired, + placeholder: PropTypes.bool.isRequired, + showInlineError: PropTypes.bool.isRequired + }, + + onChange: PropTypes.func.isRequired, + randomId: PropTypes.func.isRequired +}; + +export const __childContextTypesBuild = type => + isPlainObject(type) + ? PropTypes.shape(mapValues(type, __childContextTypesBuild)).isRequired + : type; export default class BaseForm extends Component { static displayName = 'Form'; @@ -50,47 +84,10 @@ export default class BaseForm extends Component { autosaveDelay: PropTypes.number }; - static shapifyPropTypes (propTypes) { - return mapValues(propTypes, shapify); - } - - static plainChildContextTypes = { - uniforms: { - name: PropTypes.arrayOf(PropTypes.string).isRequired, - - error: PropTypes.object, - model: PropTypes.object.isRequired, - - schema: { - getError: PropTypes.func.isRequired, - getErrorMessage: PropTypes.func.isRequired, - getErrorMessages: PropTypes.func.isRequired, - getField: PropTypes.func.isRequired, - getInitialValue: PropTypes.func.isRequired, - getProps: PropTypes.func.isRequired, - getSubfields: PropTypes.func.isRequired, - getType: PropTypes.func.isRequired, - getValidator: PropTypes.func.isRequired - }, - - state: { - changed: PropTypes.bool.isRequired, - changedMap: PropTypes.object.isRequired, - submitting: PropTypes.bool.isRequired, - - label: PropTypes.bool.isRequired, - disabled: PropTypes.bool.isRequired, - placeholder: PropTypes.bool.isRequired, - showInlineError: PropTypes.bool.isRequired - }, - - onChange: PropTypes.func.isRequired, - randomId: PropTypes.func.isRequired - } + static childContextTypes = { + uniforms: __childContextTypesBuild(__childContextTypes) }; - static childContextTypes = BaseForm.shapifyPropTypes(BaseForm.plainChildContextTypes); - constructor () { super(...arguments); @@ -98,8 +95,8 @@ export default class BaseForm extends Component { bridge: createSchemaBridge(this.props.schema), changed: null, changedMap: {}, - submitting: false, - resetCount: 0 + resetCount: 0, + submitting: false }; this.delayId = false; @@ -146,9 +143,9 @@ export default class BaseForm extends Component { getChildContextState () { return { - changed: !!this.state.changed, - changedMap: this.state.changedMap, - submitting: !!this.state.submitting, + changed: !!this.state.changed, + changedMap: this.state.changedMap, + submitting: this.state.submitting, label: !!this.props.label, disabled: !!this.props.disabled, diff --git a/packages/uniforms/src/ValidatedForm.js b/packages/uniforms/src/ValidatedForm.js index 81c7e20a0..764de9169 100644 --- a/packages/uniforms/src/ValidatedForm.js +++ b/packages/uniforms/src/ValidatedForm.js @@ -4,203 +4,194 @@ import merge from 'lodash/merge'; import isEqual from 'lodash/isEqual'; import set from 'lodash/set'; -import BaseForm from './BaseForm'; +import BaseForm from './BaseForm'; +import {__childContextTypesBuild} from './BaseForm'; +import {__childContextTypes} from './BaseForm'; -const Validated = parent => { +const childContextTypes = __childContextTypesBuild(merge( + {state: {validating: PropTypes.bool.isRequired}}, + __childContextTypes +)); - const plainChildContextTypes = merge( - { - uniforms: { - state: { - validating: PropTypes.bool - } - } - }, - parent.plainChildContextTypes - ); - - return class extends parent { - static Validated = Validated; - - static displayName = `Validated${parent.displayName}`; +const Validated = parent => class extends parent { + static Validated = Validated; - static defaultProps = { - ...parent.defaultProps, + static displayName = `Validated${parent.displayName}`; - onValidate (model, error, callback) { - callback(); - }, + static defaultProps = { + ...parent.defaultProps, - validate: 'onChangeAfterSubmit' - }; + onValidate (model, error, callback) { + callback(); + }, - static propTypes = { - ...parent.propTypes, + validate: 'onChangeAfterSubmit' + }; - onValidate: PropTypes.func.isRequired, + static propTypes = { + ...parent.propTypes, - validator: PropTypes.any, - validate: PropTypes.oneOf([ - 'onChange', - 'onChangeAfterSubmit', - 'onSubmit' - ]).isRequired - }; + onValidate: PropTypes.func.isRequired, + validator: PropTypes.any, + validate: PropTypes.oneOf([ + 'onChange', + 'onChangeAfterSubmit', + 'onSubmit' + ]).isRequired + }; - static plainChildContextTypes = plainChildContextTypes; - static childContextTypes = parent.shapifyPropTypes(plainChildContextTypes); + static childContextTypes = { + ...parent.childContextTypes || {}, + uniforms: childContextTypes + }; - constructor () { - super(...arguments); + constructor () { + super(...arguments); - this.state = { - ...this.state, + this.state = { + ...this.state, - error: null, - validate: false, - validating: false, - validator: this - .getChildContextSchema() - .getValidator(this.props.validator) - }; + error: null, + validate: false, + validating: false, + validator: this + .getChildContextSchema() + .getValidator(this.props.validator) + }; - this.onValidate = this.validate = this.onValidate.bind(this); - this.onValidateModel = this.validateModel = this.onValidateModel.bind(this); - } + this.onValidate = this.validate = this.onValidate.bind(this); + this.onValidateModel = this.validateModel = this.onValidateModel.bind(this); + } - getChildContextError () { - return super.getChildContextError() || this.state.error; - } + getChildContextError () { + return super.getChildContextError() || this.state.error; + } - getChildContextState () { - return { - ...super.getChildContextState(), + getChildContextState () { + return { + ...super.getChildContextState(), - validating: this.state.validating - }; - } + validating: this.state.validating + }; + } - getNativeFormProps () { - const { - onValidate, // eslint-disable-line no-unused-vars - validator, // eslint-disable-line no-unused-vars - validate, // eslint-disable-line no-unused-vars + getNativeFormProps () { + const { + onValidate, // eslint-disable-line no-unused-vars + validator, // eslint-disable-line no-unused-vars + validate, // eslint-disable-line no-unused-vars - ...props - } = super.getNativeFormProps(); + ...props + } = super.getNativeFormProps(); - return props; - } + return props; + } - componentWillReceiveProps ({model, schema, validate, validator}) { - super.componentWillReceiveProps(...arguments); + componentWillReceiveProps ({model, schema, validate, validator}) { + super.componentWillReceiveProps(...arguments); - if (this.props.schema !== schema || this.props.validator !== validator) { - this.setState(({bridge}) => ({ - validator: bridge.getValidator(validator) - }), () => { - if (validate === 'onChange' || validate === 'onChangeAfterSubmit' && this.state.validate) { - this.onValidate(); - } - }); - } else if (!isEqual(this.props.model, model)) { + if (this.props.schema !== schema || this.props.validator !== validator) { + this.setState(({bridge}) => ({ + validator: bridge.getValidator(validator) + }), () => { if (validate === 'onChange' || validate === 'onChangeAfterSubmit' && this.state.validate) { - this.onValidateModel(model); + this.onValidate().catch(() => {}); } + }); + } else if (!isEqual(this.props.model, model)) { + if (validate === 'onChange' || validate === 'onChangeAfterSubmit' && this.state.validate) { + this.onValidateModel(model).catch(() => {}); } } + } - onChange (key, value) { - // eslint-disable-next-line max-len - if (this.props.validate === 'onChange' || this.props.validate === 'onChangeAfterSubmit' && this.state.validate) { - this.onValidate(key, value).catch(() => {}); - } + onChange (key, value) { + // eslint-disable-next-line max-len + if (this.props.validate === 'onChange' || this.props.validate === 'onChangeAfterSubmit' && this.state.validate) { + this.onValidate(key, value).catch(() => {}); + } - // FIXME: https://github.com/vazco/uniforms/issues/293 - // if (this.props.validate === 'onSubmit' && this.state.validate) { - // this.setState(() => ({error: null})); - // } + // FIXME: https://github.com/vazco/uniforms/issues/293 + // if (this.props.validate === 'onSubmit' && this.state.validate) { + // this.setState(() => ({error: null})); + // } - super.onChange(...arguments); - } + super.onChange(...arguments); + } - __reset (state) { - return {...super.__reset(state), error: null, validate: false, validating: false}; - } + __reset (state) { + return {...super.__reset(state), error: null, validate: false, validating: false}; + } - onSubmit (event) { - if (event) { - event.preventDefault(); - event.stopPropagation(); - } + onSubmit (event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } - const promise = new Promise((resolve, reject) => { - this.setState(() => ({validate: true, submitting: true}), () => { - this.onValidate().then( - () => { - super.onSubmit().then( - resolve, - error => { - this.setState({error}); - reject(error); - } - ); - }, - reject - ); - }); + const promise = new Promise((resolve, reject) => { + this.setState(() => ({submitting: true, validate: true}), () => { + this.onValidate().then( + () => { + super.onSubmit().then( + resolve, + error => { + this.setState({error}); + reject(error); + } + ); + }, + reject + ); + }); + }); + + promise + .catch(() => { + // `onSubmit` should never reject, so we ignore this rejection. + }) + .then(() => { + // If validation fails, or `super.onSubmit` doesn't touch `submitting`, we need to reset it. + this.setState(state => state.submitting ? {submitting: false} : null); }); - promise - .catch(() => { - // `onSubmit` should never reject, so we ignore this rejection. - }) - .then(() => { - // If validation fails, or `super.onSubmit` doesn't touch `submitting`, we need to reset it. - this.setState(state => state.submitting ? {submitting: false} : null); - }); + return promise; + } - return promise; + onValidate (key, value) { + let model = this.getChildContextModel(); + if (model && key) { + model = set(cloneDeep(model), key, cloneDeep(value)); } - onValidate (key, value) { - let model = this.getChildContextModel(); - if (model && key) { - model = set(cloneDeep(model), key, cloneDeep(value)); - } + return this.onValidateModel(model); + } - return this.onValidateModel(model); - } + onValidateModel (model) { + model = this.getModel('validate', model); - onValidateModel (model) { - model = this.getModel('validate', model); - - let catched = this.props.error || null; - try { - this.state.validator(model); - } catch (error) { - catched = error; - } + let catched = this.props.error || null; + try { + this.state.validator(model); + } catch (error) { + catched = error; + } - this.setState({validating: true}); - return new Promise((resolve, reject) => { - this.props.onValidate(model, catched, (error = catched) => { - // Do not copy error from props to state. - this.setState(() => ({ - error: error === this.props.error ? null : error, - validating: false - }), () => { - if (error) { - reject(error); - } else { - resolve(); - } - }); + this.setState({validating: true}); + return new Promise((resolve, reject) => { + this.props.onValidate(model, catched, (error = catched) => { + // Do not copy error from props to state. + this.setState(() => ({error: error === this.props.error ? null : error, validating: false}), () => { + if (error) { + reject(error); + } else { + resolve(); + } }); }); - } - }; + }); + } }; export default Validated(BaseForm);