react-formal
Version:
Classy HTML form management for React
522 lines (519 loc) • 18.7 kB
JavaScript
;
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;