From 2dfd5b1e5b705392752ef86ca49638668dfb1df6 Mon Sep 17 00:00:00 2001 From: Sandhya Govindaraju Date: Thu, 17 Oct 2024 19:00:30 -0500 Subject: [PATCH 1/7] give errors from submit() exception --- src/index.js | 18 ++++++++-- src/index.test.js | 86 +++++++++++++++++++++++++++++++++++++++++++++-- src/util.js | 8 +++++ 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index 407e681..42f01ea 100644 --- a/src/index.js +++ b/src/index.js @@ -2,11 +2,16 @@ import F from 'futil' import _ from 'lodash/fp.js' import { extendObservable, reaction } from 'mobx' import * as validators from './validators.js' -import { tokenizePath, safeJoinPaths, gatherFormValues } from './util.js' +import { + tokenizePath, + safeJoinPaths, + gatherFormValues, + ValidationError, +} from './util.js' import { treePath, omitByPrefixes, pickByPrefixes } from './futil.js' import { get, set, toJS, observable } from './mobx.js' -export { validators } +export { validators, ValidationError } let changed = (x, y) => !_.isEqual(x, y) && !(F.isBlank(x) && F.isBlank(y)) let Command = F.aspects.command(x => y => extendObservable(y, x)) @@ -176,7 +181,14 @@ export default ({ let submit = Command(() => { if (_.isEmpty(form.validate())) { form.submit.state.error = null - return configSubmit(form.getSnapshot(), form) + try { + return configSubmit(form.getSnapshot(), form) + } catch (err) { + if (err instanceof ValidationError) { + state.errors = err.cause + } + throw err + } } throw 'Validation Error' }) diff --git a/src/index.test.js b/src/index.test.js index 1335338..565a0ce 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -1,6 +1,6 @@ import _ from 'lodash/fp.js' -import { reaction } from 'mobx' -import Form, { jsonSchemaKeys } from './index.js' +import { reaction, runInAction } from 'mobx' +import Form, { jsonSchemaKeys, ValidationError } from './index.js' import { toJS } from './mobx.js' require('util').inspect.defaultOptions.depth = null @@ -299,7 +299,9 @@ describe('Methods and computeds', () => { describe('getPatch()', () => { it('Array fields', () => { let addresses = form.getField('location.addresses') - addresses.value.push({ street: undefined, tenants: undefined }) + runInAction(() => { + addresses.value.push({ street: undefined, tenants: undefined }) + }) // Ignores undefined values expect(form.getPatch()).toStrictEqual({}) // Picks up new values @@ -336,3 +338,81 @@ describe('Methods and computeds', () => { }) }) }) + +let goodFields = { + location: { + fields: { + 'country.state': { + label: 'Dotted field name', + fields: { + zip: {}, + name: {}, + }, + }, + addresses: { + label: 'Array field', + itemField: { + label: 'Item field is a record', + fields: { + street: {}, + tenants: { + label: 'Array field', + itemField: { + label: 'Item field is a primitive', + }, + }, + }, + }, + }, + }, + }, +} + +let goodValue = { + location: { + 'country.state': { zip: '07016' }, + addresses: [{ street: 'Meridian', tenants: ['John'] }], + }, +} + +describe('submit()', () => { + let form = null + + afterEach(() => form.dispose()) + it('fails when validation fails', async () => { + const submit = async () => { + return true + } + form = Form({ fields, value, submit }) + await form.submit() + expect(form.submit.state.status).toBe('failed') + expect(form.submit.state.error).toBe('Validation Error') + }) + it('succeeds when validation and submit() run', async () => { + const submit = () => { + return 'submit run' + } + form = Form({ fields: goodFields, value: goodValue, submit }) + const result = await form.submit() + expect(form.submit.state.status).toBe('succeeded') + expect(result).toBe('submit run') + }) + it('has errors when submit throws with cause', async () => { + const submit = () => { + throw new ValidationError('My submit failed', + { 'location.addresses.0.tenants.0': ['invalid format'] } + ) + } + form = Form({ fields: goodFields, value: goodValue, submit }) + const result = await form.submit() + expect(form.submit.state.status).toBe('failed') + expect(form.submit.state.error.message).toBe('My submit failed') + expect(form.submit.state.error.cause).toEqual({ + 'location.addresses.0.tenants.0': ['invalid format'], + }) + expect(form.errors).toEqual({ + 'location.addresses.0.tenants.0': ['invalid format'], + }) + expect(result).toBeUndefined() + }) +}) diff --git a/src/util.js b/src/util.js index f9176e8..142d8f5 100644 --- a/src/util.js +++ b/src/util.js @@ -25,3 +25,11 @@ export let gatherFormValues = form => ? tree : _.set(treePath(x, ...xs), x.value, tree) )({})(form) + + export class ValidationError extends Error { + name = 'ValidationError' + constructor(message, errors) { + super(message) + this.cause = errors + } + } \ No newline at end of file From d1b138b66bbeeae5bf9e523cb2b13d9a6af0a7bd Mon Sep 17 00:00:00 2001 From: Sandhya Govindaraju Date: Thu, 17 Oct 2024 19:18:36 -0500 Subject: [PATCH 2/7] DOC: update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 714c182..a66adf5 100644 --- a/README.md +++ b/README.md @@ -219,3 +219,7 @@ export let CommandButton = observer(({command, children}) => ( )) ``` + +## Capturing submission errors + +Custom errors can be captured from `form.submit()` by throwing `ValidationError("Submission failed", errors)`. The `errros` object is of the same shape as the errors given by `validate()` on the form, that is, the keys are the fields and the values are arrays of errors for the field. \ No newline at end of file From 2cbd5d580467bdd244a13efbe534f6fdf43ea1ed Mon Sep 17 00:00:00 2001 From: Decrapifier Date: Fri, 18 Oct 2024 00:19:54 +0000 Subject: [PATCH 3/7] Automagically formatted by Duti! https://github.com/smartprocure/duti --- src/index.test.js | 6 +++--- src/util.js | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/index.test.js b/src/index.test.js index 565a0ce..49cefd2 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -399,9 +399,9 @@ describe('submit()', () => { }) it('has errors when submit throws with cause', async () => { const submit = () => { - throw new ValidationError('My submit failed', - { 'location.addresses.0.tenants.0': ['invalid format'] } - ) + throw new ValidationError('My submit failed', { + 'location.addresses.0.tenants.0': ['invalid format'], + }) } form = Form({ fields: goodFields, value: goodValue, submit }) const result = await form.submit() diff --git a/src/util.js b/src/util.js index 142d8f5..58af99a 100644 --- a/src/util.js +++ b/src/util.js @@ -26,10 +26,10 @@ export let gatherFormValues = form => : _.set(treePath(x, ...xs), x.value, tree) )({})(form) - export class ValidationError extends Error { - name = 'ValidationError' - constructor(message, errors) { - super(message) - this.cause = errors - } - } \ No newline at end of file +export class ValidationError extends Error { + name = 'ValidationError' + constructor(message, errors) { + super(message) + this.cause = errors + } +} From f9ab788d3e5aac5b09c3c3e54bf63c5873b1a1c7 Mon Sep 17 00:00:00 2001 From: Sandhya Govindaraju Date: Thu, 17 Oct 2024 20:12:32 -0500 Subject: [PATCH 4/7] DEV: bump version --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14aedb9..12d67fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.14.0 + +- Support form errors via an `ValidationError` on `submit`. + # 0.13.2 - Do not extend submit when already present diff --git a/package.json b/package.json index 716cfb4..317e290 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-autoform", - "version": "0.13.2", + "version": "0.14.0", "description": "Ridiculously simple form state management with mobx", "type": "module", "main": "dist/cjs/index.js", From cd1c53906a50706d7c079311a21c3a7aff8ed877 Mon Sep 17 00:00:00 2001 From: Sandhya Govindaraju Date: Thu, 17 Oct 2024 20:17:14 -0500 Subject: [PATCH 5/7] DEV: slight improvement to testing --- src/index.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.test.js b/src/index.test.js index 49cefd2..cde73cd 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -387,6 +387,7 @@ describe('submit()', () => { await form.submit() expect(form.submit.state.status).toBe('failed') expect(form.submit.state.error).toBe('Validation Error') + expect(form.submitError).toBe('Validation Error') }) it('succeeds when validation and submit() run', async () => { const submit = () => { @@ -397,7 +398,7 @@ describe('submit()', () => { expect(form.submit.state.status).toBe('succeeded') expect(result).toBe('submit run') }) - it('has errors when submit throws with cause', async () => { + it('has errors when submit throws with ValidationError', async () => { const submit = () => { throw new ValidationError('My submit failed', { 'location.addresses.0.tenants.0': ['invalid format'], @@ -413,6 +414,7 @@ describe('submit()', () => { expect(form.errors).toEqual({ 'location.addresses.0.tenants.0': ['invalid format'], }) + expect(form.submitError).toBe('My submit failed') expect(result).toBeUndefined() }) }) From 10b205f621c2e46191a20c1a237e98b31411d912 Mon Sep 17 00:00:00 2001 From: Sandhya Govindaraju Date: Fri, 18 Oct 2024 10:53:54 -0500 Subject: [PATCH 6/7] FIX: handle both sync and async function --- src/index.js | 17 ++++++++++++----- src/index.test.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/index.js b/src/index.js index 42f01ea..98f897e 100644 --- a/src/index.js +++ b/src/index.js @@ -39,6 +39,13 @@ let defaultGetSnapshot = form => F.flattenObject(toJS(gatherFormValues(form))) let defaultGetNestedSnapshot = form => F.unflattenObject(form.getSnapshot()) +const handleSubmitErr = (state, err) => { + if (err instanceof ValidationError) { + state.errors = err.cause + } + throw err +} + export default ({ submit: configSubmit, value = {}, @@ -182,12 +189,12 @@ export default ({ if (_.isEmpty(form.validate())) { form.submit.state.error = null try { - return configSubmit(form.getSnapshot(), form) + // Handle both sync and sync configSubmit + return Promise.resolve(configSubmit(form.getSnapshot(), form)).catch( + err => handleSubmitErr(state, err) + ) } catch (err) { - if (err instanceof ValidationError) { - state.errors = err.cause - } - throw err + handleSubmitErr(state, err) } } throw 'Validation Error' diff --git a/src/index.test.js b/src/index.test.js index cde73cd..8404ea4 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -389,7 +389,7 @@ describe('submit()', () => { expect(form.submit.state.error).toBe('Validation Error') expect(form.submitError).toBe('Validation Error') }) - it('succeeds when validation and submit() run', async () => { + it('succeeds when validation and sync submit() run', async () => { const submit = () => { return 'submit run' } @@ -398,7 +398,16 @@ describe('submit()', () => { expect(form.submit.state.status).toBe('succeeded') expect(result).toBe('submit run') }) - it('has errors when submit throws with ValidationError', async () => { + it('succeeds when validation and async submit() run', async () => { + const submit = async () => { + return 'submit run' + } + form = Form({ fields: goodFields, value: goodValue, submit }) + const result = await form.submit() + expect(form.submit.state.status).toBe('succeeded') + expect(result).toBe('submit run') + }) + it('has errors when sync submit throws with ValidationError', async () => { const submit = () => { throw new ValidationError('My submit failed', { 'location.addresses.0.tenants.0': ['invalid format'], @@ -417,4 +426,23 @@ describe('submit()', () => { expect(form.submitError).toBe('My submit failed') expect(result).toBeUndefined() }) + it('has errors when async submit throws with ValidationError', async () => { + const submit = async () => { + throw new ValidationError('My submit failed', { + 'location.addresses.0.tenants.0': ['invalid format'], + }) + } + form = Form({ fields: goodFields, value: goodValue, submit }) + const result = await form.submit() + expect(form.submit.state.status).toBe('failed') + expect(form.submit.state.error.message).toBe('My submit failed') + expect(form.submit.state.error.cause).toEqual({ + 'location.addresses.0.tenants.0': ['invalid format'], + }) + expect(form.errors).toEqual({ + 'location.addresses.0.tenants.0': ['invalid format'], + }) + expect(form.submitError).toBe('My submit failed') + expect(result).toBeUndefined() + }) }) From 65d4311e66f73e848f86b49775302898e7cdef4b Mon Sep 17 00:00:00 2001 From: Sandhya Govindaraju Date: Fri, 18 Oct 2024 16:05:28 -0500 Subject: [PATCH 7/7] FIX: no need to handle async/sync separately --- src/index.js | 19 ++++++------------- src/index.test.js | 2 ++ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/index.js b/src/index.js index 98f897e..e900425 100644 --- a/src/index.js +++ b/src/index.js @@ -39,13 +39,6 @@ let defaultGetSnapshot = form => F.flattenObject(toJS(gatherFormValues(form))) let defaultGetNestedSnapshot = form => F.unflattenObject(form.getSnapshot()) -const handleSubmitErr = (state, err) => { - if (err instanceof ValidationError) { - state.errors = err.cause - } - throw err -} - export default ({ submit: configSubmit, value = {}, @@ -185,16 +178,16 @@ export default ({ return F.getOrReturn('message', form.submit.state.error) }, }) - let submit = Command(() => { + let submit = Command(async () => { if (_.isEmpty(form.validate())) { form.submit.state.error = null try { - // Handle both sync and sync configSubmit - return Promise.resolve(configSubmit(form.getSnapshot(), form)).catch( - err => handleSubmitErr(state, err) - ) + return await configSubmit(form.getSnapshot(), form) } catch (err) { - handleSubmitErr(state, err) + if (err instanceof ValidationError) { + state.errors = { '': err.message, ...err.cause } + } + throw err } } throw 'Validation Error' diff --git a/src/index.test.js b/src/index.test.js index 8404ea4..37c0fbd 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -421,6 +421,7 @@ describe('submit()', () => { 'location.addresses.0.tenants.0': ['invalid format'], }) expect(form.errors).toEqual({ + '': 'My submit failed', 'location.addresses.0.tenants.0': ['invalid format'], }) expect(form.submitError).toBe('My submit failed') @@ -440,6 +441,7 @@ describe('submit()', () => { 'location.addresses.0.tenants.0': ['invalid format'], }) expect(form.errors).toEqual({ + '': 'My submit failed', 'location.addresses.0.tenants.0': ['invalid format'], }) expect(form.submitError).toBe('My submit failed')