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 7 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
25 changes: 22 additions & 3 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 @@ -34,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 = {},
Expand Down Expand Up @@ -176,7 +188,14 @@ export default ({
let submit = Command(() => {
if (_.isEmpty(form.validate())) {
form.submit.state.error = null
return configSubmit(form.getSnapshot(), form)
try {
// Handle both sync and sync configSubmit
return Promise.resolve(configSubmit(form.getSnapshot(), form)).catch(
err => handleSubmitErr(state, err)
)
Copy link
Member

@stellarhoof stellarhoof Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I was wrong. Command does support async functions, so you don't have to deal with Promise.resolve and catch.

Also, you can use

        if (err instanceof ValidationError) {
          state.errors = {
            '': [err.message],
            ...err.cause,
          }
        }

to use the error message as the message for the entire form.

Screenshot 2024-10-18 at 3 45 00 PM

} catch (err) {
handleSubmitErr(state, err)
}
}
throw 'Validation Error'
})
Expand Down
116 changes: 113 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,111 @@ 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({
'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({
'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