UNPKG

react-use-form-state

Version:

React hook for managing form and inputs state

845 lines (691 loc) 25 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var react = require('react'); function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { return; } var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } /** * Returns a function that can be called with an object. The return value of the * new function is a copy of the object excluding the key passed initially. */ function omit(key) { return function (object) { var toRemove = object[key], rest = _objectWithoutProperties(object, [key].map(_toPropertyKey)); return rest; }; } /** * An empty function. It does nothing. */ function noop() {} /** * Like `noop`, but passes through the first argument. */ function identity(val) { return val; } /** * Cast non-string values to a string, with the exception of functions, symbols, * and undefined. */ function toString(value) { switch (typeof value) { case 'function': case 'symbol': case 'undefined': return ''; default: return '' + value; // eslint-disable-line prefer-template } } function isFunction(value) { return typeof value === 'function'; } var objectToString = function objectToString(value) { return Object.prototype.toString.call(value); }; /** * Determines if a value is an empty collection (object, array, string, map, set) * @note this returns false for anything else. */ function isEmpty(value) { if (value == null) { return true; } if (Array.isArray(value) || typeof value === 'string') { return !value.length; } if (objectToString(value) === '[object Map]' || objectToString(value) === '[object Set]') { return !value.size; } if (objectToString(value) === '[object Object]') { return !Object.keys(value).length; } return false; } function isEqual(value, other) { if (Array.isArray(value) && Array.isArray(other)) { return value.length === other.length && value.every(function (a) { return other.indexOf(a) > -1; }) && other.every(function (b) { return value.indexOf(b) > -1; }); } return value === other; } function testIsEqualCompatibility(value) { /* istanbul ignore if */ if (Array.isArray(value)) { return value.every(testIsEqualCompatibility); } return value == null || /^[sbn]/.test(typeof value); // basic primitives } var defaultInputOptions = { onChange: identity, onBlur: noop, validate: null, validateOnBlur: undefined, touchOnChange: false, compare: null }; function parseInputArgs(args) { var name; var ownValue; var options; if (typeof args[0] === 'string' || typeof args[0] === 'number') { var _args = _slicedToArray(args, 2); name = _args[0]; ownValue = _args[1]; } else { var _args2 = _slicedToArray(args, 1); var _args2$ = _args2[0]; name = _args2$.name; ownValue = _args2$.value; options = _objectWithoutProperties(_args2$, ["name", "value"]); } ownValue = toString(ownValue); return _objectSpread2(_objectSpread2({ name: name, ownValue: ownValue, hasOwnValue: !!ownValue }, defaultInputOptions), options); } var defaultCreateId = function defaultCreateId(name, value) { return ['__ufs', name, value].filter(Boolean).join('__'); }; function useInputId(implementation) { var getId = react.useCallback(function (name, ownValue) { var createId; if (!implementation) { createId = noop; } else if (isFunction(implementation)) { createId = implementation; } else { createId = defaultCreateId; } var value = toString(ownValue); return value ? createId(name, value) : createId(name); }, [implementation]); var getIdProp = react.useCallback(function (prop, name, value) { var id = getId(name, value); return id === undefined ? {} : _defineProperty({}, prop, id); }, [getId]); return { getIdProp: getIdProp }; } function useMap() { var map = react.useRef(new Map()); return { set: function set(key, value) { return map.current.set(key, value); }, has: function has(key) { return map.current.has(key); }, get: function get(key) { return map.current.get(key); } }; } function useReferencedCallback() { var callbacks = useMap(); return function (key, current) { if (!callbacks.has(key)) { var callback = function callback() { return callback.current.apply(callback, arguments); }; callbacks.set(key, callback); } callbacks.get(key).current = current; return callbacks.get(key); }; } function useWarnOnce() { var didWarnRef = react.useRef(new Set()); return function (key, message) { if (!didWarnRef.current.has(key)) { didWarnRef.current.add(key); console.warn('[useFormState]', message); } }; } function stateReducer(state, newState) { return isFunction(newState) ? newState(state) : _objectSpread2(_objectSpread2({}, state), newState); } function useState(_ref) { var initialState = _ref.initialState; var state = react.useRef(); var initialValues = useMap(); var comparators = useMap(); var _useReducer = react.useReducer(stateReducer, initialState || {}), _useReducer2 = _slicedToArray(_useReducer, 2), values = _useReducer2[0], setValues = _useReducer2[1]; var _useReducer3 = react.useReducer(stateReducer, {}), _useReducer4 = _slicedToArray(_useReducer3, 2), touched = _useReducer4[0], setTouched = _useReducer4[1]; var _useReducer5 = react.useReducer(stateReducer, {}), _useReducer6 = _slicedToArray(_useReducer5, 2), validity = _useReducer6[0], setValidity = _useReducer6[1]; var _useReducer7 = react.useReducer(stateReducer, {}), _useReducer8 = _slicedToArray(_useReducer7, 2), errors = _useReducer8[0], setError = _useReducer8[1]; var _useReducer9 = react.useReducer(stateReducer, {}), _useReducer10 = _slicedToArray(_useReducer9, 2), pristine = _useReducer10[0], setPristine = _useReducer10[1]; state.current = { values: values, touched: touched, validity: validity, errors: errors, pristine: pristine }; function getInitialValue(name) { return initialValues.has(name) ? initialValues.get(name) : initialState[name]; } function updatePristine(name, value) { var comparator = comparators.get(name); // If comparator isn't available for an input, that means the input wasn't // mounted, or manually added via setField. comparator = isFunction(comparator) ? comparator : isEqual; setPristine(_defineProperty({}, name, !!comparator(getInitialValue(name), value))); } function setFieldState(name, value, inputValidity, inputTouched, inputError) { setValues(_defineProperty({}, name, value)); setTouched(_defineProperty({}, name, inputTouched)); setValidity(_defineProperty({}, name, inputValidity)); setError(_defineProperty({}, name, inputError)); updatePristine(name, value); } function setField(name, value) { // need to store the initial value via setField in case it's before the // input of the given name is rendered. if (!initialValues.has(name)) { initialValues.set(name, value); } setFieldState(name, value, true, true); } function clearField(name) { setField(name); } function resetField(name) { setField(name, getInitialValue(name)); } function isPristine() { return Object.keys(state.current.pristine).every(function (key) { return !!state.current.pristine[key]; }); } function forEach(cb) { Object.keys(state.current.values).forEach(cb); } return { get current() { return state.current; }, setValues: setValues, setTouched: setTouched, setValidity: setValidity, setError: setError, setField: setField, setPristine: setPristine, updatePristine: updatePristine, initialValues: initialValues, resetField: resetField, clearField: clearField, forEach: forEach, isPristine: isPristine, comparators: comparators }; } var CHECKBOX = 'checkbox'; var COLOR = 'color'; var DATE = 'date'; var EMAIL = 'email'; var MONTH = 'month'; var NUMBER = 'number'; var PASSWORD = 'password'; var RADIO = 'radio'; var RANGE = 'range'; var RAW = 'raw'; var SEARCH = 'search'; var SELECT = 'select'; var SELECT_MULTIPLE = 'selectMultiple'; var TEL = 'tel'; var TEXT = 'text'; var TEXTAREA = 'textarea'; var TIME = 'time'; var URL = 'url'; var WEEK = 'week'; var LABEL = 'label'; var INPUT_TYPES = [CHECKBOX, COLOR, DATE, EMAIL, MONTH, NUMBER, PASSWORD, RADIO, RANGE, RAW, SEARCH, SELECT, SELECT_MULTIPLE, TEL, TEXT, TEXTAREA, TIME, URL, WEEK]; var defaultFormOptions = { onBlur: noop, onChange: noop, onClear: noop, onReset: noop, onTouched: noop, withIds: false }; function useFormState(initialState, options) { var formOptions = _objectSpread2(_objectSpread2({}, defaultFormOptions), options); var formState = useState({ initialState: initialState }); var _useInputId = useInputId(formOptions.withIds), getIdProp = _useInputId.getIdProp; var _useMap = useMap(), setDirty = _useMap.set, isDirty = _useMap.get; var referencedCallback = useReferencedCallback(); var warn = useWarnOnce(); var createPropsGetter = function createPropsGetter(type) { return function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var inputOptions = parseInputArgs(args); var name = inputOptions.name, ownValue = inputOptions.ownValue, hasOwnValue = inputOptions.hasOwnValue; var isCheckbox = type === CHECKBOX; var isRadio = type === RADIO; var isSelectMultiple = type === SELECT_MULTIPLE; var isRaw = type === RAW; var hasValueInState = formState.current.values[name] !== undefined; // This is used to cache input props that shouldn't change across // re-renders. Note that for `raw` values, `ownValue` // will return '[object Object]'. This means we can't have multiple // raw inputs with the same name and different values, but this is // probably fine. var key = "".concat(type, ".").concat(name, ".").concat(ownValue); function setDefaultValue() { /* istanbul ignore else */ if (process.env.NODE_ENV === 'development') { if (isRaw) { warn("missingInitialValue.".concat(key), "The initial value for input \"".concat(name, "\" is missing. Custom inputs ") + 'controlled with raw() are expected to have an initial value ' + 'provided to useFormState(). To prevent React from treating ' + 'this input as uncontrolled, an empty string will be used instead.'); } } var value = ''; if (isCheckbox) { /** * If a checkbox has a user-defined value, its value the form state * value will be an array. Otherwise it will be considered a toggle. */ value = hasOwnValue ? [] : false; } if (isSelectMultiple) { value = []; } formState.setValues(_defineProperty({}, name, value)); } function getNextCheckboxValue(e) { var _e$target = e.target, value = _e$target.value, checked = _e$target.checked; if (!hasOwnValue) { return checked; } var checkedValues = new Set(formState.current.values[name]); if (checked) { checkedValues.add(value); } else { checkedValues["delete"](value); } return Array.from(checkedValues); } function getNextSelectMultipleValue(e) { return Array.from(e.target.options).reduce(function (values, option) { return option.selected ? [].concat(_toConsumableArray(values), [option.value]) : values; }, []); } function getCompareFn() { if (isFunction(inputOptions.compare)) { return inputOptions.compare; } return function (value, other) { /* istanbul ignore else */ if (process.env.NODE_ENV === 'development') { if (isRaw && ![value, other].every(testIsEqualCompatibility)) { warn("missingCompare.".concat(key), "You used a raw input type for \"".concat(name, "\" without providing a ") + 'custom compare method. As a result, the pristine value of ' + 'this input will be calculated using strict equality check ' + '(====), which is insufficient. Please provide a custom ' + 'compare method for this input in order to get an accurate ' + 'pristine value.'); } } return isEqual(value, other); }; } formState.comparators.set(name, getCompareFn()); function getValidateOnBlur() { var _formOptions$validate; return (_formOptions$validate = formOptions.validateOnBlur) !== null && _formOptions$validate !== void 0 ? _formOptions$validate : inputOptions.validateOnBlur; } function validate(e) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : isRaw ? formState.current.values[name] : e.target.value; var values = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : formState.current.values; var error; var isValid = true; /* istanbul ignore else */ if (isFunction(inputOptions.validate)) { var result = inputOptions.validate(value, values, e); if (result !== true && result != null) { isValid = false; error = result !== false ? result : ''; } } else if (!isRaw) { isValid = e.target.validity.valid; error = e.target.validationMessage; } else if (process.env.NODE_ENV === 'development') { warn("missingValidate.".concat(key), "You used a raw input type for \"".concat(name, "\" without providing a ") + 'custom validate method. As a result, validation of this input ' + 'will be set to "true" automatically. If you need to validate ' + 'this input, provide a custom validation option.'); } formState.setValidity(_defineProperty({}, name, isValid)); formState.setError(isEmpty(error) ? omit(name) : _defineProperty({}, name, error)); } function touch(e) { if (!formState.current.touched[name]) { formState.setTouched(_defineProperty({}, name, true)); formOptions.onTouched(e); } } var inputProps = _objectSpread2({ name: name, get type() { if (type !== SELECT && type !== SELECT_MULTIPLE && type !== TEXTAREA) { return type; } }, get multiple() { if (type === SELECT_MULTIPLE) { return true; } }, get checked() { var values = formState.current.values; if (isRadio) { return values[name] === ownValue; } if (isCheckbox) { if (!hasOwnValue) { return values[name] || false; } /** * @todo Handle the case where two checkbox inputs share the same * name, but one has a value, the other doesn't (throws currently). * <input {...input.checkbox('option1')} /> * <input {...input.checkbox('option1', 'value_of_option1')} /> */ return hasValueInState ? values[name].includes(ownValue) : false; } }, get value() { if (!hasValueInState) { // auto populating default values if an initial value is not provided setDefaultValue(); } else if (!formState.initialValues.has(name)) { // keep track of user-provided initial values on first render formState.initialValues.set(name, formState.current.values[name]); } // auto populating default values of touched if (formState.current.touched[name] == null) { formState.setTouched(_defineProperty({}, name, false)); } // auto populating default values of pristine if (formState.current.pristine[name] == null) { formState.setPristine(_defineProperty({}, name, true)); } /** * Since checkbox and radio inputs have their own user-defined values, * and since checkbox inputs can be either an array or a boolean, * returning the value of input from the current form state is illogical */ if (isCheckbox || isRadio) { return ownValue; } return hasValueInState ? formState.current.values[name] : ''; }, onChange: referencedCallback("onChange.".concat(key), function (e) { setDirty(name, true); var value; if (isRaw) { value = inputOptions.onChange(e); if (value === undefined) { // setting value to its current state if onChange does not return // value to prevent React from complaining about the input switching // from controlled to uncontrolled value = formState.current.values[name]; /* istanbul ignore else */ if (process.env.NODE_ENV === 'development') { warn("onChangeUndefined.".concat(key), "You used a raw input type for \"".concat(name, "\" with an onChange() ") + 'option without returning a value. The onChange callback ' + 'of raw inputs, when provided, is used to determine the ' + 'custom value that will be stored in the form state. ' + 'Therefore, a value must be returned from the onChange callback.'); } } } else { if (isCheckbox) { value = getNextCheckboxValue(e); } else if (isSelectMultiple) { value = getNextSelectMultipleValue(e); } else { value = e.target.value; } inputOptions.onChange(e); } // Mark raw fields as touched on change, since we might not get an // `onBlur` event from them. if (inputOptions.touchOnChange) { touch(e); } var partialNewState = _defineProperty({}, name, value); var newValues = _objectSpread2(_objectSpread2({}, formState.current.values), partialNewState); formOptions.onChange(e, formState.current.values, newValues); if (!getValidateOnBlur()) { validate(e, value, newValues); } formState.updatePristine(name, value); formState.setValues(partialNewState); }), onBlur: referencedCallback("onBlur.".concat(key), function (e) { touch(e); inputOptions.onBlur(e); formOptions.onBlur(e); /** * Limiting input validation on blur to: * A) when it's either touched for the first time * B) when it's marked as dirty due to a value change */ if (!formState.current.touched[name] || isDirty(name)) { var _getValidateOnBlur; setDirty(name, false); // http://github.com/wsmd/react-use-form-state/issues/127#issuecomment-597989364 if ((_getValidateOnBlur = getValidateOnBlur()) !== null && _getValidateOnBlur !== void 0 ? _getValidateOnBlur : true) { validate(e); } } }) }, getIdProp('id', name, ownValue)); if (isRaw) { return { onChange: inputProps.onChange, onBlur: inputProps.onBlur, value: inputProps.value, name: inputProps.name }; } return inputProps; }; }; var formStateAPI = react.useRef({ isPristine: formState.isPristine, clearField: formState.clearField, resetField: formState.resetField, setField: formState.setField, setFieldError: function setFieldError(name, error) { formState.setValidity(_defineProperty({}, name, false)); formState.setError(_defineProperty({}, name, error)); }, clear: function clear() { formState.forEach(formState.clearField); formOptions.onClear(); }, reset: function reset() { formState.forEach(formState.resetField); formOptions.onReset(); } }); // exposing current form state (e.g. values, touched, validity, etc) Object.keys(formState.current).forEach(function (key) { formStateAPI.current[key] = formState.current[key]; }); var inputPropsCreators = _defineProperty({}, LABEL, function (name, ownValue) { return getIdProp('htmlFor', name, ownValue); }); INPUT_TYPES.forEach(function (type) { inputPropsCreators[type] = createPropsGetter(type); }); return [formStateAPI.current, inputPropsCreators]; } exports.useFormState = useFormState;