Skip to content

Commit 58ae5c8

Browse files
author
Robin Schultz
committed
add option to return map value
1 parent 8fbdce0 commit 58ae5c8

File tree

6 files changed

+182
-17
lines changed

6 files changed

+182
-17
lines changed

.npmignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea/
2+
.github/
23
src/
34
node_modules/
45
coverage/
@@ -10,3 +11,6 @@ CONTRIBUTING.md
1011
.gitignore
1112
.travis.yml
1213
tsconfig.json
14+
15+
jest.config.js
16+
jest.setup.ts

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ npm install --save react-class-validator
1515
const validatorOptions: ValidatorContextOptions = {
1616
onErrorMessage: (error): string => {
1717
// custom error message handling (localization, etc)
18-
}
18+
},
19+
resultType: 'boolean' // default, can also be set to 'map'
1920
}
2021

2122
render((
@@ -99,13 +100,12 @@ class. Individual fields will have to be validated with `onBlur` functionality.
99100

100101
#### Formik error messages
101102

102-
To display error messages without custom handling, messages will need to be flattened when working with Formik. Do this
103-
by overriding the default `onErrorMessage`.
103+
To display error messages without custom handling, messages will need to be outputted as a map upon validation.
104+
Do this by overriding the default `resultType` (you can also do this at the component-level).
104105

105106
```typescript
106107
const options: ValidatorContextOptions = {
107-
onErrorMessage: (error) => Object.keys(error.constraints)
108-
.map((accum, key) => `${accum}. ${error.constraints[key]}`, '')
108+
resultType: 'map'
109109
};
110110
```
111111

@@ -114,14 +114,14 @@ Then you can simply integrate with the default Formik flow.
114114
```typescript jsx
115115
export const Login: FunctionComponent = () => {
116116

117-
const [validate, errors] = useValidation(LoginValidation);
117+
const [validate] = useValidation(LoginValidation);
118118

119119
return (
120120
<Formik initialValues={{username: '', password: ''}}
121121
validateOnBlur
122122
validateOnChange
123123
validate={validate}>
124-
{({values, touched, handleChange, handleBlur}) => (
124+
{({values, touched, errors, handleChange, handleBlur}) => (
125125
<Form>
126126

127127
<label htmlFor="username">Username</label>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-class-validator",
3-
"version": "1.2.2",
3+
"version": "1.3.0",
44
"description": "React hook for validation with class-validator",
55
"main": "dist/index.js",
66
"scripts": {

src/__tests__/map.spec.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import 'reflect-metadata';
2+
import React, {FunctionComponent, useState} from "react";
3+
import {create} from "react-test-renderer";
4+
import {useValidation, ValidatorProvider} from "../index";
5+
import {mount} from "enzyme";
6+
import {IsNotEmpty} from "class-validator";
7+
import toJson from "enzyme-to-json";
8+
9+
class ContactFormValidation {
10+
11+
@IsNotEmpty({
12+
message: 'First name cannot be empty'
13+
})
14+
public firstName: string;
15+
16+
@IsNotEmpty({
17+
message: 'Last name cannot be empty'
18+
})
19+
public lastName: string;
20+
21+
}
22+
23+
const ContactForm: FunctionComponent = () => {
24+
25+
const [firstName, setFirstName] = useState('');
26+
const [lastName, setLastName] = useState('');
27+
28+
const [validate, errorMessages] = useValidation(ContactFormValidation);
29+
30+
return (
31+
<form id="form" onSubmit={async (evt) => {
32+
evt.preventDefault();
33+
await validate({firstName, lastName});
34+
}}>
35+
<input id="fname-input" value={firstName} onChange={({target: {value}}) => setFirstName(value)}
36+
onBlur={() => validate({firstName}, ['firstName'])}/>
37+
{errorMessages.firstName && errorMessages.firstName.map((error, index) => (
38+
<strong key={index}>{error}</strong>
39+
))}
40+
<input id="lname-input" value={lastName} onChange={({target: {value}}) => setLastName(value)}
41+
onBlur={() => validate({lastName}, ['lastName'])}/>
42+
{errorMessages.lastName && errorMessages.lastName.map((error, index) => (
43+
<strong key={index}>{error}</strong>
44+
))}
45+
</form>
46+
);
47+
48+
};
49+
50+
describe('context', () => {
51+
52+
it('provider should mount correctly', () => {
53+
54+
const tree = create(
55+
<ValidatorProvider options={{resultType: 'map'}}>
56+
<ContactForm/>
57+
</ValidatorProvider>
58+
).toJSON();
59+
60+
expect(tree).toMatchSnapshot();
61+
62+
});
63+
64+
it('validation success on form submit', async () => {
65+
66+
const wrapper = mount(
67+
<ValidatorProvider options={{resultType: 'map'}}>
68+
<ContactForm/>
69+
</ValidatorProvider>
70+
);
71+
72+
const firstNameInput = wrapper.find('#fname-input');
73+
firstNameInput.simulate('change', {target: {value: 'Nick'}});
74+
75+
const lastNameInput = wrapper.find('#lname-input');
76+
lastNameInput.simulate('change', {target: {value: 'Fury'}});
77+
78+
const form = wrapper.find('#form');
79+
form.simulate('submit');
80+
81+
expect(toJson(wrapper)).toMatchSnapshot();
82+
83+
});
84+
85+
it('validation error on form submit', async () => {
86+
87+
const wrapper = mount(
88+
<ValidatorProvider options={{resultType: 'map'}}>
89+
<ContactForm/>
90+
</ValidatorProvider>
91+
);
92+
93+
const firstNameInput = wrapper.find('#fname-input');
94+
firstNameInput.simulate('change', {target: {value: 'Nick'}});
95+
96+
const form = wrapper.find('#form');
97+
form.simulate('submit');
98+
99+
expect(toJson(wrapper)).toMatchSnapshot();
100+
101+
});
102+
103+
it('validation error on blur field', async () => {
104+
105+
const wrapper = mount(
106+
<ValidatorProvider options={{resultType: 'map'}}>
107+
<ContactForm/>
108+
</ValidatorProvider>
109+
);
110+
111+
const firstNameInput = wrapper.find('#fname-input');
112+
firstNameInput.simulate('blur');
113+
114+
expect(toJson(wrapper)).toMatchSnapshot();
115+
116+
});
117+
118+
it('validation error custom handler', async () => {
119+
120+
const wrapper = mount(
121+
<ValidatorProvider options={{
122+
onErrorMessage() {
123+
return ['this is a custom error']
124+
},
125+
resultType: 'map',
126+
}}>
127+
<ContactForm/>
128+
</ValidatorProvider>
129+
);
130+
131+
const form = wrapper.find('#form');
132+
form.simulate('submit');
133+
134+
expect(toJson(wrapper)).toMatchSnapshot();
135+
136+
});
137+
138+
});

src/context.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
import {ValidationError} from "class-validator";
22
import React, {createContext, FunctionComponent} from "react";
33

4+
export type ValidatorResultType = 'map' | 'boolean';
5+
46
export type OnErrorMessageHandler = (error: ValidationError) => string[];
57
export type ValidatorContextOptions = {
6-
onErrorMessage: OnErrorMessageHandler;
8+
onErrorMessage?: OnErrorMessageHandler;
9+
resultType?: ValidatorResultType;
710
};
811

12+
const defaultOnErrorMessage: OnErrorMessageHandler = (error) =>
13+
Object.keys(error.constraints).map((key) => error.constraints[key]);
14+
915
const _getDefaultContextOptions = (): ValidatorContextOptions => ({
10-
onErrorMessage: (error) => Object.keys(error.constraints).map((key) => error.constraints[key])
16+
onErrorMessage: defaultOnErrorMessage,
17+
resultType: 'boolean',
1118
});
1219

1320
export const ValidatorContext = createContext<ValidatorContextOptions>(null);
1421

1522
export const ValidatorProvider: FunctionComponent<{ options?: ValidatorContextOptions }> =
1623
({options = _getDefaultContextOptions(), children}) => (
17-
<ValidatorContext.Provider value={options}>
24+
<ValidatorContext.Provider value={{
25+
onErrorMessage: options.onErrorMessage || defaultOnErrorMessage,
26+
resultType: options.resultType || 'boolean'
27+
}}>
1828
{children}
1929
</ValidatorContext.Provider>
2030
);

src/index.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,44 @@
11
import {validate} from 'class-validator';
22
import {useContext, useState} from 'react';
3-
import {ValidatorContext} from "./context";
3+
import {ValidatorContext, ValidatorContextOptions} from "./context";
44

55
export {ValidatorProvider, ValidatorContextOptions, OnErrorMessageHandler} from './context';
66

77
type Newable<T> = {
88
new(): T;
99
} | Function;
1010

11+
type ValidationOptions = Pick<ValidatorContextOptions, 'resultType'>;
1112
type ValidationErrorMap<T, K extends keyof T> = { [key in K]?: string[] };
1213
type ValidationPayload<T, K extends keyof T> = { [key in K]?: T[K] };
13-
type ValidationFunction<T, K extends keyof T> = (payload: ValidationPayload<T, K>, filter?: K[]) => Promise<boolean>;
14+
type ValidationFunction<T, K extends keyof T> = (payload: ValidationPayload<T, K>, filter?: K[]) => Promise<ValidationErrorMap<T, K> | boolean>;
1415
type UseValidationResult<T, K extends keyof T> = [ValidationFunction<T, K>, ValidationErrorMap<T, K>];
1516

16-
export const useValidation = <T, K extends keyof T>(validationClass: Newable<T>): UseValidationResult<T, K> => {
17+
export const useValidation = <T, K extends keyof T>(validationClass: Newable<T>, opts: ValidationOptions = {}): UseValidationResult<T, K> => {
1718

18-
const {onErrorMessage} = useContext(ValidatorContext);
19+
const {onErrorMessage, resultType} = useContext(ValidatorContext);
20+
opts = {
21+
...opts,
22+
resultType: opts.resultType || resultType
23+
}
1924

2025
const [validationErrors, setErrors] = useState<ValidationErrorMap<T, K>>({});
2126

27+
const _resolve = (errors: ValidationErrorMap<T, K>) => {
28+
if (errors && Object.keys(errors).length === 0 && errors.constructor === Object) {
29+
return opts.resultType === 'boolean' ? true : errors;
30+
} else {
31+
return opts.resultType === 'boolean' ? false : errors;
32+
}
33+
}
34+
2235
const validateCallback: ValidationFunction<T, K> = async (payload, filter: K[] = []) => {
2336

2437
let errors = await validate(Object.assign(new (validationClass as any)(), payload));
2538
if (errors.length === 0) {
2639

2740
setErrors({});
28-
return true;
41+
return _resolve({});
2942

3043
} else {
3144

@@ -60,7 +73,7 @@ export const useValidation = <T, K extends keyof T>(validationClass: Newable<T>)
6073
setErrors(validation);
6174
}
6275

63-
return false;
76+
return _resolve(validation);
6477

6578
}
6679

0 commit comments

Comments
 (0)