UNPKG

mobx-autoform

Version:

Ridiculously simple form state management with mobx

203 lines (202 loc) 6.62 kB
import F from "futil"; import _ from "lodash/fp.js"; import { extendObservable, reaction } from "mobx"; import * as validators from "./validators.js"; import { tokenizePath, safeJoinPaths, gatherFormValues, ValidationError } from "./util.js"; import { treePath, omitByPrefixes, pickByPrefixes } from "./futil.js"; import { get, set, toJS, observable } from "./mobx.js"; let changed = (x, y) => !_.isEqual(x, y) && !(F.isBlank(x) && F.isBlank(y)); let Command = F.aspects.command((x) => (y) => extendObservable(y, x)); let jsonSchemaKeys = { label: "title", fields: "properties", itemField: "items", defaultValue: "default" }; let legacyKeys = { label: "label", fields: "fields", itemField: "itemField", defaultValue: "value" }; let defaultGetPatch = (form) => _.mapValues("to", F.diff(form.saved.value, toJS(form.value))); let defaultGetSnapshot = (form) => F.flattenObject(toJS(gatherFormValues(form))); let defaultGetNestedSnapshot = (form) => F.unflattenObject(form.getSnapshot()); const spliceErrors = (errors, parentPath, nodePosition) => { const parent = parentPath.join("."); const parentLength = parentPath.length; const arrayErrors = pickByPrefixes([parent], errors); const newErrors = omitByPrefixes([parent], errors); for (const [key, value] of Object.entries(arrayErrors)) { const keyFields = key.split("."); const idx = parseInt(keyFields[parentLength]); if (idx > nodePosition) { keyFields.splice(parentLength, 1, idx - 1); const newKey = keyFields.join("."); newErrors[newKey] = value; } else if (idx < nodePosition) { newErrors[key] = value; } } return newErrors; }; var src_default = ({ submit: configSubmit, value = {}, afterInitField = (x) => x, validate = validators.functions, identifier = "unknown", keys = legacyKeys, getPatch = defaultGetPatch, getSnapshot = defaultGetSnapshot, getNestedSnapshot = defaultGetNestedSnapshot, ...autoFormConfig }) => { let fieldPath = _.flow(F.intersperse(keys.fields), _.compact); let flattenField = F.flattenTree((x) => x[keys.fields])( (...x) => _.join(".", treePath(...x)) ); let saved = {}; let state = observable({ value, errors: {}, disposers: {} }); let initField = (config, rootPath = []) => { let dotPath = _.join(".", rootPath); let valuePath = ["value", ...rootPath]; let node = observable({ ...config, field: _.last(rootPath), [keys.label]: config[keys.label] || _.startCase(_.last(rootPath)), "data-testid": _.snakeCase([identifier, ...rootPath]), get value() { return get(valuePath, state); }, set value(x) { set(valuePath, x, state); }, get errors() { return get(_.compact(["errors", dotPath]), state) || []; }, get isValid() { return _.isEmpty(node.errors); }, get isDirty() { return changed(_.get(valuePath, saved), toJS(node.value)); }, reset() { node.value = toJS(_.get(valuePath, saved)); state.errors = omitByPrefixes([dotPath], state.errors); }, validate(paths = [dotPath]) { let errors = validate(form, pickByPrefixes(paths, flattenField(form))); state.errors = { ...omitByPrefixes(paths, state.errors), ...errors }; return errors; }, clean() { F.setOn(valuePath, toJS(node.value), saved); }, getField(path) { return _.get( safeJoinPaths(fieldPath(tokenizePath(path))), node[keys.fields] ); }, dispose() { _.over(_.values(pickByPrefixes([dotPath], state.disposers)))(); state.disposers = omitByPrefixes([dotPath], state.disposers); }, remove() { let parent = form.getField(_.dropRight(1, rootPath)) || form; if (parent[keys.itemField]) { parent.value.splice(node.field, 1); state.errors = spliceErrors(state.errors, parent.path, node.field); } else { node.dispose(); F.unsetOn(node.field, parent.value); F.unsetOn(node.field, parent[keys.fields]); state.errors = omitByPrefixes([dotPath], state.errors, node.field); } } }); node.path = rootPath; if (_.isUndefined(node.value) && !_.isUndefined(config[keys.defaultValue])) node.value = toJS(config[keys.defaultValue]); if (node[keys.fields]) node.add = (configs) => extendObservable( node[keys.fields], F.mapValuesIndexed((x, k) => initTree(x, [...rootPath, k]), configs) ); if (node[keys.itemField]) { node[keys.fields] = observable([]); state.disposers[dotPath] = reaction( () => _.size(node.value), (size) => { _.each((x) => x.dispose(), node[keys.fields]); node[keys.fields].replace( _.times( (index) => initTree(node[keys.itemField], [...rootPath, index]), size ) ); } ); } return afterInitField(node, { ...config, keys }); }; let initTree = (config, rootPath = []) => F.reduceTree((x) => x[keys.fields])((tree, node, ...args) => { let path = treePath(node, ...args); let field = initField(node, [...rootPath, ...path]); if (node[keys.itemField]) node[keys.fields] = _.times( () => toJS(node[keys.itemField]), _.size(field.value) ); return _.isEmpty(path) ? field : set([keys.fields, ...fieldPath(path)], field, tree); })({})(toJS(config)); let form = extendObservable(initTree(autoFormConfig), { getPatch: () => getPatch(form), getSnapshot: () => getSnapshot(form), getNestedSnapshot: () => getNestedSnapshot(form), get submitError() { return F.getOrReturn("message", form.submit.state.error); } }); let submit = Command(async () => { if (_.isEmpty(form.validate())) { form.submit.state.error = null; try { return await configSubmit(form.getSnapshot(), form); } catch (err) { if (err instanceof ValidationError) { state.errors = err.cause; } throw err; } } throw "Validation Error"; }); extendObservable(form, { submit }); form.submit.state = submit.state; form.keys = keys; form.saved = saved; form.reset = F.aspectSync({ before: () => form.submit.state.error = null })(form.reset); F.unsetOn("field", form); F.unsetOn("remove", form); form.clean(); return form; }; export { ValidationError, src_default as default, jsonSchemaKeys, legacyKeys, validators };