mobx-form
Version:
A simple form helper for mobx
547 lines (542 loc) • 15 kB
JavaScript
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
enumerable: true
});
return to;
};
var __moduleCache = /* @__PURE__ */ new WeakMap;
var __toCommonJS = (from) => {
var entry = __moduleCache.get(from), desc;
if (entry)
return entry;
entry = __defProp({}, "__esModule", { value: true });
if (from && typeof from === "object" || typeof from === "function")
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
get: () => from[key],
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
}));
__moduleCache.set(from, entry);
return entry;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
});
};
// src/index.ts
var exports_src = {};
__export(exports_src, {
createModelFromState: () => createModelFromState,
createModel: () => createModel,
FormModel: () => FormModel,
Field: () => Field
});
module.exports = __toCommonJS(exports_src);
// src/FormModel.ts
var import_mobx = require("mobx");
var import_trim = __toESM(require("lodash/trim"));
var import_debounce = __toESM(require("lodash/debounce"));
var toString = Object.prototype.toString;
var isObject = (o) => o && toString.call(o) === "[object Object]";
var isNullishOrEmpty = (value) => {
return typeof value === "undefined" || value === null || value === "";
};
class Field {
_name;
meta;
_model;
_waitForBlur = false;
_disabled = false;
_required = false;
_validatedOnce = false;
_clearErrorOnValueChange = false;
_hasValueFn;
get name() {
return this._name;
}
get model() {
return this._model;
}
get validatedAtLeastOnce() {
return this._validatedOnce;
}
get waitForBlur() {
return !!this._waitForBlur;
}
get disabled() {
return !!this._disabled;
}
get required() {
if (this.disabled)
return false;
return !!this._required;
}
resetInteractedFlag() {
this._interacted = false;
}
markAsInteracted() {
this._interacted = true;
}
resetValidatedOnce() {
this._validatedOnce = false;
}
get hasValue() {
if (this._hasValueFn) {
return this._hasValueFn(this.value);
}
if (Array.isArray(this.value)) {
return this.value.length > 0;
}
return !isNullishOrEmpty(this.value);
}
_validationTs = 0;
_validating = false;
_initialValue;
_value;
_interacted = false;
_blurredOnce = false;
get blurred() {
return !!this._blurredOnce;
}
rawError;
get errorMessage() {
return this.rawError?.message;
}
_autoValidate = false;
get autoValidate() {
return this._autoValidate;
}
_originalErrorMessage;
get valid() {
return !this.errorMessage;
}
get interacted() {
return this._interacted;
}
get value() {
return this._value;
}
_setValueOnly = (val) => {
if (!this._interacted) {
this._interacted = true;
}
if (this._value === val) {
return;
}
this._value = val;
};
_setValue = (val) => {
if (this._value !== val && this._clearErrorOnValueChange && !this.valid) {
this.resetError();
}
this._setValueOnly(val);
if (this._autoValidate) {
this._debouncedValidation?.();
}
};
set value(val) {
this._setValue(val);
}
setValue = (value, { resetInteractedFlag, commit } = {}) => {
if (resetInteractedFlag) {
this._setValueOnly(value);
this.rawError = undefined;
this._interacted = false;
} else {
this._setValue(value);
}
if (commit) {
this.commit();
}
};
restoreInitialValue = ({ resetInteractedFlag = true, commit = true } = {}) => {
this.setValue(this._initialValue, { resetInteractedFlag, commit });
};
get dirty() {
return this._initialValue !== this.value;
}
commit() {
this._initialValue = this.value;
}
resetError() {
this.rawError = undefined;
}
clearValidation() {
this.resetError();
}
markBlurredAndValidate = () => {
if (!this._blurredOnce) {
this._blurredOnce = true;
}
this.validate();
};
_validateFn;
_doValidate = async () => {
const { _validateFn, model } = this;
if (!_validateFn)
return Promise.resolve(true);
const invokeFn = async (vfn, field, fields, model2) => {
if (!vfn)
return true;
if (typeof vfn !== "function") {
throw new Error("Validator must be a function or a function[]");
}
ret = await vfn({ value: this.value, field, fields, model: model2 });
if (ret === false || ret?.error) {
return ret;
}
};
let ret;
if (Array.isArray(_validateFn)) {
for (let i = 0;i < _validateFn.length; i++) {
const vfn = _validateFn[i];
ret = await invokeFn(vfn, this, model.fields, model);
}
} else {
ret = await invokeFn(_validateFn, this, model.fields, model);
}
return ret;
};
setDisabled(disabled) {
if (disabled) {
this.resetError();
}
this._disabled = disabled;
}
validate = async (opts) => {
this._debouncedValidation?.cancel?.();
return await this._validate(opts);
};
get originalErrorMessage() {
return this._originalErrorMessage || `Validation for "${this.name}" failed`;
}
setValidating = (validating) => {
this._validating = validating;
};
get validating() {
return this._validating;
}
_validate = async ({ force = false } = {}) => {
const { required } = this;
if (!this._validatedOnce) {
this._validatedOnce = true;
}
const shouldSkipValidation = this.disabled || !required && !this._validateFn;
if (shouldSkipValidation)
return;
if (!force) {
const userDidntInteractedWithTheField = !this._interacted;
if (userDidntInteractedWithTheField && !this.hasValue) {
this.resetError();
return;
}
if (this.waitForBlur && !this._blurredOnce && !this.errorMessage) {
return;
}
} else {
this._blurredOnce = true;
}
if (required) {
if (!this.hasValue) {
this.setError({
message: typeof this._required === "string" ? this._required : `Field: "${this.name}" is required`
});
return;
}
this.resetError();
}
this.setValidating(true);
const validationTs = this._validationTs = Date.now();
let res;
try {
res = await this._doValidate();
if (validationTs !== this._validationTs)
return;
this.setValidating(false);
if (typeof res === "boolean") {
this.setErrorMessage(res ? undefined : this.originalErrorMessage);
return;
}
if (res?.error) {
this.setErrorMessage(res.error);
return;
}
this.resetError();
} catch (err) {
const errorArg = err;
if (validationTs !== this._validationTs)
return;
this.setValidating(false);
let errorToSet = errorArg;
const message = errorArg.message;
if (!message) {
errorToSet = {
...errorArg,
message: message || this.originalErrorMessage
};
}
const error = errorArg.error;
if (error) {
errorToSet = {
...errorToSet,
message: error
};
}
this.setError(errorToSet);
}
};
setRequired = (val) => {
this._required = val;
};
setErrorMessage = (msg) => {
if (import_trim.default(msg) === "") {
msg = undefined;
}
if (!msg) {
this.resetError();
} else {
this.setError({ message: msg });
}
};
setError = (error) => {
this.rawError = error;
};
get error() {
return this.errorMessage;
}
_debouncedValidation;
constructor(model, value, validatorDescriptor, fieldName) {
const DEBOUNCE_THRESHOLD = 300;
this._value = value;
this._model = model;
this._name = fieldName;
this._initialValue = value;
const {
waitForBlur,
disabled,
errorMessage,
validator,
hasValue,
required,
autoValidate = true,
meta,
validationDebounceThreshold = DEBOUNCE_THRESHOLD,
clearErrorOnValueChange
} = validatorDescriptor;
this._debouncedValidation = import_debounce.default(this._validate, validationDebounceThreshold);
this._waitForBlur = waitForBlur;
this._originalErrorMessage = errorMessage;
this._validateFn = validator;
this._clearErrorOnValueChange = clearErrorOnValueChange;
this._hasValueFn = hasValue;
this._required = required;
this._autoValidate = autoValidate;
this._disabled = disabled;
this.meta = meta;
import_mobx.makeAutoObservable(this, {
_value: import_mobx.observable.ref,
_initialValue: import_mobx.observable.ref
});
}
}
class FormModel {
get validatedAtLeastOnce() {
const keys = this._fieldKeys;
return keys.every((key) => this.fields[key].validatedAtLeastOnce);
}
get dataIsReady() {
return this.interacted && this.requiredAreFilled && this.valid;
}
get requiredFields() {
const keys = this._fieldKeys;
return keys.filter((key) => this.fields[key].required);
}
get requiredAreFilled() {
const keys = this._fieldKeys;
return keys.every((key) => {
const field = this.fields[key];
if (field.required) {
return !!field.hasValue;
}
return true;
});
}
fields = {};
_validating = false;
get valid() {
if (this._validating) {
return false;
}
const keys = this._fieldKeys;
return keys.every((key) => {
const field = this.fields[key];
return !!field.valid;
});
}
get interacted() {
const keys = this._fieldKeys;
return keys.some((key) => {
const field = this.fields[key];
return !!field.interacted;
});
}
restoreInitialValues(opts = {}) {
this._eachField((field) => field.restoreInitialValue(opts));
}
commit() {
this._eachField((field) => field.commit());
}
get dirty() {
return this._fieldKeys.some((key) => {
const f = this._getField(key);
return f.dirty;
});
}
updateFrom(obj, { resetInteractedFlag = true, ...opts } = {}) {
const keys = Object.keys(obj);
keys.forEach((key) => this.updateField(key, obj[key], { resetInteractedFlag, ...opts }));
}
get summary() {
return this._fieldKeys.reduce((seq, key) => {
const field = this.fields[key];
if (field.errorMessage) {
seq.push(field.errorMessage);
}
return seq;
}, []);
}
setValidating = (validating) => {
this._validating = validating;
};
get validating() {
return this._validating || this._fieldKeys.some((key) => {
const f = this._getField(key);
return f.validating;
});
}
validate = async () => {
this._validating = true;
try {
await Promise.all(this._fieldKeys.map((key) => {
const field = this.fields[key];
return field.validate({ force: true });
}));
this.setValidating(false);
} catch (_) {
this.setValidating(false);
}
};
updateField = (name, value, opts = {}) => {
const { throwIfMissingField, ...restOpts } = opts;
const theField = this._getField(name, { throwIfMissingField });
theField?.setValue(value, restOpts);
};
get serializedData() {
const keys = this._fieldKeys;
return keys.reduce((seq, key) => {
const field = this.fields[key];
const value = field.value;
const valueToSet = typeof value === "string" ? import_trim.default(value) : value;
seq[key] = valueToSet;
return seq;
}, {});
}
constructor(args) {
const { descriptors = {}, initialState, options = {} } = args || {};
this.addFields(descriptors);
if (initialState) {
this.updateFrom(initialState, {
throwIfMissingField: options.throwIfMissingField,
commit: true
});
}
import_mobx.makeAutoObservable(this, {
fields: import_mobx.observable.shallow
});
}
_getField(name, { throwIfMissingField = true } = {}) {
const theName = name;
const theField = this.fields[name];
if (!theField && throwIfMissingField) {
throw new Error(`Field "${theName}" not found`);
}
return theField;
}
_eachField(cb) {
const keys = this._fieldKeys;
keys.forEach((key) => cb(this.fields[key]));
}
get _fieldKeys() {
const keys = Object.keys(this.fields);
return keys;
}
resetInteractedFlag() {
this._eachField((field) => field.resetInteractedFlag());
}
disableFields = (fieldKeys) => {
if (!Array.isArray(fieldKeys))
throw new TypeError("fieldKeys should be an array with the names of the fields to disable");
fieldKeys.forEach((key) => {
const field = this._getField(key);
field.setDisabled(true);
});
};
_createField({
name,
descriptor
}) {
const { value, ...restDescriptor } = descriptor;
import_mobx.extendObservable(this.fields, {
[name]: new Field(this, value, restDescriptor, name)
});
}
addFields = (fieldsDescriptor) => {
if (fieldsDescriptor == null || !isObject(fieldsDescriptor)) {
throw new Error("fieldDescriptor has to be an Object");
}
const fieldsToAdd = Object.keys(fieldsDescriptor);
fieldsToAdd.forEach((key) => {
this._createField({ name: key, descriptor: fieldsDescriptor[key] });
});
};
enableFields(fieldKeys) {
if (!Array.isArray(fieldKeys))
throw new TypeError("fieldKeys should be an array with the names of the fields to disable");
fieldKeys.forEach((key) => {
const field = this._getField(key);
field.setDisabled(false);
});
}
resetValidatedOnce() {
this._fieldKeys.forEach((key) => {
this.fields[key].resetValidatedOnce();
});
}
}
var createModel = (args) => new FormModel(args);
var createModelFromState = (initialState = {}, validators = {}, options) => {
const theValidators = validators || {};
const stateKeys = Object.keys(initialState);
const validatorsKeys = Object.keys(theValidators);
const descriptors = Array.from(new Set([...stateKeys, ...validatorsKeys])).reduce((seq, key) => {
const res = theValidators[key] || {};
seq[key] = res;
return seq;
}, {});
return createModel({ initialState, descriptors, options });
};