Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cypress/components/localRemoteValidation.cy.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import EmailComponentWithFields from '../testComponents/EmailComponentWithFields
import EmailComponentWithLocalValidation from '../testComponents/EmailComponentWithLocalValidation.jsx';
import EmailComponentWithRemoteValidation from '../testComponents/EmailComponentWithRemoteValidation.jsx';
import EmailComponentWithSubmit from '../testComponents/EmailComponentWithSubmit.jsx';
import { submitForm } from '../../src/formUtils.js';
import { useValidation } from '../../src/useValidation.js';
import { submitForm } from '../../src/formUtils';
import { useValidation } from '../../src/useValidation';

const emailValidationRemote = (value) => new Promise((resolve) => setTimeout(() => resolve(isValidEmail(value)), 2000));
const testEmailValid = "matej.radovix@enterwell.net";
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
"test": "cross-env NODE_ENV=test cypress run --component",
"test:open": "cross-env NODE_ENV=test cypress open"
},
"types": "./types/index.d.ts",
"types": "./dist/index.d.ts",
"main": "./dist/index.js",
"files": [
"dist",
"types"
"dist"
],
"keywords": [
"react",
Expand All @@ -41,7 +40,9 @@
"cypress": "15.9.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"vite": "7.3.1"
"typescript": "^6.0.2",
"vite": "7.3.1",
"vite-plugin-dts": "^4.5.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17 || ^18 || ^19"
Expand Down
616 changes: 616 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

86 changes: 44 additions & 42 deletions src/formUtils.js → src/formUtils.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,50 @@
import type { Fields } from './useValidation';

/**
* Checks whether some value is function or not.
*
* @param {any} f Value that should be checked whether it is function or not
* @returns true if value is a function, false otherwise
*
* @param f Value that should be checked whether it is function or not
* @returns true if value is a function, false otherwise
*/
const isFunction = (f) => Object.prototype.toString.call(f) == '[object Function]';
const isFunction = (f: unknown): f is Function => Object.prototype.toString.call(f) == '[object Function]';

/**
* Extracts the values from form fields' objects.
*
* @param {Object.<string, any>} fields Form's fields
*
* @param fields Form's fields
* @returns object containing form fields' values
*/
export const extractValues = (fields) => {
export const extractValues = (fields: Fields) => {
return Object
.entries(fields)
.reduce((acc, [k, v]) => ({ ...acc, [k]: v.value }), {});
.reduce<Record<string, unknown>>((acc, [k, v]) => ({ ...acc, [k]: v.value }), {});
};

/**
* Sets the values of form fields without changing dirty flag.
* When form is reset, these values will be used as initial values.
*
* @param {Object.<string, any>} fields Form's fields
* @param {Object.<string, any>} values Form's fields new values
*
* @param fields Form's fields
* @param values Form's fields new values
*/
export const setValues = (fields, values) => {
export const setValues = (fields: Fields, values: Record<string, unknown>) => {
Object
.entries(fields)
.forEach(([k, v]) => {
if (values.hasOwnProperty(k)) {
if (Object.prototype.hasOwnProperty.call(values, k)) {
v.setValue(values[k]);
}
});
};

/**
* Validates all forms' fields.
*
* @param {Object.<string, Object>} fields Form's fields
* @returns {boolean|Promise<boolean>} true if there is any error in the form, false otherwise.
* Promise with same result when at least one validation function resolved to Promise.
*
* @param fields Form's fields
* @returns true if there is any error in the form, false otherwise.
* Promise with same result when at least one validation function resolved to Promise.
*/
export const validateFields = (fields) => {
export const validateFields = (fields: Fields) => {
// Checks whether all fields have correct validation function
Object
.entries(fields)
Expand All @@ -53,16 +55,16 @@ export const validateFields = (fields) => {
});

// Validate all fields
var validationResults =
const validationResults =
Object.values(fields)
.map(field => field.validate(field.value));

// When all results are known (not Promise) return true if there are any errors
if (validationResults.every(result => typeof result === "boolean"))
return validationResults.some(result => result);

// Resolve all validation promises and return
return new Promise((resolve, reject) => {
// Resolve all validation promises and return
return new Promise<boolean>((resolve, reject) => {
Promise.all(validationResults.map(result => Promise.resolve(result)))
.then(results => resolve(results.some(result => result)))
.catch((reason) => reject(reason));
Expand All @@ -71,20 +73,20 @@ export const validateFields = (fields) => {

/**
* Checks if any of the form fields are dirty (have changed from initial values).
*
* @param {Object.<string, Object>} fields Form's fields
* @returns {boolean} true if any field is dirty, false otherwise
*
* @param fields Form's fields
* @returns true if any field is dirty, false otherwise
*/
export const isDirty = (fields) => {
export const isDirty = (fields: Fields) => {
return Object.values(fields).some(field => field.dirty);
};

/**
* Resets forms' fields to their initial values.
*
* @param {Object.<string, Object>} fields Form's fields
*
* @param fields Form's fields
*/
export const resetFields = (fields) => {
export const resetFields = (fields: Fields) => {
// Checks whether all fields have correct validation function
Object
.entries(fields)
Expand All @@ -94,22 +96,22 @@ export const resetFields = (fields) => {
}
});

// Validates all fields
// Resets all fields
Object.values(fields)
.forEach((cur) => cur.reset());
};

/**
* Validates the forms' fields and invokes the provided callback with extracted
* Validates the forms' fields and invokes the provided callback with extracted
* form's values.
*
* @param {Object.<string, Object>} fields Form's fields
* @param {function} onSubmit On submit callback
* @returns {Promise<unknown> | unknown | undefined} Returns the return value of onSubmit callback,
* wrapped in Promise if at least one validation function resolved to Promise.
* Returns undefined when form is not valid and onSubmit callback is not invoked or onSubmit function returns void.
*
* @param fields Form's fields
* @param onSubmit On submit callback
* @returns Returns the return value of onSubmit callback,
* wrapped in Promise if at least one validation function resolved to Promise.
* Returns undefined when form is not valid and onSubmit callback is not invoked or onSubmit function returns void.
*/
export const submitForm = (fields, onSubmit) => {
export const submitForm = (fields: Fields, onSubmit: (values: Record<string, unknown>) => unknown) => {
const validationResultHasErrors = validateFields(fields);
if (typeof validationResultHasErrors === "boolean") {
if (validationResultHasErrors) {
Expand All @@ -120,7 +122,7 @@ export const submitForm = (fields, onSubmit) => {
} else {
return new Promise((resolve, reject) => {
validationResultHasErrors.then(hasErrors => {
if (hasErrors) resolve();
if (hasErrors) resolve(undefined);
else resolve(onSubmit(extractValues(fields)));
}).catch(reject);
});
Expand All @@ -129,11 +131,11 @@ export const submitForm = (fields, onSubmit) => {

/**
* Resets the forms' fields and invokes the provided callback.
*
* @param {Object.<string, Object>} fields Form's fields
* @param {function} onCancel On cancel callback
*
* @param fields Form's fields
* @param onCancel On cancel callback
*/
export const cancelForm = (fields, onCancel) => {
export const cancelForm = (fields: Fields, onCancel: () => void) => {
resetFields(fields);
onCancel();
};
17 changes: 11 additions & 6 deletions src/index.js → src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
export {
export {
useValidation
} from './useValidation';
export {
extractValues,
validateFields,
export type {
FieldConfig,
Field,
Fields
} from './useValidation';
export {
extractValues,
validateFields,
setValues,
resetFields,
submitForm,
resetFields,
submitForm,
cancelForm,
isDirty
} from './formUtils';
Expand Down
79 changes: 59 additions & 20 deletions src/useValidation.js → src/useValidation.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,80 @@
import { useState } from 'react';

/**
* Configuration for the useValidation hook.
*/
export interface FieldConfig {
receiveEvent?: boolean;
reversed?: boolean;
ignoreDirtiness?: boolean;
}

/**
* Represents the return type of the useValidation hook.
*/
export interface Field<T = unknown> {
value: T;
error: boolean;
dirty: boolean;
setValue: (v: T) => void;
onChange: (e: unknown, config?: FieldConfig) => void;
onBlur: (event: unknown, config?: FieldConfig) => void;
validate: (v: T, config?: FieldConfig) => boolean | Promise<boolean>;
reset: () => void;
props: {
value: T;
onChange: Field<T>['onChange'];
onBlur: Field<T>['onBlur'];
};
}

/**
* Represents a map of form fields.
*/
export interface Fields {
[key: string]: Field;
}

// Default configuration
const DEFAULT_CONFIG = {
const DEFAULT_CONFIG: Required<FieldConfig> = {
receiveEvent: true,
reversed: false,
ignoreDirtiness: false
}
};

/**
* Function represents the useValidation hook.
*
* @param {any} defaultValue Default value
* @param {function} validationFn Function used for value validation
* @param {Object} config Hook configuration
*
* @param defaultValue Default value
* @param validationFn Function used for value validation
* @param config Hook configuration
* @returns object containing current value, error flag, onBlur and onChange callbacks, validate function and reset function
*/
export const useValidation = (defaultValue, validationFn, config) => {
export const useValidation = <T = unknown>(
defaultValue: T,
validationFn: (v: T) => boolean | Promise<boolean>,
config?: FieldConfig
): Field<T> => {
// Checks whether validation function is really function
if (!(Object.prototype.toString.call(validationFn) == '[object Function]')) {
throw new Error('Incorrect type of the validation function.')
}

const _config = {
const _config: Required<FieldConfig> = {
...DEFAULT_CONFIG,
...config
};

const [resetToValue, setResetToValue] = useState(defaultValue);
const [value, setValue] = useState(resetToValue);
const [resetToValue, setResetToValue] = useState<T>(defaultValue);
const [value, setValue] = useState<T>(resetToValue);
const [error, setError] = useState(false);

// dirty compares current value with initial value
const dirty = value !== resetToValue;

const onChange = (e, config) => {
const onChange = (e: unknown, config?: FieldConfig) => {
const activeConfig = config ?? _config;
const v = activeConfig.receiveEvent ? e.target.value : e;
const v = activeConfig.receiveEvent ? (e as { target: { value: T } }).target.value : e as T;
setValue(v);

// Value is validated on change, only if previously was incorrect
Expand All @@ -44,7 +83,7 @@ export const useValidation = (defaultValue, validationFn, config) => {
}
};

const onBlur = (_event, config) => {
const onBlur = (_event: unknown, config?: FieldConfig) => {
const activeConfig = config ?? _config;

// Value is validated if it is dirty or if dirtiness should be ignored
Expand All @@ -53,27 +92,27 @@ export const useValidation = (defaultValue, validationFn, config) => {
}
};

const _handleSetValue = (v) => {
const _handleSetValue = (v: T) => {
setResetToValue(v);
setValue(v);
}
};

const _setValidationResult = (isError, config) => {
const _setValidationResult = (isError: boolean, config?: FieldConfig) => {
const activeConfig = config ?? _config;

// Applies the reverse logic if needed
const _error = activeConfig.reversed ? !isError : isError;
setError(_error);
return _error;
}
};

const validate = (v, config) => {
const validate = (v: T, config?: FieldConfig) => {
// Validates the value
const validationResult = validationFn(v);
if (typeof validationResult === "boolean") {
return _setValidationResult(!validationResult, config);
} else {
return new Promise((resolve, reject) =>
return new Promise<boolean>((resolve, reject) =>
Promise.resolve(validationResult)
.then(result => resolve(_setValidationResult(!result, config)))
.catch(reason => reject(reason)));
Expand Down
Loading