├── .gitignore
├── README.md
├── index.html
├── package.json
├── src
├── App.tsx
├── constants.ts
├── index.css
├── logic
│ ├── __snapshots__
│ │ ├── findRemovedFieldAndRemoveListener.test.ts.snap
│ │ ├── transformToNestObject.test.ts.snap
│ │ └── validateField.test.tsx.snap
│ ├── appendErrors.ts
│ ├── assignWatchFields.test.ts
│ ├── assignWatchFields.ts
│ ├── attachEventListeners.test.ts
│ ├── attachEventListeners.ts
│ ├── findRemovedFieldAndRemoveListener.test.ts
│ ├── findRemovedFieldAndRemoveListener.ts
│ ├── focusOnErrorField.test.ts
│ ├── focusOnErrorField.ts
│ ├── generateId.test.ts
│ ├── generateId.ts
│ ├── getCheckboxValue.test.ts
│ ├── getCheckboxValue.ts
│ ├── getFieldArrayParentName.ts
│ ├── getFieldValue.test.ts
│ ├── getFieldValue.ts
│ ├── getFieldsValues.test.ts
│ ├── getFieldsValues.ts
│ ├── getInputValue.test.ts
│ ├── getInputValue.ts
│ ├── getIsFieldsDifferent.test.ts
│ ├── getIsFieldsDifferent.ts
│ ├── getMultipleSelectValue.test.ts
│ ├── getMultipleSelectValue.ts
│ ├── getRadioValue.test.ts
│ ├── getRadioValue.ts
│ ├── getSortedArrayFIeldIndexes.test.ts
│ ├── getSortedArrayFieldIndexes.ts
│ ├── getValidateError.test.ts
│ ├── getValidateError.ts
│ ├── getValueAndMessage.test.ts
│ ├── getValueAndMessage.ts
│ ├── index.ts
│ ├── isNameInFieldArray.test.ts
│ ├── isNameInFieldArray.ts
│ ├── removeAllEventListeners.test.ts
│ ├── removeAllEventListeners.ts
│ ├── shouldRenderBasedOnError.test.ts
│ ├── shouldRenderBasedOnError.ts
│ ├── skipValidation.test.ts
│ ├── skipValidation.ts
│ ├── transformToNestObject.test.ts
│ ├── transformToNestObject.ts
│ ├── validateField.test.tsx
│ └── validateField.ts
├── main.js
├── types
│ ├── form.ts
│ ├── index.ts
│ ├── props.ts
│ └── utils.ts
├── useForm.ts
└── utils
│ ├── fillEmptyArray.test.ts
│ ├── fillEmptyArray.ts
│ ├── filterBooleanArray.test.ts
│ ├── filterBooleanArray.ts
│ ├── get.test.ts
│ ├── get.ts
│ ├── getPath.test.ts
│ ├── getPath.ts
│ ├── index.ts
│ ├── insert.test.ts
│ ├── insert.ts
│ ├── isArray.test.ts
│ ├── isArray.ts
│ ├── isBoolean.test.ts
│ ├── isBoolean.ts
│ ├── isCheckBoxInput.test.ts
│ ├── isCheckBoxInput.ts
│ ├── isDetached.test.ts
│ ├── isDetached.ts
│ ├── isEmptyObject.test.ts
│ ├── isEmptyObject.ts
│ ├── isFileInput.test.ts
│ ├── isFileInput.ts
│ ├── isFunction.test.ts
│ ├── isFunction.ts
│ ├── isHTMLElement.test.ts
│ ├── isHTMLElement.ts
│ ├── isKey.test.ts
│ ├── isKey.ts
│ ├── isMessage.test.ts
│ ├── isMessage.ts
│ ├── isMultipleSelect.test.ts
│ ├── isMultipleSelect.ts
│ ├── isNullOrUndefined.test.ts
│ ├── isNullOrUndefined.ts
│ ├── isObject.test.ts
│ ├── isObject.ts
│ ├── isPrimitive.test.ts
│ ├── isPrimitive.ts
│ ├── isRadioInput.test.ts
│ ├── isRadioInput.ts
│ ├── isRadioOrCheckbox.test.ts
│ ├── isRadioOrCheckbox.ts
│ ├── isRegex.test.ts
│ ├── isRegex.ts
│ ├── isSameError.test.ts
│ ├── isSameError.ts
│ ├── isSelectInput.ts
│ ├── isString.test.ts
│ ├── isString.ts
│ ├── isUndefined.test.ts
│ ├── isUndefined.ts
│ ├── move.test.ts
│ ├── move.ts
│ ├── onDomRemove.test.ts
│ ├── onDomRemove.ts
│ ├── prepend.ts
│ ├── remove.test.ts
│ ├── remove.ts
│ ├── set.ts
│ ├── stringToPath.test.ts
│ ├── stringToPath.ts
│ ├── swap.ts
│ ├── unique.ts
│ ├── unset.test.ts
│ ├── unset.ts
│ ├── validationModeChecker.test.ts
│ └── validationModeChecker.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Coverage directory used by tools like istanbul
9 | coverage
10 |
11 | # Dependency directories
12 | node_modules/
13 | app/node_modules/
14 | example/node_modules/
15 | example/build/
16 |
17 | # Optional npm cache directory
18 | .npm
19 |
20 | # Optional eslint cache
21 | .eslintcache
22 |
23 | # Output of 'npm pack'
24 | *.tgz
25 |
26 | # Yarn Integrity file
27 | .yarn-integrity
28 |
29 | .DS_Store
30 | /lib
31 | /dist
32 | .idea/
33 | .rpt2_cache
34 | .coveralls.yml
35 | package-lock.json
36 | /cypress/videos
37 | /cypress/screenshots
38 | /cypress/fixtures
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🧪 Vue Hook Form (experiential)
2 |
3 | This is an experiential repo to port react hook form into the Vue world.
4 |
5 | ## Quickstart
6 |
7 | ```tsx
8 | import { useForm } from "@hookform/vue";
9 |
10 | export default {
11 | setup() {
12 | return useForm();
13 | },
14 | onSubmit(data) {
15 | console.log(data);
16 | },
17 | render() {
18 | return (
19 |
28 | );
29 | }
30 | };
31 | ```
32 |
33 | ## Sponsors
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Want your logo here? [DM on Twitter](https://twitter.com/HookForm)
42 |
43 | ## Backers
44 |
45 | Thanks goes to all our backers! [[Become a backer](https://opencollective.com/react-hook-form#backer)].
46 |
47 |
48 |
49 |
50 |
51 | ## Organizations
52 |
53 | Thanks goes to these wonderful organizations! [[Contribute](https://opencollective.com/react-hook-form/contribute)].
54 |
55 |
56 |
57 |
58 |
59 | ## Contributors
60 |
61 | Thanks goes to these wonderful people! [[Become a contributor](CONTRIBUTING.md)].
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Vue Demi with Vite
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo-vue-3-vite",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vite build"
8 | },
9 | "dependencies": {
10 | "@vue-demi/use-mouse": "^0.0.1",
11 | "vue": "^3.0.0-beta.15"
12 | },
13 | "devDependencies": {
14 | "@vue/compiler-sfc": "^3.0.0-beta.15",
15 | "vite": "^1.0.0-beta.3"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "./useForm";
2 |
3 | export default {
4 | setup() {
5 | return useForm({
6 | mode: "onChange"
7 | });
8 | },
9 | onSubmit(data) {
10 | console.log(data);
11 | },
12 | render() {
13 | return (
14 |
21 | );
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | import { ValidationMode } from './types/form';
2 |
3 | export const EVENTS = {
4 | BLUR: 'blur',
5 | CHANGE: 'change',
6 | INPUT: 'input',
7 | };
8 |
9 | export const VALIDATION_MODE: ValidationMode = {
10 | onBlur: 'onBlur',
11 | onChange: 'onChange',
12 | onSubmit: 'onSubmit',
13 | all: 'all',
14 | };
15 |
16 | export const SELECT = 'select';
17 |
18 | export const UNDEFINED = 'undefined';
19 |
20 | export const INPUT_VALIDATION_RULES = {
21 | max: 'max',
22 | min: 'min',
23 | maxLength: 'maxLength',
24 | minLength: 'minLength',
25 | pattern: 'pattern',
26 | required: 'required',
27 | validate: 'validate',
28 | };
29 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | #app {
2 | font-family: Avenir, Helvetica, Arial, sans-serif;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | color: #2c3e50;
6 | margin-top: 60px;
7 | }
8 |
9 | button,
10 | input {
11 | font-size: 40px;
12 | }
13 |
--------------------------------------------------------------------------------
/src/logic/__snapshots__/findRemovedFieldAndRemoveListener.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`findMissDomAndClean radio should not remove event listener when type is not Element 1`] = `
4 | Object {
5 | "current": Object {
6 | "test": Object {
7 | "name": "test",
8 | "options": Array [
9 | Object {
10 | "mutationWatcher": Object {
11 | "disconnect": [MockFunction],
12 | },
13 | "ref": "test",
14 | },
15 | ],
16 | "ref": Object {},
17 | "type": "radio",
18 | },
19 | },
20 | }
21 | `;
22 |
23 | exports[`findMissDomAndClean radio should not remove event listener when type is not Element 2`] = `undefined`;
24 |
25 | exports[`findMissDomAndClean radio should remove none radio field when found 1`] = `
26 | Object {
27 | "current": Object {
28 | "test1": Object {
29 | "name": "test",
30 | "ref": Object {},
31 | },
32 | },
33 | }
34 | `;
35 |
36 | exports[`findMissDomAndClean radio should work for checkbox type input 1`] = `undefined`;
37 |
38 | exports[`findMissDomAndClean radio should work for radio type input 1`] = `undefined`;
39 |
--------------------------------------------------------------------------------
/src/logic/__snapshots__/transformToNestObject.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`transformToNestObject should combine all the array fields 1`] = `
4 | Object {
5 | "email": Array [
6 | undefined,
7 | "asdasd@dsad.com",
8 | "asdasd@.com",
9 | ],
10 | "firstName": Array [
11 | undefined,
12 | "asdasd",
13 | "asdasd",
14 | ],
15 | "lastName": Array [
16 | undefined,
17 | "asdasd",
18 | "asd",
19 | ],
20 | "test": "test",
21 | }
22 | `;
23 |
24 | exports[`transformToNestObject should combine array object correctly 1`] = `
25 | Object {
26 | "name": Array [
27 | Object {
28 | "firstName": "testFirst",
29 | "lastName": "testLast",
30 | },
31 | ],
32 | "test": Array [
33 | undefined,
34 | Object {
35 | "task": "testLast",
36 | "what": "testLast",
37 | },
38 | Object {
39 | "what": Array [
40 | undefined,
41 | Object {
42 | "test": "testLast",
43 | },
44 | ],
45 | },
46 | ],
47 | }
48 | `;
49 |
50 | exports[`transformToNestObject should combine object correctly 1`] = `
51 | Object {
52 | "name": Object {
53 | "firstName": "testFirst",
54 | "lastName": Object {
55 | "bill": Object {
56 | "luo": "testLast",
57 | },
58 | },
59 | },
60 | }
61 | `;
62 |
63 | exports[`transformToNestObject should combine with results 1`] = `
64 | Object {
65 | "name": "testFirst",
66 | "name1": "testFirst",
67 | "name2": "testFirst",
68 | }
69 | `;
70 |
71 | exports[`transformToNestObject should handle quoted values 1`] = `
72 | Object {
73 | "name": Object {
74 | "foobar": "testFirst",
75 | },
76 | }
77 | `;
78 |
79 | exports[`transformToNestObject should handle quoted values 2`] = `
80 | Object {
81 | "name": Object {
82 | "b2ill": "testFirst",
83 | },
84 | }
85 | `;
86 |
87 | exports[`transformToNestObject should return default name value 1`] = `
88 | Object {
89 | "name": "testFirst",
90 | }
91 | `;
92 |
--------------------------------------------------------------------------------
/src/logic/__snapshots__/validateField.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`validateField should return all validation error messages 1`] = `
4 | Object {
5 | "test": Object {
6 | "message": "test",
7 | "ref": Object {
8 | "name": "test",
9 | "type": "text",
10 | "value": "",
11 | },
12 | "type": "required",
13 | "types": Object {
14 | "required": "test",
15 | "test": true,
16 | "test1": "Luo",
17 | "test2": "Bill",
18 | },
19 | },
20 | }
21 | `;
22 |
23 | exports[`validateField should return all validation error messages 2`] = `
24 | Object {
25 | "test": Object {
26 | "message": "minLength",
27 | "ref": Object {
28 | "name": "test",
29 | "type": "text",
30 | "value": "bil",
31 | },
32 | "type": "minLength",
33 | "types": Object {
34 | "minLength": "minLength",
35 | "pattern": "pattern",
36 | "test": true,
37 | "test1": "Luo",
38 | "test2": "Bill",
39 | },
40 | },
41 | }
42 | `;
43 |
44 | exports[`validateField should return all validation error messages 3`] = `
45 | Object {
46 | "test": Object {
47 | "message": "minLength",
48 | "ref": Object {
49 | "name": "test",
50 | "type": "text",
51 | "value": "bil",
52 | },
53 | "type": "minLength",
54 | "types": Object {
55 | "minLength": "minLength",
56 | "pattern": "pattern",
57 | "test": true,
58 | "test1": "Luo",
59 | "test2": "Bill",
60 | },
61 | },
62 | }
63 | `;
64 |
65 | exports[`validateField should return all validation errors 1`] = `
66 | Object {
67 | "test": Object {
68 | "message": "",
69 | "ref": Object {
70 | "name": "test",
71 | "type": "text",
72 | "value": "",
73 | },
74 | "type": "required",
75 | "types": Object {
76 | "required": true,
77 | "validate": true,
78 | },
79 | },
80 | }
81 | `;
82 |
83 | exports[`validateField should return all validation errors 2`] = `
84 | Object {
85 | "test": Object {
86 | "message": "",
87 | "ref": Object {
88 | "name": "test",
89 | "type": "text",
90 | "value": "123",
91 | },
92 | "type": "minLength",
93 | "types": Object {
94 | "minLength": true,
95 | "pattern": true,
96 | "validate": true,
97 | },
98 | },
99 | }
100 | `;
101 |
--------------------------------------------------------------------------------
/src/logic/appendErrors.ts:
--------------------------------------------------------------------------------
1 | import {
2 | InternalFieldName,
3 | ValidateResult,
4 | FlatFieldErrors,
5 | } from '../types/form';
6 |
7 | export default (
8 | name: InternalFieldName,
9 | validateAllFieldCriteria: boolean,
10 | errors: FlatFieldErrors,
11 | type: string,
12 | message: ValidateResult,
13 | ) => {
14 | if (validateAllFieldCriteria) {
15 | const error = errors[name];
16 |
17 | return {
18 | ...error,
19 | types: {
20 | ...(error && error.types ? error.types : {}),
21 | [type]: message || true,
22 | },
23 | };
24 | }
25 |
26 | return {};
27 | };
28 |
--------------------------------------------------------------------------------
/src/logic/assignWatchFields.test.ts:
--------------------------------------------------------------------------------
1 | import assignWatchFields from './assignWatchFields';
2 |
3 | describe('assignWatchFields', () => {
4 | it('should return undefined when field values is empty object or undefined', () => {
5 | expect(assignWatchFields({}, '', new Set(''), {})).toEqual(undefined);
6 | });
7 |
8 | it('should return watched value and update watchFields', () => {
9 | const watchFields = new Set();
10 | expect(
11 | assignWatchFields({ test: '' }, 'test', watchFields as any, {}),
12 | ).toEqual('');
13 | expect(watchFields).toEqual(new Set(['test']));
14 | });
15 |
16 | it('should get array fields value', () => {
17 | const watchFields = new Set();
18 | expect(
19 | assignWatchFields(
20 | { test: ['', ''] },
21 | 'test',
22 | watchFields as any,
23 | {},
24 | ),
25 | ).toEqual(['', '']);
26 | expect(watchFields).toEqual(new Set(['test', 'test[0]', 'test[1]']));
27 | });
28 |
29 | it('should return default value correctly', () => {
30 | expect(
31 | assignWatchFields({ a: true }, 'b', new Set(), { b: true } as any),
32 | ).toEqual(true);
33 | });
34 |
35 | it('should return undefined when there is no value match', () => {
36 | expect(
37 | assignWatchFields({}, 'test', new Set(), 'test' as any),
38 | ).toEqual(undefined);
39 | });
40 |
41 | it('should not append to more watchFields when value is null or undefined', () => {
42 | const watchFields = new Set();
43 |
44 | expect(
45 | assignWatchFields(
46 | {
47 | test: {
48 | test: null,
49 | },
50 | },
51 | 'test.test',
52 | watchFields as any,
53 | 'test' as any,
54 | ),
55 | ).toEqual(null);
56 |
57 | expect(watchFields).toEqual(new Set(['test.test']));
58 |
59 | expect(
60 | assignWatchFields(
61 | {
62 | test: {
63 | test: undefined,
64 | },
65 | },
66 | 'test.test',
67 | watchFields as any,
68 | 'test' as any,
69 | ),
70 | ).toEqual(undefined);
71 |
72 | expect(watchFields).toEqual(new Set(['test.test']));
73 |
74 | expect(
75 | assignWatchFields(
76 | {
77 | test: {
78 | test: '123',
79 | },
80 | },
81 | 'test.test',
82 | watchFields as any,
83 | 'test' as any,
84 | ),
85 | ).toEqual('123');
86 |
87 | expect(watchFields).toEqual(new Set(['test.test']));
88 |
89 | expect(
90 | assignWatchFields(
91 | {
92 | test: {
93 | test: false,
94 | },
95 | },
96 | 'test.test',
97 | watchFields as any,
98 | 'test' as any,
99 | ),
100 | ).toEqual(false);
101 |
102 | expect(watchFields).toEqual(new Set(['test.test']));
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/src/logic/assignWatchFields.ts:
--------------------------------------------------------------------------------
1 | import get from '../utils/get';
2 | import { getPath } from '../utils/getPath';
3 | import isEmptyObject from '../utils/isEmptyObject';
4 | import isUndefined from '../utils/isUndefined';
5 | import isObject from '../utils/isObject';
6 | import isArray from '../utils/isArray';
7 | import { DeepPartial } from '../types/utils';
8 | import {
9 | FieldValue,
10 | FieldValues,
11 | InternalFieldName,
12 | UnpackNestedValue,
13 | } from '../types/form';
14 |
15 | export default (
16 | fieldValues: TFieldValues,
17 | fieldName: InternalFieldName,
18 | watchFields: Set>,
19 | inputValue: UnpackNestedValue>,
20 | isSingleField?: boolean,
21 | ):
22 | | FieldValue
23 | | UnpackNestedValue>
24 | | undefined => {
25 | let value;
26 |
27 | watchFields.add(fieldName);
28 |
29 | if (isEmptyObject(fieldValues)) {
30 | value = undefined;
31 | } else {
32 | value = get(fieldValues, fieldName);
33 |
34 | if (isObject(value) || isArray(value)) {
35 | getPath(
36 | fieldName,
37 | value as TFieldValues,
38 | ).forEach((name: string) => watchFields.add(name));
39 | }
40 | }
41 |
42 | return isUndefined(value)
43 | ? isSingleField
44 | ? inputValue
45 | : get(inputValue, fieldName)
46 | : value;
47 | };
48 |
--------------------------------------------------------------------------------
/src/logic/attachEventListeners.test.ts:
--------------------------------------------------------------------------------
1 | import attachEventListeners from './attachEventListeners';
2 | import isHTMLElement from '../utils/isHTMLElement';
3 |
4 | jest.mock('../utils/isHTMLElement');
5 | (isHTMLElement as any).mockReturnValue(true);
6 |
7 | describe('attachEventListeners', () => {
8 | it('should attach change event for radio and return undefined', () => {
9 | const handleChange = jest.fn();
10 | const addEventListener = jest.fn();
11 | const fields = {
12 | test: {
13 | ref: {
14 | name: 'test',
15 | addEventListener,
16 | },
17 | eventAttached: [],
18 | name: 'test',
19 | },
20 | };
21 |
22 | expect(
23 | attachEventListeners(fields.test, true, handleChange),
24 | ).toBeUndefined();
25 |
26 | expect(addEventListener).toBeCalledWith('change', handleChange);
27 | expect(fields.test.eventAttached).toBeTruthy();
28 | });
29 |
30 | it('should attach on change event on radio type input when it is watched', () => {
31 | const handleChange = jest.fn();
32 | const addEventListener = jest.fn();
33 | const fields = {
34 | test: {
35 | ref: {
36 | name: 'test',
37 | addEventListener,
38 | },
39 | eventAttached: [],
40 | watch: true,
41 | },
42 | };
43 |
44 | expect(
45 | attachEventListeners(fields.test, true, handleChange),
46 | ).toBeUndefined();
47 |
48 | expect(addEventListener).toBeCalledWith('change', handleChange);
49 | expect(fields.test.eventAttached).toBeTruthy();
50 | });
51 |
52 | it('should attach blur event when it is under blur mode', () => {
53 | const handleChange = jest.fn();
54 | const addEventListener = jest.fn();
55 | const fields = {
56 | test: {
57 | ref: {
58 | name: 'test',
59 | addEventListener,
60 | },
61 | eventAttached: [],
62 | watch: true,
63 | },
64 | };
65 |
66 | expect(
67 | attachEventListeners(fields.test, true, handleChange),
68 | ).toBeUndefined();
69 |
70 | expect(addEventListener).toBeCalledWith('blur', handleChange);
71 | });
72 |
73 | it('should attach blur event when re validate mode is under blur', () => {
74 | const handleChange = jest.fn();
75 | const addEventListener = jest.fn();
76 | const fields = {
77 | test: {
78 | ref: {
79 | name: 'test',
80 | addEventListener,
81 | },
82 | eventAttached: [],
83 | watch: true,
84 | },
85 | };
86 |
87 | expect(
88 | attachEventListeners(fields.test, true, handleChange),
89 | ).toBeUndefined();
90 |
91 | expect(addEventListener).toBeCalledWith('blur', handleChange);
92 | });
93 |
94 | it('should attach input event on none radio type input', () => {
95 | const handleChange = jest.fn();
96 | const addEventListener = jest.fn();
97 | const fields = {
98 | test: {
99 | ref: {
100 | name: 'test',
101 | addEventListener,
102 | },
103 | eventAttached: [],
104 | },
105 | };
106 |
107 | expect(
108 | attachEventListeners(fields.test, false, handleChange),
109 | ).toBeUndefined();
110 |
111 | expect(addEventListener).toBeCalledWith('input', handleChange);
112 | expect(fields.test.eventAttached).toBeTruthy();
113 | });
114 |
115 | it('should attach input event on none radio type input when it is watched', () => {
116 | const handleChange = jest.fn();
117 | const addEventListener = jest.fn();
118 | const fields = {
119 | test: {
120 | ref: {
121 | name: 'test',
122 | addEventListener,
123 | },
124 | eventAttached: [],
125 | watch: true,
126 | },
127 | };
128 |
129 | expect(
130 | attachEventListeners(fields.test, false, handleChange),
131 | ).toBeUndefined();
132 |
133 | expect(addEventListener).toBeCalledWith('input', handleChange);
134 | expect(fields.test.eventAttached).toBeTruthy();
135 | });
136 |
137 | it('should attach on blur event on radio type input', () => {
138 | const handleChange = jest.fn();
139 | const addEventListener = jest.fn();
140 | const fields = {
141 | test: {
142 | ref: {
143 | name: 'test',
144 | addEventListener,
145 | },
146 | eventAttached: [],
147 | },
148 | };
149 |
150 | expect(
151 | attachEventListeners(fields.test, true, handleChange),
152 | ).toBeUndefined();
153 |
154 | expect(addEventListener).toBeCalledWith('change', handleChange);
155 | expect(fields.test.eventAttached).toBeTruthy();
156 | });
157 |
158 | it('should attach input event on none radio type input', () => {
159 | const handleChange = jest.fn();
160 | const addEventListener = jest.fn();
161 | const fields = {
162 | test: {
163 | ref: {
164 | name: 'test',
165 | addEventListener,
166 | },
167 | eventAttached: [],
168 | },
169 | };
170 |
171 | expect(
172 | attachEventListeners(fields.test, false, handleChange),
173 | ).toBeUndefined();
174 |
175 | expect(addEventListener).toBeCalledWith('input', handleChange);
176 | expect(fields.test.eventAttached).toBeTruthy();
177 | });
178 |
179 | it('should not call addEventListener if ref is not HTMLElement type', () => {
180 | (isHTMLElement as any).mockReturnValue(false);
181 | const addEventListener = jest.fn();
182 | const fields = {
183 | test: {
184 | ref: {
185 | name: 'test',
186 | addEventListener,
187 | },
188 | eventAttached: [],
189 | name: 'test',
190 | },
191 | };
192 |
193 | expect(attachEventListeners(fields.test, false, () => {})).toBeUndefined();
194 | expect(addEventListener).not.toBeCalled();
195 | });
196 |
197 | it('should not call addEventListener if handleChange is undefined', () => {
198 | (isHTMLElement as any).mockReturnValue(false);
199 | const addEventListener = jest.fn();
200 | const fields = {
201 | test: {
202 | ref: {
203 | name: 'test',
204 | addEventListener,
205 | },
206 | eventAttached: [],
207 | name: 'test',
208 | },
209 | };
210 |
211 | expect(attachEventListeners(fields.test, false)).toBeUndefined();
212 | expect(addEventListener).not.toBeCalled();
213 | });
214 | });
215 |
--------------------------------------------------------------------------------
/src/logic/attachEventListeners.ts:
--------------------------------------------------------------------------------
1 | import isHTMLElement from '../utils/isHTMLElement';
2 | import { EVENTS } from '../constants';
3 | import { Field } from '../types/form';
4 |
5 | export default function attachEventListeners(
6 | { ref }: Field,
7 | shouldAttachChangeEvent: boolean,
8 | handleChange?: EventListenerOrEventListenerObject,
9 | ): void {
10 | if (isHTMLElement(ref) && handleChange) {
11 | ref.addEventListener(
12 | shouldAttachChangeEvent ? EVENTS.CHANGE : EVENTS.INPUT,
13 | handleChange,
14 | );
15 | ref.addEventListener(EVENTS.BLUR, handleChange);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/logic/findRemovedFieldAndRemoveListener.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import findRemovedFieldAndRemoveListener from './findRemovedFieldAndRemoveListener';
3 | import isDetached from '../utils/isDetached';
4 | import removeAllEventListeners from './removeAllEventListeners';
5 |
6 | jest.mock('./removeAllEventListeners');
7 | jest.mock('../utils/isDetached');
8 |
9 | describe('findMissDomAndClean', () => {
10 | beforeEach(() => {
11 | jest.resetAllMocks();
12 |
13 | (isDetached as any).mockImplementation(() => {
14 | return true;
15 | });
16 | });
17 |
18 | describe('radio', () => {
19 | it('should return default fields value if nothing matches', () => {
20 | document.body.contains = jest.fn(() => true);
21 | const fields = {
22 | current: {
23 | test: 'test',
24 | },
25 | };
26 | expect(
27 | findRemovedFieldAndRemoveListener(
28 | fields as any,
29 | () => ({} as any),
30 | {
31 | ref: { name: 'bill', type: 'radio' },
32 | },
33 | {},
34 | true,
35 | ),
36 | ).toEqual(undefined);
37 | });
38 |
39 | it('should remove options completely if option found and no option left', () => {
40 | document.body.contains = jest.fn(() => false);
41 |
42 | const ref = document.createElement('input');
43 | ref.setAttribute('name', 'test');
44 | ref.setAttribute('type', 'radio');
45 |
46 | const disconnect = jest.fn();
47 | const fields = {
48 | current: {
49 | test: {
50 | name: 'test',
51 | ref,
52 | options: [
53 | {
54 | ref,
55 | mutationWatcher: {
56 | disconnect,
57 | },
58 | },
59 | ],
60 | },
61 | },
62 | };
63 |
64 | findRemovedFieldAndRemoveListener(
65 | fields,
66 | () => ({} as any),
67 | {
68 | ref,
69 | },
70 | {},
71 | true,
72 | );
73 |
74 | expect(fields).toEqual({
75 | current: {},
76 | });
77 | });
78 |
79 | it('should remove none radio field when found', () => {
80 | const ref = document.createElement('input');
81 | ref.setAttribute('name', 'test');
82 | ref.setAttribute('type', 'radio');
83 | document.body.contains = jest.fn(() => false);
84 | const disconnect = jest.fn();
85 | const fields = {
86 | current: {
87 | test: {
88 | name: 'test',
89 | ref: {},
90 | mutationWatcher: {
91 | disconnect,
92 | },
93 | },
94 | test1: {
95 | name: 'test',
96 | ref: {},
97 | },
98 | },
99 | };
100 |
101 | findRemovedFieldAndRemoveListener(
102 | fields,
103 | () => ({} as any),
104 | {
105 | ref,
106 | mutationWatcher: {
107 | disconnect,
108 | },
109 | },
110 | {},
111 | );
112 |
113 | expect(fields).toMatchSnapshot();
114 | });
115 |
116 | it('should work for radio type input', () => {
117 | const ref = document.createElement('input');
118 | ref.setAttribute('name', 'test');
119 | ref.setAttribute('type', 'radio');
120 | document.body.contains = jest.fn(() => false);
121 | const disconnect = jest.fn();
122 | const fields = {
123 | current: {
124 | test: {
125 | name: 'test',
126 | ref: {},
127 | mutationWatcher: {
128 | disconnect,
129 | },
130 | },
131 | test1: {
132 | name: 'test',
133 | ref: {
134 | type: 'radio',
135 | },
136 | },
137 | },
138 | };
139 |
140 | expect(
141 | findRemovedFieldAndRemoveListener(
142 | fields,
143 | () => ({} as any),
144 | {
145 | ref: { name: 'test', type: 'radio' },
146 | options: [{ ref }],
147 | mutationWatcher: {
148 | disconnect,
149 | },
150 | },
151 | {},
152 | ),
153 | ).toMatchSnapshot();
154 | });
155 |
156 | it('should work for checkbox type input', () => {
157 | const ref = document.createElement('input');
158 | ref.setAttribute('name', 'test');
159 | ref.setAttribute('type', 'checkbox');
160 | document.body.contains = jest.fn(() => false);
161 | const disconnect = jest.fn();
162 | const fields = {
163 | current: {
164 | test: {
165 | name: 'test',
166 | ref: {},
167 | mutationWatcher: {
168 | disconnect,
169 | },
170 | },
171 | test1: {
172 | name: 'test',
173 | ref: {
174 | type: 'checkbox',
175 | },
176 | },
177 | },
178 | };
179 |
180 | expect(
181 | findRemovedFieldAndRemoveListener(
182 | fields,
183 | () => ({} as any),
184 | {
185 | ref: { name: 'test', type: 'checkbox' },
186 | options: [{ ref }],
187 | mutationWatcher: {
188 | disconnect,
189 | },
190 | },
191 | {},
192 | ),
193 | ).toMatchSnapshot();
194 | });
195 |
196 | it('should not delete field when option have value', () => {
197 | (isDetached as any).mockImplementation(() => {
198 | return false;
199 | });
200 |
201 | const fields = {
202 | current: {
203 | test: {
204 | name: 'test',
205 | type: 'radio',
206 | ref: {},
207 | options: [{ ref: { name: 'test', type: 'radio' } }],
208 | },
209 | },
210 | };
211 |
212 | findRemovedFieldAndRemoveListener(
213 | fields,
214 | () => ({} as any),
215 | {
216 | ref: { name: 'test', type: 'radio' },
217 | },
218 | {},
219 | );
220 |
221 | expect(fields).toEqual({
222 | current: {
223 | test: {
224 | name: 'test',
225 | type: 'radio',
226 | ref: {},
227 | options: [{ ref: { name: 'test', type: 'radio' } }],
228 | },
229 | },
230 | });
231 | });
232 |
233 | it('should not remove event listener when type is not Element', () => {
234 | (isDetached as any).mockImplementation(() => {
235 | return false;
236 | });
237 | document.body.contains = jest.fn(() => false);
238 |
239 | const disconnect = jest.fn();
240 | const fields = {
241 | current: {
242 | test: {
243 | name: 'test',
244 | type: 'radio',
245 | ref: {},
246 | options: [
247 | {
248 | ref: 'test',
249 | mutationWatcher: {
250 | disconnect,
251 | },
252 | },
253 | ],
254 | },
255 | },
256 | };
257 |
258 | findRemovedFieldAndRemoveListener(
259 | fields,
260 | () => ({} as any),
261 | {
262 | ref: { name: 'test', type: 'text' },
263 | options: [
264 | {
265 | mutationWatcher: {
266 | disconnect,
267 | },
268 | ref: {},
269 | },
270 | ],
271 | },
272 | {},
273 | );
274 |
275 | expect(fields).toMatchSnapshot();
276 |
277 | expect(
278 | findRemovedFieldAndRemoveListener(
279 | fields,
280 | () => ({} as any),
281 | {
282 | ref: { name: 'test', type: 'text' },
283 | },
284 | {},
285 | ),
286 | ).toMatchSnapshot();
287 | });
288 |
289 | it('should remove options when force delete is set to true', () => {
290 | (isDetached as any).mockImplementation(() => {
291 | return false;
292 | });
293 |
294 | document.body.contains = jest.fn(() => false);
295 |
296 | const ref = document.createElement('input');
297 | ref.setAttribute('name', 'test');
298 | ref.setAttribute('type', 'radio');
299 |
300 | const disconnect = jest.fn();
301 | const fields = {
302 | current: {
303 | test: {
304 | name: 'test',
305 | ref: {},
306 | options: [],
307 | },
308 | },
309 | };
310 | findRemovedFieldAndRemoveListener(
311 | fields,
312 | () => ({} as any),
313 | {
314 | ref: { name: 'test', type: 'radio' },
315 | options: [
316 | {
317 | mutationWatcher: {
318 | disconnect,
319 | },
320 | ref,
321 | },
322 | ],
323 | },
324 | {},
325 | false,
326 | true,
327 | );
328 |
329 | expect(fields).toEqual({
330 | current: {},
331 | });
332 | });
333 | });
334 |
335 | describe('text', () => {
336 | it('should delete field if type is text', () => {
337 | const mockWatcher = jest.fn();
338 | const state = { current: {} };
339 | const fields = {
340 | current: {
341 | test: {
342 | name: 'test',
343 | ref: {
344 | name: 'test',
345 | type: 'text',
346 | value: 'test',
347 | },
348 | mutationWatcher: {
349 | disconnect: mockWatcher,
350 | },
351 | },
352 | },
353 | };
354 |
355 | findRemovedFieldAndRemoveListener(
356 | fields,
357 | () => ({} as any),
358 | fields.current.test,
359 | state,
360 | );
361 |
362 | expect(state).toEqual({
363 | current: { test: 'test' },
364 | });
365 | expect(mockWatcher).toBeCalled();
366 | expect(fields).toEqual({
367 | current: {},
368 | });
369 | });
370 |
371 | it('should delete field if forceDelete is true', () => {
372 | (isDetached as any).mockReturnValue(false);
373 | const state = { current: {} };
374 | const fields = {
375 | current: {
376 | test: {
377 | name: 'test',
378 | ref: {
379 | name: 'test',
380 | type: 'text',
381 | value: 'test',
382 | },
383 | },
384 | },
385 | };
386 |
387 | findRemovedFieldAndRemoveListener(
388 | fields,
389 | () => ({} as any),
390 | fields.current.test,
391 | state,
392 | false,
393 | true,
394 | );
395 |
396 | expect(state).toEqual({
397 | current: { test: 'test' },
398 | });
399 | expect(removeAllEventListeners).toBeCalled();
400 | expect(fields).toEqual({
401 | current: {},
402 | });
403 | });
404 |
405 | it('should store state when component is getting unmount', () => {
406 | const state = { current: {} };
407 | const fields = {
408 | current: {
409 | test: {
410 | name: 'test',
411 | ref: {
412 | value: 'test',
413 | },
414 | },
415 | },
416 | };
417 |
418 | findRemovedFieldAndRemoveListener(
419 | fields,
420 | () => ({} as any),
421 | {
422 | ref: { name: 'test', type: 'text' },
423 | },
424 | state,
425 | false,
426 | );
427 |
428 | expect(state).toEqual({
429 | current: { test: 'test' },
430 | });
431 | });
432 |
433 | it('should not store state when component is getting unmount and value is return undefined', () => {
434 | const state = { current: {} };
435 | const fields = {
436 | current: {
437 | test: {
438 | name: 'test',
439 | ref: {},
440 | },
441 | },
442 | };
443 |
444 | findRemovedFieldAndRemoveListener(
445 | fields,
446 | () => ({} as any),
447 | {
448 | ref: { name: 'test', type: 'text' },
449 | },
450 | state,
451 | false,
452 | );
453 |
454 | expect(state).toEqual({
455 | current: {},
456 | });
457 | });
458 | });
459 |
460 | it('should not call mutation watcher when not available', () => {
461 | jest.spyOn(document.body, 'contains').mockReturnValue(false);
462 |
463 | const ref = document.createElement('input');
464 | ref.setAttribute('name', 'test');
465 | ref.setAttribute('type', 'radio');
466 |
467 | const fields = {
468 | current: {
469 | test: {
470 | name: 'test',
471 | ref,
472 | options: [
473 | {
474 | ref,
475 | },
476 | ],
477 | },
478 | },
479 | };
480 |
481 | expect(() => {
482 | findRemovedFieldAndRemoveListener(
483 | fields,
484 | () => ({} as any),
485 | {
486 | ref,
487 | },
488 | {},
489 | true,
490 | );
491 | }).not.toThrow();
492 |
493 | document.body.contains.mockRestore();
494 | });
495 | });
496 |
--------------------------------------------------------------------------------
/src/logic/findRemovedFieldAndRemoveListener.ts:
--------------------------------------------------------------------------------
1 | import removeAllEventListeners from "./removeAllEventListeners";
2 | import getFieldValue from "./getFieldValue";
3 | import isRadioInput from "../utils/isRadioInput";
4 | import isCheckBoxInput from "../utils/isCheckBoxInput";
5 | import isDetached from "../utils/isDetached";
6 | import isArray from "../utils/isArray";
7 | import unset from "../utils/unset";
8 | import unique from "../utils/unique";
9 | import isUndefined from "../utils/isUndefined";
10 | import { Field, FieldRefs, FieldValues, Ref } from "../types/form";
11 |
12 | const isSameRef = (fieldValue: Field, ref: Ref) =>
13 | fieldValue && fieldValue.ref === ref;
14 |
15 | export default function findRemovedFieldAndRemoveListener<
16 | TFieldValues extends FieldValues
17 | >(
18 | fieldsRef: React.MutableRefObject>,
19 | handleChange: ({ type, target }: Event) => Promise,
20 | field: Field,
21 | unmountFieldsStateRef: React.MutableRefObject>,
22 | shouldUnregister?: boolean,
23 | forceDelete?: boolean
24 | ): void {
25 | const {
26 | ref,
27 | ref: { name, type },
28 | mutationWatcher
29 | } = field;
30 | const fieldRef = fieldsRef[name] as Field;
31 |
32 | if (!shouldUnregister) {
33 | const value = getFieldValue(fieldsRef, name, unmountFieldsStateRef);
34 |
35 | if (!isUndefined(value)) {
36 | unmountFieldsStateRef[name] = value;
37 | }
38 | }
39 |
40 | if (!type) {
41 | delete fieldsRef[name];
42 | return;
43 | }
44 |
45 | if ((isRadioInput(ref) || isCheckBoxInput(ref)) && fieldRef) {
46 | const { options } = fieldRef;
47 |
48 | if (isArray(options) && options.length) {
49 | unique(options).forEach(
50 | (option, index): void => {
51 | const { ref, mutationWatcher } = option;
52 | if (
53 | (ref && isDetached(ref) && isSameRef(option, ref)) ||
54 | forceDelete
55 | ) {
56 | removeAllEventListeners(ref, handleChange);
57 |
58 | if (mutationWatcher) {
59 | mutationWatcher.disconnect();
60 | }
61 |
62 | unset(options, `[${index}]`);
63 | }
64 | }
65 | );
66 |
67 | if (options && !unique(options).length) {
68 | delete fieldsRef[name];
69 | }
70 | } else {
71 | delete fieldsRef[name];
72 | }
73 | } else if ((isDetached(ref) && isSameRef(fieldRef, ref)) || forceDelete) {
74 | removeAllEventListeners(ref, handleChange);
75 |
76 | if (mutationWatcher) {
77 | mutationWatcher.disconnect();
78 | }
79 |
80 | delete fieldsRef[name];
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/logic/focusOnErrorField.test.ts:
--------------------------------------------------------------------------------
1 | import focusErrorField from './focusOnErrorField';
2 |
3 | jest.mock('../utils/isHTMLElement', () => ({
4 | default: () => true,
5 | }));
6 |
7 | describe('focusErrorField', () => {
8 | it('should focus on the first error it encounter', () => {
9 | const focus = jest.fn();
10 | focusErrorField(
11 | {
12 | test: {
13 | ref: {
14 | focus,
15 | } as any,
16 | },
17 | },
18 | {
19 | test: 'test' as any,
20 | },
21 | );
22 |
23 | expect(focus).toBeCalled();
24 | });
25 |
26 | it('should focus on first option when options input error encounters', () => {
27 | const focus = jest.fn();
28 | focusErrorField(
29 | {
30 | test: {
31 | ref: {} as any,
32 | options: [
33 | {
34 | ref: {
35 | focus,
36 | } as any,
37 | },
38 | ],
39 | },
40 | },
41 | {
42 | test: 'test' as any,
43 | },
44 | );
45 |
46 | expect(focus).toBeCalled();
47 | });
48 |
49 | it('should not call focus when field is undefined', () => {
50 | expect(() => {
51 | focusErrorField(
52 | {
53 | test: undefined,
54 | },
55 | {
56 | test: 'test' as any,
57 | },
58 | );
59 | }).not.toThrow();
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/logic/focusOnErrorField.ts:
--------------------------------------------------------------------------------
1 | import get from '../utils/get';
2 | import { FieldErrors, FieldRefs } from '../types/form';
3 |
4 | export default (
5 | fields: FieldRefs,
6 | fieldErrors: FieldErrors,
7 | ) => {
8 | for (const key in fields) {
9 | if (get(fieldErrors, key)) {
10 | const field = fields[key];
11 |
12 | if (field) {
13 | if (field.ref.focus) {
14 | field.ref.focus();
15 |
16 | break;
17 | } else if (field.options) {
18 | field.options[0].ref.focus();
19 |
20 | break;
21 | }
22 | }
23 | }
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/logic/generateId.test.ts:
--------------------------------------------------------------------------------
1 | import generateId from './generateId';
2 |
3 | describe('generateId', () => {
4 | it('should generate a unique id', () => {
5 | expect(/\w{8}-\w{4}-4\w{3}-\w{4}-\w{12}/i.test(generateId())).toBeTruthy();
6 | });
7 |
8 | it('should fallback to current date if performance is undefined', () => {
9 | const { performance } = window;
10 | delete (window as any).performance;
11 | expect(/\w{8}-\w{4}-4\w{3}-\w{4}-\w{12}/i.test(generateId())).toBeTruthy();
12 | (window as any).performance = performance;
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/logic/generateId.ts:
--------------------------------------------------------------------------------
1 | import { UNDEFINED } from '../constants';
2 |
3 | export default () => {
4 | const d =
5 | typeof performance === UNDEFINED ? Date.now() : performance.now() * 1000;
6 |
7 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
8 | const r = (Math.random() * 16 + d) % 16 | 0;
9 |
10 | return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16);
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/src/logic/getCheckboxValue.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import getCheckboxValue from './getCheckboxValue';
3 |
4 | describe('getCheckboxValue', () => {
5 | it('should return default value if not valid or empty options', () => {
6 | expect(getCheckboxValue(undefined)).toEqual({
7 | value: false,
8 | isValid: false,
9 | });
10 | });
11 |
12 | it('should return checked value if single checkbox is checked', () => {
13 | expect(
14 | getCheckboxValue([
15 | {
16 | ref: {
17 | name: 'bill',
18 | checked: true,
19 | value: '3',
20 | attributes: { value: '3' },
21 | },
22 | },
23 | ]),
24 | ).toEqual({ value: '3', isValid: true });
25 | });
26 |
27 | it('should return true if single checkbox is checked and has no value', () => {
28 | expect(
29 | getCheckboxValue([
30 | { ref: { name: 'bill', checked: true, attributes: {} } },
31 | ]),
32 | ).toEqual({ value: true, isValid: true });
33 | });
34 |
35 | it('should return true if single checkbox is checked and has empty value', () => {
36 | expect(
37 | getCheckboxValue([
38 | {
39 | ref: {
40 | name: 'bill',
41 | checked: true,
42 | value: '',
43 | attributes: { value: 'test' },
44 | },
45 | },
46 | ]),
47 | ).toEqual({ value: true, isValid: true });
48 | expect(
49 | getCheckboxValue([
50 | {
51 | ref: {
52 | name: 'bill',
53 | checked: true,
54 | attributes: { value: 'test' },
55 | },
56 | },
57 | ]),
58 | ).toEqual({ value: true, isValid: true });
59 | });
60 |
61 | it('should return false if single checkbox is un-checked', () => {
62 | expect(
63 | getCheckboxValue([
64 | {
65 | ref: {
66 | name: 'bill',
67 | checked: false,
68 | attributes: {},
69 | },
70 | },
71 | ]),
72 | ).toEqual({ value: false, isValid: false });
73 | });
74 |
75 | it('should return multiple selected values', () => {
76 | expect(
77 | getCheckboxValue([
78 | {
79 | ref: {
80 | name: 'bill',
81 | checked: true,
82 | value: '2',
83 | attributes: { value: '2' },
84 | },
85 | },
86 | {
87 | ref: {
88 | name: 'bill',
89 | checked: true,
90 | value: '3',
91 | attributes: { value: '3' },
92 | },
93 | },
94 | ]),
95 | ).toEqual({ value: ['2', '3'], isValid: true });
96 | });
97 |
98 | it('should return values for checked boxes only', () => {
99 | expect(
100 | getCheckboxValue([
101 | {
102 | ref: {
103 | name: 'bill',
104 | checked: false,
105 | value: '2',
106 | attributes: { value: '2' },
107 | },
108 | },
109 | {
110 | ref: {
111 | name: 'bill',
112 | checked: true,
113 | value: '3',
114 | attributes: { value: '3' },
115 | },
116 | },
117 | {
118 | ref: {
119 | name: 'bill',
120 | checked: false,
121 | value: '4',
122 | attributes: { value: '4' },
123 | },
124 | },
125 | ]),
126 | ).toEqual({ value: ['3'], isValid: true });
127 | });
128 |
129 | it('should return empty array for multi checkbox with no checked box', () => {
130 | expect(
131 | getCheckboxValue([
132 | {
133 | ref: {
134 | name: 'bill',
135 | checked: false,
136 | value: '2',
137 | attributes: { value: '2' },
138 | },
139 | },
140 | {
141 | ref: {
142 | name: 'bill',
143 | checked: false,
144 | value: '3',
145 | attributes: { value: '3' },
146 | },
147 | },
148 | ]),
149 | ).toEqual({ value: [], isValid: false });
150 | });
151 |
152 | it('should not return error when check box ref is undefined', () => {
153 | expect(
154 | getCheckboxValue([
155 | undefined,
156 | {
157 | ref: {
158 | name: 'bill',
159 | checked: false,
160 | value: '2',
161 | attributes: { value: '2' },
162 | },
163 | },
164 | ]),
165 | ).toEqual({ value: [], isValid: false });
166 | });
167 | });
168 |
--------------------------------------------------------------------------------
/src/logic/getCheckboxValue.ts:
--------------------------------------------------------------------------------
1 | import isArray from '../utils/isArray';
2 | import isUndefined from '../utils/isUndefined';
3 | import { RadioOrCheckboxOption } from '../types/form';
4 |
5 | type CheckboxFieldResult = {
6 | isValid: boolean;
7 | value: string | string[] | boolean;
8 | };
9 |
10 | const defaultResult: CheckboxFieldResult = {
11 | value: false,
12 | isValid: false,
13 | };
14 |
15 | const validResult = { value: true, isValid: true };
16 |
17 | export default (options?: RadioOrCheckboxOption[]): CheckboxFieldResult => {
18 | if (isArray(options)) {
19 | if (options.length > 1) {
20 | const values = options
21 | .filter((option) => option && option.ref.checked)
22 | .map(({ ref: { value } }) => value);
23 | return { value: values, isValid: !!values.length };
24 | }
25 |
26 | const { checked, value, attributes } = options[0].ref;
27 |
28 | return checked
29 | ? attributes && !isUndefined((attributes as any).value)
30 | ? isUndefined(value) || value === ''
31 | ? validResult
32 | : { value: value, isValid: true }
33 | : validResult
34 | : defaultResult;
35 | }
36 |
37 | return defaultResult;
38 | };
39 |
--------------------------------------------------------------------------------
/src/logic/getFieldArrayParentName.ts:
--------------------------------------------------------------------------------
1 | export default (name: string) => name.substring(0, name.indexOf('['));
2 |
--------------------------------------------------------------------------------
/src/logic/getFieldValue.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import getFieldValue from './getFieldValue';
3 |
4 | jest.mock('./getRadioValue', () => ({
5 | default: () => ({
6 | value: 2,
7 | }),
8 | }));
9 |
10 | jest.mock('./getMultipleSelectValue', () => ({
11 | default: () => 3,
12 | }));
13 |
14 | jest.mock('./getCheckboxValue', () => ({
15 | default: () => ({
16 | value: 'testValue',
17 | }),
18 | }));
19 |
20 | describe('getFieldValue', () => {
21 | it('should return correct value when type is radio', () => {
22 | expect(
23 | getFieldValue(
24 | {
25 | current: {
26 | test: {
27 | ref: {
28 | type: 'radio',
29 | },
30 | },
31 | },
32 | },
33 | 'test',
34 | ),
35 | ).toBe(2);
36 | });
37 |
38 | it('should return the correct select value when type is select-multiple', () => {
39 | expect(
40 | getFieldValue(
41 | {
42 | current: {
43 | test: {
44 | ref: {
45 | type: 'select-multiple',
46 | name: 'test',
47 | value: 'test',
48 | },
49 | },
50 | },
51 | },
52 | 'test',
53 | ),
54 | ).toBe(3);
55 | });
56 |
57 | it('should return the correct value when type is checkbox', () => {
58 | expect(
59 | getFieldValue(
60 | {
61 | current: {
62 | test: {
63 | ref: {
64 | name: 'test',
65 | type: 'checkbox',
66 | },
67 | },
68 | },
69 | },
70 | 'test',
71 | ),
72 | ).toBe('testValue');
73 | });
74 |
75 | it('should return it value for other types', () => {
76 | expect(
77 | getFieldValue(
78 | {
79 | current: {
80 | test: {
81 | ref: {
82 | type: 'text',
83 | name: 'bill',
84 | value: 'value',
85 | },
86 | },
87 | },
88 | },
89 | 'test',
90 | ),
91 | ).toBe('value');
92 | });
93 |
94 | it('should return empty string when radio input value is not found', () => {
95 | expect(getFieldValue({ current: {} }, '')).toEqual(undefined);
96 | });
97 |
98 | it('should return false when checkbox input value is not found', () => {
99 | expect(
100 | getFieldValue(
101 | { current: {} },
102 | {
103 | type: 'checkbox',
104 | value: 'value',
105 | name: 'test',
106 | },
107 | ),
108 | ).toBeFalsy();
109 | });
110 |
111 | it('should return files for input type file', () => {
112 | expect(
113 | getFieldValue(
114 | {
115 | current: {
116 | test: {
117 | ref: {
118 | type: 'file',
119 | name: 'test',
120 | files: 'files',
121 | },
122 | },
123 | },
124 | },
125 | 'test',
126 | ),
127 | ).toEqual('files');
128 | });
129 |
130 | it('should return undefined when input is not found', () => {
131 | expect(
132 | getFieldValue(
133 | {
134 | current: {
135 | test: {
136 | ref: {
137 | files: 'files',
138 | },
139 | },
140 | },
141 | },
142 | {},
143 | ),
144 | ).toEqual(undefined);
145 | });
146 |
147 | it('should return unmount field value when field is not found', () => {
148 | expect(
149 | getFieldValue(
150 | {
151 | current: {
152 | test: {
153 | ref: {
154 | files: 'files',
155 | },
156 | },
157 | },
158 | },
159 | 'what',
160 | { current: { what: 'data' } },
161 | ),
162 | ).toEqual('data');
163 | });
164 | });
165 |
--------------------------------------------------------------------------------
/src/logic/getFieldValue.ts:
--------------------------------------------------------------------------------
1 | import getRadioValue from './getRadioValue';
2 | import getMultipleSelectValue from './getMultipleSelectValue';
3 | import isRadioInput from '../utils/isRadioInput';
4 | import isFileInput from '../utils/isFileInput';
5 | import isCheckBox from '../utils/isCheckBoxInput';
6 | import isMultipleSelect from '../utils/isMultipleSelect';
7 | import getCheckboxValue from './getCheckboxValue';
8 | import { FieldRefs, FieldValues, InternalFieldName } from '../types/form';
9 |
10 | export default function getFieldValue(
11 | fieldsRef,
12 | name: InternalFieldName,
13 | unmountFieldsStateRef?,
14 | ) {
15 | const field = fieldsRef[name]!;
16 |
17 | if (field) {
18 | const {
19 | ref: { value },
20 | ref,
21 | } = field;
22 |
23 | if (isFileInput(ref)) {
24 | return ref.files;
25 | }
26 |
27 | if (isRadioInput(ref)) {
28 | return getRadioValue(field.options).value;
29 | }
30 |
31 | if (isMultipleSelect(ref)) {
32 | return getMultipleSelectValue(ref.options);
33 | }
34 |
35 | if (isCheckBox(ref)) {
36 | return getCheckboxValue(field.options).value;
37 | }
38 |
39 | return value;
40 | }
41 |
42 | if (unmountFieldsStateRef) {
43 | return unmountFieldsStateRef.current[name];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/logic/getFieldsValues.test.ts:
--------------------------------------------------------------------------------
1 | import getFieldsValues from './getFieldsValues';
2 | import getFieldValue from './getFieldValue';
3 |
4 | jest.mock('./getFieldValue');
5 |
6 | describe('getFieldsValues', () => {
7 | it('should return all fields value', () => {
8 | // @ts-ignore
9 | getFieldValue.mockImplementation(() => 'test');
10 | expect(
11 | getFieldsValues(
12 | {
13 | current: {
14 | test: {
15 | ref: { name: 'test' },
16 | },
17 | test1: {
18 | ref: { name: 'test1' },
19 | },
20 | },
21 | },
22 | { current: {} },
23 | ),
24 | ).toEqual({
25 | test: 'test',
26 | test1: 'test',
27 | });
28 | });
29 |
30 | it('should return searched string with fields value', () => {
31 | expect(
32 | getFieldsValues(
33 | {
34 | current: {
35 | test: {
36 | ref: { name: 'test' },
37 | },
38 | tex: {
39 | ref: { name: 'test1' },
40 | },
41 | tex123: {
42 | ref: { name: 'test1' },
43 | },
44 | },
45 | },
46 | { current: {} },
47 | 'test',
48 | ),
49 | ).toEqual({
50 | test: 'test',
51 | });
52 | });
53 |
54 | it('should return searched array string with fields value', () => {
55 | expect(
56 | getFieldsValues(
57 | {
58 | current: {
59 | test: {
60 | ref: { name: 'test' },
61 | },
62 | tex: {
63 | ref: { name: 'test1' },
64 | },
65 | 123: {
66 | ref: { name: 'test1' },
67 | },
68 | 1456: {
69 | ref: { name: 'test1' },
70 | },
71 | },
72 | },
73 | { current: {} },
74 | ['test', 'tex'],
75 | ),
76 | ).toEqual({
77 | test: 'test',
78 | tex: 'test',
79 | });
80 | });
81 |
82 | it('should return unmounted values', () => {
83 | expect(
84 | getFieldsValues(
85 | {
86 | current: {
87 | test: {
88 | ref: { name: 'test' },
89 | },
90 | },
91 | },
92 | {
93 | current: {
94 | test1: 'test',
95 | },
96 | },
97 | ),
98 | ).toEqual({
99 | test: 'test',
100 | test1: 'test',
101 | });
102 | });
103 |
104 | it('should combined unmounted flat form values with form values', () => {
105 | // @ts-ignore
106 | getFieldValue.mockImplementation(() => 'test');
107 | expect(
108 | getFieldsValues(
109 | {
110 | current: {
111 | test: {
112 | ref: { name: 'test' },
113 | },
114 | },
115 | },
116 | {
117 | current: {
118 | test1: 'test',
119 | 'test2.test': 'test1',
120 | },
121 | },
122 | ),
123 | ).toEqual({
124 | test: 'test',
125 | test1: 'test',
126 | test2: {
127 | test: 'test1',
128 | },
129 | });
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/src/logic/getFieldsValues.ts:
--------------------------------------------------------------------------------
1 | import getFieldValue from './getFieldValue';
2 | import isString from '../utils/isString';
3 | import isArray from '../utils/isArray';
4 | import isUndefined from '../utils/isUndefined';
5 | import { InternalFieldName, FieldValues, FieldRefs } from '../types/form';
6 | import transformToNestObject from './transformToNestObject';
7 |
8 | export default (
9 | fieldsRef,
10 | unmountFieldsStateRef?,
11 | search?:
12 | | InternalFieldName
13 | | InternalFieldName[]
14 | | { nest: boolean },
15 | ) => {
16 | const output = {} as TFieldValues;
17 |
18 | for (const name in fieldsRef) {
19 | if (
20 | isUndefined(search) ||
21 | (isString(search)
22 | ? name.startsWith(search)
23 | : isArray(search) && search.find((data) => name.startsWith(data)))
24 | ) {
25 | output[name as InternalFieldName] = getFieldValue(
26 | fieldsRef,
27 | name,
28 | );
29 | }
30 | }
31 |
32 | return {
33 | ...transformToNestObject((unmountFieldsStateRef || {}).current || {}),
34 | ...transformToNestObject(output),
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/logic/getInputValue.test.ts:
--------------------------------------------------------------------------------
1 | import getInputValue from './getInputValue';
2 |
3 | test('getInputValue should return correct value', () => {
4 | expect(getInputValue({ target: { checked: true }, type: 'test' })).toEqual(
5 | true,
6 | );
7 | expect(getInputValue({ target: { checked: true } })).toEqual({
8 | target: { checked: true },
9 | });
10 | expect(getInputValue({ target: { value: 'test' }, type: 'test' })).toEqual(
11 | 'test',
12 | );
13 | expect(getInputValue({ data: 'test' })).toEqual({ data: 'test' });
14 | expect(getInputValue('test')).toEqual('test');
15 | expect(getInputValue(undefined)).toEqual(undefined);
16 | expect(getInputValue(null)).toEqual(null);
17 | });
18 |
--------------------------------------------------------------------------------
/src/logic/getInputValue.ts:
--------------------------------------------------------------------------------
1 | import isUndefined from '../utils/isUndefined';
2 | import isObject from '../utils/isObject';
3 | import isPrimitive from '../utils/isPrimitive';
4 |
5 | export default (event: any) =>
6 | isPrimitive(event) ||
7 | !isObject(event.target) ||
8 | (isObject(event.target) && !event.type)
9 | ? event
10 | : isUndefined(event.target.value)
11 | ? event.target.checked
12 | : event.target.value;
13 |
--------------------------------------------------------------------------------
/src/logic/getIsFieldsDifferent.test.ts:
--------------------------------------------------------------------------------
1 | import getIsFieldsDifferent from './getIsFieldsDifferent';
2 |
3 | describe('getIsFieldsDifferent', () => {
4 | it('should return true when two sets not match', () => {
5 | expect(
6 | getIsFieldsDifferent(
7 | [{ test: '123' }, { test: '455' }, { test: '455' }],
8 | [],
9 | ),
10 | ).toBeTruthy();
11 | expect(
12 | getIsFieldsDifferent(
13 | [{ test: '123' }, { test: '455' }, { test: '455' }],
14 | [{ test: '123' }, { test: '455' }, { test: '455', test1: 'what' }],
15 | ),
16 | ).toBeTruthy();
17 | expect(getIsFieldsDifferent([{}], [])).toBeTruthy();
18 | expect(getIsFieldsDifferent([], [{}])).toBeTruthy();
19 | });
20 |
21 | it('should return true when two sets matches', () => {
22 | expect(
23 | getIsFieldsDifferent(
24 | [{ name: 'useFieldArray' }],
25 | [{ name: 'useFieldArray' }],
26 | ),
27 | ).toBeFalsy();
28 | expect(
29 | getIsFieldsDifferent(
30 | [{ test: '123' }, { test: '455' }, { test: '455' }],
31 | [{ test: '123' }, { test: '455' }, { test: '455' }],
32 | ),
33 | ).toBeFalsy();
34 | expect(getIsFieldsDifferent([], [])).toBeFalsy();
35 | expect(
36 | getIsFieldsDifferent(
37 | [{ test: '123' }, { test: '455' }],
38 | [{ test: '123' }, { test: '455' }],
39 | ),
40 | ).toBeFalsy();
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/logic/getIsFieldsDifferent.ts:
--------------------------------------------------------------------------------
1 | import isUndefined from '../utils/isUndefined';
2 | import isArray from '../utils/isArray';
3 |
4 | export default function getIsFieldsDifferent(
5 | referenceArray: unknown[],
6 | differenceArray: unknown[],
7 | ) {
8 | if (
9 | !isArray(referenceArray) ||
10 | !isArray(differenceArray) ||
11 | referenceArray.length !== differenceArray.length
12 | ) {
13 | return true;
14 | }
15 |
16 | for (let i = 0; i < referenceArray.length; i++) {
17 | const dataA = referenceArray[i];
18 | const dataB = differenceArray[i];
19 |
20 | if (
21 | isUndefined(dataB) ||
22 | Object.keys(dataA).length !== Object.keys(dataB).length
23 | ) {
24 | return true;
25 | }
26 |
27 | for (const key in dataA) {
28 | if (dataA[key] !== dataB[key]) {
29 | return true;
30 | }
31 | }
32 | }
33 |
34 | return false;
35 | }
36 |
--------------------------------------------------------------------------------
/src/logic/getMultipleSelectValue.test.ts:
--------------------------------------------------------------------------------
1 | import getMultipleSelectValue from './getMultipleSelectValue';
2 |
3 | describe('getMultipleSelectValue', () => {
4 | it('should return selected values', () => {
5 | expect(
6 | // @ts-ignore
7 | getMultipleSelectValue([{ selected: true, value: 'test' }]),
8 | ).toEqual(['test']);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/logic/getMultipleSelectValue.ts:
--------------------------------------------------------------------------------
1 | export default (
2 | options: HTMLOptionElement[] | HTMLOptionsCollection,
3 | ): string[] =>
4 | [...options]
5 | .filter(({ selected }): boolean => selected)
6 | .map(({ value }): string => value);
7 |
--------------------------------------------------------------------------------
/src/logic/getRadioValue.test.ts:
--------------------------------------------------------------------------------
1 | import getRadioValue from './getRadioValue';
2 |
3 | describe('getRadioValue', () => {
4 | it('should return default value if not valid or empty options', () => {
5 | expect(getRadioValue(undefined)).toEqual({
6 | isValid: false,
7 | value: '',
8 | });
9 | });
10 |
11 | it('should return valid to true when value found', () => {
12 | expect(
13 | getRadioValue([
14 | { ref: { name: 'bill', checked: false, value: '1' } },
15 | { ref: { name: 'bill', checked: true, value: '2' } },
16 | ] as any),
17 | ).toEqual({
18 | isValid: true,
19 | value: '2',
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/src/logic/getRadioValue.ts:
--------------------------------------------------------------------------------
1 | import isArray from '../utils/isArray';
2 | import { RadioOrCheckboxOption } from '../types/form';
3 |
4 | type RadioFieldResult = {
5 | isValid: boolean;
6 | value: number | string;
7 | };
8 |
9 | const defaultReturn: RadioFieldResult = {
10 | isValid: false,
11 | value: '',
12 | };
13 |
14 | export default (options?: RadioOrCheckboxOption[]): RadioFieldResult =>
15 | isArray(options)
16 | ? options.reduce(
17 | (previous, option): RadioFieldResult =>
18 | option && option.ref.checked
19 | ? {
20 | isValid: true,
21 | value: option.ref.value,
22 | }
23 | : previous,
24 | defaultReturn,
25 | )
26 | : defaultReturn;
27 |
--------------------------------------------------------------------------------
/src/logic/getSortedArrayFIeldIndexes.test.ts:
--------------------------------------------------------------------------------
1 | import getSortItems from './getSortedArrayFieldIndexes';
2 |
3 | describe('getSortItems', () => {
4 | it('should sort removed item correctly', () => {
5 | expect(getSortItems([1, 2, 3, 4], [2, 3])).toEqual([1, -1, -1, 2]);
6 | expect(getSortItems([2, 3, 4], [2, 3])).toEqual([-1, -1, 2]);
7 | expect(getSortItems([1, 3, 5], [3])).toEqual([1, -1, 4]);
8 | expect(getSortItems([1], [1])).toEqual([-1]);
9 | expect(getSortItems([4], [4])).toEqual([-1]);
10 | expect(getSortItems([4, 1], [4])).toEqual([1, -1]);
11 | expect(getSortItems([1, 2, 3, 4, 5], [4])).toEqual([1, 2, 3, -1, 4]);
12 | expect(getSortItems([0, 1], [1])).toEqual([0, -1]);
13 | expect(getSortItems([0, 3], [2])).toEqual([0, 2]);
14 | expect(getSortItems([1, 3, 5], [2])).toEqual([1, 2, 4]);
15 | expect(getSortItems([1, 3, 5], [2, 4])).toEqual([1, 2, 3]);
16 | expect(getSortItems([0, 1, 2, 3], [0])).toEqual([-1, 0, 1, 2]);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/logic/getSortedArrayFieldIndexes.ts:
--------------------------------------------------------------------------------
1 | import unique from '../utils/unique';
2 |
3 | export default (
4 | indexes: number[],
5 | removeIndexes: number[],
6 | updatedIndexes: number[] = [],
7 | count = 0,
8 | notFoundIndexes: number[] = [],
9 | ): number[] => {
10 | for (const removeIndex of removeIndexes) {
11 | if (indexes.indexOf(removeIndex) < 0) {
12 | notFoundIndexes.push(removeIndex);
13 | }
14 | }
15 |
16 | for (const index of indexes.sort()) {
17 | if (removeIndexes.indexOf(index) > -1) {
18 | updatedIndexes.push(-1);
19 | count++;
20 | } else {
21 | updatedIndexes.push(
22 | index -
23 | count -
24 | (notFoundIndexes.length
25 | ? unique(
26 | notFoundIndexes.map((notFoundIndex) => notFoundIndex < index),
27 | ).length
28 | : 0),
29 | );
30 | }
31 | }
32 |
33 | return updatedIndexes;
34 | };
35 |
--------------------------------------------------------------------------------
/src/logic/getValidateError.test.ts:
--------------------------------------------------------------------------------
1 | import getValidateError from './getValidateError';
2 |
3 | describe('getValidateError', () => {
4 | it('should return field error in correct format', () => {
5 | expect(
6 | getValidateError(
7 | 'This is a required field',
8 | {
9 | name: 'test1',
10 | value: '',
11 | },
12 | 'required',
13 | ),
14 | ).toEqual({
15 | type: 'required',
16 | message: 'This is a required field',
17 | ref: {
18 | name: 'test1',
19 | value: '',
20 | },
21 | });
22 |
23 | expect(
24 | getValidateError(
25 | false,
26 | {
27 | name: 'test1',
28 | value: '',
29 | },
30 | 'required',
31 | ),
32 | ).toEqual({
33 | type: 'required',
34 | message: '',
35 | ref: {
36 | name: 'test1',
37 | value: '',
38 | },
39 | });
40 | });
41 |
42 | it('should return undefined when called with non string result', () => {
43 | expect(getValidateError(undefined, () => {})).toBeUndefined();
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/logic/getValidateError.ts:
--------------------------------------------------------------------------------
1 | import isBoolean from '../utils/isBoolean';
2 | import isMessage from '../utils/isMessage';
3 | import { FieldError, ValidateResult, Ref } from '../types/form';
4 |
5 | export default function getValidateError(
6 | result: ValidateResult,
7 | ref: Ref,
8 | type = 'validate',
9 | ): FieldError | void {
10 | if (isMessage(result) || (isBoolean(result) && !result)) {
11 | return {
12 | type,
13 | message: isMessage(result) ? result : '',
14 | ref,
15 | };
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/logic/getValueAndMessage.test.ts:
--------------------------------------------------------------------------------
1 | import getValueAndMessage from './getValueAndMessage';
2 |
3 | describe('getValueAndMessage', () => {
4 | it('should return message and value correctly', () => {
5 | expect(getValueAndMessage(0).value).toEqual(0);
6 | expect(getValueAndMessage(3).value).toEqual(3);
7 | expect(getValueAndMessage({ value: 0, message: 'what' }).value).toEqual(0);
8 | expect(getValueAndMessage({ value: 2, message: 'what' }).value).toEqual(2);
9 | expect(getValueAndMessage({ value: 1, message: 'test' }).message).toEqual(
10 | 'test',
11 | );
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/logic/getValueAndMessage.ts:
--------------------------------------------------------------------------------
1 | import isObject from '../utils/isObject';
2 | import isRegex from '../utils/isRegex';
3 | import { ValidationRule, ValidationValueMessage } from '../types/form';
4 |
5 | const isValueMessage = (
6 | value?: ValidationRule,
7 | ): value is ValidationValueMessage => isObject(value) && !isRegex(value);
8 |
9 | export default (validationData?: ValidationRule) =>
10 | isValueMessage(validationData)
11 | ? validationData
12 | : {
13 | value: validationData,
14 | message: '',
15 | };
16 |
--------------------------------------------------------------------------------
/src/logic/index.ts:
--------------------------------------------------------------------------------
1 | import appendErrors from './appendErrors';
2 | import transformToNestObject from './transformToNestObject';
3 |
4 | export { appendErrors, transformToNestObject };
5 |
--------------------------------------------------------------------------------
/src/logic/isNameInFieldArray.test.ts:
--------------------------------------------------------------------------------
1 | import { isMatchFieldArrayName } from './isNameInFieldArray';
2 |
3 | describe('isMatchFieldArrayName', () => {
4 | it('should find match array field', () => {
5 | expect(isMatchFieldArrayName('test[0]', 'test')).toBeTruthy();
6 | expect(isMatchFieldArrayName('test[0]', 'test1')).toBeFalsy();
7 | expect(
8 | isMatchFieldArrayName('test[0].data[0]', 'test[0].data'),
9 | ).toBeTruthy();
10 | expect(
11 | isMatchFieldArrayName('test[0].data[0]', 'test[1].data'),
12 | ).toBeFalsy();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/logic/isNameInFieldArray.ts:
--------------------------------------------------------------------------------
1 | export const isMatchFieldArrayName = (name: string, searchName: string) =>
2 | RegExp(
3 | `^${searchName}[\\d+]`.replace(/\[/g, '\\[').replace(/\]/g, '\\]'),
4 | ).test(name);
5 |
6 | export default (names: Set, name: string) =>
7 | [...names].some((current) => isMatchFieldArrayName(name, current));
8 |
--------------------------------------------------------------------------------
/src/logic/removeAllEventListeners.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import removeAllEventListeners from './removeAllEventListeners';
3 |
4 | jest.mock('../utils/isHTMLElement', () => ({
5 | default: () => true,
6 | }));
7 |
8 | describe('removeAllEventListeners', () => {
9 | it('should return undefined when removeEventListener is not defined', () => {
10 | expect(removeAllEventListeners({}, () => {})).toBeUndefined();
11 | });
12 |
13 | it('should remove all events', () => {
14 | const removeEventListener = jest.fn();
15 | const validateWithStateUpdate = jest.fn();
16 | const ref = {
17 | removeEventListener,
18 | };
19 | removeAllEventListeners(ref, validateWithStateUpdate);
20 | expect(removeEventListener).toBeCalledWith(
21 | 'input',
22 | validateWithStateUpdate,
23 | );
24 | expect(removeEventListener).toBeCalledWith(
25 | 'change',
26 | validateWithStateUpdate,
27 | );
28 | expect(removeEventListener).toBeCalledWith('blur', validateWithStateUpdate);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/logic/removeAllEventListeners.ts:
--------------------------------------------------------------------------------
1 | import isHTMLElement from '../utils/isHTMLElement';
2 | import { EVENTS } from '../constants';
3 | import { Ref } from '../types/form';
4 |
5 | export default (
6 | ref: Ref,
7 | validateWithStateUpdate: EventListenerOrEventListenerObject,
8 | ): void => {
9 | if (isHTMLElement(ref) && ref.removeEventListener) {
10 | ref.removeEventListener(EVENTS.INPUT, validateWithStateUpdate);
11 | ref.removeEventListener(EVENTS.CHANGE, validateWithStateUpdate);
12 | ref.removeEventListener(EVENTS.BLUR, validateWithStateUpdate);
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/src/logic/shouldRenderBasedOnError.test.ts:
--------------------------------------------------------------------------------
1 | import shouldRenderBasedOnError from './shouldRenderBasedOnError';
2 |
3 | describe('shouldUpdateWithError', () => {
4 | it('should return true when error message empty and error exists', () => {
5 | expect(
6 | shouldRenderBasedOnError({
7 | errors: {},
8 | name: 'test',
9 | error: { test: 'test' } as any,
10 | validFields: new Set(),
11 | fieldsWithValidation: new Set(),
12 | }),
13 | ).toBeTruthy();
14 | });
15 |
16 | it('should return false when form is valid and field is valid', () => {
17 | expect(
18 | shouldRenderBasedOnError({
19 | errors: {},
20 | name: 'test',
21 | error: {},
22 | validFields: new Set(),
23 | fieldsWithValidation: new Set(),
24 | }),
25 | ).toBeFalsy();
26 | });
27 |
28 | it('should return true when error disappeared', () => {
29 | expect(
30 | shouldRenderBasedOnError({
31 | errors: { test: 'test' } as any,
32 | name: 'test',
33 | error: {},
34 | validFields: new Set(),
35 | fieldsWithValidation: new Set(),
36 | }),
37 | ).toBeTruthy();
38 | });
39 |
40 | it('should return true when error return and not found in error message', () => {
41 | expect(
42 | shouldRenderBasedOnError({
43 | errors: { test: 'test' } as any,
44 | name: '',
45 | error: { data: 'bill' } as any,
46 | validFields: new Set(),
47 | fieldsWithValidation: new Set(),
48 | }),
49 | ).toBeTruthy();
50 | });
51 |
52 | it('should return true when error type or message not match in error message', () => {
53 | expect(
54 | shouldRenderBasedOnError({
55 | errors: { test: { type: 'test' } } as any,
56 | name: 'test',
57 | error: { test: { type: 'bill' } } as any,
58 | validFields: new Set(),
59 | fieldsWithValidation: new Set(),
60 | }),
61 | ).toBeTruthy();
62 | });
63 |
64 | it('should return false if nothing matches', () => {
65 | expect(
66 | shouldRenderBasedOnError({
67 | errors: { test: { message: 'test', type: 'input' } } as any,
68 | name: 'test',
69 | error: { test: { type: 'input', message: 'test' } } as any,
70 | validFields: new Set(),
71 | fieldsWithValidation: new Set(),
72 | }),
73 | ).toBeFalsy();
74 | });
75 |
76 | it('should not clear error when it is set manually', () => {
77 | expect(
78 | shouldRenderBasedOnError({
79 | errors: {
80 | test: { message: 'test', type: 'input' },
81 | } as any,
82 | name: 'test',
83 | error: { test: { type: 'input', message: 'test' } } as any,
84 | validFields: new Set(),
85 | fieldsWithValidation: new Set(),
86 | }),
87 | ).toBeFalsy();
88 | });
89 |
90 | it('should return true when new validate field is been introduced', () => {
91 | expect(
92 | shouldRenderBasedOnError({
93 | errors: { test: { message: 'test', type: 'input' } } as any,
94 | name: 'test1',
95 | error: {},
96 | validFields: new Set(['test']),
97 | fieldsWithValidation: new Set(['test1']),
98 | }),
99 | ).toBeTruthy();
100 | });
101 |
102 | it('should return false when same valid input been triggered', () => {
103 | expect(
104 | shouldRenderBasedOnError({
105 | errors: { test: { message: 'test', type: 'input' } } as any,
106 | name: 'test',
107 | error: {},
108 | validFields: new Set(['test']),
109 | fieldsWithValidation: new Set(['test']),
110 | }),
111 | ).toBeFalsy();
112 | });
113 |
114 | it('should return true when schema errors is different', () => {
115 | expect(
116 | shouldRenderBasedOnError({
117 | errors: { test: { message: 'test', type: 'input' } } as any,
118 | name: 'test',
119 | error: {},
120 | validFields: new Set(),
121 | fieldsWithValidation: new Set(),
122 | }),
123 | ).toBeTruthy();
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/src/logic/shouldRenderBasedOnError.ts:
--------------------------------------------------------------------------------
1 | import isEmptyObject from '../utils/isEmptyObject';
2 | import isSameError from '../utils/isSameError';
3 | import get from '../utils/get';
4 | import {
5 | FieldValues,
6 | InternalFieldName,
7 | FieldErrors,
8 | FlatFieldErrors,
9 | } from '../types/form';
10 |
11 | export default function shouldRenderBasedOnError<
12 | TFieldValues extends FieldValues
13 | >({
14 | errors,
15 | name,
16 | error,
17 | validFields,
18 | fieldsWithValidation,
19 | }: {
20 | errors: FieldErrors;
21 | error: FlatFieldErrors;
22 | name: InternalFieldName;
23 | validFields: Set>;
24 | fieldsWithValidation: Set>;
25 | }): boolean {
26 | const isFieldValid = isEmptyObject(error);
27 | const isFormValid = isEmptyObject(errors);
28 | const currentFieldError = get(error, name);
29 | const existFieldError = get(errors, name);
30 |
31 | if (isFieldValid && validFields.has(name)) {
32 | return false;
33 | }
34 |
35 | if (
36 | isFormValid !== isFieldValid ||
37 | (!isFormValid && !existFieldError) ||
38 | (isFieldValid && fieldsWithValidation.has(name) && !validFields.has(name))
39 | ) {
40 | return true;
41 | }
42 |
43 | return currentFieldError && !isSameError(existFieldError, currentFieldError);
44 | }
45 |
--------------------------------------------------------------------------------
/src/logic/skipValidation.test.ts:
--------------------------------------------------------------------------------
1 | import skipValidation from './skipValidation';
2 |
3 | describe('should skip validation', () => {
4 | it('when is onChange mode and blur event', () => {
5 | expect(
6 | skipValidation({
7 | isOnChange: true,
8 | isBlurEvent: false,
9 | isReValidateOnChange: true,
10 | isOnBlur: true,
11 | isReValidateOnBlur: false,
12 | isSubmitted: false,
13 | }),
14 | ).toBeTruthy();
15 | });
16 |
17 | it('when is onSubmit mode and re-validate on Submit', () => {
18 | expect(
19 | skipValidation({
20 | isOnChange: false,
21 | isReValidateOnChange: false,
22 | isBlurEvent: false,
23 | isOnBlur: false,
24 | isReValidateOnBlur: false,
25 | isSubmitted: false,
26 | }),
27 | ).toBeTruthy();
28 | });
29 |
30 | it('when is onSubmit mode and not submitted yet', () => {
31 | expect(
32 | skipValidation({
33 | isOnChange: false,
34 | isBlurEvent: false,
35 | isReValidateOnChange: true,
36 | isOnBlur: false,
37 | isReValidateOnBlur: false,
38 | isSubmitted: false,
39 | }),
40 | ).toBeTruthy();
41 | });
42 |
43 | it('when on blur mode, not blur event and error gets clear', () => {
44 | expect(
45 | skipValidation({
46 | isOnChange: false,
47 | isBlurEvent: false,
48 | isReValidateOnChange: true,
49 | isOnBlur: true,
50 | isReValidateOnBlur: false,
51 | isSubmitted: false,
52 | }),
53 | ).toBeTruthy();
54 | });
55 |
56 | it('when re-validate mode is blur, not blur event and has error ', () => {
57 | expect(
58 | skipValidation({
59 | isOnChange: false,
60 | isBlurEvent: false,
61 | isReValidateOnChange: true,
62 | isOnBlur: false,
63 | isReValidateOnBlur: true,
64 | isSubmitted: true,
65 | }),
66 | ).toBeTruthy();
67 | });
68 |
69 | it('when is re-validate mode on submit and have error', () => {
70 | expect(
71 | skipValidation({
72 | isOnChange: false,
73 | isBlurEvent: false,
74 | isOnBlur: false,
75 | isReValidateOnChange: false,
76 | isReValidateOnBlur: false,
77 | isSubmitted: true,
78 | }),
79 | ).toBeTruthy();
80 | });
81 | });
82 |
83 | describe('should validate the input', () => {
84 | it('when form is submitted and there is error', () => {
85 | expect(
86 | skipValidation({
87 | isOnChange: false,
88 | isBlurEvent: false,
89 | isReValidateOnChange: true,
90 | isOnBlur: false,
91 | isReValidateOnBlur: false,
92 | isSubmitted: true,
93 | }),
94 | ).toBeFalsy();
95 | });
96 |
97 | it('when user blur input and there is no more error', () => {
98 | expect(
99 | skipValidation({
100 | isOnChange: false,
101 | isBlurEvent: true,
102 | isReValidateOnChange: true,
103 | isOnBlur: true,
104 | isReValidateOnBlur: false,
105 | isSubmitted: false,
106 | }),
107 | ).toBeFalsy();
108 | });
109 |
110 | it('when user blur and there is an error', () => {
111 | expect(
112 | skipValidation({
113 | isOnChange: false,
114 | isBlurEvent: true,
115 | isReValidateOnChange: true,
116 | isOnBlur: true,
117 | isReValidateOnBlur: false,
118 | isSubmitted: false,
119 | }),
120 | ).toBeFalsy();
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/src/logic/skipValidation.ts:
--------------------------------------------------------------------------------
1 | export default ({
2 | isOnBlur,
3 | isOnChange,
4 | isReValidateOnBlur,
5 | isReValidateOnChange,
6 | isBlurEvent,
7 | isSubmitted,
8 | }: {
9 | isOnBlur: boolean;
10 | isOnChange: boolean;
11 | isReValidateOnBlur: boolean;
12 | isReValidateOnChange: boolean;
13 | isBlurEvent?: boolean;
14 | isSubmitted: boolean;
15 | }) => {
16 | if (isSubmitted ? isReValidateOnBlur : isOnBlur) {
17 | return !isBlurEvent;
18 | } else if (isSubmitted ? isReValidateOnChange : isOnChange) {
19 | return isBlurEvent;
20 | }
21 | return true;
22 | };
23 |
--------------------------------------------------------------------------------
/src/logic/transformToNestObject.test.ts:
--------------------------------------------------------------------------------
1 | import transformToNestObject from './transformToNestObject';
2 |
3 | describe('transformToNestObject', () => {
4 | it('should combine all the array fields', () => {
5 | expect(
6 | transformToNestObject({
7 | 'email[1]': 'asdasd@dsad.com',
8 | 'email[2]': 'asdasd@.com',
9 | 'firstName[1]': 'asdasd',
10 | 'firstName[2]': 'asdasd',
11 | 'lastName[1]': 'asdasd',
12 | 'lastName[2]': 'asd',
13 | test: 'test',
14 | }),
15 | ).toMatchSnapshot();
16 | });
17 |
18 | it('should combine array object correctly', () => {
19 | expect(
20 | transformToNestObject({
21 | 'name[0].firstName': 'testFirst',
22 | 'name[0].lastName': 'testLast',
23 | 'test[1].what': 'testLast',
24 | 'test[1].task': 'testLast',
25 | 'test[2].what': 'testLast',
26 | 'test[2].what[1].test': 'testLast',
27 | }),
28 | ).toMatchSnapshot();
29 | });
30 |
31 | it('should combine object correctly', () => {
32 | expect(
33 | transformToNestObject({
34 | 'name.firstName': 'testFirst',
35 | 'name.lastName': 'testLast',
36 | 'name.lastName.bill.luo': 'testLast',
37 | }),
38 | ).toMatchSnapshot();
39 | });
40 |
41 | it('should return default name value', () => {
42 | expect(
43 | transformToNestObject({
44 | name: 'testFirst',
45 | }),
46 | ).toMatchSnapshot();
47 | });
48 |
49 | it('should handle quoted values', () => {
50 | expect(
51 | transformToNestObject({
52 | 'name["foobar"]': 'testFirst',
53 | }),
54 | ).toMatchSnapshot();
55 |
56 | expect(
57 | transformToNestObject({
58 | 'name["b2ill"]': 'testFirst',
59 | }),
60 | ).toMatchSnapshot();
61 | });
62 |
63 | it('should combine with results', () => {
64 | expect(
65 | transformToNestObject({
66 | name: 'testFirst',
67 | name1: 'testFirst',
68 | name2: 'testFirst',
69 | }),
70 | ).toMatchSnapshot();
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/src/logic/transformToNestObject.ts:
--------------------------------------------------------------------------------
1 | import set from '../utils/set';
2 | import isKey from '../utils/isKey';
3 | import { FieldValues } from '../types/form';
4 |
5 | export default (data: FieldValues): any =>
6 | Object.entries(data).reduce(
7 | (previous: FieldValues, [key, value]): FieldValues => {
8 | if (!isKey(key)) {
9 | set(previous, key, value);
10 | return previous;
11 | }
12 |
13 | return { ...previous, [key]: value };
14 | },
15 | {},
16 | );
17 |
--------------------------------------------------------------------------------
/src/logic/validateField.ts:
--------------------------------------------------------------------------------
1 | import getRadioValue from './getRadioValue';
2 | import getCheckboxValue from './getCheckboxValue';
3 | import isNullOrUndefined from '../utils/isNullOrUndefined';
4 | import isRadioInput from '../utils/isRadioInput';
5 | import getValueAndMessage from './getValueAndMessage';
6 | import isCheckBoxInput from '../utils/isCheckBoxInput';
7 | import isString from '../utils/isString';
8 | import isEmptyObject from '../utils/isEmptyObject';
9 | import isObject from '../utils/isObject';
10 | import isFunction from '../utils/isFunction';
11 | import getFieldsValue from './getFieldValue';
12 | import isRegex from '../utils/isRegex';
13 | import isBoolean from '../utils/isBoolean';
14 | import isMessage from '../utils/isMessage';
15 | import getValidateError from './getValidateError';
16 | import appendErrors from './appendErrors';
17 | import { INPUT_VALIDATION_RULES } from '../constants';
18 | import {
19 | Field,
20 | FieldValues,
21 | FieldRefs,
22 | Message,
23 | FieldError,
24 | InternalFieldName,
25 | FlatFieldErrors,
26 | } from '../types/form';
27 |
28 | export default async (
29 | fieldsRef: React.MutableRefObject>,
30 | validateAllFieldCriteria: boolean,
31 | {
32 | ref,
33 | ref: { type, value },
34 | options,
35 | required,
36 | maxLength,
37 | minLength,
38 | min,
39 | max,
40 | pattern,
41 | validate,
42 | }: Field,
43 | unmountFieldsStateRef: React.MutableRefObject>,
44 | ): Promise> => {
45 | const fields = fieldsRef.current;
46 | const name: InternalFieldName = ref.name;
47 | const error: FlatFieldErrors = {};
48 | const isRadio = isRadioInput(ref);
49 | const isCheckBox = isCheckBoxInput(ref);
50 | const isRadioOrCheckbox = isRadio || isCheckBox;
51 | const isEmpty = value === '';
52 | const appendErrorsCurry = appendErrors.bind(
53 | null,
54 | name,
55 | validateAllFieldCriteria,
56 | error,
57 | );
58 | const getMinMaxMessage = (
59 | exceedMax: boolean,
60 | maxLengthMessage: Message,
61 | minLengthMessage: Message,
62 | maxType = INPUT_VALIDATION_RULES.maxLength,
63 | minType = INPUT_VALIDATION_RULES.minLength,
64 | ) => {
65 | const message = exceedMax ? maxLengthMessage : minLengthMessage;
66 | error[name] = {
67 | type: exceedMax ? maxType : minType,
68 | message,
69 | ref,
70 | ...(exceedMax
71 | ? appendErrorsCurry(maxType, message)
72 | : appendErrorsCurry(minType, message)),
73 | };
74 | };
75 |
76 | if (
77 | required &&
78 | ((!isRadio && !isCheckBox && (isEmpty || isNullOrUndefined(value))) ||
79 | (isBoolean(value) && !value) ||
80 | (isCheckBox && !getCheckboxValue(options).isValid) ||
81 | (isRadio && !getRadioValue(options).isValid))
82 | ) {
83 | const { value: requiredValue, message: requiredMessage } = isMessage(
84 | required,
85 | )
86 | ? { value: !!required, message: required }
87 | : getValueAndMessage(required);
88 |
89 | if (requiredValue) {
90 | error[name] = {
91 | type: INPUT_VALIDATION_RULES.required,
92 | message: requiredMessage,
93 | ref: isRadioOrCheckbox
94 | ? ((fields[name] as Field).options || [])[0].ref
95 | : ref,
96 | ...appendErrorsCurry(INPUT_VALIDATION_RULES.required, requiredMessage),
97 | };
98 | if (!validateAllFieldCriteria) {
99 | return error;
100 | }
101 | }
102 | }
103 |
104 | if (!isNullOrUndefined(min) || !isNullOrUndefined(max)) {
105 | let exceedMax;
106 | let exceedMin;
107 | const { value: maxValue, message: maxMessage } = getValueAndMessage(max);
108 | const { value: minValue, message: minMessage } = getValueAndMessage(min);
109 |
110 | if (type === 'number' || (!type && !isNaN(value))) {
111 | const valueNumber =
112 | (ref as HTMLInputElement).valueAsNumber || parseFloat(value);
113 | if (!isNullOrUndefined(maxValue)) {
114 | exceedMax = valueNumber > maxValue;
115 | }
116 | if (!isNullOrUndefined(minValue)) {
117 | exceedMin = valueNumber < minValue;
118 | }
119 | } else {
120 | const valueDate =
121 | (ref as HTMLInputElement).valueAsDate || new Date(value);
122 | if (isString(maxValue)) {
123 | exceedMax = valueDate > new Date(maxValue);
124 | }
125 | if (isString(minValue)) {
126 | exceedMin = valueDate < new Date(minValue);
127 | }
128 | }
129 |
130 | if (exceedMax || exceedMin) {
131 | getMinMaxMessage(
132 | !!exceedMax,
133 | maxMessage,
134 | minMessage,
135 | INPUT_VALIDATION_RULES.max,
136 | INPUT_VALIDATION_RULES.min,
137 | );
138 | if (!validateAllFieldCriteria) {
139 | return error;
140 | }
141 | }
142 | }
143 |
144 | if (isString(value) && !isEmpty && (maxLength || minLength)) {
145 | const {
146 | value: maxLengthValue,
147 | message: maxLengthMessage,
148 | } = getValueAndMessage(maxLength);
149 | const {
150 | value: minLengthValue,
151 | message: minLengthMessage,
152 | } = getValueAndMessage(minLength);
153 | const inputLength = value.toString().length;
154 | const exceedMax =
155 | !isNullOrUndefined(maxLengthValue) && inputLength > maxLengthValue;
156 | const exceedMin =
157 | !isNullOrUndefined(minLengthValue) && inputLength < minLengthValue;
158 |
159 | if (exceedMax || exceedMin) {
160 | getMinMaxMessage(!!exceedMax, maxLengthMessage, minLengthMessage);
161 | if (!validateAllFieldCriteria) {
162 | return error;
163 | }
164 | }
165 | }
166 |
167 | if (pattern && !isEmpty) {
168 | const { value: patternValue, message: patternMessage } = getValueAndMessage(
169 | pattern,
170 | );
171 |
172 | if (isRegex(patternValue) && !patternValue.test(value)) {
173 | error[name] = {
174 | type: INPUT_VALIDATION_RULES.pattern,
175 | message: patternMessage,
176 | ref,
177 | ...appendErrorsCurry(INPUT_VALIDATION_RULES.pattern, patternMessage),
178 | };
179 | if (!validateAllFieldCriteria) {
180 | return error;
181 | }
182 | }
183 | }
184 |
185 | if (validate) {
186 | const fieldValue = getFieldsValue(fieldsRef, name, unmountFieldsStateRef);
187 | const validateRef = isRadioOrCheckbox && options ? options[0].ref : ref;
188 |
189 | if (isFunction(validate)) {
190 | const result = await validate(fieldValue);
191 | const validateError = getValidateError(result, validateRef);
192 |
193 | if (validateError) {
194 | error[name] = {
195 | ...validateError,
196 | ...appendErrorsCurry(
197 | INPUT_VALIDATION_RULES.validate,
198 | validateError.message,
199 | ),
200 | };
201 | if (!validateAllFieldCriteria) {
202 | return error;
203 | }
204 | }
205 | } else if (isObject(validate)) {
206 | let validationResult = {} as FieldError;
207 | for (const [key, validateFunction] of Object.entries(validate)) {
208 | if (!isEmptyObject(validationResult) && !validateAllFieldCriteria) {
209 | break;
210 | }
211 |
212 | const validateResult = await validateFunction(fieldValue);
213 | const validateError = getValidateError(
214 | validateResult,
215 | validateRef,
216 | key,
217 | );
218 |
219 | if (validateError) {
220 | validationResult = {
221 | ...validateError,
222 | ...appendErrorsCurry(key, validateError.message),
223 | };
224 |
225 | if (validateAllFieldCriteria) {
226 | error[name] = validationResult;
227 | }
228 | }
229 | }
230 |
231 | if (!isEmptyObject(validationResult)) {
232 | error[name] = {
233 | ref: validateRef,
234 | ...validationResult,
235 | };
236 | if (!validateAllFieldCriteria) {
237 | return error;
238 | }
239 | }
240 | }
241 | }
242 |
243 | return error;
244 | };
245 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.tsx'
3 | import './index.css'
4 |
5 | createApp(App).mount('#app')
6 |
--------------------------------------------------------------------------------
/src/types/form.ts:
--------------------------------------------------------------------------------
1 | import {
2 | EmptyObject,
3 | NonUndefined,
4 | LiteralToPrimitive,
5 | DeepPartial,
6 | DeepMap,
7 | IsFlatObject
8 | } from "./utils";
9 |
10 | declare const $NestedValue: unique symbol;
11 |
12 | export type FieldValues = Record;
13 |
14 | export type InternalFieldName =
15 | | (keyof TFieldValues & string)
16 | | string;
17 |
18 | export type FieldName = IsFlatObject<
19 | TFieldValues
20 | > extends true
21 | ? Extract
22 | : string;
23 |
24 | export type FieldValue<
25 | TFieldValues extends FieldValues
26 | > = TFieldValues[InternalFieldName];
27 |
28 | export type NestedValue = {
29 | [$NestedValue]: never;
30 | } & TValue;
31 |
32 | export type UnpackNestedValue = NonUndefined extends NestedValue
33 | ? U
34 | : NonUndefined extends Date | FileList
35 | ? T
36 | : NonUndefined extends object
37 | ? { [K in keyof T]: UnpackNestedValue }
38 | : T;
39 |
40 | export type DefaultValuesAtRender = UnpackNestedValue<
41 | DeepPartial, FieldValue>>
42 | >;
43 |
44 | export type FieldElement =
45 | | HTMLInputElement
46 | | HTMLSelectElement
47 | | HTMLTextAreaElement
48 | | CustomElement;
49 |
50 | export type Ref = FieldElement;
51 |
52 | export type ValidationMode = {
53 | onBlur: "onBlur";
54 | onChange: "onChange";
55 | onSubmit: "onSubmit";
56 | all: "all";
57 | };
58 |
59 | export type Mode = keyof ValidationMode;
60 |
61 | export type SubmitHandler = (
62 | data: UnpackNestedValue,
63 | event?: React.BaseSyntheticEvent
64 | ) => void | Promise;
65 |
66 | export type ResolverSuccess = {
67 | values: UnpackNestedValue;
68 | errors: EmptyObject;
69 | };
70 |
71 | export type ResolverError = {
72 | values: EmptyObject;
73 | errors: FieldErrors;
74 | };
75 |
76 | export type ResolverResult =
77 | | ResolverSuccess
78 | | ResolverError;
79 |
80 | export type Resolver<
81 | TFieldValues extends FieldValues = FieldValues,
82 | TContext extends object = object
83 | > = (
84 | values: TFieldValues,
85 | context?: TContext,
86 | validateAllFieldCriteria?: boolean
87 | ) => Promise>;
88 |
89 | export type UseFormOptions<
90 | TFieldValues extends FieldValues = FieldValues,
91 | TContext extends object = object
92 | > = Partial<{
93 | mode: Mode;
94 | reValidateMode: Mode;
95 | defaultValues: UnpackNestedValue>;
96 | resolver: Resolver;
97 | context: TContext;
98 | shouldFocusError: boolean;
99 | shouldUnregister: boolean;
100 | criteriaMode: "firstError" | "all";
101 | }>;
102 |
103 | export type MutationWatcher = {
104 | disconnect: VoidFunction;
105 | observe?: (target: Node, options?: MutationObserverInit) => void;
106 | };
107 |
108 | export type Message = string;
109 |
110 | export type ValidationValue = boolean | number | string | RegExp;
111 |
112 | export type ValidationRule<
113 | TValidationValue extends ValidationValue = ValidationValue
114 | > = TValidationValue | ValidationValueMessage;
115 |
116 | export type ValidationValueMessage<
117 | TValidationValue extends ValidationValue = ValidationValue
118 | > = {
119 | value: TValidationValue;
120 | message: Message;
121 | };
122 |
123 | export type ValidateResult = Message | Message[] | boolean | undefined;
124 |
125 | export type Validate = (data: any) => ValidateResult | Promise;
126 |
127 | export type ValidationRules = Partial<{
128 | required: Message | ValidationRule;
129 | min: ValidationRule;
130 | max: ValidationRule;
131 | maxLength: ValidationRule;
132 | minLength: ValidationRule;
133 | pattern: ValidationRule;
134 | validate: Validate | Record;
135 | }>;
136 |
137 | export type MultipleFieldErrors = Record;
138 |
139 | export type FieldError = {
140 | type: keyof ValidationRules | string;
141 | ref?: Ref;
142 | types?: MultipleFieldErrors;
143 | message?: Message;
144 | };
145 |
146 | export type ErrorOption =
147 | | {
148 | types: MultipleFieldErrors;
149 | }
150 | | {
151 | message?: Message;
152 | type: string;
153 | };
154 |
155 | export type Field = {
156 | ref: Ref;
157 | mutationWatcher?: MutationWatcher;
158 | options?: RadioOrCheckboxOption[];
159 | } & ValidationRules;
160 |
161 | export type FieldRefs = Partial<
162 | Record, Field>
163 | >;
164 |
165 | export type FieldErrors<
166 | TFieldValues extends FieldValues = FieldValues
167 | > = DeepMap;
168 |
169 | export type FlatFieldErrors = Partial<
170 | Record, FieldError>
171 | >;
172 |
173 | export type Touched = DeepMap<
174 | TFieldValues,
175 | true
176 | >;
177 |
178 | export type Dirtied = DeepMap<
179 | TFieldValues,
180 | true
181 | >;
182 |
183 | export type SetValueConfig = Partial<{
184 | shouldValidate: boolean;
185 | shouldDirty: boolean;
186 | }>;
187 |
188 | export type FormStateProxy = {
189 | isDirty: boolean;
190 | dirtyFields: Dirtied;
191 | isSubmitted: boolean;
192 | submitCount: number;
193 | touched: Touched;
194 | isSubmitting: boolean;
195 | isValid: boolean;
196 | };
197 |
198 | export type ReadFormState = { [K in keyof FormStateProxy]: boolean };
199 |
200 | export type RadioOrCheckboxOption = {
201 | ref: HTMLInputElement;
202 | mutationWatcher?: MutationWatcher;
203 | };
204 |
205 | export type CustomElement = {
206 | name: FieldName;
207 | type?: string;
208 | value?: any;
209 | checked?: boolean;
210 | options?: HTMLOptionsCollection;
211 | files?: FileList | null;
212 | focus?: VoidFunction;
213 | };
214 |
215 | export type HandleChange = (event: Event) => Promise;
216 |
217 | export type FieldValuesFromControl<
218 | TControl extends Control
219 | > = TControl extends Control ? TFieldValues : never;
220 |
221 | export type FieldArrayName = string;
222 |
223 | export type UseFieldArrayOptions<
224 | TKeyName extends string = "id",
225 | TControl extends Control = Control
226 | > = {
227 | name: FieldArrayName;
228 | keyName?: TKeyName;
229 | control?: TControl;
230 | };
231 |
232 | export type Control = Pick<
233 | UseFormMethods,
234 | "register" | "unregister" | "setValue" | "getValues" | "trigger" | "formState"
235 | > & {
236 | reRender: VoidFunction;
237 | removeFieldEventListener: (field: Field, forceDelete?: boolean) => void;
238 | mode: {
239 | readonly isOnBlur: boolean;
240 | readonly isOnSubmit: boolean;
241 | readonly isOnChange: boolean;
242 | };
243 | reValidateMode: {
244 | readonly isReValidateOnBlur: boolean;
245 | readonly isReValidateOnChange: boolean;
246 | };
247 | fieldArrayDefaultValues: React.MutableRefObject<
248 | Record
249 | >;
250 | dirtyFieldsRef: React.MutableRefObject>;
251 | validateSchemaIsValid?: (fieldsValues: any) => void;
252 | touchedFieldsRef: React.MutableRefObject>;
253 | watchFieldsRef: React.MutableRefObject>>;
254 | isWatchAllRef: React.MutableRefObject;
255 | validFieldsRef: React.MutableRefObject>>;
256 | fieldsWithValidationRef: React.MutableRefObject<
257 | Set>
258 | >;
259 | errorsRef: React.MutableRefObject>;
260 | fieldsRef: React.MutableRefObject>;
261 | resetFieldArrayFunctionRef: React.MutableRefObject<
262 | Record
263 | >;
264 | unmountFieldsStateRef: Record, any>;
265 | fieldArrayNamesRef: React.MutableRefObject<
266 | Set>
267 | >;
268 | isDirtyRef: React.MutableRefObject;
269 | isSubmittedRef: React.MutableRefObject;
270 | readFormStateRef: React.MutableRefObject<
271 | { [k in keyof FormStateProxy]: boolean }
272 | >;
273 | defaultValuesRef: React.MutableRefObject<
274 | | FieldValue>
275 | | UnpackNestedValue>
276 | >;
277 | watchFieldsHookRef: React.MutableRefObject<
278 | Record>>
279 | >;
280 | watchFieldsHookRenderRef: React.MutableRefObject>;
281 | watchInternal: (
282 | fieldNames?: string | string[],
283 | defaultValue?: unknown,
284 | watchId?: string
285 | ) => unknown;
286 | renderWatchedInputs: (name: string, found?: boolean) => void;
287 | };
288 |
289 | export type ArrayField<
290 | TFieldArrayValues extends FieldValues = FieldValues,
291 | TKeyName extends string = "id"
292 | > = TFieldArrayValues & Record;
293 |
294 | export type OmitResetState = Partial<
295 | {
296 | errors: boolean;
297 | } & ReadFormState
298 | >;
299 |
300 | export type UseWatchOptions = {
301 | defaultValue?: unknown;
302 | name?: string | string[];
303 | control?: Control;
304 | };
305 |
306 | export type UseFormMethods = {
307 | register>(
308 | rules?: ValidationRules
309 | ): (ref: (TFieldElement & Ref) | null) => void;
310 | register(name: FieldName, rules?: ValidationRules): void;
311 | register>(
312 | ref: (TFieldElement & Ref) | null,
313 | rules?: ValidationRules
314 | ): void;
315 | unregister(name: FieldName | FieldName[]): void;
316 | watch(): UnpackNestedValue;
317 | watch(
318 | name: TFieldName,
319 | defaultValue?: TFieldName extends keyof TFieldValues
320 | ? UnpackNestedValue
321 | : UnpackNestedValue>
322 | ): TFieldName extends keyof TFieldValues
323 | ? UnpackNestedValue
324 | : UnpackNestedValue>;
325 | watch(
326 | names: TFieldName[],
327 | defaultValues?: UnpackNestedValue<
328 | DeepPartial>
329 | >
330 | ): UnpackNestedValue>;
331 | watch(
332 | names: string[],
333 | defaultValues?: UnpackNestedValue>
334 | ): UnpackNestedValue>;
335 | setError(name: FieldName, error: ErrorOption): void;
336 | clearErrors(name?: FieldName | FieldName[]): void;
337 | setValue<
338 | TFieldName extends string,
339 | TFieldValue extends TFieldValues[TFieldName]
340 | >(
341 | name: TFieldName,
342 | value?: NonUndefined extends NestedValue
343 | ? U
344 | : UnpackNestedValue>>,
345 | options?: SetValueConfig
346 | ): void;
347 | trigger(
348 | name?: FieldName | FieldName[]
349 | ): Promise;
350 | errors: FieldErrors;
351 | formState: FormStateProxy;
352 | reset: (
353 | values?: UnpackNestedValue>,
354 | omitResetState?: OmitResetState
355 | ) => void;
356 | getValues(): UnpackNestedValue;
357 | getValues(
358 | name: TFieldName
359 | ): TFieldName extends keyof TFieldValues
360 | ? UnpackNestedValue[TFieldName]
361 | : TFieldValue;
362 | getValues(
363 | names: TFieldName[]
364 | ): UnpackNestedValue>;
365 | handleSubmit: (
366 | callback: SubmitHandler
367 | ) => (e?: React.BaseSyntheticEvent) => Promise;
368 | control: Control;
369 | };
370 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | NestedValue,
3 | Resolver,
4 | SubmitHandler,
5 | Control,
6 | UseFormMethods,
7 | UseFormOptions,
8 | UseFieldArrayOptions,
9 | ValidationRules,
10 | FieldError,
11 | Field,
12 | ArrayField,
13 | Mode,
14 | } from './form';
15 |
--------------------------------------------------------------------------------
/src/types/props.ts:
--------------------------------------------------------------------------------
1 | import {
2 | UseFormMethods,
3 | FieldValues,
4 | FieldValuesFromControl,
5 | FieldName,
6 | ValidationRules,
7 | Control,
8 | } from './form';
9 | import { Assign } from './utils';
10 |
11 | export type FormProviderProps<
12 | TFieldValues extends FieldValues = FieldValues
13 | > = {
14 | children: React.ReactNode;
15 | } & UseFormMethods;
16 |
17 | type AsProps = TAs extends undefined
18 | ? {}
19 | : TAs extends React.ReactElement
20 | ? Record
21 | : TAs extends React.ComponentType
22 | ? P
23 | : TAs extends keyof JSX.IntrinsicElements
24 | ? JSX.IntrinsicElements[TAs]
25 | : never;
26 |
27 | export type ControllerProps<
28 | TAs extends
29 | | React.ReactElement
30 | | React.ComponentType
31 | | 'input'
32 | | 'select'
33 | | 'textarea',
34 | TControl extends Control = Control
35 | > = Assign<
36 | {
37 | name: FieldName>;
38 | as?: TAs;
39 | rules?: ValidationRules;
40 | onFocus?: () => void;
41 | defaultValue?: unknown;
42 | control?: TControl;
43 | render?: (data: {
44 | onChange: (...event: any[]) => void;
45 | onBlur: () => void;
46 | value: any;
47 | }) => React.ReactElement;
48 | },
49 | AsProps
50 | >;
51 |
--------------------------------------------------------------------------------
/src/types/utils.ts:
--------------------------------------------------------------------------------
1 | import { NestedValue } from './form';
2 |
3 | export type Primitive = string | boolean | number | symbol | null | undefined;
4 |
5 | export type EmptyObject = { [K in string | number]: never };
6 |
7 | export type NonUndefined = T extends undefined ? never : T;
8 |
9 | export type LiteralToPrimitive = T extends string
10 | ? string
11 | : T extends number
12 | ? number
13 | : T extends boolean
14 | ? boolean
15 | : T;
16 |
17 | export type Assign = T & Omit;
18 |
19 | export type DeepPartial = {
20 | [K in keyof T]?: T[K] extends Array
21 | ? Array>
22 | : T[K] extends ReadonlyArray
23 | ? ReadonlyArray>
24 | : T[K] extends Record
25 | ? DeepPartial
26 | : T[K];
27 | };
28 |
29 | export type IsAny = boolean extends (T extends never ? true : false)
30 | ? true
31 | : false;
32 |
33 | export type DeepMap = {
34 | [K in keyof T]?: IsAny extends true
35 | ? any
36 | : NonUndefined extends NestedValue | Date | FileList
37 | ? TValue
38 | : NonUndefined extends object
39 | ? DeepMap
40 | : NonUndefined extends Array
41 | ? IsAny extends true
42 | ? Array
43 | : U extends NestedValue | Date | FileList
44 | ? Array
45 | : U extends object
46 | ? Array>
47 | : Array
48 | : TValue;
49 | };
50 |
51 | export type IsFlatObject = Extract<
52 | Exclude,
53 | any[] | object
54 | > extends never
55 | ? true
56 | : false;
57 |
--------------------------------------------------------------------------------
/src/useForm.ts:
--------------------------------------------------------------------------------
1 | import { ref, reactive, watch } from "vue";
2 | import getFieldsValues from "./logic/getFieldsValues";
3 | import getFieldValue from "./logic/getFieldValue";
4 | import {
5 | Field,
6 | FieldElement,
7 | FieldErrors,
8 | FieldName,
9 | FieldValue,
10 | FieldValues,
11 | FlatFieldErrors,
12 | InternalFieldName,
13 | Ref,
14 | UnpackNestedValue,
15 | UseFormOptions,
16 | ValidationRules
17 | } from "./types/form";
18 | import isString from "./utils/isString";
19 | import isArray from "./utils/isArray";
20 | import { EVENTS, UNDEFINED, VALIDATION_MODE } from "./constants";
21 | import isUndefined from "./utils/isUndefined";
22 | import isRadioOrCheckboxFunction from "./utils/isRadioOrCheckbox";
23 | import onDomRemove from "./utils/onDomRemove";
24 | import unique from "./utils/unique";
25 | import { get } from "./utils";
26 | import isEmptyObject from "./utils/isEmptyObject";
27 | import isNameInFieldArray from "./logic/isNameInFieldArray";
28 | import findRemovedFieldAndRemoveListener from "./logic/findRemovedFieldAndRemoveListener";
29 | import isObject from "./utils/isObject";
30 | import modeChecker from "./utils/validationModeChecker";
31 | import attachEventListeners from "./logic/attachEventListeners";
32 | import isSelectInput from "./utils/isSelectInput";
33 | import skipValidation from "./logic/skipValidation";
34 | import getFieldArrayParentName from "./logic/getFieldArrayParentName";
35 | import set from "./utils/set";
36 | import unset from "./utils/unset";
37 | import getIsFieldsDifferent from "./logic/getIsFieldsDifferent";
38 | import isHTMLElement from "./utils/isHTMLElement";
39 | import isNullOrUndefined from "./utils/isNullOrUndefined";
40 | import { DeepPartial } from "./types/utils";
41 | import validateField from "./logic/validateField";
42 | import shouldRenderBasedOnError from "./logic/shouldRenderBasedOnError";
43 | import isSameError from "./utils/isSameError";
44 | import isRadioInput from "./utils/isRadioInput";
45 | import isFileInput from "./utils/isFileInput";
46 | import isMultipleSelect from "./utils/isMultipleSelect";
47 | import isCheckBoxInput from "./utils/isCheckBoxInput";
48 |
49 | const isWindowUndefined = typeof window === UNDEFINED;
50 | const isWeb =
51 | typeof document !== UNDEFINED &&
52 | !isWindowUndefined &&
53 | !isUndefined(window.HTMLElement);
54 | const isProxyEnabled = isWeb ? "Proxy" in window : typeof Proxy !== UNDEFINED;
55 |
56 | export function useForm<
57 | TFieldValues extends FieldValues = FieldValues,
58 | TContext extends object = object
59 | >({
60 | resolver,
61 | shouldUnregister,
62 | mode,
63 | context,
64 | criteriaMode,
65 | reValidateMode
66 | }: UseFormOptions = {}) {
67 | let isDirtyRef = false;
68 | let isValidRef = false;
69 | const fieldsRef: any = {};
70 | const isUnMount = false;
71 | const validFieldsRef: any = new Set();
72 | const dirtyFieldsRef: any = {};
73 | let errorsRef: any = ref({});
74 | const defaultValuesAtRenderRef: any = {};
75 | const watchFieldsHookRef: any = {};
76 | const watchFieldsHookRenderRef: any = {};
77 | const fieldArrayNamesRef: any = new Set();
78 | const unmountFieldsStateRef = {};
79 | const defaultValuesRef = {};
80 | const touchedFieldsRef = {};
81 | const isWatchAllRef = false;
82 | const fieldsWithValidationRef: any = new Set();
83 | const isSubmittedRef = false;
84 | const watchFieldsRef = new Set();
85 | const { isOnBlur, isOnSubmit, isOnChange, isOnAll } = modeChecker(mode);
86 | const {
87 | isOnBlur: isReValidateOnBlur,
88 | isOnChange: isReValidateOnChange
89 | } = modeChecker(reValidateMode);
90 | const formState = ref({
91 | isSubmitting: false,
92 | isValid: false
93 | });
94 | const isValidateAllFieldCriteria = criteriaMode === VALIDATION_MODE.all;
95 |
96 | const isFieldWatched = (name: string) =>
97 | isWatchAllRef ||
98 | watchFieldsRef.has(name) ||
99 | watchFieldsRef.has((name.match(/\w+/) || [])[0]);
100 |
101 | const setDirty = (name: InternalFieldName): boolean => {
102 | if (!fieldsRef[name]) {
103 | return false;
104 | }
105 |
106 | const isFieldDirty =
107 | defaultValuesAtRenderRef[name] !==
108 | getFieldValue(fieldsRef, name, unmountFieldsStateRef);
109 | const isDirtyFieldExist = get(dirtyFieldsRef, name);
110 | const isFieldArray = isNameInFieldArray(fieldArrayNamesRef, name);
111 | const previousIsDirty = isDirtyRef;
112 |
113 | if (isFieldDirty) {
114 | set(dirtyFieldsRef, name, true);
115 | } else {
116 | unset(dirtyFieldsRef, name);
117 | }
118 |
119 | isDirtyRef =
120 | (isFieldArray &&
121 | getIsFieldsDifferent(
122 | get(getValues(), getFieldArrayParentName(name)),
123 | get(defaultValuesRef, getFieldArrayParentName(name))
124 | )) ||
125 | !isEmptyObject(dirtyFieldsRef);
126 |
127 | return (
128 | previousIsDirty !== isDirtyRef ||
129 | isDirtyFieldExist !== get(dirtyFieldsRef, name)
130 | );
131 | };
132 |
133 | const renderWatchedInputs = (name: string, found = true): boolean => {
134 | // if (!isEmptyObject(watchFieldsHookRef)) {
135 | // for (const key in watchFieldsHookRef) {
136 | // if (
137 | // name === "" ||
138 | // watchFieldsHookRef[key].has(name) ||
139 | // watchFieldsHookRef[key].has(getFieldArrayParentName(name)) ||
140 | // !watchFieldsHookRef[key].size
141 | // ) {
142 | // watchFieldsHookRenderRef[key]();
143 | // found = false;
144 | // }
145 | // }
146 | // }
147 |
148 | return found;
149 | };
150 |
151 | const shouldRenderBaseOnError = (
152 | name: InternalFieldName,
153 | error: FlatFieldErrors
154 | ): boolean | void => {
155 | const errors = { ...errorsRef.value };
156 |
157 | if (isEmptyObject(error)) {
158 | if (fieldsWithValidationRef.has(name) || resolver) {
159 | validFieldsRef.add(name);
160 | }
161 |
162 | if (errorsRef.value[name]) {
163 | unset(errors, name);
164 | errorsRef.value = errors;
165 | }
166 | } else {
167 | validFieldsRef.delete(name);
168 |
169 | set(errors, name, error[name]);
170 | errorsRef.value = errors;
171 | }
172 | };
173 |
174 | const handleChangeRef = async ({
175 | type,
176 | target
177 | }: Event): Promise => {
178 | const name = (target as Ref)!.name;
179 | const field = fieldsRef[name];
180 | let error: FlatFieldErrors;
181 |
182 | if (field) {
183 | const isBlurEvent = type === EVENTS.BLUR;
184 | const shouldSkipValidation =
185 | !isOnAll &&
186 | skipValidation({
187 | isOnChange,
188 | isOnBlur,
189 | isBlurEvent,
190 | isReValidateOnChange,
191 | isReValidateOnBlur,
192 | isSubmitted: isSubmittedRef
193 | });
194 |
195 | if (isBlurEvent && !get(touchedFieldsRef, name)) {
196 | set(touchedFieldsRef, name, true);
197 | }
198 |
199 | if (shouldSkipValidation) {
200 | renderWatchedInputs(name);
201 | return;
202 | }
203 |
204 | if (resolver) {
205 | const { errors } = await resolver(
206 | getValues() as TFieldValues,
207 | context,
208 | isValidateAllFieldCriteria
209 | );
210 | isValidRef = isEmptyObject(errors);
211 |
212 | error = (get(errors, name)
213 | ? { [name]: get(errors, name) }
214 | : {}) as FlatFieldErrors;
215 |
216 | formState.value.isValid = isValidRef;
217 | } else {
218 | error = await validateField(
219 | fieldsRef,
220 | isValidateAllFieldCriteria,
221 | field,
222 | unmountFieldsStateRef
223 | );
224 | }
225 |
226 | renderWatchedInputs(name);
227 |
228 | shouldRenderBaseOnError(name, error);
229 | }
230 | };
231 |
232 | const removeFieldEventListener = (field: Field, forceDelete?: boolean) =>
233 | findRemovedFieldAndRemoveListener(
234 | fieldsRef,
235 | handleChangeRef!,
236 | field,
237 | unmountFieldsStateRef,
238 | shouldUnregister,
239 | forceDelete
240 | );
241 |
242 | const removeFieldEventListenerAndRef = (
243 | field: Field | undefined,
244 | forceDelete?: boolean
245 | ) => {
246 | if (
247 | field &&
248 | (!isNameInFieldArray(fieldArrayNamesRef, field.ref.name) || forceDelete)
249 | ) {
250 | removeFieldEventListener(field, forceDelete);
251 |
252 | if (shouldUnregister) {
253 | [
254 | errorsRef,
255 | touchedFieldsRef,
256 | dirtyFieldsRef,
257 | defaultValuesAtRenderRef
258 | ].forEach(data => unset(data, field.ref.name));
259 |
260 | [fieldsWithValidationRef, validFieldsRef].forEach(data =>
261 | data.delete(field.ref.name)
262 | );
263 |
264 | isDirtyRef = !isEmptyObject(dirtyFieldsRef);
265 |
266 | if (resolver) {
267 | validateResolver();
268 | }
269 | }
270 | }
271 | };
272 |
273 | const setFieldValue = (
274 | { ref, options }: Field,
275 | rawValue:
276 | | FieldValue
277 | | UnpackNestedValue>
278 | | undefined
279 | | null
280 | | boolean
281 | ) => {
282 | const value =
283 | isWeb && isHTMLElement(ref) && isNullOrUndefined(rawValue)
284 | ? ""
285 | : rawValue;
286 |
287 | if (isRadioInput(ref) && options) {
288 | options.forEach(
289 | ({ ref: radioRef }: { ref: HTMLInputElement }) =>
290 | (radioRef.checked = radioRef.value === value)
291 | );
292 | } else if (isFileInput(ref) && !isString(value)) {
293 | ref.files = value as FileList;
294 | } else if (isMultipleSelect(ref)) {
295 | [...ref.options].forEach(
296 | selectRef =>
297 | (selectRef.selected = (value as string).includes(selectRef.value))
298 | );
299 | } else if (isCheckBoxInput(ref) && options) {
300 | options.length > 1
301 | ? options.forEach(
302 | ({ ref: checkboxRef }) =>
303 | (checkboxRef.checked = (value as string).includes(
304 | checkboxRef.value
305 | ))
306 | )
307 | : (options[0].ref.checked = !!value);
308 | } else {
309 | ref.value = value;
310 | }
311 | };
312 |
313 | const validateResolver = (values = {}) => {
314 | resolver(
315 | {
316 | ...defaultValuesRef,
317 | ...getValues(),
318 | ...values
319 | },
320 | context,
321 | isValidateAllFieldCriteria
322 | ).then(({ errors }) => {
323 | const previousFormIsValid = isValidRef;
324 | isValidRef = isEmptyObject(errors);
325 |
326 | if (previousFormIsValid !== isValidRef) {
327 | formState.value.isValid = isValidRef;
328 | }
329 | });
330 | };
331 |
332 | function registerFieldRef>(
333 | ref: TFieldElement & Ref,
334 | validateOptions: ValidationRules | null = {}
335 | ): ((name: InternalFieldName) => void) | void {
336 | if (process.env.NODE_ENV !== "production" && !ref.name) {
337 | // eslint-disable-next-line no-console
338 | return console.warn("Missing name @", ref);
339 | }
340 |
341 | const { name, type, value } = ref;
342 | const fieldRefAndValidationOptions = {
343 | ref,
344 | ...validateOptions
345 | };
346 | const fields = fieldsRef;
347 | const isRadioOrCheckbox = isRadioOrCheckboxFunction(ref);
348 | let field = fields[name];
349 | let isEmptyDefaultValue = true;
350 | let isFieldArray;
351 | let defaultValue;
352 |
353 | if (
354 | field &&
355 | (isRadioOrCheckbox
356 | ? isArray(field.options) &&
357 | unique(field.options).find(option => {
358 | return value === option.ref.value && option.ref === ref;
359 | })
360 | : ref === field.ref)
361 | ) {
362 | fields[name] = {
363 | ...field,
364 | ...validateOptions
365 | };
366 | return;
367 | }
368 |
369 | if (type) {
370 | const mutationWatcher = onDomRemove(ref, () =>
371 | removeFieldEventListenerAndRef(field)
372 | );
373 |
374 | field = isRadioOrCheckbox
375 | ? {
376 | options: [
377 | ...unique((field && field.options) || []),
378 | {
379 | ref,
380 | mutationWatcher
381 | }
382 | ],
383 | ref: { type, name },
384 | ...validateOptions
385 | }
386 | : {
387 | ...fieldRefAndValidationOptions,
388 | mutationWatcher
389 | };
390 | } else {
391 | field = fieldRefAndValidationOptions;
392 | }
393 |
394 | fields[name] = field;
395 |
396 | const isEmptyUnmountFields = isUndefined(get(unmountFieldsStateRef, name));
397 |
398 | if (!isEmptyObject(defaultValuesRef) || !isEmptyUnmountFields) {
399 | defaultValue = get(
400 | isEmptyUnmountFields ? defaultValuesRef : unmountFieldsStateRef,
401 | name
402 | );
403 | isEmptyDefaultValue = isUndefined(defaultValue);
404 | isFieldArray = isNameInFieldArray(fieldArrayNamesRef, name);
405 |
406 | if (!isEmptyDefaultValue && !isFieldArray) {
407 | setFieldValue(field, defaultValue);
408 | }
409 | }
410 |
411 | if (resolver && !isFieldArray) {
412 | validateResolver();
413 | } else if (!isEmptyObject(validateOptions)) {
414 | fieldsWithValidationRef.add(name);
415 |
416 | if (!isOnSubmit) {
417 | validateField(
418 | fieldsRef,
419 | isValidateAllFieldCriteria,
420 | field,
421 | unmountFieldsStateRef
422 | ).then((error: FieldErrors) => {
423 | const previousFormIsValid = isValidRef;
424 |
425 | isEmptyObject(error)
426 | ? validFieldsRef.add(name)
427 | : (isValidRef = false) && validFieldsRef.delete(name);
428 |
429 | if (previousFormIsValid !== isValidRef) {
430 | formState.value.isValid = isValidRef;
431 | }
432 | });
433 | }
434 | }
435 |
436 | if (
437 | !defaultValuesAtRenderRef[name] &&
438 | !(isFieldArray && isEmptyDefaultValue)
439 | ) {
440 | const fieldValue = getFieldValue(fieldsRef, name, unmountFieldsStateRef);
441 | defaultValuesAtRenderRef[name] = isEmptyDefaultValue
442 | ? isObject(fieldValue)
443 | ? { ...fieldValue }
444 | : fieldValue
445 | : defaultValue;
446 | }
447 |
448 | if (type) {
449 | attachEventListeners(
450 | isRadioOrCheckbox && field.options
451 | ? field.options[field.options.length - 1]
452 | : field,
453 | isRadioOrCheckbox || isSelectInput(ref),
454 | handleChangeRef
455 | );
456 | }
457 | }
458 |
459 | function register>(
460 | rules?: ValidationRules
461 | ): (ref: (TFieldElement & Ref) | null) => void;
462 | function register(
463 | name: FieldName,
464 | rules?: ValidationRules
465 | ): void;
466 | function register>(
467 | ref: (TFieldElement & Ref) | null,
468 | rules?: ValidationRules
469 | ): void;
470 | function register>(
471 | refOrValidationOptions?:
472 | | FieldName
473 | | ValidationRules
474 | | (TFieldElement & Ref)
475 | | null,
476 | rules?: ValidationRules
477 | ): ((ref: (TFieldElement & Ref) | null) => void) | void {
478 | if (!isWindowUndefined) {
479 | if (isString(refOrValidationOptions)) {
480 | registerFieldRef({ name: refOrValidationOptions }, rules);
481 | } else if (
482 | isObject(refOrValidationOptions) &&
483 | "name" in refOrValidationOptions
484 | ) {
485 | registerFieldRef(refOrValidationOptions, rules);
486 | } else {
487 | return (ref: (TFieldElement & Ref) | null) =>
488 | ref && registerFieldRef(ref, refOrValidationOptions);
489 | }
490 | }
491 | }
492 |
493 | function getValues(): UnpackNestedValue;
494 | function getValues(
495 | name: TFieldName
496 | ): TFieldName extends keyof TFieldValues
497 | ? UnpackNestedValue[TFieldName]
498 | : TFieldValue;
499 | function getValues(
500 | names: TFieldName[]
501 | ): UnpackNestedValue>;
502 | function getValues(payload?: string | string[]): unknown {
503 | if (isString(payload)) {
504 | return getFieldValue(fieldsRef, payload, unmountFieldsStateRef);
505 | }
506 |
507 | if (isArray(payload)) {
508 | return payload.reduce(
509 | (previous, name) => ({
510 | ...previous,
511 | [name]: getFieldValue(fieldsRef, name, unmountFieldsStateRef)
512 | }),
513 | {}
514 | );
515 | }
516 |
517 | return getFieldsValues(fieldsRef, unmountFieldsStateRef);
518 | }
519 |
520 | return {
521 | formState,
522 | getValues,
523 | register,
524 | errors: errorsRef,
525 | handleSubmit: () => async (e): Promise => {
526 | if (e && e.preventDefault) {
527 | e.preventDefault();
528 | }
529 |
530 | console.log(getValues());
531 |
532 | // let fieldErrors = {};
533 | // let fieldValues = getValues();
534 | //
535 | // if (readFormStateRef.isSubmitting) {
536 | // formState.value = {
537 | // ...formState.value,
538 | // isSubmitting: true
539 | // };
540 | // }
541 | //
542 | // try {
543 | // if (resolverRef) {
544 | // const { errors, values } = await resolverRef(
545 | // fieldValues as TFieldValues,
546 | // contextRef,
547 | // isValidateAllFieldCriteria
548 | // );
549 | // errorsRef = errors;
550 | // fieldErrors = errors;
551 | // fieldValues = values;
552 | // } else {
553 | // for (const field of Object.values(fieldsRef)) {
554 | // if (field) {
555 | // const {
556 | // ref: { name }
557 | // } = field;
558 | //
559 | // const fieldError = await validateField(
560 | // fieldsRef,
561 | // isValidateAllFieldCriteria,
562 | // field,
563 | // unmountFieldsStateRef
564 | // );
565 | //
566 | // if (fieldError[name]) {
567 | // set(fieldErrors, name, fieldError[name]);
568 | // validFieldsRef.delete(name);
569 | // } else if (fieldsWithValidationRef.has(name)) {
570 | // unset(errorsRef, name);
571 | // validFieldsRef.add(name);
572 | // }
573 | // }
574 | // }
575 | // }
576 | //
577 | // if (
578 | // isEmptyObject(fieldErrors) &&
579 | // Object.keys(errorsRef).every(name =>
580 | // Object.keys(fieldsRef).includes(name)
581 | // )
582 | // ) {
583 | // errorsRef = {};
584 | // reRender();
585 | // await callback(
586 | // fieldValues as UnpackNestedValue,
587 | // e
588 | // );
589 | // } else {
590 | // errorsRef = { ...errorsRef, ...fieldErrors };
591 | // if (shouldFocusError) {
592 | // focusOnErrorField(fieldsRef, fieldErrors);
593 | // }
594 | // }
595 | // } finally {
596 | // isSubmittedRef = true;
597 | // isSubmittingRef = false;
598 | // submitCountRef = submitCountRef + 1;
599 | // reRender();
600 | // }
601 | }
602 | };
603 | }
604 |
--------------------------------------------------------------------------------
/src/utils/fillEmptyArray.test.ts:
--------------------------------------------------------------------------------
1 | import fillEmptyArray from './fillEmptyArray';
2 |
3 | describe('fillEmptyArray', () => {
4 | it('shoudl return an array of undefined or empty array when value is an array', () => {
5 | expect(fillEmptyArray([1])).toEqual([undefined]);
6 | expect(fillEmptyArray([])).toEqual([]);
7 | expect(fillEmptyArray(['2', true])).toEqual([undefined, undefined]);
8 | expect(fillEmptyArray([{}, {}])).toEqual([undefined, undefined]);
9 | expect(fillEmptyArray([[], [3]])).toEqual([undefined, undefined]);
10 | });
11 |
12 | it('shoudl return undefined when value is not an array', () => {
13 | expect(fillEmptyArray(1)).toEqual(undefined);
14 | expect(fillEmptyArray({})).toEqual(undefined);
15 | expect(fillEmptyArray('')).toEqual(undefined);
16 | expect(fillEmptyArray(true)).toEqual(undefined);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/utils/fillEmptyArray.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 |
3 | export default (value: T | T[]): undefined[] | undefined =>
4 | isArray(value) ? Array(value.length).fill(undefined) : undefined;
5 |
--------------------------------------------------------------------------------
/src/utils/filterBooleanArray.test.ts:
--------------------------------------------------------------------------------
1 | import { filterBooleanArray } from './filterBooleanArray';
2 |
3 | describe('filterBooleanArray', () => {
4 | it('should be filtered array', () => {
5 | expect(
6 | filterBooleanArray([
7 | { test: 'test', test1: 'test1' },
8 | 'test2',
9 | { test3: 'test3', test4: 'test4' },
10 | ]),
11 | ).toEqual([
12 | {
13 | test: true,
14 | test1: true,
15 | },
16 | true,
17 | { test3: true, test4: true },
18 | ]);
19 | });
20 |
21 | it('should be filtered object', () => {
22 | expect(filterBooleanArray({ test: 'test', test1: 'test1' })).toEqual([
23 | {
24 | test: true,
25 | test1: true,
26 | },
27 | ]);
28 | });
29 |
30 | it('should be filtered string', () => {
31 | expect(filterBooleanArray('test')).toEqual([true]);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/utils/filterBooleanArray.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 | import isObject from './isObject';
3 |
4 | function mapValueToBoolean(value: any) {
5 | if (isObject(value)) {
6 | const object: any = {};
7 |
8 | for (const key in value) {
9 | object[key] = true;
10 | }
11 |
12 | return [object];
13 | }
14 |
15 | return [true];
16 | }
17 |
18 | export const filterBooleanArray = (value: T): T[] =>
19 | isArray(value)
20 | ? value.map(mapValueToBoolean).flat()
21 | : mapValueToBoolean(value);
22 |
--------------------------------------------------------------------------------
/src/utils/get.test.ts:
--------------------------------------------------------------------------------
1 | import get from './get';
2 |
3 | describe('get', () => {
4 | it('should get the right data', () => {
5 | const test = {
6 | bill: [1, 2, 3],
7 | luo: [1, 3, { betty: 'test' }],
8 | betty: { test: { test1: [{ test2: 'bill' }] } },
9 | 'betty.test.test1[0].test1': 'test',
10 | 'dotted.filled': 'content',
11 | 'dotted.empty': '',
12 | };
13 | expect(get(test, 'bill')).toEqual([1, 2, 3]);
14 | expect(get(test, 'bill[0]')).toEqual(1);
15 | expect(get(test, 'luo[2].betty')).toEqual('test');
16 | expect(get(test, 'betty.test.test1[0].test2')).toEqual('bill');
17 | expect(get(test, 'betty.test.test1[0].test1')).toEqual('test');
18 | expect(get(test, 'betty.test.test1[0].test3')).toEqual(undefined);
19 | expect(get(test, 'dotted.filled')).toEqual(test['dotted.filled']);
20 | expect(get(test, 'dotted.empty')).toEqual(test['dotted.empty']);
21 | expect(get(test, 'dotted.nonexistent', 'default')).toEqual('default');
22 | });
23 |
24 | it('should get from the flat data', () => {
25 | const test = {
26 | bill: 'test',
27 | };
28 | expect(get(test, 'bill')).toEqual('test');
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/utils/get.ts:
--------------------------------------------------------------------------------
1 | import isUndefined from './isUndefined';
2 | import isNullOrUndefined from './isNullOrUndefined';
3 | import unique from './unique';
4 |
5 | export default (obj: any, path: string, defaultValue?: any) => {
6 | const result = unique(path.split(/[,[\].]+?/)).reduce(
7 | (result, key) => (isNullOrUndefined(result) ? result : result[key]),
8 | obj,
9 | );
10 |
11 | return isUndefined(result) || result === obj
12 | ? isUndefined(obj[path])
13 | ? defaultValue
14 | : obj[path]
15 | : result;
16 | };
17 |
--------------------------------------------------------------------------------
/src/utils/getPath.test.ts:
--------------------------------------------------------------------------------
1 | import { getPath } from './getPath';
2 |
3 | describe('getPath', () => {
4 | it('should generate the correct path', () => {
5 | expect(
6 | getPath('test' as any, [
7 | 1,
8 | [1, 2],
9 | {
10 | data: 'test',
11 | kidding: { test: 'data' },
12 | foo: { bar: {} },
13 | what: [{ bill: { haha: 'test' } }, [3, 4]],
14 | one: 1,
15 | empty: null,
16 | absent: undefined,
17 | isAwesome: true,
18 | answer: Symbol(42),
19 | },
20 | ]),
21 | ).toEqual([
22 | 'test[0]',
23 | 'test[1][0]',
24 | 'test[1][1]',
25 | 'test[2].data',
26 | 'test[2].kidding.test',
27 | 'test[2].what[0].bill.haha',
28 | 'test[2].what[1][0]',
29 | 'test[2].what[1][1]',
30 | 'test[2].one',
31 | 'test[2].empty',
32 | 'test[2].absent',
33 | 'test[2].isAwesome',
34 | 'test[2].answer',
35 | ]);
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/utils/getPath.ts:
--------------------------------------------------------------------------------
1 | import isPrimitive from './isPrimitive';
2 | import isObject from './isObject';
3 | import { FieldValues, InternalFieldName } from '../types/form';
4 |
5 | export const getPath = (
6 | path: InternalFieldName,
7 | values: TFieldValues | any[],
8 | ): any[] => {
9 | const getInnerPath = (
10 | value: any,
11 | key: number | string,
12 | isObject?: boolean,
13 | ) => {
14 | const pathWithIndex = isObject ? `${path}.${key}` : `${path}[${key}]`;
15 | return isPrimitive(value) ? pathWithIndex : getPath(pathWithIndex, value);
16 | };
17 |
18 | return Object.entries(values)
19 | .map(([key, value]) => getInnerPath(value, key, isObject(values)))
20 | .flat(Infinity);
21 | };
22 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import get from './get';
2 |
3 | export { get };
4 |
--------------------------------------------------------------------------------
/src/utils/insert.test.ts:
--------------------------------------------------------------------------------
1 | import insert from './insert';
2 |
3 | describe('insert', () => {
4 | it('should insert value to specific index into an array', () => {
5 | expect(insert([1, 3, 4], 1, 2)).toEqual([1, 2, 3, 4]);
6 | expect(
7 | insert(
8 | [
9 | {
10 | firstName: '1',
11 | lastName: 'Luo',
12 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
13 | },
14 | {
15 | firstName: '2',
16 | lastName: 'Luo',
17 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
18 | },
19 | {
20 | firstName: '4',
21 | lastName: 'Luo',
22 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
23 | },
24 | ],
25 | 2,
26 | {
27 | firstName: '3',
28 | lastName: 'Luo',
29 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
30 | },
31 | ),
32 | ).toEqual([
33 | {
34 | firstName: '1',
35 | lastName: 'Luo',
36 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
37 | },
38 | {
39 | firstName: '2',
40 | lastName: 'Luo',
41 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
42 | },
43 | {
44 | firstName: '3',
45 | lastName: 'Luo',
46 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
47 | },
48 | {
49 | firstName: '4',
50 | lastName: 'Luo',
51 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
52 | },
53 | ]);
54 | });
55 |
56 | it('should insert undefined as value when value to be inserted is falsy', () => {
57 | expect(insert([1, 2, 4], 2)).toEqual([1, 2, undefined, 4]);
58 | expect(insert([1, 2, 4], 2, 0)).toEqual([1, 2, undefined, 4]);
59 | expect(insert([1, 2, 4], 2, false as any)).toEqual([1, 2, undefined, 4]);
60 | expect(insert([1, 2, 4], 2, '' as any)).toEqual([1, 2, undefined, 4]);
61 | expect(insert([1, 2, 4], 2, undefined as any)).toEqual([
62 | 1,
63 | 2,
64 | undefined,
65 | 4,
66 | ]);
67 | });
68 |
69 | it('should spread value when it is an array at one deep-level', () => {
70 | expect(insert([1, 2], 2, [3, 4])).toEqual([1, 2, 3, 4]);
71 | expect(insert([1, 2], 2, [3, [4]])).toEqual([1, 2, 3, [4]]);
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/src/utils/insert.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 |
3 | export default function insert(data: T[], index: number): (T | undefined)[];
4 | export default function insert(
5 | data: T[],
6 | index: number,
7 | value: T | T[],
8 | ): T[];
9 | export default function insert(
10 | data: T[],
11 | index: number,
12 | value?: T | T[],
13 | ): (T | undefined)[] {
14 | return [
15 | ...data.slice(0, index),
16 | ...(isArray(value) ? value : [value || undefined]),
17 | ...data.slice(index),
18 | ];
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/isArray.test.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 |
3 | describe('isArray', () => {
4 | it('should return true when value is an object', () => {
5 | expect(isArray([])).toBeTruthy();
6 | expect(isArray(['foo', 'bar'])).toBeTruthy();
7 | });
8 |
9 | it('should return false when value is not an object or is null', () => {
10 | expect(isArray(null)).toBeFalsy();
11 | expect(isArray(undefined)).toBeFalsy();
12 | expect(isArray(-1)).toBeFalsy();
13 | expect(isArray(0)).toBeFalsy();
14 | expect(isArray(1)).toBeFalsy();
15 | expect(isArray('')).toBeFalsy();
16 | expect(isArray({})).toBeFalsy();
17 | expect(isArray({ foo: 'bar' })).toBeFalsy();
18 | expect(isArray(() => null)).toBeFalsy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/isArray.ts:
--------------------------------------------------------------------------------
1 | export default (value: unknown): value is T[] => Array.isArray(value);
2 |
--------------------------------------------------------------------------------
/src/utils/isBoolean.test.ts:
--------------------------------------------------------------------------------
1 | import isBoolean from './isBoolean';
2 |
3 | describe('isBoolean', () => {
4 | it('should return true when value is a boolean', () => {
5 | expect(isBoolean(true)).toBeTruthy();
6 | expect(isBoolean(false)).toBeTruthy();
7 | });
8 |
9 | it('should return false when value is not a boolean', () => {
10 | expect(isBoolean(null)).toBeFalsy();
11 | expect(isBoolean(undefined)).toBeFalsy();
12 | expect(isBoolean(-1)).toBeFalsy();
13 | expect(isBoolean(0)).toBeFalsy();
14 | expect(isBoolean(1)).toBeFalsy();
15 | expect(isBoolean('')).toBeFalsy();
16 | expect(isBoolean({})).toBeFalsy();
17 | expect(isBoolean([])).toBeFalsy();
18 | expect(isBoolean(() => null)).toBeFalsy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/isBoolean.ts:
--------------------------------------------------------------------------------
1 | export default (value: unknown): value is boolean => typeof value === 'boolean';
2 |
--------------------------------------------------------------------------------
/src/utils/isCheckBoxInput.test.ts:
--------------------------------------------------------------------------------
1 | import isCheckBoxInput from './isCheckBoxInput';
2 |
3 | describe('isCheckBoxInput', () => {
4 | it('should return true when type is checkbox', () => {
5 | expect(isCheckBoxInput({ name: 'test', type: 'checkbox' })).toBeTruthy();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/utils/isCheckBoxInput.ts:
--------------------------------------------------------------------------------
1 | import { FieldElement } from '../types/form';
2 |
3 | export default (element: FieldElement): element is HTMLInputElement =>
4 | element.type === 'checkbox';
5 |
--------------------------------------------------------------------------------
/src/utils/isDetached.test.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import isDetached from './isDetached';
3 |
4 | describe('isDetached', () => {
5 | it('should return false when the node is still in the main document', () => {
6 | document.body.innerHTML = ''; // Make sure the body is empty
7 | const node = document.createElement('div');
8 | document.body.appendChild(node);
9 |
10 | expect(isDetached(node)).toBeFalsy();
11 | });
12 |
13 | it('should return true when the node was never attached to the main document', () => {
14 | document.body.innerHTML = ''; // Make sure the body is empty
15 | const node = document.createElement('div');
16 |
17 | expect(isDetached(node)).toBeTruthy();
18 | });
19 |
20 | it('should return false when the given element is not an HTMLElement', () => {
21 | expect(isDetached({ name: 'myComp' })).toBeFalsy();
22 | expect(isDetached('myComp')).toBeFalsy();
23 | expect(isDetached(20)).toBeFalsy();
24 | });
25 |
26 | it('should return true when the given element undefined or otherwise falsy', () => {
27 | expect(isDetached(undefined)).toBeTruthy();
28 | expect(isDetached(null)).toBeTruthy();
29 | expect(isDetached('')).toBeTruthy();
30 | expect(isDetached(0)).toBeTruthy();
31 | expect(isDetached(NaN)).toBeTruthy();
32 | expect(isDetached(false)).toBeTruthy();
33 | });
34 |
35 | it('should return true when the node is no longer in the main document', () => {
36 | document.body.innerHTML = ''; // Make sure the body is empty
37 | const node = document.createElement('div');
38 | document.body.appendChild(node);
39 | expect(isDetached(node)).toBeFalsy();
40 | document.body.removeChild(node);
41 | expect(isDetached(node)).toBeTruthy();
42 | });
43 |
44 | it('should return false when the node is nested deep in the main document', () => {
45 | document.body.innerHTML = ''; // Make sure the body is empty
46 |
47 | let lastNode = document.body;
48 | for (let i = 0; i < 10; ++i) {
49 | const newNode = document.createElement('div');
50 | lastNode.appendChild(newNode);
51 | lastNode = newNode;
52 | }
53 |
54 | expect(isDetached(lastNode)).toBeFalsy();
55 | });
56 |
57 | it('should return false when the node is an attached iframe', () => {
58 | document.body.innerHTML = ''; // Make sure the body is empty
59 | const iframe = document.createElement('iframe');
60 | document.body.appendChild(iframe);
61 |
62 | expect(isDetached(iframe)).toBeFalsy();
63 | });
64 |
65 | it('should return true when the node is a detached iframe', () => {
66 | document.body.innerHTML = ''; // Make sure the body is empty
67 | const iframe = document.createElement('iframe');
68 | document.body.appendChild(iframe);
69 | expect(isDetached(iframe)).toBeFalsy();
70 | document.body.removeChild(iframe);
71 | expect(isDetached(iframe)).toBeTruthy();
72 | });
73 |
74 | it('should return false when the node is nested inside an iframe', () => {
75 | return new Promise((resolve, reject) => {
76 | document.body.innerHTML = ''; // Make sure the body is empty
77 |
78 | const iframe = document.createElement('iframe');
79 | iframe.src = 'about:blank';
80 | iframe.addEventListener(
81 | 'load',
82 | function () {
83 | const node = document.createElement('div');
84 | if (iframe.contentDocument) {
85 | iframe.contentDocument.body.appendChild(node);
86 |
87 | resolve(isDetached(node));
88 | } else {
89 | reject('Could not find iframe contentDocument');
90 | }
91 | },
92 | false,
93 | );
94 |
95 | document.body.appendChild(iframe);
96 | }).then((detached) => expect(detached).toBeFalsy());
97 | });
98 |
99 | it('should return true when the node is nested inside an iframe and the iframe is detached', () => {
100 | return new Promise((resolve, reject) => {
101 | document.body.innerHTML = ''; // Make sure the body is empty
102 |
103 | const iframe = document.createElement('iframe');
104 | iframe.src = 'about:blank';
105 | iframe.addEventListener(
106 | 'load',
107 | function () {
108 | const node = document.createElement('div');
109 | if (iframe.contentDocument) {
110 | iframe.contentDocument.body.appendChild(node);
111 |
112 | expect(isDetached(node)).toBeFalsy();
113 |
114 | // Now detach the iframe
115 | document.body.removeChild(iframe);
116 |
117 | resolve(isDetached(node));
118 | } else {
119 | reject('Could not find iframe contentDocument');
120 | }
121 | },
122 | false,
123 | );
124 |
125 | document.body.appendChild(iframe);
126 | }).then((detached) => expect(detached).toBeTruthy());
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/src/utils/isDetached.ts:
--------------------------------------------------------------------------------
1 | import { Ref } from '../types/form';
2 |
3 | export default function isDetached(element: Ref): boolean {
4 | if (!element) {
5 | return true;
6 | }
7 |
8 | if (
9 | !(element instanceof HTMLElement) ||
10 | element.nodeType === Node.DOCUMENT_NODE
11 | ) {
12 | return false;
13 | }
14 |
15 | return isDetached(element.parentNode as Ref);
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/isEmptyObject.test.ts:
--------------------------------------------------------------------------------
1 | import isEmptyObject from './isEmptyObject';
2 |
3 | describe('isEmptyObject', () => {
4 | it('should return true when value is an empty object', () => {
5 | expect(isEmptyObject({})).toBeTruthy();
6 | });
7 |
8 | it('should return false when value is not an empty object', () => {
9 | expect(isEmptyObject(null)).toBeFalsy();
10 | expect(isEmptyObject(undefined)).toBeFalsy();
11 | expect(isEmptyObject(-1)).toBeFalsy();
12 | expect(isEmptyObject(0)).toBeFalsy();
13 | expect(isEmptyObject(1)).toBeFalsy();
14 | expect(isEmptyObject('')).toBeFalsy();
15 | expect(isEmptyObject(() => null)).toBeFalsy();
16 | expect(isEmptyObject({ foo: 'bar' })).toBeFalsy();
17 | expect(isEmptyObject([])).toBeFalsy();
18 | expect(isEmptyObject(['foo', 'bar'])).toBeFalsy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/isEmptyObject.ts:
--------------------------------------------------------------------------------
1 | import isObject from './isObject';
2 | import { EmptyObject } from '../types/utils';
3 |
4 | export default (value: unknown): value is EmptyObject =>
5 | isObject(value) && !Object.keys(value).length;
6 |
--------------------------------------------------------------------------------
/src/utils/isFileInput.test.ts:
--------------------------------------------------------------------------------
1 | import isFileInput from './isFileInput';
2 |
3 | describe('isFileInput', () => {
4 | it('should return true when type is file', () => {
5 | expect(isFileInput({ name: 'test', type: 'file' })).toBeTruthy();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/utils/isFileInput.ts:
--------------------------------------------------------------------------------
1 | import { FieldElement } from '../types/form';
2 |
3 | export default (element: FieldElement): element is HTMLInputElement =>
4 | element.type === 'file';
5 |
--------------------------------------------------------------------------------
/src/utils/isFunction.test.ts:
--------------------------------------------------------------------------------
1 | import isFunction from './isFunction';
2 |
3 | describe('isFunction', () => {
4 | it('should return true when value is a function', () => {
5 | expect(isFunction(() => null)).toBeTruthy();
6 | expect(
7 | isFunction(function foo() {
8 | return null;
9 | }),
10 | ).toBeTruthy();
11 | });
12 |
13 | it('should return false when value is not a function', () => {
14 | expect(isFunction(null)).toBeFalsy();
15 | expect(isFunction(undefined)).toBeFalsy();
16 | expect(isFunction(-1)).toBeFalsy();
17 | expect(isFunction(0)).toBeFalsy();
18 | expect(isFunction(1)).toBeFalsy();
19 | expect(isFunction('')).toBeFalsy();
20 | expect(isFunction({})).toBeFalsy();
21 | expect(isFunction([])).toBeFalsy();
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/utils/isFunction.ts:
--------------------------------------------------------------------------------
1 | export default (value: unknown): value is Function =>
2 | typeof value === 'function';
3 |
--------------------------------------------------------------------------------
/src/utils/isHTMLElement.test.ts:
--------------------------------------------------------------------------------
1 | import isHTMLElement from './isHTMLElement';
2 |
3 | describe('isHTMLElement', () => {
4 | it('should return true when value is HTMLElement', () => {
5 | expect(isHTMLElement(document.createElement('input'))).toBeTruthy();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/utils/isHTMLElement.ts:
--------------------------------------------------------------------------------
1 | export default (value: any): value is HTMLElement =>
2 | value instanceof HTMLElement;
3 |
--------------------------------------------------------------------------------
/src/utils/isKey.test.ts:
--------------------------------------------------------------------------------
1 | import isKey from './isKey';
2 |
3 | describe('isKey', () => {
4 | it('should return false if it is array', () => {
5 | expect(isKey([])).toBeFalsy();
6 | });
7 | it('should return true when it is not a deep key', () => {
8 | expect(isKey('test')).toBeTruthy();
9 | expect(isKey('fooBar')).toBeTruthy();
10 | });
11 | it('should return false when it is a deep key', () => {
12 | expect(isKey('test.foo')).toBeFalsy();
13 | expect(isKey('test.foo[0]')).toBeFalsy();
14 | expect(isKey('test[1]')).toBeFalsy();
15 | expect(isKey('test.foo[0].bar')).toBeFalsy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/utils/isKey.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 |
3 | export default (value: [] | string) =>
4 | !isArray(value) &&
5 | (/^\w*$/.test(value) ||
6 | !/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/.test(value));
7 |
--------------------------------------------------------------------------------
/src/utils/isMessage.test.ts:
--------------------------------------------------------------------------------
1 | import isMessage from './isMessage';
2 |
3 | describe('isBoolean', () => {
4 | it('should return true when value is a Message', () => {
5 | expect(isMessage('test')).toBeTruthy();
6 | expect(isMessage(React.createElement('p'))).toBeTruthy();
7 | });
8 |
9 | it('should return false when value is not a Message', () => {
10 | expect(isMessage(null)).toBeFalsy();
11 | expect(isMessage(undefined)).toBeFalsy();
12 | expect(isMessage(-1)).toBeFalsy();
13 | expect(isMessage(0)).toBeFalsy();
14 | expect(isMessage(1)).toBeFalsy();
15 | expect(isMessage({})).toBeFalsy();
16 | expect(isMessage([])).toBeFalsy();
17 | expect(isMessage(() => null)).toBeFalsy();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/utils/isMessage.ts:
--------------------------------------------------------------------------------
1 | import isString from "../utils/isString";
2 | import isObject from "../utils/isObject";
3 | import { Message } from "../types/form";
4 |
5 | export default (value: unknown): value is Message =>
6 | isString(value) || isObject(value);
7 |
--------------------------------------------------------------------------------
/src/utils/isMultipleSelect.test.ts:
--------------------------------------------------------------------------------
1 | import isMultipleSelect from './isMultipleSelect';
2 |
3 | describe('isMultipleSelect', () => {
4 | it('should return true when type is select-multiple', () => {
5 | expect(
6 | isMultipleSelect({ name: 'test', type: 'select-multiple' }),
7 | ).toBeTruthy();
8 | });
9 | });
10 |
--------------------------------------------------------------------------------
/src/utils/isMultipleSelect.ts:
--------------------------------------------------------------------------------
1 | import { FieldElement } from '../types/form';
2 | import { SELECT } from '../constants';
3 |
4 | export default (element: FieldElement): element is HTMLSelectElement =>
5 | element.type === `${SELECT}-multiple`;
6 |
--------------------------------------------------------------------------------
/src/utils/isNullOrUndefined.test.ts:
--------------------------------------------------------------------------------
1 | import isNullOrUndefined from './isNullOrUndefined';
2 |
3 | describe('isNullOrUndefined', () => {
4 | it('should return true when object is null or undefined', () => {
5 | expect(isNullOrUndefined(null)).toBeTruthy();
6 | expect(isNullOrUndefined(undefined)).toBeTruthy();
7 | });
8 |
9 | it('should return false when object is neither null nor undefined', () => {
10 | expect(isNullOrUndefined(-1)).toBeFalsy();
11 | expect(isNullOrUndefined(0)).toBeFalsy();
12 | expect(isNullOrUndefined(1)).toBeFalsy();
13 | expect(isNullOrUndefined('')).toBeFalsy();
14 | expect(isNullOrUndefined({})).toBeFalsy();
15 | expect(isNullOrUndefined([])).toBeFalsy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/utils/isNullOrUndefined.ts:
--------------------------------------------------------------------------------
1 | export default (value: unknown): value is null | undefined => value == null;
2 |
--------------------------------------------------------------------------------
/src/utils/isObject.test.ts:
--------------------------------------------------------------------------------
1 | import isObject from './isObject';
2 |
3 | describe('isObject', () => {
4 | it('should return true when value is an object', () => {
5 | expect(isObject({})).toBeTruthy();
6 | expect(isObject({ foo: 'bar' })).toBeTruthy();
7 | });
8 |
9 | it('should return false when value is not an object or is null', () => {
10 | expect(isObject(null)).toBeFalsy();
11 | expect(isObject(undefined)).toBeFalsy();
12 | expect(isObject(-1)).toBeFalsy();
13 | expect(isObject(0)).toBeFalsy();
14 | expect(isObject(1)).toBeFalsy();
15 | expect(isObject('')).toBeFalsy();
16 | expect(isObject([])).toBeFalsy();
17 | expect(isObject(['foo', 'bar'])).toBeFalsy();
18 | expect(isObject(() => null)).toBeFalsy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/isObject.ts:
--------------------------------------------------------------------------------
1 | import isNullOrUndefined from './isNullOrUndefined';
2 | import isArray from './isArray';
3 |
4 | export const isObjectType = (value: unknown) => typeof value === 'object';
5 |
6 | export default (value: unknown): value is T =>
7 | !isNullOrUndefined(value) && !isArray(value) && isObjectType(value);
8 |
--------------------------------------------------------------------------------
/src/utils/isPrimitive.test.ts:
--------------------------------------------------------------------------------
1 | import isPrimitive from './isPrimitive';
2 |
3 | describe('isPrimitive', () => {
4 | it('should return true when value is a string', () => {
5 | expect(isPrimitive('foobar')).toBeTruthy();
6 | });
7 |
8 | it('should return true when value is a boolean', () => {
9 | expect(isPrimitive(false)).toBeTruthy();
10 | });
11 |
12 | it('should return true when value is a number', () => {
13 | expect(isPrimitive(123)).toBeTruthy();
14 | });
15 |
16 | it('should return true when value is a symbol', () => {
17 | expect(isPrimitive(Symbol())).toBeTruthy();
18 | });
19 |
20 | it('should return true when value is null', () => {
21 | expect(isPrimitive(null)).toBeTruthy();
22 | });
23 |
24 | it('should return true when value is undefined', () => {
25 | expect(isPrimitive(undefined)).toBeTruthy();
26 | });
27 |
28 | it('should return false when value is an object', () => {
29 | expect(isPrimitive({})).toBeFalsy();
30 | });
31 |
32 | it('should return false when value is an array', () => {
33 | expect(isPrimitive([])).toBeFalsy();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/utils/isPrimitive.ts:
--------------------------------------------------------------------------------
1 | import isNullOrUndefined from './isNullOrUndefined';
2 | import { isObjectType } from './isObject';
3 | import { Primitive } from '../types/utils';
4 |
5 | export default (value: unknown): value is Primitive =>
6 | isNullOrUndefined(value) || !isObjectType(value);
7 |
--------------------------------------------------------------------------------
/src/utils/isRadioInput.test.ts:
--------------------------------------------------------------------------------
1 | import isRadioInput from './isRadioInput';
2 |
3 | describe('isRadioInput', () => {
4 | it('should return true when type is radio', () => {
5 | expect(isRadioInput({ name: 'test', type: 'radio' })).toBeTruthy();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/utils/isRadioInput.ts:
--------------------------------------------------------------------------------
1 | import { FieldElement } from '../types/form';
2 |
3 | export default (element: FieldElement): element is HTMLInputElement =>
4 | element.type === 'radio';
5 |
--------------------------------------------------------------------------------
/src/utils/isRadioOrCheckbox.test.ts:
--------------------------------------------------------------------------------
1 | import isRadioOrCheckbox from './isRadioOrCheckbox';
2 |
3 | describe('isRadioOrCheckbox', () => {
4 | it('should return true when type is either radio or checkbox', () => {
5 | expect(isRadioOrCheckbox({ name: 'test', type: 'radio' })).toBeTruthy();
6 | expect(isRadioOrCheckbox({ name: 'test', type: 'checkbox' })).toBeTruthy();
7 | });
8 |
9 | it('shoudl return false when type is neither radio nor checkbox', () => {
10 | expect(isRadioOrCheckbox({ name: 'test', type: 'text' })).toBeFalsy();
11 | expect(isRadioOrCheckbox({ name: 'test', type: 'email' })).toBeFalsy();
12 | expect(isRadioOrCheckbox({ name: 'test', type: 'date' })).toBeFalsy();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/utils/isRadioOrCheckbox.ts:
--------------------------------------------------------------------------------
1 | import isRadioInput from './isRadioInput';
2 | import isCheckBoxInput from './isCheckBoxInput';
3 | import { FieldElement } from '../types/form';
4 |
5 | export default (ref: FieldElement): ref is HTMLInputElement =>
6 | isRadioInput(ref) || isCheckBoxInput(ref);
7 |
--------------------------------------------------------------------------------
/src/utils/isRegex.test.ts:
--------------------------------------------------------------------------------
1 | import isRegex from './isRegex';
2 |
3 | describe('isRegex', () => {
4 | it('should return true when it is a regex', () => {
5 | expect(isRegex(new RegExp('[a-z]'))).toBeTruthy();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/utils/isRegex.ts:
--------------------------------------------------------------------------------
1 | export default (value: unknown): value is RegExp => value instanceof RegExp;
2 |
--------------------------------------------------------------------------------
/src/utils/isSameError.test.ts:
--------------------------------------------------------------------------------
1 | import isSameError from './isSameError';
2 | import { FieldError } from '../types/form';
3 |
4 | describe('isSameError', () => {
5 | it('should detect if it contain the same error', () => {
6 | expect(
7 | isSameError(
8 | {
9 | type: 'test',
10 | message: 'what',
11 | } as FieldError,
12 | {
13 | type: 'test',
14 | message: 'what',
15 | },
16 | ),
17 | ).toBeTruthy();
18 |
19 | expect(
20 | isSameError(
21 | {
22 | type: '',
23 | message: '',
24 | } as FieldError,
25 | {
26 | type: '',
27 | message: '',
28 | },
29 | ),
30 | ).toBeTruthy();
31 |
32 | expect(
33 | isSameError(
34 | {
35 | type: '',
36 | types: {
37 | minLength: 'min',
38 | },
39 | message: '',
40 | } as FieldError,
41 | {
42 | type: '',
43 | types: {
44 | minLength: 'min',
45 | },
46 | message: '',
47 | },
48 | ),
49 | ).toBeTruthy();
50 | });
51 |
52 | it('should return false when error is not even defined', () => {
53 | expect(
54 | isSameError(undefined, {
55 | type: '',
56 | message: '',
57 | }),
58 | ).toBeFalsy();
59 |
60 | expect(
61 | isSameError(
62 | {
63 | type: '',
64 | message: 'test',
65 | },
66 | {
67 | type: '',
68 | message: '',
69 | },
70 | ),
71 | ).toBeFalsy();
72 |
73 | expect(
74 | isSameError('test' as any, {
75 | type: '',
76 | message: '',
77 | }),
78 | ).toBeFalsy();
79 |
80 | expect(
81 | isSameError(5 as any, {
82 | type: '',
83 | message: '',
84 | }),
85 | ).toBeFalsy();
86 | });
87 |
88 | it('should return false when they are not the same error', () => {
89 | expect(
90 | isSameError(
91 | {
92 | type: 'test',
93 | message: 'what',
94 | types: {
95 | minLength: 'min',
96 | },
97 | } as FieldError,
98 | {
99 | type: 'test',
100 | message: 'what',
101 | },
102 | ),
103 | ).toBeFalsy();
104 |
105 | expect(
106 | isSameError(
107 | {
108 | type: '',
109 | message: '',
110 | types: {
111 | maxLength: 'max',
112 | },
113 | } as FieldError,
114 | {
115 | type: '',
116 | message: '',
117 | types: {
118 | minLength: 'min',
119 | },
120 | },
121 | ),
122 | ).toBeFalsy();
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/src/utils/isSameError.ts:
--------------------------------------------------------------------------------
1 | import isObject from './isObject';
2 | import { FieldError } from '../types/form';
3 |
4 | export default (
5 | error: FieldError | undefined,
6 | { type, types = {}, message }: FieldError,
7 | ): boolean =>
8 | isObject(error) &&
9 | error.type === type &&
10 | error.message === message &&
11 | Object.keys(error.types || {}).length === Object.keys(types).length &&
12 | Object.entries(error.types || {}).every(
13 | ([key, value]) => types[key] === value,
14 | );
15 |
--------------------------------------------------------------------------------
/src/utils/isSelectInput.ts:
--------------------------------------------------------------------------------
1 | import { FieldElement } from '../types/form';
2 | import { SELECT } from '../constants';
3 |
4 | export default (element: FieldElement): element is HTMLSelectElement =>
5 | element.type === `${SELECT}-one`;
6 |
--------------------------------------------------------------------------------
/src/utils/isString.test.ts:
--------------------------------------------------------------------------------
1 | import isString from './isString';
2 |
3 | describe('isString', () => {
4 | it('should return true when value is a string', () => {
5 | expect(isString('')).toBeTruthy();
6 | expect(isString('foobar')).toBeTruthy();
7 | });
8 |
9 | it('should return false when value is not a string', () => {
10 | expect(isString(null)).toBeFalsy();
11 | expect(isString(undefined)).toBeFalsy();
12 | expect(isString(-1)).toBeFalsy();
13 | expect(isString(0)).toBeFalsy();
14 | expect(isString(1)).toBeFalsy();
15 | expect(isString({})).toBeFalsy();
16 | expect(isString([])).toBeFalsy();
17 | expect(isString(new String('test'))).toBeFalsy();
18 | expect(isString(() => null)).toBeFalsy();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/utils/isString.ts:
--------------------------------------------------------------------------------
1 | export default (value: unknown): value is string => typeof value === 'string';
2 |
--------------------------------------------------------------------------------
/src/utils/isUndefined.test.ts:
--------------------------------------------------------------------------------
1 | import isUndefined from './isUndefined';
2 |
3 | describe('isUndefined', () => {
4 | it('should return true when it is an undefined value', () => {
5 | expect(isUndefined(undefined)).toBeTruthy();
6 | });
7 |
8 | it('should return false when it is not an undefined value', () => {
9 | expect(isUndefined(null)).toBeFalsy();
10 | expect(isUndefined('')).toBeFalsy();
11 | expect(isUndefined('undefined')).toBeFalsy();
12 | expect(isUndefined(0)).toBeFalsy();
13 | expect(isUndefined([])).toBeFalsy();
14 | expect(isUndefined({})).toBeFalsy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/utils/isUndefined.ts:
--------------------------------------------------------------------------------
1 | export default (val: unknown): val is undefined => val === undefined;
2 |
--------------------------------------------------------------------------------
/src/utils/move.test.ts:
--------------------------------------------------------------------------------
1 | import moveArrayAt from './move';
2 |
3 | describe('move', () => {
4 | it('should be able to move element of array', () => {
5 | const data = [
6 | {
7 | firstName: '1',
8 | lastName: 'Luo',
9 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
10 | },
11 | {
12 | firstName: '2',
13 | lastName: 'Luo',
14 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
15 | },
16 | {
17 | firstName: '3',
18 | lastName: 'Luo',
19 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
20 | },
21 | ];
22 | moveArrayAt(data, 0, 2);
23 | expect(data).toEqual([
24 | {
25 | firstName: '2',
26 | lastName: 'Luo',
27 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
28 | },
29 | {
30 | firstName: '3',
31 | lastName: 'Luo',
32 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
33 | },
34 | {
35 | firstName: '1',
36 | lastName: 'Luo',
37 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
38 | },
39 | ]);
40 | });
41 |
42 | it('should return emtpy array when data passed was not an array', () => {
43 | expect(moveArrayAt({} as any, 0, 3)).toEqual([]);
44 | });
45 |
46 | it('should move nested item with empty slot', () => {
47 | expect(moveArrayAt([{ subFields: [{ test: '1' }] }], 0, 1)).toEqual([
48 | undefined,
49 | { subFields: [{ test: '1' }] },
50 | ]);
51 |
52 | expect(moveArrayAt([{ subFields: [{ test: '1' }] }], 0, 2)).toEqual([
53 | undefined,
54 | undefined,
55 | { subFields: [{ test: '1' }] },
56 | ]);
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/utils/move.ts:
--------------------------------------------------------------------------------
1 | import isUndefined from './isUndefined';
2 | import isArray from './isArray';
3 |
4 | export default (data: T[], from: number, to: number): (T | undefined)[] => {
5 | if (isArray(data)) {
6 | if (isUndefined(data[to])) {
7 | data[to] = undefined as any;
8 | }
9 | data.splice(to, 0, data.splice(from, 1)[0]);
10 | return data;
11 | }
12 |
13 | return [];
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/onDomRemove.test.ts:
--------------------------------------------------------------------------------
1 | import onDomRemove from './onDomRemove';
2 | import isDetached from './isDetached';
3 |
4 | jest.mock('./isDetached');
5 |
6 | describe('onDomRemove', () => {
7 | beforeEach(() => {
8 | (isDetached as any).mockReturnValue(true);
9 | });
10 |
11 | it('should call the observer', () => {
12 | // @ts-ignore
13 | window.MutationObserver = class {
14 | observe = jest.fn();
15 | };
16 | // @ts-ignore
17 | const observer = onDomRemove({}, () => {});
18 | expect(observer.observe).toBeCalledWith(window.document, {
19 | childList: true,
20 | subtree: true,
21 | });
22 | });
23 |
24 | it('should call the mutation callback', () => {
25 | let mockCallback: () => void;
26 | // @ts-ignore
27 | window.MutationObserver = class {
28 | constructor(callback: any) {
29 | mockCallback = callback;
30 | }
31 | disconnect = jest.fn();
32 | observe = jest.fn();
33 | };
34 |
35 | const mockOnDetachCallback = jest.fn();
36 |
37 | // @ts-ignore
38 | const observer = onDomRemove({}, mockOnDetachCallback);
39 |
40 | // @ts-ignore
41 | mockCallback();
42 |
43 | expect(observer.observe).toBeCalledWith(window.document, {
44 | childList: true,
45 | subtree: true,
46 | });
47 | expect(observer.disconnect).toBeCalled();
48 | expect(mockOnDetachCallback).toBeCalled();
49 | });
50 |
51 | it('should not call observer.disconnect if isDetached is false', () => {
52 | (isDetached as any).mockReturnValue(false);
53 | let mockCallback: () => void;
54 | // @ts-ignore
55 | window.MutationObserver = class {
56 | constructor(callback: any) {
57 | mockCallback = callback;
58 | }
59 | disconnect = jest.fn();
60 | observe = jest.fn();
61 | };
62 |
63 | const mockOnDetachCallback = jest.fn();
64 |
65 | // @ts-ignore
66 | const observer = onDomRemove({}, mockOnDetachCallback);
67 |
68 | // @ts-ignore
69 | mockCallback();
70 |
71 | expect(observer.observe).toBeCalledWith(window.document, {
72 | childList: true,
73 | subtree: true,
74 | });
75 | expect(observer.disconnect).not.toBeCalled();
76 | expect(mockOnDetachCallback).not.toBeCalled();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/src/utils/onDomRemove.ts:
--------------------------------------------------------------------------------
1 | import { Ref, MutationWatcher } from '../types/form';
2 | import isDetached from './isDetached';
3 |
4 | export default function onDomRemove(
5 | element: Ref,
6 | onDetachCallback: VoidFunction,
7 | ): MutationWatcher {
8 | const observer = new MutationObserver((): void => {
9 | if (isDetached(element)) {
10 | observer.disconnect();
11 | onDetachCallback();
12 | }
13 | });
14 |
15 | observer.observe(window.document, {
16 | childList: true,
17 | subtree: true,
18 | });
19 |
20 | return observer;
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/prepend.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 |
3 | export default function prepend(data: T[]): (T | undefined)[];
4 | export default function prepend(data: T[], value: T | T[]): T[];
5 | export default function prepend(
6 | data: T[],
7 | value?: T | T[],
8 | ): (T | undefined)[] {
9 | return [...(isArray(value) ? value : [value || undefined]), ...data];
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/remove.test.ts:
--------------------------------------------------------------------------------
1 | import remove from './remove';
2 |
3 | test('should remove item accordingly', () => {
4 | expect(
5 | remove([, , { type: 'required', message: '', ref: 'test' }], 1),
6 | ).toEqual([undefined, { type: 'required', message: '', ref: 'test' }]);
7 |
8 | expect(
9 | remove([, , { type: 'required', message: '', ref: 'test' }], [1, 2]),
10 | ).toEqual([]);
11 |
12 | expect(
13 | remove([, , { type: 'required', message: '', ref: 'test' }], [0, 1]),
14 | ).toEqual([{ type: 'required', message: '', ref: 'test' }]);
15 |
16 | expect(
17 | remove(
18 | [
19 | ,
20 | ,
21 | { type: 'required', message: '', ref: 'test' },
22 | { type: 'required', message: '', ref: 'test' },
23 | null,
24 | ,
25 | ],
26 | [3, 2],
27 | ),
28 | ).toEqual([]);
29 |
30 | expect(
31 | remove(
32 | [
33 | ,
34 | ,
35 | { type: 'required', message: '', ref: 'test' },
36 | { type: 'required', message: '', ref: 'test' },
37 | null,
38 | ,
39 | ],
40 | [1, 4],
41 | ),
42 | ).toEqual([
43 | { type: 'required', message: '', ref: 'test' },
44 | { type: 'required', message: '', ref: 'test' },
45 | ]);
46 |
47 | expect(remove([true, true, true], [1])).toEqual([true, true]);
48 | expect(remove([true, true, true], [0])).toEqual([true, true]);
49 | });
50 |
51 | test('should remove all items', () => {
52 | expect(
53 | remove(
54 | [
55 | {
56 | firstName: '1',
57 | lastName: 'Luo',
58 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
59 | },
60 | {
61 | firstName: '2',
62 | lastName: 'Luo',
63 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
64 | },
65 | {
66 | firstName: '3',
67 | lastName: 'Luo',
68 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
69 | },
70 | {
71 | firstName: '4',
72 | lastName: 'Luo',
73 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
74 | },
75 | {
76 | firstName: '5',
77 | lastName: 'Luo',
78 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
79 | },
80 | {
81 | firstName: '6',
82 | lastName: 'Luo',
83 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
84 | },
85 | {
86 | firstName: '7',
87 | lastName: 'Luo',
88 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
89 | },
90 | {
91 | firstName: '8',
92 | lastName: 'Luo',
93 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
94 | },
95 | {
96 | firstName: '9',
97 | lastName: 'Luo',
98 | id: '75309979-e340-49eb-8016-5f67bfb56c1c',
99 | },
100 | ],
101 | [0, 1, 2, 3, 4, 5, 6, 7, 8],
102 | ),
103 | ).toEqual([]);
104 | });
105 |
--------------------------------------------------------------------------------
/src/utils/remove.ts:
--------------------------------------------------------------------------------
1 | import isUndefined from './isUndefined';
2 | import isArray from './isArray';
3 | import unique from './unique';
4 |
5 | const removeAt = (data: T[], index: number): T[] => [
6 | ...data.slice(0, index),
7 | ...data.slice(index + 1),
8 | ];
9 |
10 | function removeAtIndexes(data: T[], index: number[]): T[] {
11 | let k = -1;
12 |
13 | while (++k < data.length) {
14 | if (index.indexOf(k) >= 0) {
15 | delete data[k];
16 | }
17 | }
18 |
19 | return unique(data);
20 | }
21 |
22 | export default (data: T[], index?: number | number[]): T[] =>
23 | isUndefined(index)
24 | ? []
25 | : isArray(index)
26 | ? removeAtIndexes(data, index)
27 | : removeAt(data, index);
28 |
--------------------------------------------------------------------------------
/src/utils/set.ts:
--------------------------------------------------------------------------------
1 | import isObject from './isObject';
2 | import isArray from './isArray';
3 | import isKey from './isKey';
4 | import stringToPath from './stringToPath';
5 | import { FieldValues } from '../types/form';
6 |
7 | export default function set(object: FieldValues, path: string, value: any) {
8 | let index = -1;
9 | const tempPath = isKey(path) ? [path] : stringToPath(path);
10 | const length = tempPath.length;
11 | const lastIndex = length - 1;
12 |
13 | while (++index < length) {
14 | const key = tempPath[index];
15 | let newValue: string | object = value;
16 |
17 | if (index !== lastIndex) {
18 | const objValue = object[key];
19 | newValue =
20 | isObject(objValue) || isArray(objValue)
21 | ? objValue
22 | : !isNaN(+tempPath[index + 1])
23 | ? []
24 | : {};
25 | }
26 | object[key] = newValue;
27 | object = object[key];
28 | }
29 | return object;
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/stringToPath.test.ts:
--------------------------------------------------------------------------------
1 | import stringToPath from './stringToPath';
2 |
3 | describe('stringToPath', () => {
4 | it('should convert string to path', () => {
5 | expect(stringToPath('test.test[2].data')).toEqual([
6 | 'test',
7 | 'test',
8 | '2',
9 | 'data',
10 | ]);
11 |
12 | expect(stringToPath('test.test["2"].data')).toEqual([
13 | 'test',
14 | 'test',
15 | '2',
16 | 'data',
17 | ]);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/utils/stringToPath.ts:
--------------------------------------------------------------------------------
1 | export default (input: string): (string | number)[] => {
2 | const result: (string | number)[] = [];
3 |
4 | input.replace(
5 | /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,
6 | (
7 | match: string,
8 | mathNumber: string,
9 | mathQuote: string,
10 | originalString: string,
11 | ): any => {
12 | result.push(
13 | mathQuote
14 | ? originalString.replace(/\\(\\)?/g, '$1')
15 | : mathNumber || match,
16 | );
17 | },
18 | );
19 |
20 | return result;
21 | };
22 |
--------------------------------------------------------------------------------
/src/utils/swap.ts:
--------------------------------------------------------------------------------
1 | export default (data: T[], indexA: number, indexB: number): void => {
2 | const temp = [data[indexB], data[indexA]];
3 | data[indexA] = temp[0];
4 | data[indexB] = temp[1];
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/unique.ts:
--------------------------------------------------------------------------------
1 | export default (value: any[]) => value.filter(Boolean);
2 |
--------------------------------------------------------------------------------
/src/utils/unset.test.ts:
--------------------------------------------------------------------------------
1 | import unset from './unset';
2 |
3 | describe('unset', () => {
4 | it('should unset the array', () => {
5 | const test = ['test', 'test1', 'test2'];
6 | expect(unset(test, '[0]')).toEqual([undefined, 'test1', 'test2']);
7 | expect(unset(test, '[1]')).toEqual([undefined, undefined, 'test2']);
8 | expect(unset(test, '[2]')).toEqual([undefined, undefined, undefined]);
9 | });
10 |
11 | it('should unset the flat object', () => {
12 | const test = {
13 | test: 'test',
14 | };
15 |
16 | expect(unset(test, 'test')).toEqual({});
17 | });
18 |
19 | it('should not unset if specified field is undefined', () => {
20 | const test = {
21 | test: {
22 | test1: 'test',
23 | },
24 | };
25 |
26 | expect(unset(test, 'testDummy.test1')).toEqual({ test: { test1: 'test' } });
27 | });
28 |
29 | it('should unset the nest object', () => {
30 | const test = {
31 | test: {
32 | min: 'test',
33 | },
34 | };
35 |
36 | expect(unset(test, 'test.min')).toEqual({});
37 | });
38 |
39 | it('should unset deep object', () => {
40 | const test = {
41 | test: {
42 | bill: {
43 | min: 'test',
44 | },
45 | },
46 | };
47 |
48 | expect(unset(test, 'test.bill.min')).toEqual({});
49 | });
50 |
51 | it('should unset the including multiple field object', () => {
52 | const deep = {
53 | data: {
54 | firstName: 'test',
55 | clear: undefined,
56 | test: [{ data1: '' }, { data2: '' }],
57 | data: {
58 | test: undefined,
59 | test1: {
60 | ref: {
61 | test: '',
62 | },
63 | },
64 | },
65 | },
66 | };
67 |
68 | const test = {
69 | test: {
70 | bill: {
71 | min: [{ deep }],
72 | },
73 | test: 'ha',
74 | },
75 | };
76 |
77 | expect(unset(test, 'test.bill.min[0].deep')).toEqual({
78 | test: {
79 | test: 'ha',
80 | },
81 | });
82 | });
83 |
84 | it('should unset the object in array', () => {
85 | const test = {
86 | test: [{ min: 'required' }],
87 | };
88 | expect(unset(test, 'test[0].min')).toEqual({});
89 | });
90 |
91 | it('should return empty object when inner object is empty object', () => {
92 | const test = {
93 | data: {
94 | firstName: {},
95 | },
96 | };
97 |
98 | expect(unset(test, 'data.firstName')).toEqual({});
99 | });
100 |
101 | it('should clear empty array', () => {
102 | const test = {
103 | data: {
104 | firstName: {
105 | test: [
106 | { name: undefined, email: undefined },
107 | { name: 'test', email: 'last' },
108 | ],
109 | deep: {
110 | last: [
111 | { name: undefined, email: undefined },
112 | { name: 'test', email: 'last' },
113 | ],
114 | },
115 | },
116 | },
117 | };
118 |
119 | expect(unset(test, 'data.firstName.test[0]')).toEqual({
120 | data: {
121 | firstName: {
122 | test: [undefined, { name: 'test', email: 'last' }],
123 | deep: {
124 | last: [
125 | { name: undefined, email: undefined },
126 | { name: 'test', email: 'last' },
127 | ],
128 | },
129 | },
130 | },
131 | });
132 |
133 | const test2 = {
134 | arrayItem: [
135 | {
136 | test1: undefined,
137 | test2: undefined,
138 | },
139 | ],
140 | data: 'test',
141 | };
142 |
143 | expect(unset(test2, 'arrayItem[0].test1')).toEqual({
144 | arrayItem: [
145 | {
146 | test2: undefined,
147 | },
148 | ],
149 | data: 'test',
150 | });
151 | });
152 |
153 | it('should only remove relevant data', () => {
154 | const data = {
155 | test: {},
156 | testing: {
157 | key1: 1,
158 | key2: [
159 | {
160 | key4: 4,
161 | key5: [],
162 | key6: null,
163 | key7: '',
164 | key8: undefined,
165 | key9: {},
166 | },
167 | ],
168 | key3: [],
169 | },
170 | };
171 |
172 | expect(unset(data, 'test')).toEqual({
173 | testing: {
174 | key1: 1,
175 | key2: [
176 | {
177 | key4: 4,
178 | key5: [],
179 | key6: null,
180 | key7: '',
181 | key8: undefined,
182 | key9: {},
183 | },
184 | ],
185 | key3: [],
186 | },
187 | });
188 | });
189 |
190 | it('should remove empty array item', () => {
191 | const data = {
192 | name: [
193 | {
194 | message: 'test',
195 | },
196 | ],
197 | };
198 |
199 | expect(unset(data, 'name[0]')).toEqual({});
200 | });
201 |
202 | it('should not remove nested empty array item', () => {
203 | const data = {
204 | scenario: {
205 | steps: [
206 | {
207 | content: {
208 | question: 'isRequired',
209 | },
210 | },
211 | ],
212 | },
213 | };
214 |
215 | expect(unset(data, 'scenario.steps[1].messages[0]')).toEqual({
216 | scenario: {
217 | steps: [
218 | {
219 | content: {
220 | question: 'isRequired',
221 | },
222 | },
223 | ],
224 | },
225 | });
226 | });
227 | });
228 |
--------------------------------------------------------------------------------
/src/utils/unset.ts:
--------------------------------------------------------------------------------
1 | import isArray from './isArray';
2 | import isKey from './isKey';
3 | import stringToPath from './stringToPath';
4 | import isEmptyObject from './isEmptyObject';
5 | import isObject from './isObject';
6 | import isUndefined from './isUndefined';
7 |
8 | function baseGet(object: any, updatePath: (string | number)[]) {
9 | const path = updatePath.slice(0, -1);
10 | const length = path.length;
11 | let index = 0;
12 |
13 | while (index < length) {
14 | object = isUndefined(object) ? index++ : object[updatePath[index++]];
15 | }
16 |
17 | return object;
18 | }
19 |
20 | export default function unset(object: any, path: string) {
21 | const updatePath = isKey(path) ? [path] : stringToPath(path);
22 | const childObject =
23 | updatePath.length == 1 ? object : baseGet(object, updatePath);
24 | const key = updatePath[updatePath.length - 1];
25 | let previousObjRef = undefined;
26 |
27 | if (childObject) {
28 | delete childObject[key];
29 | }
30 |
31 | for (let k = 0; k < updatePath.slice(0, -1).length; k++) {
32 | let index = -1;
33 | let objectRef = undefined;
34 | const currentPaths = updatePath.slice(0, -(k + 1));
35 | const currentPathsLength = currentPaths.length - 1;
36 |
37 | if (k > 0) {
38 | previousObjRef = object;
39 | }
40 |
41 | while (++index < currentPaths.length) {
42 | const item = currentPaths[index];
43 | objectRef = objectRef ? objectRef[item] : object[item];
44 |
45 | if (
46 | currentPathsLength === index &&
47 | ((isObject(objectRef) && isEmptyObject(objectRef)) ||
48 | (isArray(objectRef) &&
49 | !objectRef.filter((data) => isObject(data) && !isEmptyObject(data))
50 | .length))
51 | ) {
52 | previousObjRef ? delete previousObjRef[item] : delete object[item];
53 | }
54 |
55 | previousObjRef = objectRef;
56 | }
57 | }
58 |
59 | return object;
60 | }
61 |
--------------------------------------------------------------------------------
/src/utils/validationModeChecker.test.ts:
--------------------------------------------------------------------------------
1 | import validationModeChecker from './validationModeChecker';
2 | import { VALIDATION_MODE } from '../constants';
3 |
4 | describe('validationModeChecker', () => {
5 | it('should return correct mode', () => {
6 | expect(validationModeChecker(VALIDATION_MODE.onBlur)).toEqual({
7 | isOnSubmit: false,
8 | isOnBlur: true,
9 | isOnChange: false,
10 | isOnAll: false,
11 | });
12 |
13 | expect(validationModeChecker(VALIDATION_MODE.onChange)).toEqual({
14 | isOnSubmit: false,
15 | isOnBlur: false,
16 | isOnChange: true,
17 | isOnAll: false,
18 | });
19 |
20 | expect(validationModeChecker(VALIDATION_MODE.onSubmit)).toEqual({
21 | isOnSubmit: true,
22 | isOnBlur: false,
23 | isOnChange: false,
24 | isOnAll: false,
25 | });
26 |
27 | expect(validationModeChecker(undefined)).toEqual({
28 | isOnSubmit: true,
29 | isOnBlur: false,
30 | isOnChange: false,
31 | isOnAll: false,
32 | });
33 |
34 | expect(validationModeChecker(VALIDATION_MODE.all)).toEqual({
35 | isOnSubmit: false,
36 | isOnBlur: false,
37 | isOnChange: false,
38 | isOnAll: true,
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/utils/validationModeChecker.ts:
--------------------------------------------------------------------------------
1 | import { VALIDATION_MODE } from '../constants';
2 | import { Mode } from '../types/form';
3 |
4 | export default (
5 | mode?: Mode,
6 | ): {
7 | isOnSubmit: boolean;
8 | isOnBlur: boolean;
9 | isOnChange: boolean;
10 | isOnAll: boolean;
11 | } => ({
12 | isOnSubmit: !mode || mode === VALIDATION_MODE.onSubmit,
13 | isOnBlur: mode === VALIDATION_MODE.onBlur,
14 | isOnChange: mode === VALIDATION_MODE.onChange,
15 | isOnAll: mode === VALIDATION_MODE.all,
16 | });
17 |
--------------------------------------------------------------------------------