Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] give errors from submit() exception #73

Merged
merged 8 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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', {
stellarhoof marked this conversation as resolved.
Show resolved Hide resolved
'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
}
}
Loading