react-formstate-fp
Version:
An API to build, query, update, and bind formstate in React. Develop forms DRYly in React.
365 lines (304 loc) • 14.7 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault")["default"];
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard")["default"];
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.FormField = FormField;
exports.FormScope = FormScope;
exports.bindToSetStateComponent = bindToSetStateComponent;
exports.createRffAdaptor = createRffAdaptor;
exports.createRffNestedFormAdaptor = createRffNestedFormAdaptor;
exports.useFormstate = useFormstate;
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = _interopRequireWildcard(require("react"));
var _helperFunctions = require("./helperFunctions.js");
var _schemaAndLookup = require("./schemaAndLookup.js");
var _excluded = ["formstate", "form", "activeModelKey", "name", "required", "validate", "validateAsync", "children"];
//
// create RffAdaptor (to make TypeScript happy)
//
// typescript version
//
// export function createRffAdaptor<Props, Model>(
// Component: React.FunctionComponent<Props & RffProps<Model>>
// ): React.FunctionComponent<Props & FormFieldName>
// {
// return (props) => {
// // rff used React.mapChildren to add form, formstate, and modelKey to render props
// // have to do some casting to keep typescript happy...
// const { form, formstate, modelKey, ...other } = props as Props & RffProps<Model>;
// const unadaptedProps = (other as unknown) as Props;
// return <Component form={form} formstate={formstate} modelKey={modelKey} {...unadaptedProps}/>;
// };
// };
// javascript version
//
// Because this isn't typescript, you don't have to do any casting.
// Despite appearances this function does accomplish something.
// It's the typescript type signature for this function that does the work.
function createRffAdaptor(Component) {
return Component;
}
function createRffNestedFormAdaptor(Component) {
return Component;
} //
// useFormstate
//
function useFormstate(initialFormstate, options) {
var _useState = (0, _react.useState)(initialFormstate),
_useState2 = (0, _slicedToArray2["default"])(_useState, 2),
formstate = _useState2[0],
setFormstateHook = _useState2[1]; // Prevent React from complaining about async validation trying to update formstate for an unmounted form:
// --> react-dom.development.js:530 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
// I do not believe asynchronous validation completing after the form is unmounted constitutes a real problem to be concerned about,
// but to prevent spurious warnings from popping up in the console, you can make the form instance a ref...
var formRef = (0, _react.useRef)(null);
var formstateRef = (0, _react.useRef)(formstate); // So that people can use await if they want.
if (formRef.current === null) {
formRef.current = (0, _objectSpread2["default"])({
getFormstate: function getFormstate() {
return formstateRef.current;
},
setFormstate: function setFormstate(p) {
formstateRef.current = typeof p === 'function' ? p(formstateRef.current) : p;
setFormstateHook(formstateRef.current);
}
}, options);
}
(0, _react.useEffect)(function () {
return function () {
// Return a cleanup function that will run when the form is unmounted.
formRef.current.setFormstate = function () {}; // Suppress further attempts to update form state.
};
}, []); // Only create this cleanup function once.
return [formstate, formRef.current];
} //
// bindToSetStateComponent
//
function bindToSetStateComponent(component) {
var statePropertyName = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'formstate';
return {
getFormstate: function getFormstate() {
return component.state[statePropertyName];
},
// So that people can use await if they want.
setFormstate: function setFormstate(p) {
var updates = {};
updates[statePropertyName] = typeof p === 'function' ? p(component.state[statePropertyName]) : p;
component.setState(updates);
}
};
} //
// Form, FormScope, FormField
//
// export function Form(props) {
// return createElement('Form', props);
// }
function FormScope(props) {
return createElement('FormScope', props);
}
function FormField(props) {
return createElement('FormField', props);
}
function createElement(elementName, props) {
var isScopeElement = elementName !== 'FormField';
var formstate = props.formstate,
form = props.form,
activeModelKey = props.activeModelKey,
name = props.name,
required = props.required,
validate = props.validate,
validateAsync = props.validateAsync,
children = props.children,
otherProps = (0, _objectWithoutProperties2["default"])(props, _excluded);
if (!formstate || !form) {
throw new Error("An RFF ".concat(elementName, " element requires \"formstate\" and \"form\" props."));
}
if (typeof form.setFormstate !== 'function') {
throw new Error("The \"form\" prop provided to an RFF ".concat(elementName, " element must contain a \"setFormstate\" function."));
} // if (elementName === 'Form' && (activeModelKey || formstate.nestedScopeId)) { // This won't catch all cases.
// throw new Error('An RFF Form element should not be nested underneath another Form or FormScope element, even across components.');
// }
var _name = (0, _helperFunctions.exists)(name) ? String(name) : '';
var newModelKey = (0, _helperFunctions.addScope)(activeModelKey, (0, _helperFunctions.normalizeModelKey)(_name));
var rootModelKey = (0, _schemaAndLookup.convertToRootModelKey)(formstate, newModelKey);
var id = formstate.lookup.idsByRootModelKey[rootModelKey];
if (!id) {
throw new Error("The RFF ".concat(elementName, " element with name \"").concat(_name, "\" mapped to root model key \"").concat(rootModelKey, "\" does not correspond to anything in your model. Did you provide an initial model to initializeFormstate?"));
}
if (isScopeElement && !(0, _schemaAndLookup.isScope)(formstate, id) || !isScopeElement && (0, _schemaAndLookup.isScope)(formstate, id)) {
throw new Error("The RFF ".concat(elementName, " element with name \"").concat(_name, "\" mapped to root model key \"").concat(rootModelKey, "\" conflicts with your initial model, or with a validation schema provided to initializeFormstate, which defines that model key as a ").concat(isScopeElement ? 'field' : 'scope', ", not a ").concat(isScopeElement ? 'scope' : 'field', "."));
}
if (required || validate || validateAsync) {
// This is really just to satisfy eslint wrt unused variables.
var jsxValidationSchema = (0, _schemaAndLookup.buildValidationSchema)(props, newModelKey, isScopeElement);
if (Object.keys(jsxValidationSchema).length > 0) {
// Do not mutate the form unless the user decides to use jsx validation configuration...
if (!form.validationSchemas) {
form.validationSchemas = {};
var fm = form;
while (fm.parentForm) {
fm.parentForm.validationSchemas = form.validationSchemas;
fm = fm.parentForm;
}
}
if (formstate.validationSchemas[id]) {
throw new Error("You cannot define validation in your JSX if a validation schema was already provided to initializeFormstate or addModelKey. Pick one approach please. See the RFF ".concat(elementName, " element for root model key ").concat(rootModelKey, "."));
}
var priorSchema = form.validationSchemas[id];
if (priorSchema && priorSchema.nestedScopeId !== formstate.nestedScopeId) {
throw new Error("Validation is defined for root model key \"".concat(rootModelKey, "\" in two different components? This is not supported by RFF."));
}
form.validationSchemas[id] = (0, _objectSpread2["default"])((0, _objectSpread2["default"])({}, jsxValidationSchema), {}, {
nestedScopeId: formstate.nestedScopeId
});
}
}
return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, //elementName === 'Form' ? 'form' : React.Fragment,
otherProps, children && _react["default"].Children.map(children, function (child) {
return addProps(formstate, form, newModelKey, child);
}));
} //
// validateNameRequired
//
function validateNameRequired(props, isFormScope) {
if (!(0, _helperFunctions.hasProp)(props, 'name') || !(0, _helperFunctions.isNonEmptyString)(props.name) && typeof props.name !== 'number') {
throw new Error("A ".concat(isFormScope ? 'non-root FormScope' : 'FormField', " element requires a non-empty \"name\" prop."));
}
} //
// addProps
//
function addProps(formstate, form, activeModelKey, element) {
if (!element || !element.props) {
return element;
} // An example to help understand React.Children.map:
//<Contact nestedForm>
// <FormScope name='address'>
// <Address nestedForm>
// <FormField name='anAddressChild'>
// <Input type='text'/>
// </FormField>
// </Address>
// </FormScope>
// <FormField name='anotherContactChild'>
// <Input type='text'/>
// </FormField>
//</Contact>
// The 'anAddressChild' input is not a direct child of this element so it is not in element.props.children here.
// It will be processed by a React.Children.map call when addProps is called *for the Address child*.
// Also, note that the FormScope and FormField components are contained in this library, so they can be coded to do their own work.
// For anything else you have to take a look at the children via this method (and recursively take a look at their children via this method).
// For instance, the Contact component isn't going to walk its children to add rff props. You have to do that for it here.
// if (element.type === Form) {
// throw new Error("A Form element should never be a child of another Form or FormScope element.");
// }
if (element.type === FormScope) {
// For example:
//<Form formstate={formstate} form={form}>
// <FormField name='email'>
// <Input type='text'/>
// </FormField>
// <FormScope name='address'>
// <FormField name='line1'>
// <Input type='text'/>
// </FormField>
// </FormScope>
//</Form>
validateNameRequired(element.props, true); // let the FormScope component do the work.
return /*#__PURE__*/_react["default"].cloneElement(element, {
formstate: formstate,
form: form,
activeModelKey: activeModelKey
}, // Merge in the props it needs.
element.props.children);
}
if (element.type === FormField) {
// For example:
//<Form formstate={formstate} form={form}>
// <FormField name='email'>
// <Input type='text'/>
// </FormField>
//</Form>
validateNameRequired(element.props, false); // let the FormField component do the work.
return /*#__PURE__*/_react["default"].cloneElement(element, {
formstate: formstate,
form: form,
activeModelKey: activeModelKey
}, // Merge in the props it needs.
element.props.children);
}
function addPropsToChildren(children) {
return children && _react["default"].Children.map(children, function (child) {
return addProps(formstate, form, activeModelKey, child);
});
}
if (form.adaptors) {
if (!Array.isArray(form.adaptors)) {
throw new Error('The "form.adaptors" option must be an array.');
}
for (var i = 0, len = form.adaptors.length; i < len; i++) {
if (form.adaptors[i] === element.type) {
// For example:
//<Form formstate={formstate} form={form}>
// <FormField name='email'>
// <FormGroup>
// <Form.Label>Email</Form.Label>
// <Input type='text'/>
// <InputFeedback/>
// </FormGroup>
// </FormField>
//</Form>
// Let the user override the "activeModelKey" if necessary...
var modelKey = activeModelKey;
if ((0, _helperFunctions.hasProp)(element.props, 'name')) {
// as of version 1.2 prefer name prop for adaptors
modelKey = (0, _helperFunctions.normalizeModelKey)(element.props.name);
} else if ((0, _helperFunctions.hasProp)(element.props, 'modelKey')) {
modelKey = (0, _helperFunctions.normalizeModelKey)(element.props.modelKey);
}
return /*#__PURE__*/_react["default"].cloneElement(element, {
formstate: formstate,
form: form,
modelKey: modelKey
}, // Merge in additional props to pass to the adaptor.
addPropsToChildren(element.props.children));
}
}
}
if ((0, _helperFunctions.hasProp)(element.props, 'nestedForm')) {
// For example:
//<FormScope name='homeContact'>
// <Contact nestedForm>
// <FormScope name='address'>
// <Address nestedForm/>
// </FormScope>
// </Contact>
//</FormScope>
// In the Contact component:
//<FormScope formstate={props.formstate} form={props.form}>
// <FormField name='name'>
// <Input type='text'/>
// </FormField>
// {children}
//</FormScope>
// The nested component should operate within a nested component scope that is completely ignorant of anything above it.
var newNestedScopeId = (0, _schemaAndLookup.getId)(formstate, activeModelKey);
var _createNestedScope = (0, _schemaAndLookup.createNestedScope)(newNestedScopeId, formstate, form),
_createNestedScope2 = (0, _slicedToArray2["default"])(_createNestedScope, 2),
nestedFormstate = _createNestedScope2[0],
nestedForm = _createNestedScope2[1];
return /*#__PURE__*/_react["default"].cloneElement(element, {
formstate: nestedFormstate,
form: nestedForm
}, // Merge in additional props to pass to the nested component.
addPropsToChildren(element.props.children));
} // else
if (element.props.children) {
return /*#__PURE__*/_react["default"].cloneElement(element, {}, // Don't need to pass rff-related props to something like a div, but we do need to look through its children.
addPropsToChildren(element.props.children));
}
return element;
}