UNPKG

mobx-form

Version:
547 lines (542 loc) 15 kB
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 }); };