react-browser-form
Version:
<div align="center"> <a href="https://deniskabana.github.io/react-browser-form/introduction" title="React Browser Form - Form management in React made simple for browsers."> <img src="https://raw.githubusercontent.com/deniskabana/react-browser-form/
896 lines (869 loc) • 36.6 kB
JavaScript
import { useState, useRef, useEffect } from 'react';
// BROWSER FORM HOOK TYPES
// --------------------------------------------------------------------------------
// DATA FLOW
// --------------------------------------------------------------------------------
var EventSource;
(function (EventSource) {
EventSource["User"] = "User";
EventSource["Form"] = "Form";
})(EventSource || (EventSource = {}));
var EventType;
(function (EventType) {
/** Submit event */
EventType["Submit"] = "Submit";
/** Any type of reset or clear action */
EventType["Reset"] = "Reset";
/** `EventType.Change` gets called for all listeners - `onChange`, `onBlur` and `onSubmit` */
EventType["Change"] = "Change";
/** `EventType.Blur` when a form input is blurred (loses focus) */
EventType["Blur"] = "Blur";
/** A special type of event triggered programmatically during init phase. Under the hood it just calls `reset()` without calling `onChange()` or hydrating */
EventType["FormInit"] = "FormInit";
})(EventType || (EventType = {}));
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);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
_setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _construct(Parent, args, Class) {
if (_isNativeReflectConstruct()) {
_construct = Reflect.construct.bind();
} else {
_construct = function _construct(Parent, args, Class) {
var a = [null];
a.push.apply(a, args);
var Constructor = Function.bind.apply(Parent, a);
var instance = new Constructor();
if (Class) _setPrototypeOf(instance, Class.prototype);
return instance;
};
}
return _construct.apply(null, arguments);
}
function _isNativeFunction(fn) {
return Function.toString.call(fn).indexOf("[native code]") !== -1;
}
function _wrapNativeSuper(Class) {
var _cache = typeof Map === "function" ? new Map() : undefined;
_wrapNativeSuper = function _wrapNativeSuper(Class) {
if (Class === null || !_isNativeFunction(Class)) return Class;
if (typeof Class !== "function") {
throw new TypeError("Super expression must either be null or a function");
}
if (typeof _cache !== "undefined") {
if (_cache.has(Class)) return _cache.get(Class);
_cache.set(Class, Wrapper);
}
function Wrapper() {
return _construct(Class, arguments, _getPrototypeOf(this).constructor);
}
Wrapper.prototype = Object.create(Class.prototype, {
constructor: {
value: Wrapper,
enumerable: false,
writable: true,
configurable: true
}
});
return _setPrototypeOf(Wrapper, Class);
};
return _wrapNativeSuper(Class);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (it) return (it = it.call(o)).next.bind(it);
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
/** Default options according to the schema */
var DEFAULT_OPTIONS = {
mode: "onSubmitUnlessError",
revalidationStrategy: "onChange",
liveFields: [],
validateAfterInit: false
};
/** Default validation messages */
var DEFAULT_REQUIRED_ERROR_MESSAGE = "This field is required.";
var DEFAULT_VALIDATION_ERROR_MESSAGE = "This field is incorrect.";
/** Errors returned from options combination protection */
var ERROR_PREFIX = "[react-browser-form]:";
var ERRORS = {
// ERRORS
NAME_INVALID: ERROR_PREFIX + " Option 'name' required!",
MISSING_DEFAULT_VALUES: ERROR_PREFIX + " Option 'defaultValues' required!",
VALIDATION_SCHEMA_REQUIRED: ERROR_PREFIX + " Option 'validationSchema' is required if 'validateAfterInit' is true.",
INCORRECT_VALIDATION_SCHEMA: ERROR_PREFIX + " Incorrect 'validationSchema' structure. Check the documentation.",
// WARNING
ONCHANGE_MODE_ONCHANGE_FN: "'onChange' function is required if using mode: 'onChange'.",
LIVE_FIELDS_ONCHANGE_FN: "'onChange' function is required if using 'liveFields'.",
ONCHANGE_AND_LIVEFIELDS: "When using 'onChange' mode, liveFields should be empty."
};
var InputType;
(function (InputType) {
// Number types
InputType["Number"] = "number";
InputType["Range"] = "range";
// Date types
InputType["Date"] = "date";
InputType["Month"] = "month";
InputType["Week"] = "week";
InputType["Time"] = "time";
InputType["DatetimeLocal"] = "datetime-local";
// Boolean types
InputType["Checkbox"] = "checkbox";
// String types
InputType["Text"] = "text";
InputType["Email"] = "email";
InputType["File"] = "file";
InputType["Password"] = "password";
InputType["Url"] = "url";
InputType["Tel"] = "tel";
InputType["Radio"] = "radio";
})(InputType || (InputType = {}));
/** Transform values types. */
function transformValueType(name, value, dataFlowState) {
var transformationSchema = dataFlowState.options.transformationSchema;
var definitiveValue = value;
// 1. Default data transformation (built-in, input[type] based)
if (!(transformationSchema != null && transformationSchema.disableDefaultTransformation)) {
var _domInputElem$type;
// 1.1. Get input's data type if specified
var inputType;
var domFormElem = document.forms[dataFlowState.options.name];
var domInputElem = domFormElem.elements[name];
inputType = (_domInputElem$type = domInputElem == null ? void 0 : domInputElem.type) != null ? _domInputElem$type : "text"; // Default to texts like browsers do
// Explicitly let the user pass any form of data that are not tied to inputs
if (!domInputElem) inputType = null;
// 1.2. Automatically return null values
if (value === null || value === undefined) {
definitiveValue = null;
} else {
// 1.3. Determine and return the input value with the correct type
// NOTE: Purposefully ignore valueAsNumber - just in case the change is NOT coming from the input
switch (inputType) {
case InputType.Number:
case InputType.Range:
if (value === "") {
definitiveValue = null;
} else {
definitiveValue = Number(value); // Don't implicitly handle NaN
}
break;
case InputType.Date:
case InputType.Month:
case InputType.DatetimeLocal:
case InputType.Week:
case InputType.Time:
if (value instanceof Date) {
definitiveValue = value;
} else if (typeof value === "string") {
definitiveValue = new Date(value);
} else {
definitiveValue = null; // Implicitly return null rather than a code-breaking value
}
break;
case InputType.Checkbox:
if (typeof value === "boolean") {
definitiveValue = value;
} else {
definitiveValue = Boolean(value);
}
break;
case InputType.Text:
case InputType.Email:
case InputType.File:
case InputType.Password:
case InputType.Url:
case InputType.Tel:
case InputType.Radio:
if (typeof value === "string") {
definitiveValue = value;
} else {
definitiveValue = String(value);
}
break;
default:
definitiveValue = value;
}
}
}
// 2. Execute user provided transformations
if (transformationSchema) {
if (transformationSchema.fields) {
var transformator = transformationSchema.fields[name];
switch (typeof transformator) {
case "string":
if (transformator === "string") {
definitiveValue = String(definitiveValue);
} else if (transformator === "number") {
definitiveValue = Number(definitiveValue);
} else if (transformator === "boolean") {
definitiveValue = Boolean(definitiveValue);
}
break;
case "function":
definitiveValue = transformator(definitiveValue, dataFlowState.formState);
break;
}
}
}
return definitiveValue;
}
function getDomInputValue(dataFlowState) {
var _dataFlowState$event$;
var targetInput = (_dataFlowState$event$ = dataFlowState.event.nativeEvent) == null ? void 0 : _dataFlowState$event$.target;
if (!targetInput || !targetInput.name) {
return;
}
if (!dataFlowState.fieldsData.names[targetInput.name]) {
return;
}
var value = targetInput.type === "checkbox" ? targetInput.checked : targetInput.value;
return transformValueType(targetInput.name, value, dataFlowState);
}
function hydrateFormState(dataFlowState, subset) {
// Explicit "any" because of TS issue - https://github.com/microsoft/TypeScript/issues/19437
var domFormElem = document.forms[dataFlowState.options.name];
// Use defaultValues instead of formState when hydrating formState
Object.keys(dataFlowState.options.defaultValues).forEach(function (key) {
// If a subset is provided, only update those values
if (subset && subset.length && !subset.includes(key)) return;
var domInputElem = domFormElem.elements[key];
if (domInputElem) {
var value = domInputElem.type === "checkbox" ? domInputElem.checked : domInputElem.value;
dataFlowState.formState[key] = transformValueType(key, value, dataFlowState);
}
});
}
/** Error to throw when form field validation fails */
var ValidationError = /*#__PURE__*/function (_Error) {
_inheritsLoose(ValidationError, _Error);
function ValidationError(message) {
var _this;
_this = _Error.call(this, message != null ? message : DEFAULT_VALIDATION_ERROR_MESSAGE) || this;
Object.setPrototypeOf(_assertThisInitialized(_this), ValidationError.prototype);
return _this;
}
return ValidationError;
}( /*#__PURE__*/_wrapNativeSuper(Error));
/** Validate **changed** data only. */
function validateFormState(dataFlowState) {
if (!dataFlowState.options.validationSchema) return;
var changedData = dataFlowState.changedData,
formState = dataFlowState.formState,
_dataFlowState$fields = dataFlowState.fieldsData,
required = _dataFlowState$fields.required,
validated = _dataFlowState$fields.validated;
var data = changedData != null ? changedData : formState;
var oldErrors = _extends({}, dataFlowState.errorData.errors);
var newErrors = {};
for (var key in data) {
// Clear this from the old errors
delete oldErrors[key];
// Check required
if (required.includes(key)) {
var newValue = data[key];
if (newValue === null || newValue === undefined || newValue === false || String(newValue).length === 0) {
var _dataFlowState$option, _dataFlowState$option2, _dataFlowState$option3, _dataFlowState$option4;
newErrors[key] = (_dataFlowState$option = (_dataFlowState$option2 = dataFlowState.options) == null ? void 0 : (_dataFlowState$option3 = _dataFlowState$option2.validationSchema) == null ? void 0 : (_dataFlowState$option4 = _dataFlowState$option3.required) == null ? void 0 : _dataFlowState$option4.message) != null ? _dataFlowState$option : DEFAULT_REQUIRED_ERROR_MESSAGE;
dataFlowState.hasErrors = true;
}
}
// Check validated
if (validated.includes(key)) {
var _dataFlowState$option5, _dataFlowState$option6, _dataFlowState$option7, _dataFlowState$option8;
var validator = (_dataFlowState$option5 = dataFlowState.options) != null && (_dataFlowState$option6 = _dataFlowState$option5.validationSchema) != null && _dataFlowState$option6.validators ? (_dataFlowState$option7 = dataFlowState.options) == null ? void 0 : (_dataFlowState$option8 = _dataFlowState$option7.validationSchema) == null ? void 0 : _dataFlowState$option8.validators[key] : null;
if (!validator) break;
try {
if (typeof validator === "function") {
validator(data[key], _extends({}, formState, data));
} else if (Array.isArray(validator)) {
for (var _iterator = _createForOfIteratorHelperLoose(validator), _step; !(_step = _iterator()).done;) {
var validatorFn = _step.value;
validatorFn(data[key], _extends({}, formState, data));
}
} else {
throw new Error("react-browser-form: Invalid validators provided!");
}
} catch (error) {
var _error$message;
if (!(error instanceof ValidationError)) throw error;
newErrors[key] = (_error$message = error == null ? void 0 : error.message) != null ? _error$message : DEFAULT_VALIDATION_ERROR_MESSAGE;
dataFlowState.hasErrors = true;
}
}
}
dataFlowState.setErrors(_extends({}, oldErrors, newErrors));
}
function handleBlurEvent(dataFlowState) {
var _dataFlowState$event$;
var options = dataFlowState.options;
var targetInput = (_dataFlowState$event$ = dataFlowState.event.nativeEvent) == null ? void 0 : _dataFlowState$event$.target;
if (!targetInput || !targetInput.name) return;
// 1. Conditional execution and revalidation
var hasOnBlurMode = options.mode === "onBlur" || options.mode === "onBlurUnlessError";
var shouldRevalidate = dataFlowState.errorData.errors[targetInput.name] && dataFlowState.options.revalidationStrategy === "onBlur" && (options.mode === "onBlurUnlessError" || options.mode === "onSubmitUnlessError");
var shouldExecute = hasOnBlurMode || shouldRevalidate;
if (!shouldExecute) return;
// 2. Hydrate form state from DOM inputs
hydrateFormState(dataFlowState, [targetInput.name]);
// 3. Populate changedData with the single input value that has changed
var value = getDomInputValue(dataFlowState);
dataFlowState.changedData[targetInput.name] = value;
// 4. Validate form state (validates only changedData)
validateFormState(dataFlowState);
// DEBUG: Feedback for changeReason
if (hasOnBlurMode) dataFlowState.changeReason = "Blur form - " + options.mode + " mode.\nSource: " + dataFlowState.event.source;
if (shouldRevalidate) dataFlowState.changeReason = "Blur form - error revalidation.\nSource: " + dataFlowState.event.source;
// 5. Trigger callback
if (hasOnBlurMode) {
dataFlowState.callbacks.onChange(dataFlowState.formState);
}
}
function hydrateDomInputs(options, formState) {
// Explicit "any" because of TS issue - https://github.com/microsoft/TypeScript/issues/19437
var domFormElem = document.forms[options.name];
for (var key in formState) {
var domInputElem = domFormElem.elements[key];
if (domInputElem) {
if (domInputElem.type === "checkbox") {
domInputElem.checked = Boolean(formState[key]);
} else {
if (formState[key] === null) {
domInputElem.value = "";
break;
}
domInputElem.value = String(formState[key]);
}
}
}
}
function handleChangeEvent(dataFlowState) {
var options = dataFlowState.options;
// 1. USER CHANGE EVENT
// --------------------------------------------------------------------------------
if (dataFlowState.event.source === EventSource.User) {
if (dataFlowState.event.type !== EventType.FormInit) {
// DEBUG: Feedback for changeReason
dataFlowState.changeReason = "Change form values programatically.\nSource: " + dataFlowState.event.source;
}
var eventValue = dataFlowState.event.value;
if (!eventValue) return;
// 1.1. Set form to dirty and update dirtyFields
if (!dataFlowState.isDirty) dataFlowState.setIsDirty(true);
dataFlowState.setDirtyFields(Object.keys(eventValue));
// 1.2. Populate changedData, quit if no value was provided
dataFlowState.changedData = eventValue;
// 1.3. Validate form state (validates only changedData)
validateFormState(dataFlowState);
// 1.4. Populate formState with transformed data
// TODO: Figure out a better way to do transformations
for (var key in dataFlowState.changedData) {
dataFlowState.formState[key] = transformValueType(key, dataFlowState.changedData[key], dataFlowState);
}
// 1.5. Hydrate DOM inputs from form state
hydrateDomInputs(options, dataFlowState.formState);
// 1.6. Trigger callback
dataFlowState.callbacks.onChange(_extends({}, dataFlowState.formState));
}
// 2. FORM CHANGE EVENT
// --------------------------------------------------------------------------------
if (dataFlowState.event.source === EventSource.Form) {
var _dataFlowState$event$;
var targetInput = (_dataFlowState$event$ = dataFlowState.event.nativeEvent) == null ? void 0 : _dataFlowState$event$.target;
var fieldName = targetInput == null ? void 0 : targetInput.name;
if (!targetInput || !fieldName) return;
// 2.1. Set form to dirty and update dirtyFields
if (!dataFlowState.isDirty && targetInput && fieldName) dataFlowState.setIsDirty(true);
dataFlowState.setDirtyFields([fieldName]);
// 2.2. Conditional execution and revalidation
var hasOnChangeMode = options.mode === "onChange";
var isLiveField = options.liveFields.includes(fieldName);
var shouldRevalidate = dataFlowState.errorData.errors[fieldName] && options.revalidationStrategy === "onChange" && (options.mode === "onBlurUnlessError" || options.mode === "onSubmitUnlessError");
var shouldExecute = hasOnChangeMode || isLiveField || shouldRevalidate;
if (!shouldExecute) return;
// 2.3. Hydrate form state from DOM inputs
hydrateFormState(dataFlowState, [fieldName]);
// 2.4. Populate changedData with the single input value that has changed
var value = getDomInputValue(dataFlowState);
dataFlowState.changedData[fieldName] = value;
// 2.5. Populate formState with transformed data
// TODO: Figure out a better way to do transformations
dataFlowState.formState[fieldName] = transformValueType(fieldName, value, dataFlowState);
// 2.6. If live field changed, populate changedData with all errored fields for revalidation - live fields are often conditional / dependent
if (isLiveField) {
for (var _key in dataFlowState.errorData.errors) {
dataFlowState.changedData[_key] = dataFlowState.formState[_key];
}
}
// 2.7. Validate form state (validates only changedData)
validateFormState(dataFlowState);
// DEBUG: Feedback for changeReason
if (hasOnChangeMode) dataFlowState.changeReason = "Changed form state - onChange mode.\nSource: " + dataFlowState.event.source;
if (shouldRevalidate) dataFlowState.changeReason = "Changed form state - error revalidation.\nSource: " + dataFlowState.event.source;
if (isLiveField) dataFlowState.changeReason = "Changed form state - live field.\nSource: " + dataFlowState.event.source;
// 2.8. Trigger callback
if (hasOnChangeMode || isLiveField) {
dataFlowState.callbacks.onChange(dataFlowState.formState);
}
}
}
function handleResetEvent(dataFlowState) {
var shouldResetWithValues = !!dataFlowState.event.value;
// 1. Populate changedData with values if provided, with defaultValues otherwise
if (shouldResetWithValues) {
dataFlowState.changedData = _extends({}, dataFlowState.event.value);
// DEBUG: Feedback for changeReason
dataFlowState.changeReason = "Reset form with provided values.\nSource: " + dataFlowState.event.source;
} else {
dataFlowState.changedData = _extends({}, dataFlowState.options.defaultValues);
// DEBUG: Feedback for changeReason
dataFlowState.changeReason = "Reset form to defaults.\nSource: " + dataFlowState.event.source;
}
// 2. Populate formState with transformed data, IF NOT A FORM INIT EVENT
if (dataFlowState.event.type !== EventType.FormInit) {
// Reset dirty fields and isDirty
dataFlowState.resetDirtyState();
for (var key in dataFlowState.changedData) {
dataFlowState.formState[key] = transformValueType(key, dataFlowState.changedData[key], dataFlowState);
}
} else {
// DEBUG: Provide no change reason if initializing the form
dataFlowState.changeReason = "";
}
// 3. Hydrate DOM inputs from form state
hydrateDomInputs(dataFlowState.options, dataFlowState.formState);
// 4. Validate form state if not initializing or requested upon initialization (validates only changedData)
if (dataFlowState.event.type !== EventType.FormInit || dataFlowState.options.validateAfterInit) {
validateFormState(dataFlowState);
}
// 5. Trigger callback
if (dataFlowState.event.type !== EventType.FormInit) {
dataFlowState.callbacks.onChange(dataFlowState.formState);
}
}
var DEBUG_CHANGE_EVENT = "rdf_debug_change";
/**
* Set debug data - useful for docs, testing, debugging and understanding the data flow
* **NEVER USE THIS FOR ANYTHING ELSE.**
*/
function setDebugData(data, options, shouldDispatch) {
if (shouldDispatch === void 0) {
shouldDispatch = false;
}
if (!options.debug || typeof window === "undefined") return;
var rdfDebugChangeEvent = new CustomEvent(DEBUG_CHANGE_EVENT, {
detail: options.name
});
// Set up a new debug object if it doesn't exist yet
if (!window.__rdf_debug) window.__rdf_debug = {};
if (!window.__rdf_debug[options.name]) window.__rdf_debug[options.name] = {};
// Add or replace fields on the debug object
// This object might be replaced with a Proxy, do not overwrite it
for (var key in data) {
window.__rdf_debug[options.name][key] = data[key];
}
if (shouldDispatch) {
// Defer execution
setTimeout(function () {
return document.dispatchEvent(rdfDebugChangeEvent);
});
}
}
function handleSubmitEvent(dataFlowState) {
// DEBUG: Feedback for changeReason
dataFlowState.changeReason = "Form submitted.\nSource: " + dataFlowState.event.source;
// 1. Hydrate form state from DOM inputs
hydrateFormState(dataFlowState);
// 2. Populate changedData
dataFlowState.changedData = dataFlowState.formState;
// 3. Validate form state (validates only changedData)
validateFormState(dataFlowState);
// 4. Trigger callback if there are no errors
if (!dataFlowState.hasErrors) {
dataFlowState.callbacks.onSubmit(dataFlowState.formState);
// DEBUG: Feedback from submit event
setDebugData({
isSubmitted: true
}, dataFlowState.options);
}
}
function generateMessage(phase, message, severity) {
if (severity === void 0) {
severity = "error";
}
return ERROR_PREFIX + " " + severity + " during " + phase + ": " + message;
}
function logError(phase, message, severity) {
if (severity === void 0) {
severity = "error";
}
if (severity === "error") {
console.error(generateMessage(phase, message, severity));
} else if (severity === "warning") {
console.warn(generateMessage(phase, message, severity));
}
}
// TODO: Consider changing dataFlowState structure to object of getters/setters for cleaner code if no performance impact
function useDataFlowHandler(options, formState, fieldsData, callbacks, errorData, setErrors, isDirty, setIsDirty, setDirtyFields, resetDirtyState) {
return function handleDataFlow(event) {
// An object reference to be passed around to all data flow functions
var dataFlowState = {
hasErrors: errorData.count > 0,
event: event,
options: options,
changedData: {},
formState: formState,
fieldsData: fieldsData,
callbacks: callbacks,
errorData: errorData,
setErrors: setErrors,
isDirty: isDirty,
setIsDirty: setIsDirty,
setDirtyFields: setDirtyFields,
resetDirtyState: resetDirtyState,
changeReason: ""
};
switch (event.type) {
case EventType.Submit:
handleSubmitEvent(dataFlowState);
break;
case EventType.Reset:
handleResetEvent(dataFlowState);
break;
case EventType.Change:
handleChangeEvent(dataFlowState);
break;
case EventType.Blur:
handleBlurEvent(dataFlowState);
break;
case EventType.FormInit:
handleResetEvent(dataFlowState);
break;
default:
logError("data-flow", "An unsupported event type provided");
}
setDebugData({
formState: dataFlowState.formState,
changeReason: dataFlowState.changeReason,
event: dataFlowState.event
}, options, true);
};
}
function useErrorManager() {
var _useState = useState({
count: 0,
errors: {}
}),
stateErrors = _useState[0],
stateSetErrors = _useState[1];
var generateErrorsObject = function generateErrorsObject(errors) {
return {
count: Object.keys(errors).length,
errors: errors
};
};
// RETURNED METHODS
// --------------------------------------------------------------------------------
var setErrors = function setErrors(errors) {
stateSetErrors(generateErrorsObject(errors));
};
return {
errorData: stateErrors,
setErrors: setErrors
};
}
function useFormEventHandlers(handleDataFlow) {
var _ref;
// USER EVENTS
// --------------------------------------------------------------------------------
var handleUserSetValues = function handleUserSetValues(value) {
handleDataFlow({
source: EventSource.User,
type: EventType.Change,
value: value
});
};
var handleUserSubmit = function handleUserSubmit() {
handleDataFlow({
source: EventSource.User,
type: EventType.Submit
});
};
var handleUserReset = function handleUserReset(value) {
handleDataFlow({
source: EventSource.User,
type: EventType.Reset,
value: value
});
};
// DOM FORM EVENTS
// --------------------------------------------------------------------------------
var handleFormChange = function handleFormChange(event) {
handleDataFlow({
source: EventSource.Form,
type: EventType.Change,
nativeEvent: event
});
};
var handleFormSubmit = function handleFormSubmit(event) {
event.preventDefault();
handleDataFlow({
source: EventSource.Form,
type: EventType.Submit,
nativeEvent: event
});
};
var handleFormBlur = function handleFormBlur(event) {
handleDataFlow({
source: EventSource.Form,
type: EventType.Blur,
nativeEvent: event
});
};
var handleFormReset = function handleFormReset(event) {
// A deliberately unsupported event! Stop exeuction.
event.preventDefault();
handleDataFlow({
source: EventSource.Form,
type: EventType.Reset,
nativeEvent: event
});
};
return _ref = {}, _ref[EventSource.User] = {
setValues: handleUserSetValues,
submit: handleUserSubmit,
reset: handleUserReset
}, _ref[EventSource.Form] = {
onChange: handleFormChange,
onSubmit: handleFormSubmit,
onBlur: handleFormBlur,
onReset: handleFormReset
}, _ref;
}
function getFieldsData(options) {
var _options$validationSc, _options$validationSc2, _options$validationSc3, _options$validationSc4, _options$validationSc5;
return {
// Names of fields, enum-like
names: Object.keys(options.defaultValues).reduce(function (names, key) {
var _extends2;
return _extends({}, names, (_extends2 = {}, _extends2[key] = key, _extends2));
}, {}),
// Array of fields that are tagged required
required: (_options$validationSc = (_options$validationSc2 = options.validationSchema) == null ? void 0 : (_options$validationSc3 = _options$validationSc2.required) == null ? void 0 : _options$validationSc3.fields) != null ? _options$validationSc : [],
// Array of fields that are being validated
validated: Object.keys((_options$validationSc4 = (_options$validationSc5 = options.validationSchema) == null ? void 0 : _options$validationSc5.validators) != null ? _options$validationSc4 : {})
};
}
/** TypeScript will try to catch these errors build-time, but some users might still use the any type to override our constraints. */
function protectOptionsCominations(options) {
var name = options.name,
defaultValues = options.defaultValues,
onChange = options.onChange,
validationSchema = options.validationSchema,
validateAfterInit = options.validateAfterInit,
mode = options.mode,
liveFields = options.liveFields;
// ERRORS - prevent further execution to prevent bugs
// --------------------------------------------------------------------------------
// Missing or invalid name
if (!name || name.length < 1) throw new Error(ERRORS.NAME_INVALID);
// Missing default values
if (!defaultValues) throw new Error(ERRORS.MISSING_DEFAULT_VALUES);
// Missing validationSchema if validateAfterInit is used
if (validateAfterInit && !validationSchema) throw new Error(ERRORS.VALIDATION_SCHEMA_REQUIRED);
// TODO: Add more options to take 3rd party validators into account
// Verify validation schema if provided
if (validationSchema) {
var validationKeys = Object.keys(validationSchema);
// Verify structure - we always want 1 or 2 entries
if (validationKeys.length === 0 || validationKeys.length > 2) throw new Error(ERRORS.INCORRECT_VALIDATION_SCHEMA);
// TODO: Add more validations to make sure everything is provided - only ifs
}
// WARNINGS - should not stop exeuction in production environment
// --------------------------------------------------------------------------------
if (process.env.NODE_ENV === "production") return;
// Warn if onChange mode is used without an onChange function
if (mode === "onChange" && typeof onChange !== "function") logError("init", ERRORS.ONCHANGE_MODE_ONCHANGE_FN, "warning");
// Warn if liveFields are used without an onChange function
if (liveFields && liveFields.length > 0 && typeof onChange !== "function") logError("init", ERRORS.LIVE_FIELDS_ONCHANGE_FN, "warning");
// Warn not to use onChange and liveFields together
if (mode === "onChange" && liveFields && liveFields.length > 0) logError("init", ERRORS.ONCHANGE_AND_LIVEFIELDS, "warning");
}
function uniqueNameProtection(options) {
var _document$querySelect;
if (((_document$querySelect = document.querySelectorAll("form[name=\"" + options.name + "\"]")) == null ? void 0 : _document$querySelect.length) > 1) {
logError("init", "Form name \"" + options.name + "\" is not unique! This can lead to critical bugs!");
}
}
function useDirtyFieldsManager() {
var _useState = useState([]),
stateDirtyFields = _useState[0],
stateSetDirtyFields = _useState[1];
var _useState2 = useState(false),
isDirty = _useState2[0],
setIsDirty = _useState2[1];
var setDirtyFields = function setDirtyFields(fields) {
stateSetDirtyFields(function (previousState) {
return [].concat(previousState, fields.filter(function (fieldName) {
return !stateDirtyFields.includes(fieldName);
}));
});
setIsDirty(true);
};
var resetDirtyState = function resetDirtyState() {
stateSetDirtyFields([]);
setIsDirty(false);
};
return {
dirtyFields: stateDirtyFields,
resetDirtyState: resetDirtyState,
setDirtyFields: setDirtyFields,
isDirty: isDirty,
setIsDirty: setIsDirty
};
}
/**
* **React Browser Form** - React form state management written in TypeScript with performance and developer experience in mind.
* Flexible and with built-in validation.
* - [GitHub](https://github.com/deniskabana/react-browser-form)
* - [Documentation](https://deniskabana.github.io/react-browser-form/)
* - [Examples](https://deniskabana.github.io/react-browser-form/examples)
*/
function useBrowserForm(userOptions) {
protectOptionsCominations(userOptions);
// INTERNAL STATE AND CONFIG
// --------------------------------------------------------------------------------
var options = useRef(_extends({}, DEFAULT_OPTIONS, userOptions)).current;
var formState = useRef(_extends({}, options.defaultValues)).current;
// Errors are stateful to trigger React's built-in re-rendering of DOM in children with new data
var _useErrorManager = useErrorManager(),
errorData = _useErrorManager.errorData,
setErrors = _useErrorManager.setErrors;
var _useDirtyFieldsManage = useDirtyFieldsManager(),
isDirty = _useDirtyFieldsManage.isDirty,
setIsDirty = _useDirtyFieldsManage.setIsDirty,
dirtyFields = _useDirtyFieldsManage.dirtyFields,
setDirtyFields = _useDirtyFieldsManage.setDirtyFields,
resetDirtyState = _useDirtyFieldsManage.resetDirtyState;
// STORED REFERENCES
// --------------------------------------------------------------------------------
var fieldsData = useRef(Object.freeze(getFieldsData(options))).current;
var callbacks = useRef({
onChange: function onChange(data) {
return (options == null ? void 0 : options.onChange) && options.onChange(_extends({}, data));
},
onSubmit: function onSubmit(data) {
return (options == null ? void 0 : options.onSubmit) && options.onSubmit(_extends({}, data));
}
}).current;
// INTERNAL FUNCTIONS
// --------------------------------------------------------------------------------
var handleDataFlow = useDataFlowHandler(options, formState, fieldsData, callbacks, errorData, setErrors, isDirty, setIsDirty, setDirtyFields, resetDirtyState);
var formEventHandlers = useFormEventHandlers(handleDataFlow);
// INITIALIZATION
// --------------------------------------------------------------------------------
useEffect(function () {
uniqueNameProtection(options);
handleDataFlow({
source: EventSource.User,
type: EventType.FormInit,
value: options.defaultValues
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// FORM COMPONENT PROPS
// --------------------------------------------------------------------------------
var formProps = _extends({}, formEventHandlers[EventSource.Form], {
name: options.name
});
// RETURN
// --------------------------------------------------------------------------------
var returnData = _extends({
// Values
errorData: errorData,
isDirty: isDirty,
dirtyFields: dirtyFields,
names: fieldsData.names
}, formEventHandlers[EventSource.User], {
// Form props
formProps: formProps
});
setDebugData({
returnData: returnData,
formState: formState
}, options, true);
return returnData;
}
export { DEBUG_CHANGE_EVENT, EventSource, EventType, ValidationError, useBrowserForm };
//# sourceMappingURL=react-browser-form.esm.js.map