Skip to content

Commit d864d41

Browse files
committed
Updates
1 parent deb77dd commit d864d41

File tree

3 files changed

+174
-10
lines changed

3 files changed

+174
-10
lines changed

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
1-
# React AJV Schema Hook
1+
# React AJV Schema
22

33
![Drag Racing](./assets/ajv-react.png)
44

55
[use-ajv-form](https://github.com/agjs/use-ajv-form) is a custom React Hook that enables you to generate your form logic and validation based on [Ajv JSON Schema Validator](https://ajv.js.org/).
66

77
It integrates seamlessly with any form and is completely agnostic from your form markup. It simply provides the state and validation. You choose how you want to present the errors and organise your forms. In simple words, it's completely decoupled from how your forms work, look and feel.
88

9-
# Why
9+
## Why
1010

11-
Validating forms manually is a painful process. Ajv solves that by not only providing validation based on valid JSON Schema Specification, but also, provides custom plugins and keywords that allow you to create your own custom validators. That means that you can extend your schema with infinite amount of validators, and keep your forms still purely depend on the schema, without introducing manual if statements all over the place.
11+
Validating forms manually is a painful process. Ajv solves that by not only providing validation based on valid [JSON Schema Specification](https://json-schema.org/specification.html), but also, provides [plugins](https://ajv.js.org/packages/) that allows further extension and creation of custom validators. That means that you can extend your schema with infinite amount of validators, and keep your forms still purely depend on the schema, without introducing manual if statements all over the place.
1212

1313
This library is used by all forms on [Programmer Network](https://programmer.network/) and is Open Sourced due to our amazing community on an official [Programmer Network Twitch Channel](https://twitch.tv/programmer_network).
1414

15-
# Install
15+
---
16+
17+
## Features
18+
19+
- Form JSON Schema Validation
20+
- Dirty state checking
21+
- Consume remote errors as part of the schema, e.g. `username already taken`. In simple words, errors coming from your API
22+
- Maps 1:1 with nested objects. In simple words, a form can generate the exact object shape that you want, no need for manual mapping before e.g. API submission
23+
24+
---
25+
26+
## Install
1627

1728
`yarn add @programmer_network/use-ajv-form`
1829

1930
or
2031

2132
`npm install @programmer_network/use-ajv-form`
2233

23-
# Usage
34+
---
35+
36+
## Usage
2437

2538
[Codesandbox Live Demo](https://google.com)
2639

src/hooks/useAjvForm/index.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,88 @@
1-
export const useAjvForm = (initialState, schema): number => {
2-
return 5;
3-
};
1+
import { JSONSchemaType } from 'ajv';
2+
import { useState } from 'react';
3+
import { ajv } from './ajv';
4+
5+
import { getInitialState, getValue, processAjvErrors, unflatten } from '../../utils';
6+
7+
export const useAjvForm = (initialState: Object, schema: JSONSchemaType<any>) => {
8+
const [localState, setLocalState] = useState<any>(getInitialState(initialState));
9+
const ajvValidate = ajv.compile(schema);
10+
11+
const setState = (form: any) => {
12+
const inputs = Object.keys(form).reduce((acc, name) => {
13+
return {
14+
...acc,
15+
[name]: { value: form[name], error: null },
16+
};
17+
}, {});
18+
19+
setLocalState((currentState: any) => ({ ...currentState, ...inputs }));
20+
};
21+
22+
const setErrors = (errors: any) => {
23+
return Object.keys(localState).reduce((acc, fieldName) => {
24+
return {
25+
...acc,
26+
[fieldName]: {
27+
value: getValue(localState, fieldName).value,
28+
error: errors[fieldName] || null,
29+
},
30+
};
31+
}, {});
32+
};
33+
34+
/**
35+
* 1. Takes the flat state
36+
* 2. Creates an object with an interface that fits func @ajvValidate
37+
* 3. Unflattens the state
38+
* 4. Loops through the errors and sets them for those inputs that have one
39+
*/
40+
const validate = () => {
41+
const data = unflatten(
42+
Object.keys(localState).reduce((acc, inputName) => {
43+
return {
44+
...acc,
45+
[inputName]: localState[inputName].value,
46+
};
47+
}, {}),
48+
'.',
49+
);
50+
51+
const isValid = ajvValidate(data);
52+
53+
if (!isValid) {
54+
const ajvErrors: any = processAjvErrors(ajvValidate.errors);
55+
56+
const errors: any = Object.keys(ajvErrors).reduce((acc, key) => {
57+
return {
58+
...acc,
59+
[key.split('/').join('.')]:
60+
typeof ajvErrors[key] === 'object'
61+
? Object.keys(ajvErrors[key]).reduce((a, k) => {
62+
return {
63+
...a,
64+
[k]: ajvErrors[key][k],
65+
};
66+
}, {})
67+
: ajvErrors[key],
68+
};
69+
}, {});
70+
71+
setLocalState(
72+
setErrors(
73+
Object.keys(errors).reduce((acc, fieldName) => {
74+
return {
75+
...acc,
76+
[fieldName]: errors[fieldName],
77+
};
78+
}, {}),
79+
),
80+
);
81+
return false;
82+
}
83+
84+
return true;
85+
};
486

5-
useCounter.defaultProps = {
6-
initialValue: 0,
87+
return { state: localState, setState, validate };
788
};

src/utils/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
export const processAjvErrors = (ajvErrors) => {
2+
return ajvErrors.reduce((acc, current) => {
3+
const fieldName = current.instancePath
4+
? current.instancePath.slice(1, current.instancePath.length)
5+
: current.params?.missingProperty;
6+
7+
return {
8+
...acc,
9+
[fieldName]: current.message,
10+
};
11+
}, {});
12+
};
13+
14+
export const unflatten = (data, splitOperator) => {
15+
var result = {};
16+
for (var i in data) {
17+
var keys = i.split(splitOperator);
18+
keys.reduce(function (r, e, j) {
19+
return (
20+
r[e] ||
21+
(r[e] = isNaN(Number(keys[j + 1]))
22+
? keys.length - 1 === j
23+
? data[i]
24+
: {}
25+
: [])
26+
);
27+
}, result);
28+
}
29+
return result;
30+
};
31+
32+
export const getValue = (state, fieldName) => {
33+
if (typeof state[fieldName] === 'boolean') {
34+
return state[fieldName];
35+
}
36+
37+
if (state[fieldName]) {
38+
return state[fieldName];
39+
}
40+
41+
return '';
42+
};
43+
44+
export const flattenObj = (ob) => {
45+
let result = {};
46+
for (const i in ob) {
47+
if (typeof ob[i] === 'object') {
48+
const temp = flattenObj(ob[i]);
49+
for (const j in temp) {
50+
result[i + '.' + j] = temp[j];
51+
}
52+
} else {
53+
result[i] = ob[i];
54+
}
55+
}
56+
return result;
57+
};
58+
59+
export const getInitialState = (initialState: any) => {
60+
const flattened = flattenObj(initialState);
61+
62+
return Object.keys(flattened).reduce((acc, fieldName) => {
63+
acc[fieldName] = {
64+
value: getValue(flattened, fieldName),
65+
error: null,
66+
};
67+
68+
return acc;
69+
}, {});
70+
};

0 commit comments

Comments
 (0)