Skip to content

Commit

Permalink
Merge pull request #73 from smartprocure/GS-8643/submit_error
Browse files Browse the repository at this point in the history
[FEAT] give errors from submit() exception
  • Loading branch information
stellarhoof authored Oct 29, 2024
2 parents 4522b9a + 65d4311 commit 7ea4571
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,7 @@ export let CommandButton = observer(({command, children}) => (
</button>
))
```
## 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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 16 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -173,10 +178,17 @@ 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
return configSubmit(form.getSnapshot(), form)
try {
return await configSubmit(form.getSnapshot(), form)
} catch (err) {
if (err instanceof ValidationError) {
state.errors = { '': err.message, ...err.cause }
}
throw err
}
}
throw 'Validation Error'
})
Expand Down
118 changes: 115 additions & 3 deletions src/index.test.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -336,3 +338,113 @@ 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')
expect(form.submitError).toBe('Validation Error')
})
it('succeeds when validation and sync 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('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'],
})
}
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({
'': 'My submit failed',
'location.addresses.0.tenants.0': ['invalid format'],
})
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({
'': 'My submit failed',
'location.addresses.0.tenants.0': ['invalid format'],
})
expect(form.submitError).toBe('My submit failed')
expect(result).toBeUndefined()
})
})
8 changes: 8 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit 7ea4571

Please sign in to comment.