mobx-autoform
Version:
Ridiculously simple form state management with mobx
203 lines (202 loc) • 6.62 kB
JavaScript
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
};