├── .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 |
20 | 21 | {this.errors.firstName && "This is required."} 22 | 23 | 24 | {this.errors.lastName && "This is required."} 25 | 26 | 27 |
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 |
15 | 16 | {this.errors.firstName && "Required"} 17 | 18 | {this.errors.lastName && "Required"} 19 | 20 |
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 | --------------------------------------------------------------------------------