UNPKG

@andydowell/use-form-state

Version:

A React hook for managing form state and validation

426 lines (419 loc) 14.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } var appendToFormData = function appendToFormData(formData, key, value) { if (value instanceof FileList) { Array.from(value).forEach(function (file) { return formData.append(key, file); }); return; } if (value instanceof Blob) { formData.append(key, value); return; } if (value instanceof Date) { formData.append(key, value.toISOString()); return; } if (value instanceof Map) { var entries = Array.from(value.entries()); formData.append(key, JSON.stringify(entries)); return; } if (value instanceof Set) { var _entries = Array.from(value.values()); formData.append(key, JSON.stringify(_entries)); return; } if (Array.isArray(value)) { value.forEach(function (item) { return appendToFormData(formData, key, item); }); return; } if (value !== null && typeof value === "object") { formData.append(key, JSON.stringify(value)); return; } formData.append(key, value == null ? "" : String(value)); }; // Clone the whole form state so validators cannot mutate the live object var cloneFormState = function cloneFormState(state) { var clonedState = {}; for (var key in state) { clonedState[key] = _extends({}, state[key], { value: deepCloneValue(state[key].value) }); Object.freeze(clonedState[key]); } return Object.freeze(clonedState); }; /** * Runs validations using the provided state and field params. * Makes a cloned, frozen snapshot so validators cannot mutate the live state. */ var applyValidation = function applyValidation(currentState, formFieldParams, options) { var _ref = options || {}, updateAllFieldsError = _ref.updateAllFieldsError; var stateSnapshot = cloneFormState(currentState); var nextState = {}; var allValid = true; for (var key in stateSnapshot) { var fieldKey = key; var prevFieldState = stateSnapshot[fieldKey]; var shouldUpdateError = updateAllFieldsError != null ? updateAllFieldsError : prevFieldState.isInteracted; var _getFieldValidationOu = getFieldValidationOutcome(fieldKey, formFieldParams, stateSnapshot), isValid = _getFieldValidationOu.isValid, error = _getFieldValidationOu.error; if (!isValid) allValid = false; nextState[fieldKey] = _extends({}, prevFieldState, { isValid: isValid, error: shouldUpdateError ? error : prevFieldState.error }); } return { nextState: nextState, allValid: allValid }; }; var checkIfRequiredValueFilled = function checkIfRequiredValueFilled(value) { switch (typeof value) { case "number": if (isNaN(value) || !Number.isFinite(value)) return false; return true; case "object": if (Array.isArray(value)) return value.length > 0; if (value == null) return false; if (value instanceof Date) return !isNaN(value.getTime()); if (value instanceof Map || value instanceof Set) return value.size > 0; var isFileList = typeof FileList !== "undefined" && value instanceof FileList; if (isFileList) return value.length > 0; return Object.keys(value).length > 0; case "boolean": return true; case "string": default: return Boolean(value); } }; var createInitialStateSnapshot = function createInitialStateSnapshot(formFieldParams) { var snapshot = {}; for (var key in formFieldParams) { snapshot[key] = { value: deepCloneValue(formFieldParams[key].defaultValue), label: formFieldParams[key].label || "", helperText: formFieldParams[key].helperText, isValid: false, isInteracted: false, isRequired: !!formFieldParams[key].required, error: undefined }; } for (var _key in formFieldParams) { var fieldKey = _key; var _getFieldValidationOu2 = getFieldValidationOutcome(fieldKey, formFieldParams, snapshot), isValid = _getFieldValidationOu2.isValid; snapshot[fieldKey].isValid = isValid; } return snapshot; }; var cloneFileList = function cloneFileList(value) { if (typeof DataTransfer === "undefined") return value; // best effort without DOM var dataTransfer = new DataTransfer(); Array.from(value).forEach(function (file) { return dataTransfer.items.add(file); }); return dataTransfer.files; }; var deepCloneValue = function deepCloneValue(value) { var structuredCloneFn = globalThis.structuredClone; if (structuredCloneFn) { try { return structuredCloneFn(value); } catch (_unused) { // fall through to manual clone for unsupported structured clone types } } if (value === null || typeof value !== "object") return value; if (value instanceof Date) return new Date(value.getTime()); if (value instanceof Map) { var clonedEntries = Array.from(value.entries(), function (_ref2) { var key = _ref2[0], val = _ref2[1]; return [deepCloneValue(key), deepCloneValue(val)]; }); return new Map(clonedEntries); } if (value instanceof Set) { var clonedValues = Array.from(value.values()).map(deepCloneValue); return new Set(clonedValues); } var isFileList = typeof FileList !== "undefined" && value instanceof FileList; if (isFileList) return cloneFileList(value); if (value instanceof Blob) return value; if (Array.isArray(value)) return value.map(deepCloneValue); return Object.entries(value).reduce(function (acc, _ref3) { var key = _ref3[0], val = _ref3[1]; acc[key] = deepCloneValue(val); return acc; }, {}); }; var getOrderedValidationEntries = function getOrderedValidationEntries(validationParams) { var entriesWithOrder = Object.entries(validationParams).map(function (_ref4, index) { var _params$order; var key = _ref4[0], params = _ref4[1]; return { entry: [key, params], order: (_params$order = params.order) != null ? _params$order : index, index: index }; }); return entriesWithOrder.sort(function (a, b) { if (a.order !== b.order) return a.order - b.order; return a.index - b.index; // preserve insertion order when order is omitted }).map(function (_ref5) { var entry = _ref5.entry; return entry; }); }; var getFieldValidationOutcome = function getFieldValidationOutcome(key, formFieldParams, state) { var fieldParams = formFieldParams[key]; var fieldState = state[key]; if (fieldState.isRequired && !checkIfRequiredValueFilled(fieldState.value)) { var _fieldParams$required; return { isValid: false, error: { type: "required", message: (_fieldParams$required = fieldParams.required) == null ? void 0 : _fieldParams$required.message } }; } if (fieldParams.validation) { var validationError; var isValid = getOrderedValidationEntries(fieldParams.validation).every(function (_ref6) { var validationType = _ref6[0], _ref6$ = _ref6[1], validator = _ref6$.validator, message = _ref6$.message; var passed = validator(fieldState.value, state); if (!passed) { validationError = { type: validationType, message: message || "" }; } return passed; }); return { isValid: isValid, error: validationError }; } return { isValid: true, error: undefined }; }; // -------------------------------------------------------------------- var DEFAULT_ERROR_DELAY_SECONDS = 0.5; // -------------------------------------------------------------------- var useFormState = function useFormState(formFieldParams, options) { if (options === void 0) { options = {}; } var _options = options, _options$errorUpdateD = _options.errorUpdateDelayInSeconds, errorUpdateDelayInSeconds = _options$errorUpdateD === void 0 ? DEFAULT_ERROR_DELAY_SECONDS : _options$errorUpdateD, _options$reinitialize = _options.reinitializeDependencies, reinitializeDependencies = _options$reinitialize === void 0 ? [] : _options$reinitialize; var _useState = react.useState(function () { return createInitialStateSnapshot(formFieldParams); }), state = _useState[0], setState = _useState[1]; // track timer and an incrementing run id so stale callbacks no-op var validationScheduleRef = react.useRef({ timer: null, runId: 0 }); // keep the validation result with updated errors until the delay expires, then commit it var pendingErrorStateRef = react.useRef(null); var clearPendingValidation = function clearPendingValidation() { validationScheduleRef.current.runId += 1; pendingErrorStateRef.current = null; if (validationScheduleRef.current.timer !== null) { clearTimeout(validationScheduleRef.current.timer); validationScheduleRef.current.timer = null; } }; // -------------------------------------------------------------------- // reinitialize state on dependencies change // this is useful when form state needs to be reset based on some external changes react.useEffect(function () { clearPendingValidation(); setState(createInitialStateSnapshot(formFieldParams)); }, [].concat(reinitializeDependencies)); // clear any pending timers on unmount to avoid stale commits after reset/reinit react.useEffect(function () { return function () { return clearPendingValidation(); }; }, []); // -------------------------------------------------------------------- // validation flow: runs validation immediately, but when delaying errors it stages the new errors and // keeps the previous ones visible until the timer commits, avoiding flicker when users are typing. var scheduleValidationRun = function scheduleValidationRun() { clearPendingValidation(); var runId = validationScheduleRef.current.runId; var shouldDelayErrorUpdate = errorUpdateDelayInSeconds > 0; setState(function (prevState) { var _applyValidation = applyValidation(prevState, formFieldParams), nextState = _applyValidation.nextState; if (!shouldDelayErrorUpdate) { pendingErrorStateRef.current = null; return nextState; } pendingErrorStateRef.current = nextState; var immediateState = Object.keys(nextState).reduce(function (acc, key) { var k = key; acc[k] = _extends({}, nextState[k], { error: prevState[k].error }); return acc; }, {}); return immediateState; }); if (shouldDelayErrorUpdate) { validationScheduleRef.current.timer = setTimeout(function () { if (runId !== validationScheduleRef.current.runId) return; var pending = pendingErrorStateRef.current; if (!pending) return; pendingErrorStateRef.current = null; setState(function () { return _extends({}, pending); }); }, errorUpdateDelayInSeconds * 1000); } }; // -------------------------------------------------------------------- var set = function set(key, value, setInteracted) { if (setInteracted === void 0) { setInteracted = true; } setState(function (prev) { var _extends2; var prevField = prev[key]; var nextField = _extends({}, prevField, { value: value, isInteracted: setInteracted ? true : prevField.isInteracted }); return _extends({}, prev, (_extends2 = {}, _extends2[key] = nextField, _extends2)); }); scheduleValidationRun(); }; // -------------------------------------------------------------------- var setMany = function setMany(data, setInteracted) { if (setInteracted === void 0) { setInteracted = true; } setState(function (prev) { var nextState = _extends({}, prev); Object.entries(data).forEach(function (_ref) { var k = _ref[0], v = _ref[1]; var key = k; var prevField = prev[key]; nextState[key] = _extends({}, prevField, { value: v, isInteracted: setInteracted ? true : prevField.isInteracted }); }); return nextState; }); scheduleValidationRun(); }; // -------------------------------------------------------------------- var checkIfAllValid = function checkIfAllValid(options) { var _ref2 = options || {}, _ref2$commitState = _ref2.commitState, commitState = _ref2$commitState === void 0 ? true : _ref2$commitState; // allow callers to just check validity without disrupting pending error updates or triggering renders if (!commitState) { var _applyValidation2 = applyValidation(state, formFieldParams), _allValid = _applyValidation2.allValid; return _allValid; } clearPendingValidation(); var allValid = true; setState(function (_state) { var _applyValidation3 = applyValidation(_state, formFieldParams, { updateAllFieldsError: true }), nextState = _applyValidation3.nextState, validated = _applyValidation3.allValid; allValid = validated; return nextState; }); return allValid; }; var getValues = function getValues(_ref3) { var format = _ref3.format; switch (format) { case "formdata": var formData = new FormData(); Object.entries(state).forEach(function (_ref4) { var key = _ref4[0], data = _ref4[1]; appendToFormData(formData, key, data.value); }); return formData; case "object": default: var data = {}; for (var key in state) { data[key] = state[key].value; } return data; } }; // -------------------------------------------------------------------- var reset = function reset() { clearPendingValidation(); setState(createInitialStateSnapshot(formFieldParams)); }; // -------------------------------------------------------------------- return { state: state, set: set, setMany: setMany, checkIfAllValid: checkIfAllValid, getValues: getValues, reset: reset }; }; exports.appendToFormData = appendToFormData; exports.applyValidation = applyValidation; exports.checkIfRequiredValueFilled = checkIfRequiredValueFilled; exports.createInitialStateSnapshot = createInitialStateSnapshot; exports.getFieldValidationOutcome = getFieldValidationOutcome; exports.getOrderedValidationEntries = getOrderedValidationEntries; exports.useFormState = useFormState; //# sourceMappingURL=use-form-state.cjs.development.js.map