UNPKG

formsy-react

Version:

A form input builder and validator for React

594 lines (593 loc) 25.1 kB
"use strict"; var __webpack_require__ = {}; (()=>{ __webpack_require__.n = (module)=>{ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module; __webpack_require__.d(getter, { a: getter }); return getter; }; })(); (()=>{ __webpack_require__.d = (exports1, definition)=>{ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, { enumerable: true, get: definition[key] }); }; })(); (()=>{ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop); })(); (()=>{ __webpack_require__.r = (exports1)=>{ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, { value: 'Module' }); Object.defineProperty(exports1, '__esModule', { value: true }); }; })(); var __webpack_exports__ = {}; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { addValidationRule: ()=>addValidationRule, default: ()=>src, withFormsy: ()=>withFormsy, validationRules: ()=>validationRules_validationRules }); const isPlainObject_js_namespaceObject = require("lodash/isPlainObject.js"); var isPlainObject_js_default = /*#__PURE__*/ __webpack_require__.n(isPlainObject_js_namespaceObject); function isArray(value) { return Array.isArray(value); } function isObject(value) { return isPlainObject_js_default()(value); } function isTypeUndefined(value) { return void 0 === value; } function isDate(value) { return value instanceof Date; } function isFunction(value) { return null !== value && 'function' == typeof value; } function isString(value) { return 'string' == typeof value; } function isNumber(value) { return 'number' == typeof value; } function isRegex(value) { return value instanceof RegExp; } function isValueStringEmpty(value) { return '' === value; } function isValueNullOrUndefined(value) { return null == value; } function isValueUndefined(value) { return void 0 === value; } function protectAgainstParamReassignment(value) { if (isObject(value)) return { ...value }; if (isArray(value)) return [ ...value ]; return value; } function isSame(a, b) { if (typeof a !== typeof b) return false; if (isArray(a) && isArray(b)) { if (a.length !== b.length) return false; return a.every((item, index)=>isSame(item, b[index])); } if (isFunction(a) && isFunction(b)) return a.toString() === b.toString(); if (isDate(a) && isDate(b)) return a.toString() === b.toString(); if (isObject(a) && isObject(b)) { if (Object.keys(a).length !== Object.keys(b).length) return false; return Object.keys(a).every((key)=>isSame(a[key], b[key])); } if (isRegex(a) && isRegex(b)) return a.toString() === b.toString(); return a === b; } function runRules(value, currentValues, validations, validationRules) { const results = { errors: [], failed: [], success: [] }; Object.keys(validations).forEach((validationName)=>{ const validationsVal = validations[validationName]; const validationRulesVal = validationRules[validationName]; const addToResults = (validation)=>{ if (isString(validation)) { results.errors.push(validation); results.failed.push(validationName); } else if (validation) results.success.push(validationName); else results.failed.push(validationName); }; if (validationRulesVal && isFunction(validationsVal)) throw new Error(`Formsy does not allow you to override default validations: ${validationName}`); if (!validationRulesVal && !isFunction(validationsVal)) throw new Error(`Formsy does not have the validation rule: ${validationName}`); if (isFunction(validationsVal)) return addToResults(validationsVal(currentValues, value)); return addToResults(validationRulesVal(currentValues, value, validationsVal)); }); return results; } function debounce(callback, timeout) { let timer; return (...args)=>{ clearTimeout(timer); timer = setTimeout(()=>{ callback.apply(this, args); }, timeout); }; } function isExisty(value) { return !isValueNullOrUndefined(value); } function isEmpty(value) { if (isString(value)) return isValueStringEmpty(value); if (isTypeUndefined(value)) return false; return isValueUndefined(value); } function isDefaultRequiredValue(value) { return isString(value) ? isValueStringEmpty(value) : isValueNullOrUndefined(value); } function matchRegexp(_values, value, regexp) { return !isExisty(value) || isEmpty(value) || regexp.test(`${value}`); } const REGEX_PATTERNS = { ALPHA: /^[A-Z]+$/i, ALPHANUMERIC: /^[0-9A-Z]+$/i, EMAIL: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i, FLOAT: /^(?:[-+]?(?:\d+))?(?:\.\d*)?(?:[eE][+-]?(?:\d+))?$/, INT: /^(?:[-+]?(?:0|[1-9]\d*))$/, NUMERIC: /^[-+]?(?:\d*[.])?\d+$/, SPECIAL_WORDS: /^[\sA-ZÀ-ÖØ-öø-ÿ]+$/i, URL: /^(?:\w+:)?\/\/([^\s.]+\.\S{2}|localhost[:?\d]*)\S*$/i, WORDS: /^[A-Z\s]+$/i }; const validationRules_validationRules = { equals: (_values, value, eql)=>!isExisty(value) || isEmpty(value) || value === eql, equalsField: (values, value, field)=>value === values[field], isAlpha: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.ALPHA), isAlphanumeric: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.ALPHANUMERIC), isDefaultRequiredValue: (_values, value)=>isDefaultRequiredValue(value), isEmail: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.EMAIL), isEmptyString: (_values, value)=>isEmpty(value), isExisty: (_values, value)=>isExisty(value), isFalse: (_values, value)=>false === value, isFloat: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.FLOAT), isInt: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.INT), isLength: (_values, value, length)=>!isExisty(value) || isEmpty(value) || value.length === length, isNumeric: (values, value)=>isNumber(value) || matchRegexp(values, value, REGEX_PATTERNS.NUMERIC), isSpecialWords: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.SPECIAL_WORDS), isTrue: (_values, value)=>true === value, isUndefined: (_values, value)=>isValueUndefined(value), isUrl: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.URL), isWords: (values, value)=>matchRegexp(values, value, REGEX_PATTERNS.WORDS), matchRegexp, maxLength: (_values, value, length)=>!isExisty(value) || value.length <= length, minLength: (_values, value, length)=>!isExisty(value) || isEmpty(value) || value.length >= length }; const addValidationRule = (name, func)=>{ validationRules_validationRules[name] = func; }; const external_react_namespaceObject = require("react"); var external_react_default = /*#__PURE__*/ __webpack_require__.n(external_react_namespaceObject); const noFormsyErrorMessage = 'Could not find Formsy Context Provider. Did you use withFormsy outside <Formsy />?'; const throwNoFormsyProvider = ()=>{ throw new Error(noFormsyErrorMessage); }; const defaultValue = { attachToForm: throwNoFormsyProvider, detachFromForm: throwNoFormsyProvider, isFormDisabled: true, isValidValue: throwNoFormsyProvider, validate: throwNoFormsyProvider, runValidation: throwNoFormsyProvider }; const FormsyContext = external_react_default().createContext(defaultValue); const convertValidationsToObject = (validations)=>{ if (isString(validations)) return validations.split(/,(?![^{[]*[}\]])/g).reduce((validationsAccumulator, validation)=>{ let args = validation.split(':'); const validateMethod = args.shift() || ''; args = args.map((arg)=>{ try { return JSON.parse(arg); } catch (_e) { return arg; } }); if (args.length > 1) throw new Error('Formsy does not support multiple args on string validations. Use object format of validations instead.'); const validationsAccumulatorCopy = { ...validationsAccumulator }; validationsAccumulatorCopy[validateMethod] = args.length ? args[0] : true; return validationsAccumulatorCopy; }, {}); return validations || {}; }; function getDisplayName(component) { return component.displayName || component.name || (isString(component) ? component : 'Component'); } function withFormsy(WrappedComponent) { class WithFormsyWrapper extends external_react_default().Component { validations; requiredValidations; static displayName = `Formsy(${getDisplayName(WrappedComponent)})`; constructor(props){ super(props); const { runValidation, validations, required, value = WrappedComponent.defaultValue } = props; this.state = { value }; this.setValidations(validations, required); this.state = { formSubmitted: false, isPristine: true, pristineValue: value, value: value, ...runValidation(this, value) }; } componentDidMount() { const { name, attachToForm } = this.props; if (!name) throw new Error('Form Input requires a name property when used'); attachToForm(this); } shouldComponentUpdate(nextProps, nextState) { const { props, state } = this; const isChanged = (a, b)=>Object.keys(a).some((k)=>a[k] !== b[k]); const isPropsChanged = isChanged(props, nextProps); const isStateChanged = isChanged(state, nextState); return isPropsChanged || isStateChanged; } componentDidUpdate(prevProps) { const { value, validations, required, validate } = this.props; if (!isSame(value, prevProps.value)) this.setValue(value); if (!isSame(validations, prevProps.validations) || !isSame(required, prevProps.required)) { this.setValidations(validations, required); validate(this); } } componentWillUnmount() { const { detachFromForm } = this.props; detachFromForm(this); } getErrorMessage = ()=>{ const messages = this.getErrorMessages(); return messages.length ? messages[0] : null; }; getErrorMessages = ()=>{ const { validationError } = this.state; if (!this.isValid() || this.showRequired()) return validationError || []; return []; }; getValue = ()=>this.state.value; setValidations = (validations, required)=>{ this.validations = convertValidationsToObject(validations) || {}; this.requiredValidations = true === required ? { isDefaultRequiredValue: required } : convertValidationsToObject(required); }; setValue = (value, validate = true)=>{ const { validate: validateForm } = this.props; if (validate) this.setState({ value, isPristine: false }, ()=>{ validateForm(this); }); else this.setState({ value }); }; hasValue = ()=>{ const { value } = this.state; return isDefaultRequiredValue(value); }; isFormDisabled = ()=>this.props.isFormDisabled; isFormSubmitted = ()=>this.state.formSubmitted; isPristine = ()=>this.state.isPristine; isRequired = ()=>!!this.props.required; isValid = ()=>this.state.isValid; isValidValue = (value)=>this.props.isValidValue(this, value); resetValue = ()=>{ const { pristineValue } = this.state; const { validate } = this.props; this.setState({ value: pristineValue, isPristine: true }, ()=>{ validate(this); }); }; showError = ()=>!this.showRequired() && !this.isValid(); showRequired = ()=>this.state.isRequired; render() { const { innerRef } = this.props; const propsForElement = { ...this.props, errorMessage: this.getErrorMessage(), errorMessages: this.getErrorMessages(), hasValue: this.hasValue(), isFormDisabled: this.isFormDisabled(), isFormSubmitted: this.isFormSubmitted(), isPristine: this.isPristine(), isRequired: this.isRequired(), isValid: this.isValid(), isValidValue: this.isValidValue, resetValue: this.resetValue, setValidations: this.setValidations, setValue: this.setValue, showError: this.showError(), showRequired: this.showRequired(), value: this.getValue() }; if (innerRef) propsForElement.ref = innerRef; return external_react_default().createElement(WrappedComponent, propsForElement); } } return (props)=>external_react_default().createElement(FormsyContext.Consumer, null, (contextValue)=>external_react_default().createElement(WithFormsyWrapper, { ...props, ...contextValue })); } const set_js_namespaceObject = require("lodash/set.js"); var set_js_default = /*#__PURE__*/ __webpack_require__.n(set_js_namespaceObject); const has_js_namespaceObject = require("lodash/has.js"); var has_js_default = /*#__PURE__*/ __webpack_require__.n(has_js_namespaceObject); const get_js_namespaceObject = require("lodash/get.js"); var get_js_default = /*#__PURE__*/ __webpack_require__.n(get_js_namespaceObject); const ONE_RENDER_FRAME = 66; class Formsy extends external_react_default().Component { static displayName = 'Formsy'; inputs; emptyArray; prevInputNames = null; debouncedValidateForm; constructor(props){ super(props); this.state = { canChange: false, isSubmitting: false, isValid: true, contextValue: { attachToForm: this.attachToForm, detachFromForm: this.detachFromForm, isFormDisabled: props.disabled ?? false, isValidValue: this.isValidValue, validate: this.validate, runValidation: this.runValidation } }; this.inputs = []; this.emptyArray = []; this.debouncedValidateForm = debounce(this.validateForm, ONE_RENDER_FRAME); } componentDidMount = ()=>{ this.prevInputNames = this.inputs.map((component)=>component.props.name); this.validateForm(); }; componentDidUpdate = (prevProps)=>{ const { validationErrors, disabled = false } = this.props; if (validationErrors && isObject(validationErrors) && Object.keys(validationErrors).length > 0) this.setInputValidationErrors(validationErrors); const newInputNames = this.inputs.map((component)=>component.props.name); if (this.prevInputNames && !isSame(this.prevInputNames, newInputNames)) { this.prevInputNames = newInputNames; this.validateForm(); } if ((disabled ?? false) !== (prevProps.disabled ?? false)) this.setState((state)=>({ ...state, contextValue: { ...state.contextValue, isFormDisabled: disabled } })); }; getCurrentValues = ()=>Object.fromEntries(this.inputs.map((component)=>{ const { props: { name }, state: { value } } = component; return [ name, protectAgainstParamReassignment(value) ]; })); getModel = ()=>{ const currentValues = this.getCurrentValues(); return this.mapModel(currentValues); }; getPristineValues = ()=>Object.fromEntries(this.inputs.map((component)=>{ const { props: { name, value } } = component; return [ name, protectAgainstParamReassignment(value) ]; })); setFormPristine = (isPristine)=>{ this.setState({ formSubmitted: !isPristine }); this.inputs.forEach((component)=>{ component.setState({ formSubmitted: !isPristine, isPristine }); }); }; setInputValidationErrors = (errors)=>{ const { preventExternalInvalidation = false } = this.props; const { isValid } = this.state; this.inputs.forEach((component)=>{ const { name } = component.props; component.setState({ isValid: !(name in errors), validationError: isString(errors[name]) ? [ errors[name] ] : errors[name] }); }); if (!preventExternalInvalidation && isValid) this.setFormValidState(false); }; setFormValidState = (allIsValid)=>{ this.setState({ isValid: allIsValid }); if (allIsValid) this.props.onValid?.(); else this.props.onInvalid?.(); }; isValidValue = (component, value)=>this.runValidation(component, value).isValid; isFormDisabled = ()=>this.props.disabled ?? false; mapModel = (model)=>{ const { mapping } = this.props; if ('function' == typeof mapping) return mapping(model); const returnModel = {}; Object.keys(model).forEach((key)=>{ set_js_default()(returnModel, key, model[key]); }); return returnModel; }; reset = (model)=>{ this.setFormPristine(true); this.resetModel(model); }; runValidation = (component, value = component.state.value)=>{ const { validationErrors = {} } = this.props; const { validationError, validationErrors: componentValidationErrors, name } = component.props; const currentValues = this.getCurrentValues(); const validationResults = runRules(value, currentValues, component.validations || {}, validationRules_validationRules); const requiredResults = runRules(value, currentValues, component.requiredValidations || {}, validationRules_validationRules); const isRequired = Object.keys(component.requiredValidations || {}).length ? !!requiredResults.success.length : false; const isValid = !validationResults.failed.length && !(validationErrors && validationErrors[component.props.name]); return { isRequired, isValid: isRequired ? false : isValid, validationError: (()=>{ if (isValid && !isRequired) return this.emptyArray; if (validationResults.errors.length) return validationResults.errors; if (validationErrors && validationErrors[name]) return isString(validationErrors[name]) ? [ validationErrors[name] ] : validationErrors[name]; if (isRequired) { const error = (componentValidationErrors || {})[requiredResults.success[0]] || validationError; return error ? [ error ] : null; } if (validationResults.failed.length) return validationResults.failed.map((failed)=>(componentValidationErrors || {})[failed] ? componentValidationErrors[failed] : validationError).filter((x, pos, arr)=>arr.indexOf(x) === pos); })() }; }; attachToForm = (component)=>{ if (-1 === this.inputs.indexOf(component)) this.inputs.push(component); const { canChange } = this.state; if (canChange) this.props.onChange?.(this.getModel(), this.isChanged()); this.debouncedValidateForm(); }; detachFromForm = (component)=>{ this.inputs = this.inputs.filter((input)=>input !== component); this.debouncedValidateForm(); }; isChanged = ()=>!isSame(this.getPristineValues(), this.getCurrentValues()); submit = (event)=>{ const { onSubmit, onValidSubmit, onInvalidSubmit, preventDefaultSubmit = true } = this.props; const { isValid } = this.state; if (preventDefaultSubmit && event?.preventDefault) event.preventDefault(); this.setFormPristine(false); const model = this.getModel(); onSubmit?.(model, this.resetModel, this.updateInputsWithError, event); if (isValid) onValidSubmit?.(model, this.resetModel, this.updateInputsWithError, event); else onInvalidSubmit?.(model, this.resetModel, this.updateInputsWithError, event); }; updateInputsWithError = (errors, invalidate)=>{ const { preventExternalInvalidation = false } = this.props; const { isValid } = this.state; Object.entries(errors).forEach(([name, error])=>{ const component = this.inputs.find((input)=>input.props.name === name); if (!component) throw new Error(`You are trying to update an input that does not exist. Verify errors object with input names. ${JSON.stringify(errors)}`); component.setState({ isValid: preventExternalInvalidation, validationError: isString(error) ? [ error ] : error }); }); if (invalidate && isValid) this.setFormValidState(false); }; updateInputsWithValue = (data, validate)=>{ this.inputs.forEach((component)=>{ const { name } = component.props; if (data && has_js_default()(data, name)) component.setValue(get_js_default()(data, name), validate); }); }; validate = (component)=>{ const { onChange } = this.props; const { canChange } = this.state; if (canChange) onChange?.(this.getModel(), this.isChanged()); const validationState = this.runValidation(component); component.setState(validationState, this.validateForm); }; validateForm = ()=>{ const onValidationComplete = ()=>{ const allIsValid = this.inputs.every((component)=>component.state.isValid); this.setFormValidState(allIsValid); this.setState({ canChange: true }); }; if (0 === this.inputs.length) onValidationComplete(); else this.inputs.forEach((component, index)=>{ const validationState = this.runValidation(component); const isLastInput = index === this.inputs.length - 1; const callback = isLastInput ? onValidationComplete : null; component.setState(validationState, callback); }); }; render() { const { children, mapping, onChange, onInvalid, onInvalidSubmit, onReset, onSubmit, onValid, onValidSubmit, preventDefaultSubmit, preventExternalInvalidation, validationErrors, disabled = false, formElement = 'form', ...nonFormsyProps } = this.props; const { contextValue } = this.state; return external_react_default().createElement(FormsyContext.Provider, { value: contextValue }, external_react_default().createElement(formElement || 'form', { onReset: this.resetInternal, onSubmit: this.submit, ...nonFormsyProps, disabled }, children)); } resetInternal = (event)=>{ const { onReset } = this.props; event.preventDefault(); this.reset(); onReset?.(); }; resetModel = (data)=>{ this.inputs.forEach((component)=>{ const { name } = component.props; if (data && has_js_default()(data, name)) component.setValue(get_js_default()(data, name)); else component.resetValue(); }); this.validateForm(); }; } const src = Formsy; exports.addValidationRule = __webpack_exports__.addValidationRule; exports["default"] = __webpack_exports__["default"]; exports.validationRules = __webpack_exports__.validationRules; exports.withFormsy = __webpack_exports__.withFormsy; for(var __rspack_i in __webpack_exports__)if (-1 === [ "addValidationRule", "default", "validationRules", "withFormsy" ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i]; Object.defineProperty(exports, '__esModule', { value: true });