slimeform
Version:
Form state management and validation for Vue3
340 lines (327 loc) • 9.47 kB
JavaScript
import {
__async,
__spreadProps,
__spreadValues
} from "./chunk-GONKA3OC.js";
// src/index.ts
import { reactive as reactive2, readonly as readonly2, ref as ref3 } from "vue";
// src/util/is.ts
var isFunction = (value) => typeof value === "function";
function isHasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
var isNullOrUndefined = (value) => value == null;
var isDateObject = (value) => value instanceof Date;
var isObjectType = (value) => typeof value === "object";
function isBaseType(value) {
return isNullOrUndefined(value) || !isObjectType(value);
}
var isPromise = (obj) => Promise.resolve(obj) === obj;
var isEmptyObject = (object) => Object.keys(object).length === 0;
// src/defineStatus.ts
import { computed, reactive, watchEffect } from "vue";
// src/util/deepEqual.ts
function deepEqual(value1, value2) {
return deepEqualRecursion(value1, value2);
}
function deepEqualRecursion(value1, value2, stack = /* @__PURE__ */ new WeakSet()) {
if (isBaseType(value1) || isBaseType(value2)) {
return {
equal: value1 === value2,
pointersEqual: false
};
}
let pointersEqual = value1 === value2;
if (pointersEqual || stack.has(value1))
return { equal: pointersEqual, pointersEqual };
stack.add(value1);
if (isDateObject(value1) && isDateObject(value2)) {
return {
equal: value1.getTime() === value2.getTime(),
pointersEqual: false
};
}
const keys1 = Object.keys(value1);
const keys2 = Object.keys(value2);
if (keys1.length !== keys2.length)
return { equal: false, pointersEqual: false };
for (const key of keys1) {
const item1 = value1[key];
if (!keys2.includes(key))
return { equal: false, pointersEqual };
const item2 = value2[key];
const res = deepEqualRecursion(item1, item2, stack);
if (!res.equal)
return res;
pointersEqual || (pointersEqual = res.pointersEqual);
}
stack.delete(value1);
return {
equal: true,
pointersEqual
};
}
// src/util/watchIgnorable.ts
import { ref, watch } from "vue";
function watchIgnorable(source, cb, options) {
const ignoreCount = ref(0);
const syncCount = ref(0);
const syncStop = watch(
source,
() => {
syncCount.value++;
},
__spreadProps(__spreadValues({}, options), { flush: "sync" })
);
const stop = watch(source, (...args) => {
const ignore = ignoreCount.value > 0 && ignoreCount.value === syncCount.value;
ignoreCount.value = 0;
syncCount.value = 0;
if (!ignore)
cb(...args);
}, options);
const ignoreUpdates = (updater) => {
const prev = syncCount.value;
updater();
const changes = syncCount.value - prev;
ignoreCount.value += changes;
};
const ignorePrevAsyncUpdates = () => {
ignoreCount.value = syncCount.value;
};
return {
ignoreUpdates,
ignorePrevAsyncUpdates,
stop: () => {
syncStop();
stop();
}
};
}
// src/defineStatus.ts
function initStatus(status, formObj, initialForm, formDefaultMessage, formLazy, rule) {
for (const key in formObj) {
if (!isHasOwn(formObj, key))
continue;
const fieldRule = isHasOwn(rule, key) ? rule[key] : void 0;
status[key] = reactive(__spreadValues({
message: formDefaultMessage,
isError: false,
isDirty: computed(() => {
const res = deepEqual(initialForm.value[key], formObj[key]);
if (process.env.NODE_ENV !== "production") {
if (res.pointersEqual)
console.error(new Error('[SlimeForm]: in useForm(...): The "form" parameter is an invalid factory function because a duplicate reference is returned. Maybe you need a deep copy of your initial value in the form function.'));
}
return !res.equal;
})
}, statusControl(key, status, formObj, formDefaultMessage, formLazy, fieldRule)));
}
}
function statusControl(key, status, formObj, formDefaultMessage, formLazy, fieldRule) {
function setError(message, isError = true) {
status[key].message = message;
status[key].isError = isError;
}
function ruleEffect() {
if (!fieldRule)
return;
const { message, valid } = fieldRule.validate(formObj[key], { fullResult: true });
setError(message, !valid);
}
let stopEffect = null;
const init = () => {
if (!fieldRule || formLazy || stopEffect)
return;
stopEffect = watchEffect(ruleEffect);
};
const { ignoreUpdates } = watchIgnorable(
() => formObj[key],
init,
{
deep: isObjectType(formObj[key])
}
);
function clearError() {
if (stopEffect) {
stopEffect();
stopEffect = null;
}
setError(formDefaultMessage, false);
}
function verify() {
ruleEffect();
return !status[key].isError;
}
return {
verify,
setError,
clearError,
init,
_ignoreUpdate: ignoreUpdates
};
}
// src/getters.ts
import { computed as computed2, readonly } from "vue";
function useDirtyFields(form, status) {
return computed2(() => {
const fields = {};
const keys = Object.keys(form);
for (const key of keys) {
if (status[key].isDirty && isHasOwn(form, key))
fields[key] = form[key];
}
return readonly(fields);
});
}
function useIsError(status) {
return computed2(() => {
var _a;
const keys = Object.keys(status);
for (const key of keys) {
if ((_a = status[key]) == null ? void 0 : _a.isError)
return true;
}
return false;
});
}
function useIsFormDirty(dirtyFields) {
return computed2(() => !isEmptyObject(dirtyFields.value));
}
// src/submitter.ts
import { ref as ref2 } from "vue";
// src/util/invoke.ts
function invoke(func, ...args) {
return func(...args);
}
// src/submitter.ts
function createSubmit(formData, fn, options) {
const {
enableVerify = true
} = options != null ? options : {};
const { verify } = formData;
const submitting = ref2(false);
const submitFn = () => {
if (enableVerify && !verify())
return;
const ret = fn(formData);
if (isPromise(ret)) {
return invoke(() => __async(this, null, function* () {
submitting.value = true;
try {
return yield ret;
} finally {
submitting.value = false;
}
}));
} else {
return ret;
}
};
return {
submitting,
submit: submitFn
};
}
function createSubmitter(formData) {
return (...args) => createSubmit(formData(), ...args);
}
// src/rule.ts
function initRule(formRule, defaultMessage) {
const rule = {};
for (const key in formRule) {
let validate2 = function(v, options = {}) {
const { fullResult } = options;
for (const r of fieldRules || []) {
const result = r(v);
if (!result || typeof result === "string")
return fullResult ? { valid: false, message: result || defaultMessage } : false;
}
return fullResult ? { valid: true, message: defaultMessage } : true;
};
var validate = validate2;
const fieldRules = invoke(() => {
const formRuleItem = formRule == null ? void 0 : formRule[key];
return isFunction(formRuleItem) ? [formRuleItem] : formRuleItem;
});
rule[key] = { validate: validate2 };
}
return rule;
}
// src/index.ts
var defaultParam = {
defaultMessage: "",
lazy: false,
fullValidation: false
};
function useForm(param) {
const options = Object.assign({}, defaultParam, param);
const { form: formBuilder, rule: formRule, defaultMessage: formDefaultMessage, lazy: formLazy, fullValidation } = options;
const initialForm = ref3(formBuilder());
const form = reactive2(formBuilder());
const rule = initRule(formRule, formDefaultMessage);
const status = reactive2({});
initStatus(status, form, initialForm, formDefaultMessage, formLazy, rule);
const dirtyFields = useDirtyFields(form, status);
const formData = __spreadValues({
form,
status: readonly2(status),
dirtyFields,
isDirty: useIsFormDirty(dirtyFields),
isError: useIsError(status)
}, createControl(formBuilder, initialForm, form, status, fullValidation));
return __spreadProps(__spreadValues({}, formData), {
rule,
submitter: createSubmitter(() => formData)
});
}
function createControl(formBuilder, initialForm, form, status, fullValidation) {
const verify = () => {
let isPass = true;
if (fullValidation) {
Object.keys(status).forEach((key) => {
const fieldResult = status[key].verify();
isPass = isPass && fieldResult;
});
} else {
Object.keys(status).forEach((key) => {
isPass = isPass && status[key].verify();
});
}
return isPass;
};
const clearErrors = () => {
Object.keys(status).forEach((key) => {
status[key].clearError();
});
};
const reset = (...fields) => {
initialForm.value = formBuilder();
const newForm = formBuilder();
const needReset = (field) => fields.length === 0 || fields.includes(field);
for (const key in form) {
if (!needReset(key))
continue;
if (!isHasOwn(form, key))
continue;
if (isHasOwn(newForm, key)) {
status[key]._ignoreUpdate(() => {
form[key] = newForm[key];
});
status[key].clearError();
} else {
delete form[key];
}
}
};
const onSubmit = (callback) => verify() ? callback() : null;
return {
verify,
clearErrors,
reset,
onSubmit
};
}
export {
useForm
};