UNPKG

react-formal

Version:

Classy HTML form management for React

522 lines (519 loc) 18.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; Object.defineProperty(exports, "getter", { enumerable: true, get: function () { return _BindingContext.formGetter; } }); Object.defineProperty(exports, "setter", { enumerable: true, get: function () { return _BindingContext.formSetter; } }); var _propTypes = _interopRequireDefault(require("prop-types")); var _react = _interopRequireWildcard(require("react")); var _shallowequal = _interopRequireDefault(require("shallowequal")); var _BindingContext = _interopRequireWildcard(require("./BindingContext")); var _uncontrollable = require("uncontrollable"); var _yup = require("yup"); var _useEventCallback = _interopRequireDefault(require("@restart/hooks/useEventCallback")); var _useMergeState = _interopRequireDefault(require("@restart/hooks/useMergeState")); var _useMounted = _interopRequireDefault(require("@restart/hooks/useMounted")); var _useTimeout = _interopRequireDefault(require("@restart/hooks/useTimeout")); var _Contexts = require("./Contexts"); var _errorManager = _interopRequireWildcard(require("./errorManager")); var ErrorUtils = _interopRequireWildcard(require("./Errors")); var _errToJSON = _interopRequireDefault(require("./utils/errToJSON")); var _notify = _interopRequireDefault(require("./utils/notify")); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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; } /* eslint-disable @typescript-eslint/no-use-before-define */ let done = e => setTimeout(() => { throw e; }); function useErrorContext(errors) { const ref = (0, _react.useRef)(null); if (!ref.current) { return ref.current = errors != null ? errors : null; } if (!(0, _shallowequal.default)(ref.current.errors, errors)) { ref.current = errors != null ? errors : null; } return ref.current; } function validatePath({ path }, _ref) { let { value, schema } = _ref, rest = _objectWithoutPropertiesLoose(_ref, ["value", "schema"]); const validation = path === '' || path === '.' ? schema.validate(value, rest) : schema.validateAt(path, value, rest); return validation.then(() => null).catch(err => err); } const EMPTY_TOUCHED = {}; /** @alias Form */ const _Form = /*#__PURE__*/_react.default.forwardRef((_ref2, ref) => { let { children, defaultValue, value: propValue, onChange: propOnChange, errors: propErrors, onError: propOnError, defaultErrors = ErrorUtils.EMPTY_ERRORS, defaultTouched = EMPTY_TOUCHED, touched: propTouched, onTouch: propOnTouch, schema, submitForm, getter = _BindingContext.formGetter, setter = _BindingContext.formSetter, delay = 300, debug, noValidate, onValidate, onBeforeSubmit, onSubmit, onSubmitFinished, onInvalidSubmit, onReset, context, stripUnknown, abortEarly, strict = false, as: Element = 'form' } = _ref2, elementProps = _objectWithoutPropertiesLoose(_ref2, ["children", "defaultValue", "value", "onChange", "errors", "onError", "defaultErrors", "defaultTouched", "touched", "onTouch", "schema", "submitForm", "getter", "setter", "delay", "debug", "noValidate", "onValidate", "onBeforeSubmit", "onSubmit", "onSubmitFinished", "onInvalidSubmit", "onReset", "context", "stripUnknown", "abortEarly", "strict", "as"]); const [value, onChange] = (0, _uncontrollable.useUncontrolledProp)(propValue, defaultValue, propOnChange); const [errors, onError] = (0, _uncontrollable.useUncontrolledProp)(propErrors, defaultErrors, propOnError); const [touched, onTouch] = (0, _uncontrollable.useUncontrolledProp)(propTouched, defaultTouched, propOnTouch); const shouldValidate = !!schema && !noValidate; const flushTimeout = (0, _useTimeout.default)(); const submitTimeout = (0, _useTimeout.default)(); const resetTimeout = (0, _useTimeout.default)(); const isMounted = (0, _useMounted.default)(); const queueRef = (0, _react.useRef)([]); const errorManager = (0, _react.useMemo)(() => (0, _errorManager.default)(validatePath), []); const handleChange = (0, _useEventCallback.default)((model, paths) => { let nextTouched = touched; onChange(model, paths); paths.forEach(path => { if (touched && touched[path]) return; if (nextTouched === touched) nextTouched = Object.assign({}, touched, { [path]: true });else nextTouched[path] = true; }); if (nextTouched !== touched) onTouch(nextTouched, paths); }); const getSchemaForPath = (0, _react.useCallback)((path, currentValue = value) => schema && path && (0, _yup.reach)(schema, path, currentValue, context), [value, schema, context]); const formValueContext = (0, _BindingContext.default)({ formValue: value, onChange: handleChange, setter, getter, getSchemaForPath }); const yupOptions = { strict, context, stripUnknown, abortEarly: abortEarly == null ? false : abortEarly }; const isSubmittingRef = (0, _react.useRef)(false); const [submits, setSubmitState] = (0, _useMergeState.default)(() => ({ submitCount: 0, submitAttempts: 0, submitting: false })); const [resets, setResets] = (0, _react.useState)(0); function setSubmitting(submitting) { if (!isMounted()) return; isSubmittingRef.current = submitting; setSubmitState({ submitting }); } const errorContext = useErrorContext(errors); const isUpdateRef = (0, _react.useRef)(false); (0, _react.useEffect)(() => { // don't do this on mount if (!isUpdateRef.current) { isUpdateRef.current = true; return; } if (errors) { enqueue(Object.keys(errors)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [schema]); const flush = () => { flushTimeout.set(() => { let fields = queueRef.current; if (!fields.length) return; queueRef.current = []; errorManager.collect(fields, errors, Object.assign({ schema, value }, yupOptions)).then(nextErrors => { if (nextErrors !== errors) { maybeWarn(debug, errors, 'field validation'); (0, _notify.default)(onError, [nextErrors]); } }).catch(done); }, delay); }; (0, _react.useEffect)(() => { flush(); }); function enqueue(fields) { queueRef.current.push(...fields); } const handleValidationRequest = (fields, type, args) => { if (!shouldValidate) return; (0, _notify.default)(onValidate, [{ type, fields, args }]); enqueue(fields); if (type !== 'onChange') flush(); }; const handleFieldError = (name, fieldErrors) => { handleError(Object.assign(ErrorUtils.remove(errors, name), fieldErrors)); }; const handleError = nextErrors => { (0, _notify.default)(onError, [nextErrors]); }; const handleSubmitSuccess = validatedValue => { (0, _notify.default)(onError, []); (0, _notify.default)(onSubmit, [validatedValue]); return Promise.resolve(submitForm && submitForm(validatedValue)).then(() => { setSubmitting(false); setSubmitState(s => ({ submitCount: s.submitCount + 1, submitAttempts: s.submitAttempts + 1 })); (0, _notify.default)(onSubmitFinished); return true; }, err => { setSubmitting(false); (0, _notify.default)(onSubmitFinished, [err]); throw err; }); }; const handleSubmitError = err => { if (!(0, _errorManager.isValidationError)(err)) throw err; const nextErrors = (0, _errToJSON.default)(err); maybeWarn(debug, nextErrors, 'onSubmit'); setSubmitState(s => ({ submitAttempts: s.submitAttempts + 1 })); (0, _notify.default)(onError, [nextErrors]); (0, _notify.default)(onInvalidSubmit, [nextErrors]); setSubmitting(false); (0, _notify.default)(onSubmitFinished, [err]); return false; }; const clearPendingValidations = () => { flushTimeout.clear(); queueRef.current.length = 0; }; const handleSubmit = e => { if (e && e.preventDefault && e.stopPropagation) { e.preventDefault(); e.stopPropagation(); } clearPendingValidations(); submitTimeout.set(() => submit().catch(done)); }; const handleReset = e => { if (e && e.preventDefault && e.stopPropagation) { e.preventDefault(); e.stopPropagation(); } (0, _notify.default)(onReset); onChange(defaultValue || {}, []); onError(defaultErrors || ErrorUtils.EMPTY_ERRORS); resetTimeout.set(() => setResets(prevResets => prevResets += 1)); }; const submit = () => { if (isSubmittingRef.current) { return Promise.resolve(false); } clearPendingValidations(); (0, _notify.default)(onBeforeSubmit, [{ value, errors }]); setSubmitting(true); return (!shouldValidate ? Promise.resolve(value) : schema.validate(value, Object.assign({}, yupOptions, { abortEarly: false, strict: false })) // no catch, we aren't interested in errors from onSubmit handlers ).then(handleSubmitSuccess, handleSubmitError); }; (0, _react.useImperativeHandle)(ref, () => ({ submit, validate(fields) { errorManager.collect(fields, errors, Object.assign({ schema, value }, yupOptions)); } })); const actions = Object.assign((0, _react.useRef)({}).current, { yupContext: context, onSubmit: handleSubmit, onReset: handleReset, onValidate: handleValidationRequest, onFieldError: handleFieldError, formHasValidation: () => shouldValidate }); const contextValue = (0, _react.useMemo)(() => ({ touched, actions, errors: errorContext, submits, resets }), [touched, actions, errorContext, submits, resets]); if (Element === 'form') { elementProps.noValidate = true; // disable html5 validation } elementProps.onSubmit = handleSubmit; elementProps.onReset = handleReset; let useChildren = Element == null || Element === false; // if it's a fragment no props if (Element === _react.Fragment || useChildren && _react.default.Children.only(children).type === _react.Fragment) { elementProps = {}; } return /*#__PURE__*/_react.default.createElement(_BindingContext.BindingContext.Provider, { value: formValueContext }, /*#__PURE__*/_react.default.createElement(_Contexts.FormContext.Provider, { value: contextValue }, Element == null || Element === false ? /*#__PURE__*/_react.default.cloneElement(_react.default.Children.only(children), elementProps) : /*#__PURE__*/_react.default.createElement(Element, elementProps, children))); }); function maybeWarn(debug, errors, target) { if (!debug) return; if (process.env.NODE_ENV !== 'production') { let keys = Object.keys(errors || ErrorUtils.EMPTY_ERRORS); if (keys.length) { console.error(`[react-formal] (${target}) invalid fields: ${keys.join(', ')}`); } } } _Form.propTypes = { /** * Form value object, can be left [uncontrolled](/controllables); * use the `defaultValue` prop to initialize an uncontrolled form. */ value: _propTypes.default.object, /** * Callback that is called when the `value` prop changes. * * ```ts static * function ( * value: any, * updatedPaths: string[] * ) * ``` */ onChange: _propTypes.default.func, /** * An object hash of field errors for the form. The object should be keyed with paths * with the values being an array of errors or message objects. Errors can be * left [uncontrolled](/controllables) (use `defaultErrors` to set an initial value) * or managed along with the `onError` callback. You can use any object shape you'd like for * errors, as long as you provide the Form.Message component an `extract` prop that * understands how to pull out the strings message. By default it understands strings and objects * with a `'message'` property. * * ```jsx static * <Form errors={{ * "name.first": [ * 'First names are required', * { * message: "Names must be at least 2 characters long", * type: 'min' * } * ], * }}/> * ``` */ errors: _propTypes.default.object, /** * Callback that is called when a validation error occurs. It is called with an `errors` object * * ```jsx renderAsComponent * import Form from '@docs/components/FormWithResult'; * import * as yup from 'yup' * * const schema = yup.object({ * name: yup.string().required().min(15) * }) * * const [errors, setErrors] = useState({}); * * <Form * schema={schema} * errors={errors} * onError={errors => { * if (errors.name) { * errors.name = 'hijacked!' * } * * setErrors(errors) * }}> * <label> * Name * <Form.Field name='name'/> * </label> * <Form.Message for='name' className="error" /> * * <Form.Submit type='submit'>Submit</Form.Submit> * </Form> * ``` */ onError: _propTypes.default.func, /** An object hash of field paths and whether they have been "touched" yet */ touched: _propTypes.default.object, /** * Callback that is called when a field is touched. It is called with an `touched` object */ onTouch: _propTypes.default.func, /** * Callback that is called whenever a validation is triggered. * It is called _before_ the validation is actually run. * * ```js static * function onValidate(event) { * let { type, fields, args } = event * } * ``` */ onValidate: _propTypes.default.func, /** * Callback that is fired in response to a submit, _before_ validation runs. * * ```js static * function onSubmit(formValue) { * // do something with valid value * } * ``` */ onBeforeSubmit: _propTypes.default.func, /** * Callback that is fired in response to a submit, after validation runs for the entire form. * * ```js static * function onSubmit(formValue) { * // do something with valid value * } * ``` */ onSubmit: _propTypes.default.func, /** * Callback that is fired in response to a form reset. `onReset` fires before * the accompanying `onChange`. * * ```js static * function onReset() { * // reset has been called * } * ``` */ onReset: _propTypes.default.func, onSubmitFinished: _propTypes.default.func, /* */ submitForm: _propTypes.default.func, /** * Callback that is fired when the native onSubmit event is triggered. Only relevant when * the `component` prop renders a `<form/>` tag. onInvalidSubmit will trigger only if the form is invalid. * * ```js static * function onInvalidSubmit(errors){ * // do something with errors * } * ``` */ onInvalidSubmit: _propTypes.default.func, /** * A value getter function. `getter` is called with `path` and `value` and * should return the plain **javascript** value at the path. * * ```ts static * function( * path: string, * value: any, * ): Object * ``` */ getter: _propTypes.default.func, /** * A value setter function. `setter` is called with `path`, the form `value` and the path `value`. * The `setter` must return updated form `value`, which allows you to leave the original value unmutated. * * The default implementation uses the [react immutability helpers](http://facebook.github.io/react/docs/update.html), * letting you treat the form `value` as immutable. * * ```ts static * function( * path: string, * formValue: any, * pathValue: any * ): Object * ``` */ setter: _propTypes.default.func, /** * Time in milliseconds that validations should be debounced. Reduces the amount of validation calls * made at the expense of a slight delay. Helpful for performance. */ delay: _propTypes.default.number, /** * Validations will be strict, making no attempt to coarce input values to the appropriate type. */ strict: _propTypes.default.bool, /** * Turns off input validation for the Form, value updates will continue to work. */ noValidate: _propTypes.default.bool, /** * A tag name or Component class the Form should render. * * If `null` are `false` the form will simply render it's child. In * this instance there must only be one child. */ as: _propTypes.default.oneOfType([_propTypes.default.elementType, _propTypes.default.oneOf([null, false])]), /** * A Yup schema that validates the Form `value` prop. Used to validate the form input values * For more information about the yup api check out: https://github.com/jquense/yup/blob/master/README.md * @type {Schema} */ schema(props, name, componentName) { let err = null; if (props[name]) { if (!(0, _yup.isSchema)(props[name])) err = new Error('`schema` must be a proper yup schema: (' + componentName + ')'); } return err; }, /** * yup schema context */ context: _propTypes.default.object, /** * toggle debug mode, which `console.warn`s validation errors */ debug: _propTypes.default.bool }; _Form.displayName = 'Form'; var _default = _Form; exports.default = _default;