@andydowell/use-form-state
Version:
A React hook for managing form state and validation
426 lines (419 loc) • 14.9 kB
JavaScript
;
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