-
-
Notifications
You must be signed in to change notification settings - Fork 164
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
Support multiple errors messages #32
Changes from 2 commits
4e149bf
dba4bb9
97d43af
9b91c0a
8367d6a
1ce4bbd
fabc1ab
1836657
a43f559
d720aa2
e33e7f2
e998e69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,36 +6,118 @@ Object { | |
"age": Object { | ||
"message": "age is a required field", | ||
"type": "required", | ||
"types": Object { | ||
"required": "age is a required field", | ||
}, | ||
}, | ||
"name": Object { | ||
"message": "name is a required field", | ||
"type": "required", | ||
"types": Object { | ||
"min": "name is a min field", | ||
}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above |
||
}, | ||
}, | ||
"values": Object {}, | ||
} | ||
`; | ||
|
||
exports[`yupResolver errors should get errors with validate all criteria fields 1`] = ` | ||
Object { | ||
"errors": Object { | ||
"age": Object { | ||
"message": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).", | ||
"type": "typeError", | ||
"types": Object { | ||
"typeError": Array [ | ||
"age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).", | ||
], | ||
}, | ||
}, | ||
"createdOn": Object { | ||
"message": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
"type": "typeError", | ||
"types": Object { | ||
"typeError": Array [ | ||
"createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
], | ||
}, | ||
}, | ||
"foo": Array [ | ||
Object { | ||
"loose": Object { | ||
"message": "foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`. | ||
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`", | ||
"type": "typeError", | ||
"types": Object { | ||
"typeError": Array [ | ||
"foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`. | ||
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`", | ||
], | ||
}, | ||
}, | ||
}, | ||
], | ||
"password": Object { | ||
"message": "password is a required field", | ||
"type": "required", | ||
"types": Object { | ||
"matches": Array [ | ||
"Special", | ||
"Number", | ||
"Uppercase", | ||
"Lowercase", | ||
], | ||
"min": Array [ | ||
"password must be at least 8 characters", | ||
], | ||
"required": Array [ | ||
"password is a required field", | ||
], | ||
}, | ||
}, | ||
}, | ||
"values": Object {}, | ||
} | ||
`; | ||
|
||
exports[`yupResolver should get errors 1`] = ` | ||
exports[`yupResolver errors should get errors without validate all criteria fields 1`] = ` | ||
Object { | ||
"errors": Object { | ||
"age": Object { | ||
"message": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).", | ||
"type": "typeError", | ||
"types": Object { | ||
"typeError": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).", | ||
}, | ||
}, | ||
"createdOn": Object { | ||
"message": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
"type": "typeError", | ||
"types": Object { | ||
"typeError": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
}, | ||
}, | ||
"foo": Array [ | ||
Object { | ||
"loose": Object { | ||
"message": "foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`. | ||
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`", | ||
"type": "typeError", | ||
"types": Object { | ||
"typeError": "foo[0].loose must be a \`boolean\` type, but the final value was: \`null\`. | ||
If \\"null\\" is intended as an empty value be sure to mark the schema as \`.nullable()\`", | ||
}, | ||
}, | ||
}, | ||
], | ||
"password": Object { | ||
"message": "password is a required field", | ||
"type": "required", | ||
"types": Object { | ||
"required": "password is a required field", | ||
}, | ||
}, | ||
}, | ||
"values": Object {}, | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,6 +45,14 @@ const schema = yup.object().shape({ | |
name: yup.string().required(), | ||
age: yup.number().required().positive().integer(), | ||
email: yup.string().email(), | ||
password: yup | ||
.string() | ||
.required() | ||
.min(8) | ||
.matches(RegExp('(.*[a-z].*)'), 'Lowercase') | ||
.matches(RegExp('(.*[A-Z].*)'), 'Uppercase') | ||
.matches(RegExp('(.*\\d.*)'), 'Number') | ||
.matches(RegExp('[!@#$%^&*(),.?":{}|<>]'), 'Special'), | ||
website: yup.string().url(), | ||
createdOn: yup.date().default(function () { | ||
return new Date(); | ||
|
@@ -64,6 +72,7 @@ describe('yupResolver', () => { | |
const data = { | ||
name: 'jimmy', | ||
age: '24', | ||
password: '[}tehk6Uor', | ||
createdOn: '2014-09-23T19:25:25Z', | ||
foo: [{ yup: true }], | ||
}; | ||
|
@@ -72,20 +81,101 @@ describe('yupResolver', () => { | |
values: { | ||
name: 'jimmy', | ||
age: 24, | ||
password: '[}tehk6Uor', | ||
foo: [{ yup: true }], | ||
createdOn: new Date('2014-09-23T19:25:25Z'), | ||
}, | ||
}); | ||
}); | ||
|
||
it('should get errors', async () => { | ||
const data = { | ||
name: 2, | ||
age: 'test', | ||
createdOn: null, | ||
foo: [{ loose: null }], | ||
}; | ||
expect(await yupResolver(schema)(data)).toMatchSnapshot(); | ||
describe('errors', () => { | ||
it('should get errors with validate all criteria fields', async () => { | ||
const data = { | ||
name: 2, | ||
age: 'test', | ||
password: '', | ||
createdOn: null, | ||
foo: [{ loose: null }], | ||
}; | ||
const resolve = await yupResolver(schema)(data, {}, true); | ||
expect(resolve).toMatchSnapshot(); | ||
expect(resolve.errors['age'].types).toMatchInlineSnapshot(` | ||
Object { | ||
"typeError": Array [ | ||
"age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).", | ||
], | ||
} | ||
`); | ||
expect(resolve.errors['createdOn'].types).toMatchInlineSnapshot(` | ||
Object { | ||
"typeError": Array [ | ||
"createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
], | ||
} | ||
`); | ||
expect(resolve.errors['password'].types).toMatchInlineSnapshot(` | ||
Object { | ||
"matches": Array [ | ||
"Special", | ||
"Number", | ||
"Uppercase", | ||
"Lowercase", | ||
], | ||
"min": Array [ | ||
"password must be at least 8 characters", | ||
], | ||
"required": Array [ | ||
"password is a required field", | ||
], | ||
} | ||
`); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The structure I had in mind was: expect(resolve.errors['password'].types).toMatchInlineSnapshot(`
Object {
"matches": Array [
"Special",
"Number",
"Uppercase",
"Lowercase",
],
"min": "password must be at least 8 characters",
"required": "password is a required field",
}
`); We should discuss whether all should be array message when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, @kotarella1110. Above is a breaking change, we should stick with expect(resolve.errors['password'].types).toMatchInlineSnapshot(`
Object {
"matches": Array [
"Special",
"Number",
"Uppercase",
"Lowercase",
],
"min": "password must be at least 8 characters",
"required": "password is a required field",
}
`); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bluebill1049 I agree with you 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, i was thinking if we keep this mix structure, i'm afraid that we have to handle the case where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks very much for understanding and putting this PR together! |
||
|
||
it('should get errors without validate all criteria fields', async () => { | ||
const data = { | ||
name: 2, | ||
age: 'test', | ||
createdOn: null, | ||
foo: [{ loose: null }], | ||
}; | ||
const resolve = await yupResolver(schema)(data); | ||
expect(await yupResolver(schema)(data)).toMatchSnapshot(); | ||
expect(resolve.errors['age'].types).toMatchInlineSnapshot(` | ||
Object { | ||
"typeError": "age must be a \`number\` type, but the final value was: \`NaN\` (cast from the value \`\\"test\\"\`).", | ||
} | ||
`); | ||
expect(resolve.errors['createdOn'].types).toMatchInlineSnapshot(` | ||
Object { | ||
"typeError": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
} | ||
`); | ||
expect(resolve.errors['password'].types).toMatchInlineSnapshot(` | ||
Object { | ||
"required": "password is a required field", | ||
} | ||
`); | ||
}); | ||
|
||
it('should get error if yup errors has no inner errors', async () => { | ||
const data = { | ||
name: 2, | ||
age: 'test', | ||
createdOn: null, | ||
foo: [{ loose: null }], | ||
}; | ||
const resolve = await yupResolver(schema, { | ||
abortEarly: true, | ||
})(data); | ||
expect(resolve.errors).toMatchInlineSnapshot(` | ||
Object { | ||
"createdOn": Object { | ||
"message": "createdOn must be a \`date\` type, but the final value was: \`Invalid Date\`.", | ||
"type": "typeError", | ||
}, | ||
} | ||
`); | ||
}); | ||
}); | ||
|
||
it('should pass down the yup context', async () => { | ||
|
@@ -148,6 +238,9 @@ describe('validateWithSchema', () => { | |
"name": Object { | ||
"message": "name must be at least 6 characters", | ||
"type": "min", | ||
"types": Object { | ||
"min": "name must be at least 6 characters", | ||
}, | ||
}, | ||
}, | ||
"values": Object {}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,44 @@ | ||
import { appendErrors, transformToNestObject, Resolver } from 'react-hook-form'; | ||
import { Resolver, transformToNestObject } from 'react-hook-form'; | ||
import { FieldError, FieldErrors } from 'react-hook-form/dist/types/form'; | ||
import { DeepMap } from 'react-hook-form/dist/types/utils'; | ||
import Yup from 'yup'; | ||
|
||
const parseErrorSchema = ( | ||
const parseErrorSchema = <TFieldValues extends Record<string, any>>( | ||
error: Yup.ValidationError, | ||
validateAllFieldCriteria: boolean, | ||
) => | ||
Array.isArray(error.inner) | ||
? error.inner.reduce( | ||
(previous: Record<string, any>, { path, message, type }) => ({ | ||
...previous, | ||
...(path | ||
? previous[path] && validateAllFieldCriteria | ||
? { | ||
[path]: appendErrors( | ||
path, | ||
validateAllFieldCriteria, | ||
previous, | ||
type, | ||
message, | ||
), | ||
} | ||
: { | ||
[path]: previous[path] || { | ||
message, | ||
type, | ||
...(validateAllFieldCriteria | ||
? { | ||
types: { [type]: message || true }, | ||
} | ||
: {}), | ||
}, | ||
} | ||
: {}), | ||
}), | ||
): DeepMap<TFieldValues, FieldError> => | ||
Array.isArray(error.inner) && error.inner.length | ||
? error.inner.reduce<FieldErrors<TFieldValues>>( | ||
(previous, { path, message, type }) => { | ||
const previousPath = previous[path] as FieldErrors | undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can use this variable inline (save few bytes) |
||
const previousTypes = validateAllFieldCriteria | ||
? previousPath?.types || [] | ||
: {}; | ||
return { | ||
...previous, | ||
[path]: { | ||
...(previous[path] || { | ||
message, | ||
type, | ||
types: previousTypes, | ||
}), | ||
types: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
...previousTypes, | ||
[type]: validateAllFieldCriteria | ||
? [message, ...(previousTypes[type] || [])] | ||
: message, | ||
}, | ||
}, | ||
}; | ||
}, | ||
{}, | ||
) | ||
: { | ||
[error.path]: { message: error.message, type: error.type }, | ||
}; | ||
: ({ | ||
[error.path]: { | ||
message: error.message, | ||
type: error.type, | ||
}, | ||
} as DeepMap<TFieldValues, FieldError>); | ||
|
||
export const yupResolver = <TFieldValues extends Record<string, any>>( | ||
schema: Yup.ObjectSchema | Yup.Lazy, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If validateAllFieldCriteria is false,
types
field shouldn't be included.