mobx-react-form
Version:
Reactive MobX Form State Management
1,199 lines (1,181 loc) • 121 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('mobx'), require('lodash')) :
typeof define === 'function' && define.amd ? define(['exports', 'mobx', 'lodash'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MobxReactForm = {}, global.mobx, global._));
})(this, (function (exports, mobx, lodash) { 'use strict';
/**
* ArrayMap: an ordered key-value collection backed by an observable array.
*
* Exposes the full ObservableMap-compatible API (get, set, has, delete,
* merge, forEach, keys, values, entries, intercept, etc.) while
* maintaining insertion order via an internal observable array.
*
* The observable array backing allows array-like operations such as
* `move(fromIndex, toIndex)` which directly re-orders entries via
* splice — fully MobX reactive.
*/
class ArrayMap {
/** Duck-typing marker for utility code */
_isArrayMap = true;
/** Internal observable array: entries stored as [key, value] pairs */
_entries = mobx.observable.array([]);
constructor(entries) {
if (entries && entries.length > 0) {
this._entries.replace(entries);
}
}
// ------------------------------------------------------------------
// ObservableMap-compatible API
// ------------------------------------------------------------------
get size() {
return this._entries.length;
}
get(key) {
const entry = this._entries.find(([k]) => k === key);
return entry ? entry[1] : undefined;
}
set(key, value) {
const existing = this._entries.find(([k]) => k === key);
if (existing) {
existing[1] = value;
}
else {
this._entries.push([key, value]);
}
return this;
}
has(key) {
return this._entries.some(([k]) => k === key);
}
delete(key) {
const index = this._entries.findIndex(([k]) => k === key);
if (index !== -1) {
this._entries.splice(index, 1);
return true;
}
return false;
}
clear() {
this._entries.clear();
}
keys() {
return this._entries.map(([k]) => k)[Symbol.iterator]();
}
values() {
return this._entries.map(([, v]) => v)[Symbol.iterator]();
}
entries() {
return this._entries[Symbol.iterator]();
}
forEach(callbackfn, thisArg) {
this._entries.forEach(([key, value]) => {
callbackfn.call(thisArg, value, key, this);
});
}
toJSON() {
const result = {};
this._entries.forEach(([key, value]) => {
result[String(key)] = value;
});
return result;
}
toString() {
return `[object ArrayMap]`;
}
merge(other) {
if (other && typeof other === "object") {
if (other.forEach && typeof other.forEach === "function") {
// ObservableMap or Map-like
other.forEach((value, key) => {
this.set(key, value);
});
}
else if (Array.isArray(other)) {
// Array of [key, value] pairs
other.forEach(([key, value]) => {
this.set(key, value);
});
}
else {
// Plain object { key: value }
Object.keys(other).forEach((key) => {
this.set(key, other[key]);
});
}
}
return this;
}
replace(values) {
this.clear();
return this.merge(values);
}
get intercept() {
return this._entries.intercept;
}
[Symbol.iterator]() {
return this._entries[Symbol.iterator]();
}
// ------------------------------------------------------------------
// Array-specific methods
// ------------------------------------------------------------------
/**
* Move an entry from `fromIndex` to `toIndex`.
*
* Directly splices the internal observable array, so MobX
* tracks the change as a single array splice operation.
*/
move(fromIndex, toIndex) {
mobx.runInAction(() => {
if (fromIndex < 0 || fromIndex >= this._entries.length)
return;
if (toIndex < 0 || toIndex >= this._entries.length)
return;
if (fromIndex === toIndex)
return;
const [entry] = this._entries.splice(fromIndex, 1);
this._entries.splice(toIndex, 0, entry);
});
}
/**
* Expose the underlying observable array for direct MobX
* observation (e.g. `observe(arr, cb)` from mobx).
*/
toArray() {
return this._entries;
}
}
var FieldPropsEnum;
(function (FieldPropsEnum) {
FieldPropsEnum["key"] = "key";
FieldPropsEnum["id"] = "id";
FieldPropsEnum["path"] = "path";
FieldPropsEnum["name"] = "name";
FieldPropsEnum["fields"] = "fields";
FieldPropsEnum["ref"] = "ref";
FieldPropsEnum["type"] = "type";
FieldPropsEnum["computed"] = "computed";
FieldPropsEnum["value"] = "value";
FieldPropsEnum["initial"] = "initial";
FieldPropsEnum["default"] = "default";
FieldPropsEnum["checked"] = "checked";
FieldPropsEnum["label"] = "label";
FieldPropsEnum["placeholder"] = "placeholder";
FieldPropsEnum["error"] = "error";
FieldPropsEnum["validatedWith"] = "validatedWith";
FieldPropsEnum["validators"] = "validators";
FieldPropsEnum["rules"] = "rules";
FieldPropsEnum["related"] = "related";
FieldPropsEnum["options"] = "options";
FieldPropsEnum["extra"] = "extra";
FieldPropsEnum["bindings"] = "bindings";
FieldPropsEnum["hooks"] = "hooks";
FieldPropsEnum["handlers"] = "handlers";
FieldPropsEnum["converter"] = "converter";
FieldPropsEnum["input"] = "input";
FieldPropsEnum["output"] = "output";
FieldPropsEnum["interceptors"] = "interceptors";
FieldPropsEnum["observers"] = "observers";
// computed
FieldPropsEnum["disabled"] = "disabled";
FieldPropsEnum["deleted"] = "deleted";
FieldPropsEnum["blurred"] = "blurred";
FieldPropsEnum["validating"] = "validating";
FieldPropsEnum["submitting"] = "submitting";
FieldPropsEnum["clearing"] = "clearing";
FieldPropsEnum["resetting"] = "resetting";
FieldPropsEnum["changed"] = "changed";
FieldPropsEnum["touched"] = "touched";
FieldPropsEnum["focused"] = "focused";
FieldPropsEnum["isEmpty"] = "isEmpty";
FieldPropsEnum["isDefault"] = "isDefault";
FieldPropsEnum["isPristine"] = "isPristine";
FieldPropsEnum["isDirty"] = "isDirty";
FieldPropsEnum["isValid"] = "isValid";
FieldPropsEnum["hasError"] = "hasError";
// handlers
FieldPropsEnum["onInit"] = "onInit";
FieldPropsEnum["onSync"] = "onSync";
FieldPropsEnum["onChange"] = "onChange";
FieldPropsEnum["onBlur"] = "onBlur";
FieldPropsEnum["onFocus"] = "onFocus";
FieldPropsEnum["onToggle"] = "onToggle";
FieldPropsEnum["onDrop"] = "onDrop";
FieldPropsEnum["onSubmit"] = "onSubmit";
FieldPropsEnum["onReset"] = "onReset";
FieldPropsEnum["onClear"] = "onClear";
FieldPropsEnum["onAdd"] = "onAdd";
FieldPropsEnum["onDel"] = "onDel";
FieldPropsEnum["autoFocus"] = "autoFocus";
FieldPropsEnum["inputMode"] = "inputMode";
FieldPropsEnum["onKeyDown"] = "onKeyDown";
FieldPropsEnum["onKeyUp"] = "onKeyUp";
FieldPropsEnum["class"] = "class";
FieldPropsEnum["nullable"] = "nullable";
FieldPropsEnum["autoComplete"] = "autoComplete";
})(FieldPropsEnum || (FieldPropsEnum = {}));
var AllowedFieldPropsTypes;
(function (AllowedFieldPropsTypes) {
AllowedFieldPropsTypes["computed"] = "computed";
AllowedFieldPropsTypes["observable"] = "observable";
AllowedFieldPropsTypes["editable"] = "editable";
AllowedFieldPropsTypes["all"] = "all";
})(AllowedFieldPropsTypes || (AllowedFieldPropsTypes = {}));
var FieldPropsOccurrence;
(function (FieldPropsOccurrence) {
FieldPropsOccurrence["some"] = "some";
FieldPropsOccurrence["every"] = "every";
})(FieldPropsOccurrence || (FieldPropsOccurrence = {}));
var SeparatedPropsMode;
(function (SeparatedPropsMode) {
SeparatedPropsMode["computed"] = "computed";
SeparatedPropsMode["values"] = "values";
SeparatedPropsMode["labels"] = "labels";
SeparatedPropsMode["placeholders"] = "placeholders";
SeparatedPropsMode["defaults"] = "defaults";
SeparatedPropsMode["initials"] = "initials";
SeparatedPropsMode["disabled"] = "disabled";
SeparatedPropsMode["deleted"] = "deleted";
SeparatedPropsMode["types"] = "types";
SeparatedPropsMode["related"] = "related";
SeparatedPropsMode["rules"] = "rules";
SeparatedPropsMode["options"] = "options";
SeparatedPropsMode["bindings"] = "bindings";
SeparatedPropsMode["extra"] = "extra";
SeparatedPropsMode["hooks"] = "hooks";
SeparatedPropsMode["handlers"] = "handlers";
SeparatedPropsMode["validatedWith"] = "validatedWith";
SeparatedPropsMode["validators"] = "validators";
SeparatedPropsMode["observers"] = "observers";
SeparatedPropsMode["interceptors"] = "interceptors";
SeparatedPropsMode["converters"] = "converters";
SeparatedPropsMode["input"] = "input";
SeparatedPropsMode["output"] = "output";
SeparatedPropsMode["autoFocus"] = "autoFocus";
SeparatedPropsMode["inputMode"] = "inputMode";
SeparatedPropsMode["refs"] = "refs";
SeparatedPropsMode["classes"] = "classes";
SeparatedPropsMode["nullable"] = "nullable";
SeparatedPropsMode["autoComplete"] = "autoComplete";
})(SeparatedPropsMode || (SeparatedPropsMode = {}));
const props = {
editable: [
FieldPropsEnum.type,
FieldPropsEnum.value,
FieldPropsEnum.initial,
FieldPropsEnum.default,
FieldPropsEnum.label,
FieldPropsEnum.placeholder,
FieldPropsEnum.related,
FieldPropsEnum.options,
FieldPropsEnum.extra,
FieldPropsEnum.bindings,
FieldPropsEnum.hooks,
FieldPropsEnum.handlers,
FieldPropsEnum.deleted,
FieldPropsEnum.disabled,
FieldPropsEnum.autoFocus,
FieldPropsEnum.inputMode,
FieldPropsEnum.ref,
FieldPropsEnum.nullable,
FieldPropsEnum.autoComplete,
],
handlers: [
FieldPropsEnum.onChange,
FieldPropsEnum.onToggle,
FieldPropsEnum.onFocus,
FieldPropsEnum.onBlur,
FieldPropsEnum.onDrop,
FieldPropsEnum.onSubmit,
FieldPropsEnum.onReset,
FieldPropsEnum.onClear,
FieldPropsEnum.onAdd,
FieldPropsEnum.onDel,
],
computed: [
FieldPropsEnum.error,
FieldPropsEnum.hasError,
FieldPropsEnum.isValid,
FieldPropsEnum.isDirty,
FieldPropsEnum.isPristine,
FieldPropsEnum.isDefault,
FieldPropsEnum.isEmpty,
FieldPropsEnum.focused,
FieldPropsEnum.touched,
FieldPropsEnum.changed,
FieldPropsEnum.validating,
FieldPropsEnum.submitting,
FieldPropsEnum.resetting,
FieldPropsEnum.clearing,
FieldPropsEnum.blurred,
FieldPropsEnum.deleted,
FieldPropsEnum.disabled,
],
separated: [
SeparatedPropsMode.computed,
SeparatedPropsMode.values,
SeparatedPropsMode.labels,
SeparatedPropsMode.placeholders,
SeparatedPropsMode.defaults,
SeparatedPropsMode.initials,
SeparatedPropsMode.disabled,
SeparatedPropsMode.deleted,
SeparatedPropsMode.types,
SeparatedPropsMode.related,
SeparatedPropsMode.rules,
SeparatedPropsMode.options,
SeparatedPropsMode.bindings,
SeparatedPropsMode.extra,
SeparatedPropsMode.hooks,
SeparatedPropsMode.handlers,
SeparatedPropsMode.validatedWith,
SeparatedPropsMode.validators,
SeparatedPropsMode.observers,
SeparatedPropsMode.interceptors,
SeparatedPropsMode.converters,
SeparatedPropsMode.input,
SeparatedPropsMode.output,
SeparatedPropsMode.autoFocus,
SeparatedPropsMode.inputMode,
SeparatedPropsMode.refs,
SeparatedPropsMode.classes,
SeparatedPropsMode.nullable,
SeparatedPropsMode.autoComplete,
],
functions: [
FieldPropsEnum.computed,
FieldPropsEnum.observers,
FieldPropsEnum.interceptors,
FieldPropsEnum.converter,
FieldPropsEnum.input,
FieldPropsEnum.output,
],
validation: [
FieldPropsEnum.rules,
FieldPropsEnum.validators,
FieldPropsEnum.validatedWith,
],
occurrences: {
isDirty: FieldPropsOccurrence.some,
isPristine: FieldPropsOccurrence.every,
isDefault: FieldPropsOccurrence.every,
isValid: FieldPropsOccurrence.every,
isEmpty: FieldPropsOccurrence.every,
hasError: FieldPropsOccurrence.some,
focused: FieldPropsOccurrence.some,
blurred: FieldPropsOccurrence.some,
touched: FieldPropsOccurrence.some,
deleted: FieldPropsOccurrence.every,
disabled: FieldPropsOccurrence.every,
clearing: FieldPropsOccurrence.every,
resetting: FieldPropsOccurrence.every,
},
};
const getObservableMapValues = (fields) => {
// ArrayMap duck-typing
if (fields && fields._isArrayMap) {
const result = [];
fields.forEach((value) => result.push(value));
return result;
}
return mobx.values(fields);
};
const getObservableMapKeys = (fields) => {
// ArrayMap duck-typing
if (fields && fields._isArrayMap) {
return Array.from(fields.keys());
}
return mobx.keys(fields);
};
const checkObserveItem = (change) => ({ key, to, type, exec }) => change.type === type &&
change.name === key &&
change.newValue === to &&
exec.apply(change, [change]);
const checkObserve = (collection) => (change) => collection.map(checkObserveItem(change));
const checkPropOccurrence = ({ type, data }) => {
let $check;
switch (type) {
case FieldPropsOccurrence.some:
$check = ($data) => $data.some(Boolean);
break;
case FieldPropsOccurrence.every:
$check = ($data) => $data.every(Boolean);
break;
default: throw new Error('Occurrence not found for specified prop');
}
return $check(data);
};
const hasProps = ($type, $data) => {
let $props;
switch ($type) {
case AllowedFieldPropsTypes.computed:
$props = props.computed;
break;
case AllowedFieldPropsTypes.observable:
$props = [
FieldPropsEnum.fields,
...props.computed,
...props.editable,
];
break;
case AllowedFieldPropsTypes.editable:
$props = [
...props.editable,
...props.validation,
...props.functions,
...props.handlers,
];
break;
case AllowedFieldPropsTypes.all:
$props = [
FieldPropsEnum.id,
FieldPropsEnum.key,
FieldPropsEnum.name,
FieldPropsEnum.path,
...props.computed,
...props.editable,
...props.validation,
...props.functions,
...props.handlers,
];
break;
default:
$props = null;
}
return $data.filter((x) => $props.includes(x)).length > 0;
};
/**
Check Allowed Properties
*/
const allowedProps = (type, data) => {
if (hasProps(type, data))
return;
const $msg = "The selected property is not allowed";
throw new Error(`${$msg} (${JSON.stringify(data)})`);
};
/**
Throw Error if undefined Fields
*/
const throwError = (path, fields, msg = null) => {
if (fields != null)
return;
const $msg = msg == null ? "The selected field is not defined" : msg;
throw new Error(`${$msg} (${path})`);
};
const pathToStruct = (path) => {
let struct;
struct = path.replace(/\.\d+($|\.)/g, "[].");
struct = struct.replace("..", ".");
struct = struct.replace(/^\.+|\.+$/g, "");
return struct;
};
const isArrayFromStruct = (struct, structPath) => {
if (isArrayOfStrings(struct))
return !!struct
.filter((s) => s.startsWith(structPath))
.find((s) => s.substring(structPath.length) === "[]")
|| (struct?.find((e) => e === structPath)?.endsWith('[]') ?? false);
else
return false;
};
const hasSome = (obj, keys) => keys.some((key) => lodash.has(obj, key));
const isEmptyArray = (field) => lodash.isEmpty(field) && Array.isArray(field);
const isArrayOfStrings = (struct) => Array.isArray(struct) && struct.every((s) => typeof s === 'string');
const isArrayOfObjects = (fields) => Array.isArray(fields) && fields.every((f) => lodash.isPlainObject(f));
const getKeys = (fields) => fields ? [...new Set(Object.values(fields).flatMap((values) => values ? Object.keys(values) : []))] : [];
const hasUnifiedProps = ({ fields }) => !isArrayOfStrings({ fields }) && hasProps(AllowedFieldPropsTypes.editable, getKeys(fields));
const hasSeparatedProps = (initial) => hasSome(initial, props.separated) || hasSome(initial, props.validation);
const allowNested = (field, strictProps) => field !== null && typeof field === 'object' &&
!(field instanceof Date) &&
!lodash.has(field, FieldPropsEnum.fields) &&
!lodash.has(field, FieldPropsEnum.class) &&
(!hasSome(field, [
...props.editable,
...props.handlers,
...props.validation,
...props.functions,
]) || strictProps);
const parseIntKeys = (fields) => Array.from(getObservableMapKeys(fields)).map(Number);
const hasIntKeys = (fields) => parseIntKeys(fields).every((x) => Number.isInteger(x));
const maxKey = (fields) => {
const keys = parseIntKeys(fields);
const maxVal = keys.length ? Math.max(...keys) : undefined;
return maxVal === void 0 ? 0 : maxVal + 1;
};
let _idCounter = 0;
const _localUniqueId = (prefix) => `${prefix}${++_idCounter}`;
const uniqueId = (field) => _localUniqueId([field.path.replace(/\./g, "-"), "--"].join(""));
const isEvent = (obj) => {
if (obj == null || typeof Event === "undefined")
return false;
return obj instanceof Event || obj.target != null;
};
const hasFiles = ($) => $.target.files && $.target.files.length !== 0;
const isBool = ($, val) => typeof val === 'boolean' && typeof $.target.checked === 'boolean';
const $try = (...args) => {
for (const val of args) {
if (val !== undefined)
return val;
}
return undefined;
};
const defaultValue = ({ type = undefined, value = undefined, nullable = undefined, isEmptyArray = false, fallbackValueOption = "", }) => {
if (Array.isArray(value) || isEmptyArray)
return [];
if (nullable || value instanceof Date || type === "date" || type === "datetime-local")
return null;
if (typeof value === 'number' || type === "number")
return 0;
if (typeof value === 'boolean' || type === "checkbox")
return false;
if (typeof value === 'string' || type === "file")
return "";
return fallbackValueOption;
};
const parsePath = (path) => {
let $path = String(path ?? '');
$path = $path.replace(/\[/g, ".");
$path = $path.replace(/\]/g, "");
return $path;
};
const parseInput = (input, { fallbackValueOption = "", type, isEmptyArray, separated, unified, fallback }) => input($try(separated, unified, fallback, defaultValue({
fallbackValueOption,
type,
isEmptyArray,
})));
const parseArrayProp = (val, prop, removeNullishValuesInArrays) => {
const values = Object.values(val);
const isValProp = [
FieldPropsEnum.value,
FieldPropsEnum.initial,
FieldPropsEnum.default,
].includes(prop);
if (removeNullishValuesInArrays && isValProp) {
return values.filter(v => v !== null && v !== undefined && v !== "");
}
return values;
};
const parseCheckArray = (field, value, prop, removeNullishValuesInArrays) => {
if (field.incremental && value !== null && typeof value === 'object' && lodash.isEmpty(value))
return [];
return field.hasIncrementalKeys ? parseArrayProp(value, prop, removeNullishValuesInArrays) : value;
};
const parseCheckOutput = (field, prop, retrieveNullifiedEmptyStrings = false) => {
if (prop === FieldPropsEnum.value || prop.startsWith("value.")) {
const base = field.$output ? field.$output(field[FieldPropsEnum.value]) : field[FieldPropsEnum.value];
const value = prop.startsWith("value.") ? lodash.get(base, prop.substring(6)) : base;
if (typeof value === 'string' && lodash.isEmpty(value) && retrieveNullifiedEmptyStrings)
return null;
return value;
}
return field[prop];
};
const defineFieldsFromStruct = (struct, add = false) => struct.reduceRight(($, name) => {
const obj = {};
if (name.endsWith("[]")) {
const val = add ? [$] : [];
obj[name.replace(/\[\]$/, "")] = val;
return obj;
}
// no brakets
const prev = struct[struct.indexOf(name) - 1];
const stop = !!prev && prev.endsWith("[]") && struct[struct.length - 1] === name;
if (!add && stop)
return obj;
obj[name] = $;
return obj;
}, {});
const handleFieldsArrayOfStrings = ($fields, add = false) => {
let fields = $fields;
// handle array with field struct (strings)
if (isArrayOfStrings(fields)) {
fields = lodash.transform(fields, ($obj, $) => {
const pathStruct = $.split(".");
// as array of strings (with empty values)
if (!pathStruct.length)
return Object.assign($obj, { [$]: "" });
// define flat or nested fields from pathStruct
return lodash.merge($obj, defineFieldsFromStruct(pathStruct, add));
}, {});
}
return fields;
};
const handleFieldsArrayOfObjects = ($fields) => {
let fields = $fields;
// handle array of objects (with unified props)
if (isArrayOfObjects(fields)) {
fields = lodash.transform(fields, ($obj, field) => {
if (hasUnifiedProps({ fields: { field } }) && !lodash.has(field, FieldPropsEnum.name))
return undefined;
return Object.assign($obj, { [field.name]: field });
}, {});
}
return fields;
};
const handleFieldsNested = (fields, strictProps = true) => lodash.transform(fields, (obj, field, key) => {
if (allowNested(field, strictProps)) {
// define nested field
return Object.assign(obj, {
[key]: {
fields: isEmptyArray(field) ? [] : handleFieldsNested(field),
},
});
}
return Object.assign(obj, { [key]: field });
}, {});
/* mapNestedValuesToUnifiedValues
FROM:
{
street: '123 Fake St.',
zip: '12345',
}
TO:
[{
name: 'street'
value: '123 Fake St.',
}, {
name: 'zip'
value: '12345',
}]
*/
const mapNestedValuesToUnifiedValues = (data) => lodash.isPlainObject(data)
? Object.entries(data).map(([name, value]) => ({ value, name }))
: undefined;
/* reduceValuesToUnifiedFields
FROM:
{
name: 'fatty',
address: {
street: '123 Fake St.',
zip: '12345',
},
};
TO:
{
name: {
value: 'fatty',
fields: undefined
},
address: {
value: {
street: '123 Fake St.',
zip: '12345'
},
fields: [ ... ]
},
};
*/
const reduceValuesToUnifiedFields = (values) => lodash.transform(values, (obj, value, key) => Object.assign(obj, {
[key]: {
value,
fields: mapNestedValuesToUnifiedValues(value),
},
}), {});
/*
Fallback Unified Props to Separated Mode
*/
const handleFieldsPropsFallback = (fields, initial, fallback) => {
if (!lodash.has(initial, SeparatedPropsMode.values))
return fields;
// if the 'values' object is passed in constructor
// then update the fields definitions
let { values } = initial;
if (hasUnifiedProps({ fields: initial.fields })) {
values = reduceValuesToUnifiedFields(values);
}
return lodash.merge(fields, lodash.transform(values, (result, v, k) => {
if (Array.isArray(fields[k]))
result[k] = v;
if (!(k in fields) && (!isNaN(Number(k)) || fallback))
result[k] = v;
}, {}));
};
const mergeSchemaDefaults = (fields, validator) => {
if (validator) {
const schema = lodash.get(validator.plugins, "svk.config.schema");
if (lodash.isEmpty(fields) && schema && !!schema.properties) {
lodash.each(schema.properties, (prop, key) => {
lodash.set(fields, key, {
value: prop.default,
label: prop.title,
});
});
}
}
return fields;
};
const prepareFieldsData = (initial, strictProps = true, fallback = true) => {
let fields = lodash.merge(handleFieldsArrayOfStrings(initial.fields, false), handleFieldsArrayOfStrings(initial.struct, false));
fields = handleFieldsArrayOfObjects(fields);
fields = handleFieldsPropsFallback(fields, initial, fallback);
fields = handleFieldsNested(fields, strictProps);
return fields;
};
const pathToFieldsTree = (struct, path, n = 0, add = false) => {
const $struct = (Array.isArray(struct) ? struct : Object.values(struct)).filter((item) => typeof item === 'string');
const structPath = pathToStruct(path);
const structArray = $struct.filter((item) => item.startsWith(structPath));
const $tree = handleFieldsArrayOfStrings(structArray, add);
const $structPath = structPath.replace(/\[\]/g, `[${n}]`);
const fields = handleFieldsNested(lodash.get($tree, $structPath));
// fix issues #614 & #615
$struct.length && $struct
.filter(s => s.startsWith(path + '[]'))
.map(s => s.substring((path + '[].').length))
.filter(s => s.endsWith('[]'))
.map(s => s.substring(0, s.length - 2))
.forEach(s => {
const ss = s.split('.');
let t = fields[0]?.fields;
for (let i = 0; i < ss.length; i++) {
t = t?.[ss[i]]?.[FieldPropsEnum.fields];
if (!t)
break;
}
if (t)
delete t[0];
});
return fields;
};
var OptionsEnum;
(function (OptionsEnum) {
OptionsEnum["uniqueId"] = "uniqueId";
OptionsEnum["fallback"] = "fallback";
OptionsEnum["fallbackValue"] = "fallbackValue";
OptionsEnum["defaultGenericError"] = "defaultGenericError";
OptionsEnum["submitThrowsError"] = "submitThrowsError";
OptionsEnum["showErrorsOnInit"] = "showErrorsOnInit";
OptionsEnum["showErrorsOnSubmit"] = "showErrorsOnSubmit";
OptionsEnum["showErrorsOnBlur"] = "showErrorsOnBlur";
OptionsEnum["showErrorsOnChange"] = "showErrorsOnChange";
OptionsEnum["showErrorsOnClear"] = "showErrorsOnClear";
OptionsEnum["showErrorsOnReset"] = "showErrorsOnReset";
OptionsEnum["validateOnInit"] = "validateOnInit";
OptionsEnum["validateOnSubmit"] = "validateOnSubmit";
OptionsEnum["validateOnBlur"] = "validateOnBlur";
OptionsEnum["validateOnChange"] = "validateOnChange";
OptionsEnum["validateOnChangeAfterInitialBlur"] = "validateOnChangeAfterInitialBlur";
OptionsEnum["validateOnChangeAfterSubmit"] = "validateOnChangeAfterSubmit";
OptionsEnum["validateDisabledFields"] = "validateDisabledFields";
OptionsEnum["validateDeletedFields"] = "validateDeletedFields";
OptionsEnum["validatePristineFields"] = "validatePristineFields";
OptionsEnum["validateTrimmedValue"] = "validateTrimmedValue";
OptionsEnum["validateOnClear"] = "validateOnClear";
OptionsEnum["validateOnReset"] = "validateOnReset";
OptionsEnum["strictSet"] = "strictSet";
OptionsEnum["strictUpdate"] = "strictUpdate";
OptionsEnum["strictDelete"] = "strictDelete";
OptionsEnum["strictSelect"] = "strictSelect";
OptionsEnum["softDelete"] = "softDelete";
OptionsEnum["retrieveOnlyDirtyFieldsValues"] = "retrieveOnlyDirtyFieldsValues";
OptionsEnum["retrieveOnlyEnabledFieldsValues"] = "retrieveOnlyEnabledFieldsValues";
OptionsEnum["retrieveOnlyEnabledFieldsErrors"] = "retrieveOnlyEnabledFieldsErrors";
OptionsEnum["retrieveNullifiedEmptyStrings"] = "retrieveNullifiedEmptyStrings";
OptionsEnum["removeNullishValuesInArrays"] = "removeNullishValuesInArrays";
OptionsEnum["preserveDeletedFieldsValues"] = "preserveDeletedFieldsValues";
OptionsEnum["autoTrimValue"] = "autoTrimValue";
OptionsEnum["autoParseNumbers"] = "autoParseNumbers";
OptionsEnum["validationDebounceWait"] = "validationDebounceWait";
OptionsEnum["validationDebounceOptions"] = "validationDebounceOptions";
OptionsEnum["stopValidationOnError"] = "stopValidationOnError";
OptionsEnum["validationPluginsOrder"] = "validationPluginsOrder";
OptionsEnum["resetValidationBeforeValidate"] = "resetValidationBeforeValidate";
OptionsEnum["applyInputConverterOnInit"] = "applyInputConverterOnInit";
OptionsEnum["applyInputConverterOnSet"] = "applyInputConverterOnSet";
OptionsEnum["applyInputConverterOnUpdate"] = "applyInputConverterOnUpdate";
OptionsEnum["bubbleUpErrorMessages"] = "bubbleUpErrorMessages";
})(OptionsEnum || (OptionsEnum = {}));
var ValidationHooks;
(function (ValidationHooks) {
ValidationHooks["onSuccess"] = "onSuccess";
ValidationHooks["onError"] = "onError";
})(ValidationHooks || (ValidationHooks = {}));
class Base {
noop = () => { };
state;
fields = new ArrayMap();
path;
$submitted = 0;
$submitting = false;
$validated = 0;
$validating = false;
$clearing = false;
$resetting = false;
$touched = false;
$changed = 0;
$hooks = {};
$handlers = {};
constructor() {
mobx.makeObservable(this, {
$submitted: mobx.observable,
$submitting: mobx.observable,
$validated: mobx.observable,
$validating: mobx.observable,
$clearing: mobx.observable,
$resetting: mobx.observable,
$touched: mobx.observable,
$changed: mobx.observable,
$hooks: mobx.observable,
$handlers: mobx.observable,
changed: mobx.computed,
submitted: mobx.computed,
submitting: mobx.computed,
validated: mobx.computed,
validating: mobx.computed,
clearing: mobx.computed,
resetting: mobx.computed,
hasIncrementalKeys: mobx.computed,
hasNestedFields: mobx.computed,
size: mobx.computed,
// initialization
initField: mobx.action,
// actions
submit: mobx.action,
deepUpdate: mobx.action,
set: mobx.action,
add: mobx.action,
del: mobx.action,
});
}
execHook = (name, fallback = {}) => $try(fallback[name], this.$hooks[name], this.noop).apply(this, [this]);
execHandler = (name, args, fallback = undefined, hook = null, execHook = true) => [
$try(this.$handlers[name] && this.$handlers[name].apply(this, [this]), fallback, this.noop).apply(this, [...args]),
execHook && this.execHook(hook || name),
];
get resetting() {
return this.hasNestedFields
? this.check(FieldPropsEnum.resetting, true)
: this.$resetting;
}
get clearing() {
return this.hasNestedFields
? this.check(FieldPropsEnum.clearing, true)
: this.$clearing;
}
get submitted() {
return mobx.toJS(this.$submitted);
}
get submitting() {
return mobx.toJS(this.$submitting);
}
get validated() {
return mobx.toJS(this.$validated);
}
get validating() {
return mobx.toJS(this.$validating);
}
get hasIncrementalKeys() {
return !!this.fields.size && hasIntKeys(this.fields);
}
get hasNestedFields() {
return this.fields.size !== 0;
}
get size() {
return this.fields.size;
}
get changed() {
return this.path != null && this.hasNestedFields
? this.reduce((acc, field) => acc + field.changed, 0) + this.$changed
: this.$changed;
}
/**
Interceptor
*/
intercept = (opt) => this.MOBXEvent(typeof opt === "function"
? { type: "interceptor", call: opt }
: { type: "interceptor", ...opt });
/**
Observer
*/
observe = (opt) => this.MOBXEvent(typeof opt === "function"
? { type: "observer", call: opt }
: { type: "observer", ...opt });
/**
Event Handler: On Clear
*/
onClear = (...args) => this.execHandler(FieldPropsEnum.onClear, args, (e) => {
isEvent(e) && e.preventDefault();
this.clear(true, false);
});
/**
Event Handler: On Reset
*/
onReset = (...args) => this.execHandler(FieldPropsEnum.onReset, args, (e) => {
isEvent(e) && e.preventDefault();
this.reset(true, false);
});
/**
Event Handler: On Submit
*/
onSubmit = (...args) => this.execHandler(FieldPropsEnum.onSubmit, args, (e, o = {}) => {
isEvent(e) && e.preventDefault();
this.submit(o);
}, null, false);
/**
Event Handler: On Add
*/
onAdd = (...args) => this.execHandler(FieldPropsEnum.onAdd, args, (e, val) => {
isEvent(e) && e.preventDefault();
this.add(isEvent(val) ? null : val, false);
});
/**
Event Handler: On Del
*/
onDel = (...args) => this.execHandler(FieldPropsEnum.onDel, args, (e, path) => {
isEvent(e) && e.preventDefault();
this.del(isEvent(path) ? this.path : path, false);
});
/******************************************************************
Initializer
*/
initFields(initial, update = false) {
const fallback = this.state.options.get(OptionsEnum.fallback);
const $path = (key) => [this.path, key].join(".").replace(/^\.+/, "");
let fields;
fields = prepareFieldsData(initial, this.state.strict, fallback);
fields = mergeSchemaDefaults(fields, this.validator);
// create fields
lodash.forIn(fields, (field, key) => {
const path = $path(key);
const $f = this.select(path, null, false);
if ($f == null) {
if (fallback) {
this.initField(key, path, field, update);
}
else {
const structPath = pathToStruct(path);
const struct = this.state.struct();
const found = struct
.filter((s) => s.startsWith(structPath))
.find((s) => s.charAt(structPath.length) === "." ||
s.substring(structPath.length, structPath.length + 2) ===
"[]" ||
s === structPath);
if (found)
this.initField(key, path, field, update);
}
}
});
}
initField(key, path, data, update = false) {
const initial = this.state.get("current", "props");
const struct = pathToStruct(path);
// try to get props from separated objects
const _try = (prop) => {
const t = lodash.get(initial[prop], struct);
if ([
FieldPropsEnum.input,
FieldPropsEnum.output,
FieldPropsEnum.converter,
].includes(prop) &&
typeof t !== "function")
return undefined;
return t;
};
const props = {
$value: lodash.get(initial[SeparatedPropsMode.values], path),
$computed: _try(SeparatedPropsMode.computed),
$label: _try(SeparatedPropsMode.labels),
$placeholder: _try(SeparatedPropsMode.placeholders),
$default: _try(SeparatedPropsMode.defaults),
$initial: _try(SeparatedPropsMode.initials),
$disabled: _try(SeparatedPropsMode.disabled),
$deleted: _try(SeparatedPropsMode.deleted),
$type: _try(SeparatedPropsMode.types),
$related: _try(SeparatedPropsMode.related),
$rules: _try(SeparatedPropsMode.rules),
$options: _try(SeparatedPropsMode.options),
$bindings: _try(SeparatedPropsMode.bindings),
$extra: _try(SeparatedPropsMode.extra),
$hooks: _try(SeparatedPropsMode.hooks),
$handlers: _try(SeparatedPropsMode.handlers),
$validatedWith: _try(SeparatedPropsMode.validatedWith),
$validators: _try(SeparatedPropsMode.validators),
$observers: _try(SeparatedPropsMode.observers),
$interceptors: _try(SeparatedPropsMode.interceptors),
$converters: _try(SeparatedPropsMode.converters),
$input: _try(SeparatedPropsMode.input),
$output: _try(SeparatedPropsMode.output),
$autoFocus: _try(SeparatedPropsMode.autoFocus),
$ref: _try(SeparatedPropsMode.refs),
$nullable: _try(SeparatedPropsMode.nullable),
$autoComplete: _try(SeparatedPropsMode.autoComplete),
};
const field = this.state.form.makeField({
key,
path,
struct,
data,
props,
update,
state: this.state,
}, (data && data[FieldPropsEnum.class]) || _try(SeparatedPropsMode.classes));
this.fields.merge({ [key]: field });
return field;
}
/******************************************************************
Actions
*/
validate(opt, obj) {
const $opt = lodash.merge(opt, { path: this.path });
return this.state.form.validator.validate($opt, obj);
}
/**
Submit
*/
submit(hooks = {}, { execOnSubmitHook = true, execValidationHooks = true, validate = true, } = {}) {
const execOnSubmit = () => this.execHook(FieldPropsEnum.onSubmit, hooks);
const submit = execOnSubmitHook ? execOnSubmit() : undefined;
this.$submitting = true;
this.$submitted += 1;
if (!validate ||
!this.state.options.get(OptionsEnum.validateOnSubmit, this)) {
return Promise.resolve(submit)
.then(mobx.action(() => (this.$submitting = false)))
.catch(mobx.action((err) => {
this.$submitting = false;
throw err;
}))
.then(() => this);
}
const exec = (isValid) => isValid
? this.execHook(ValidationHooks.onSuccess, hooks)
: this.execHook(ValidationHooks.onError, hooks);
return this.validate({
showErrors: this.state.options.get(OptionsEnum.showErrorsOnSubmit, this),
})
.then(({ isValid }) => {
const handler = execValidationHooks ? exec(isValid) : undefined;
if (isValid)
return Promise.all([submit, handler]);
const $err = this.state.options.get(OptionsEnum.defaultGenericError, this);
const $throw = this.state.options.get(OptionsEnum.submitThrowsError, this);
if ($throw && $err)
this.invalidate();
return Promise.all([submit, handler]);
})
.then(mobx.action(() => (this.$submitting = false)))
.catch(mobx.action((err) => {
this.$submitting = false;
throw err;
}))
.then(() => this);
}
/**
Check Field Computed Values
*/
check(prop, deep = false) {
allowedProps(AllowedFieldPropsTypes.computed, [prop]);
return deep
? checkPropOccurrence({
type: props.occurrences[prop],
data: this.deepCheck(props.occurrences[prop], prop, this.fields),
})
: this[prop];
}
deepCheck(type, prop, fields) {
const $fields = getObservableMapValues(fields);
return lodash.transform($fields, (check, field) => {
if (!field.fields.size || !Array.isArray(field.initial)) {
check.push(field[prop]);
}
check.push(checkPropOccurrence({
data: this.deepCheck(type, prop, field.fields),
type,
}));
return check;
}, []);
}
firstError() {
if (!this.state.options.get(OptionsEnum.bubbleUpErrorMessages, this))
return null;
for (const field of getObservableMapValues(this.fields)) {
if (field.error)
return field.error;
if (field.fields.size) {
const nested = field.firstError();
if (nested)
return nested;
}
}
return null;
}
/**
Update Field Values recurisvely
OR Create Field if 'undefined'
*/
update(fields) {
if (!lodash.isPlainObject(fields)) {
throw new Error("The update() method accepts only plain objects.");
}
this.deepUpdate(prepareFieldsData({ fields }, this.state.strict), undefined, undefined, fields);
}
deepUpdate(fields, path = "", recursion = true, raw) {
lodash.each(fields, (field, key) => {
const $key = lodash.has(field, FieldPropsEnum.name) ? field.name : key;
const $path = `${path}.${$key}`.replace(/^\.+/, "");
const strictUpdate = this.state.options.get(OptionsEnum.strictUpdate, this);
const $field = this.select($path, null, strictUpdate);
const $container = this.select(path, null, false) ||
this.state.form.select(this.path ?? '', null, false);
const applyInputConverterOnUpdate = this.state.options.get(OptionsEnum.applyInputConverterOnUpdate, this);
if ($field != null && field !== void 0) {
if (Array.isArray($field.values())) {
const n = Math.max(-1, ...Object.keys(field.fields || {}).map(Number));
getObservableMapValues($field.fields).forEach(($f) => {
if (Number($f.name) > n) {
$field.$changed++;
$field.state.form.$changed++;
$field.fields.delete($f.name);
}
});
}
if (field?.fields) {
const fallback = this.state.options.get(OptionsEnum.fallback);
const x = this.state
.struct()
.findIndex((s) => s.startsWith($field.path.replace(/\.\d+\./, "[].") + "[]"));
if (!fallback && $field.fields.size === 0 && x < 0) {
$field.value = parseInput(applyInputConverterOnUpdate ? $field.$input : (val) => val, {
fallbackValueOption: this.state.options.get(OptionsEnum.fallbackValue, this),
separated: lodash.get(raw, $path),
});
return;
}
}
if (field === null || field.fields == null) {
$field.value = parseInput(applyInputConverterOnUpdate ? $field.$input : (val) => val, {
fallbackValueOption: this.state.options.get(OptionsEnum.fallbackValue, this),
separated: field,
});
return;
}
}
if ($container != null && $field == null) {
// get full path when using update() with select() - FIX: #179
const $newFieldPath = [this.path, $path].join(".").replace(/^\.+/, "");
// init field into the container field
$container.$changed++;
$container.state.form.$changed++;
$container.initField($key, $newFieldPath, field, true);
}
else if (recursion) {
if (lodash.has(field, FieldPropsEnum.fields) && field.fields != null) {
// handle nested fields if defined
this.deepUpdate(field.fields, $path);
}
else {
// handle nested fields if undefined or null
const $fields = pathToFieldsTree(this.state.struct(), $path);
this.deepUpdate($fields, $path, false);
}
}
});
}
/**
Get Fields Props
*/
get(prop = null, strict = true) {
if (prop == null) {
return this.deepGet([...props.computed, ...props.editable, ...props.validation], this.fields, strict);
}
allowedProps(AllowedFieldPropsTypes.all, Ar