Skip to content

Commit a1ee7f7

Browse files
committed
Decouple custom messages and allow user defined ones
1 parent fbb70a6 commit a1ee7f7

File tree

6 files changed

+69
-55
lines changed

6 files changed

+69
-55
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@programmer_network/use-ajv-form",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"description": "Custom React Hook that integrates with Ajv JSON Schema Validator",
55
"main": "dist/use-ajv-form.es.js",
66
"author": "Aleksandar Grbic",
@@ -51,7 +51,6 @@
5151
"ajv": "8.12.0",
5252
"ajv-errors": "3.0.0",
5353
"ajv-formats": "2.1.1",
54-
"programmer-network-ajv": "github:Programmer-Network/Programmer-Network-AJV",
5554
"prop-types": "15.7.2"
5655
},
5756
"devDependencies": {
@@ -79,7 +78,8 @@
7978
"vite": "5.0.2",
8079
"vite-plugin-dts": "3.6.3",
8180
"vite-tsconfig-paths": "4.2.1",
82-
"vitest": "0.34.6"
81+
"vitest": "0.34.6",
82+
"programmer-network-ajv": "github:Programmer-Network/Programmer-Network-AJV"
8383
},
8484
"files": [
8585
"dist"

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
11
import { useState, useEffect, useMemo, useRef } from 'react';
2-
import { getInitial, getValue, getErrors } from './utils';
2+
import { getInitial, getValue, getErrors, addUserDefinedKeywords } from './utils';
33
import { ajv } from './utils/validation';
44

5-
import { FormField, IState, useFormErrors, UseFormReturn } from './utils/types';
6-
import { ErrorObject, JSONSchemaType } from 'ajv';
5+
import {
6+
AJVMessageFunction,
7+
FormField,
8+
IState,
9+
useFormErrors,
10+
UseFormReturn,
11+
} from './utils/types';
12+
import { ErrorObject, JSONSchemaType, KeywordDefinition } from 'ajv';
713
import { useDebounce } from './Hooks/useDebounce';
814

915
const useAJVForm = <T extends Record<string, any>>(
1016
initial: T,
1117
schema: JSONSchemaType<T>,
12-
errors?: ErrorObject[],
18+
options?: {
19+
customKeywords?: KeywordDefinition[];
20+
errors?: ErrorObject[];
21+
userDefinedMessages?: Record<string, AJVMessageFunction>;
22+
},
1323
): UseFormReturn<T> => {
1424
const initialStateRef = useRef<IState<T>>(getInitial(initial));
25+
1526
const [state, setState] = useState<IState<T>>(getInitial(initial));
16-
const AJVValidate = ajv.compile(schema);
1727

1828
const [currentField, setCurrentField] = useState<{
1929
name: keyof T;
@@ -22,6 +32,12 @@ const useAJVForm = <T extends Record<string, any>>(
2232
const [editCounter, setEditCounter] = useState(0);
2333
const debouncedField = useDebounce(currentField, 500);
2434

35+
if (options?.customKeywords?.length) {
36+
addUserDefinedKeywords(ajv, options.customKeywords);
37+
}
38+
39+
const AJVValidate = ajv.compile(schema);
40+
2541
const resetForm = () => {
2642
setState(
2743
Object.keys(state).reduce((acc, name) => {
@@ -39,7 +55,9 @@ const useAJVForm = <T extends Record<string, any>>(
3955
const validateField = (fieldName: keyof T) => {
4056
const isValid = AJVValidate({ [fieldName]: state[fieldName].value });
4157
const errors = AJVValidate.errors || [];
42-
const fieldErrors = isValid ? {} : getErrors(errors);
58+
const fieldErrors = isValid
59+
? {}
60+
: getErrors(errors, options?.userDefinedMessages);
4361

4462
const error = isDirty ? fieldErrors[fieldName as string] || '' : '';
4563

@@ -100,7 +118,10 @@ const useAJVForm = <T extends Record<string, any>>(
100118
}, {} as T);
101119

102120
if (!AJVValidate(data) && AJVValidate.errors) {
103-
const errors: useFormErrors<T> = getErrors(AJVValidate.errors);
121+
const errors: useFormErrors<T> = getErrors(
122+
AJVValidate.errors,
123+
options?.userDefinedMessages,
124+
);
104125

105126
setState(
106127
setErrors(
@@ -142,12 +163,12 @@ const useAJVForm = <T extends Record<string, any>>(
142163
}, [debouncedField]);
143164

144165
useEffect(() => {
145-
if (!errors?.length) {
166+
if (!options?.errors?.length) {
146167
return;
147168
}
148169

149-
setState(setErrors(getErrors(errors)));
150-
}, [errors]);
170+
setState(setErrors(getErrors(options?.errors, options?.userDefinedMessages)));
171+
}, [options?.errors]);
151172

152173
return {
153174
reset: resetForm,

src/useAjvForm.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { act, renderHook } from '@testing-library/react-hooks';
22
import useAJVForm from '.';
33
import { JSONSchemaType } from 'ajv';
44
import { vi } from 'vitest';
5+
import { keywords } from 'utils/validation';
56

67
beforeEach(() => {
78
vi.useFakeTimers();
@@ -168,7 +169,9 @@ describe('useAJVForm', () => {
168169
},
169170
};
170171

171-
const { result } = renderHook(() => useAJVForm(initialData, schema));
172+
const { result } = renderHook(() =>
173+
useAJVForm(initialData, schema, { customKeywords: [...keywords] }),
174+
);
172175

173176
result.current.set({ title: 'Hello, world ++++' });
174177
result.current.onBlur('title');

src/utils/index.ts

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,25 @@
1-
import { ErrorObject } from 'ajv';
1+
import Ajv, { ErrorObject, KeywordDefinition } from 'ajv';
22

3-
import { InitialState, useFormErrors } from './types';
3+
import { AJVMessageFunction, InitialState, useFormErrors } from './types';
44

55
import { DefaultAJVMessages } from './types';
66
import { defaultAJVMessages } from './constants';
7-
import { customKeywordNames } from './validation';
87

9-
// TODO: Is there a better way to do this?
10-
// This is error prone because not all keywords are covered.
118
const getFieldName = (field: ErrorObject): string | null => {
12-
if (customKeywordNames.includes(field.keyword)) {
13-
return field.instancePath.slice(1);
14-
}
15-
16-
switch (field.keyword) {
17-
case 'required':
18-
return field.params.missingProperty;
19-
case 'errorMessage':
20-
case 'minimum':
21-
case 'maximum':
22-
case 'type':
23-
case 'minItems':
24-
case 'maxItems':
25-
case 'minLength':
26-
case 'maxLength':
27-
case 'format':
28-
case 'secure-string':
29-
return field.instancePath.slice(1);
30-
default:
31-
return null;
32-
}
9+
return field.instancePath.slice(1);
3310
};
3411

35-
const getErrorMessage = (error: ErrorObject): string => {
12+
const getErrorMessage = (
13+
error: ErrorObject,
14+
userDefinedMessages?: Record<string, AJVMessageFunction>,
15+
): string => {
3616
const UNKNOWN_VALIDATION_ERROR = 'Unknown validation error';
3717

3818
try {
3919
const keyword = error.keyword as keyof DefaultAJVMessages;
40-
const errorMessageFunction = defaultAJVMessages[keyword];
20+
const errorMessageFunction = { ...defaultAJVMessages, ...userDefinedMessages }[
21+
keyword
22+
];
4123

4224
if (typeof errorMessageFunction === 'function') {
4325
return errorMessageFunction(error.params);
@@ -51,6 +33,7 @@ const getErrorMessage = (error: ErrorObject): string => {
5133

5234
export const getErrors = <T extends Record<string, any>>(
5335
ajvErrors: ErrorObject[],
36+
userDefinedMessages?: Record<string, AJVMessageFunction>,
5437
): useFormErrors<T> => {
5538
return ajvErrors.reduce((acc, current) => {
5639
const fieldName: string | null = getFieldName(current);
@@ -60,7 +43,7 @@ export const getErrors = <T extends Record<string, any>>(
6043

6144
return {
6245
...acc,
63-
[fieldName]: getErrorMessage(current),
46+
[fieldName]: getErrorMessage(current, userDefinedMessages),
6447
};
6548
}, {});
6649
};
@@ -86,3 +69,16 @@ export const getValue = (value: unknown) => {
8669

8770
return value ?? '';
8871
};
72+
73+
export const addUserDefinedKeywords = (
74+
ajv: Ajv,
75+
customKeywords: KeywordDefinition[],
76+
): void => {
77+
customKeywords.forEach((keyword: KeywordDefinition) => {
78+
if (ajv.getKeyword(keyword.keyword as string)) {
79+
return;
80+
}
81+
82+
ajv.addKeyword(keyword);
83+
});
84+
};

src/utils/validation.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
* @url https://github.com/ajv-validator/ajv-formats
44
* @url https://github.com/ajv-validator/ajv-errors
55
*/
6-
import Ajv, { KeywordDefinition } from 'ajv';
6+
import Ajv from 'ajv';
77
import addAjvErrors from 'ajv-errors';
88
import addFormats from 'ajv-formats';
99
// @ts-expect-error - Currently, there is no type definition for this package.
1010
import programmerNetworkAjv from 'programmer-network-ajv';
1111

12-
const { keywords } = programmerNetworkAjv;
12+
export const { keywords } = programmerNetworkAjv;
1313

1414
export const ajv = addFormats(
1515
addAjvErrors(
@@ -28,9 +28,3 @@ export const ajv = addFormats(
2828
}),
2929
),
3030
);
31-
32-
keywords.map((keyword: string | KeywordDefinition) => ajv.addKeyword(keyword));
33-
34-
export const customKeywordNames = keywords.map((keyword: { keyword: string }) => {
35-
return keyword.keyword;
36-
});

0 commit comments

Comments
 (0)