UNPKG

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
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