@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
293 lines (292 loc) • 9.1 kB
JavaScript
import isEqual from "fast-deep-equal";
import { FormApi, getBy, setBy } from "@tanstack/form-core";
//#region src/base-ui/FormKit/engine/tanstack.ts
const toEnginePath = (path) => path.replaceAll(/\.(\d+)(?=\.|$)/g, "[$1]");
const isSameOrNested = (a, b) => a === b || a.startsWith(`${b}.`) || b.startsWith(`${a}.`);
const isUnder = (path, root) => path === root || path.startsWith(`${root}.`);
const createTanstackEngine = (initialValues) => {
const form = new FormApi({ defaultValues: initialValues });
const fields = /* @__PURE__ */ new Map();
const fieldErrors = /* @__PURE__ */ new Map();
const formErrors = /* @__PURE__ */ new Map();
const serverErrors = /* @__PURE__ */ new Map();
const validating = /* @__PURE__ */ new Set();
const touched = /* @__PURE__ */ new Set();
const listeners = /* @__PURE__ */ new Set();
const valueListeners = /* @__PURE__ */ new Set();
const snapshots = /* @__PURE__ */ new Map();
let formValidator;
let baseline = initialValues;
let dirtyCache;
let inflightSubmit;
let submitting = false;
let submitCount = 0;
let status = {
dirty: false,
submitCount: 0,
submitting: false
};
const notify = () => {
for (const listener of listeners) listener();
};
const storeSubscription = form.store.subscribe(notify);
const getValue = (path) => form.getFieldValue(toEnginePath(path));
const getValues = () => form.state.values;
const isTouched = (path) => {
for (const entry of touched) if (isSameOrNested(entry, path)) return true;
return false;
};
const errorOf = (path) => serverErrors.get(path) ?? fieldErrors.get(path) ?? formErrors.get(path);
const getField = (path) => {
const next = {
error: errorOf(path),
touched: isTouched(path),
validating: validating.has(path),
value: getValue(path)
};
const prev = snapshots.get(path);
if (prev && prev.error === next.error && prev.touched === next.touched && prev.validating === next.validating && Object.is(prev.value, next.value)) return prev;
snapshots.set(path, next);
return next;
};
const getStatus = () => {
const values = getValues();
if (dirtyCache?.values !== values || dirtyCache.baseline !== baseline) dirtyCache = {
baseline,
dirty: !isEqual(values, baseline),
values
};
const { dirty } = dirtyCache;
if (status.dirty !== dirty || status.submitting !== submitting || status.submitCount !== submitCount) status = {
dirty,
submitCount,
submitting
};
return status;
};
const shouldValidate = (path, trigger) => {
const mode = fields.get(path)?.registration.validateOn ?? "auto";
if (trigger === "submit") return true;
if (mode === "change") return true;
if (mode === "blur") return trigger === "blur";
if (mode === "submit") return false;
return trigger === "blur" || errorOf(path) !== void 0;
};
const setOrDelete = (map, path, error) => {
if (error === void 0) map.delete(path);
else map.set(path, error);
};
const runField = (path, withFormValidator) => {
const runtime = fields.get(path);
if (!runtime) return Promise.resolve();
runtime.token += 1;
const token = runtime.token;
const { validate } = runtime.registration;
if (!validate && !(withFormValidator && formValidator)) return Promise.resolve();
validating.add(path);
notify();
return (async () => {
const values = getValues();
const [fieldError, formResult] = await Promise.all([validate?.(getValue(path), values), withFormValidator ? formValidator?.(values) : void 0]);
if (runtime.token !== token || fields.get(path) !== runtime) return;
setOrDelete(fieldErrors, path, fieldError);
if (withFormValidator) setOrDelete(formErrors, path, formResult?.[path]);
validating.delete(path);
notify();
})();
};
const schedule = (path, trigger) => {
const runtime = fields.get(path);
if (!runtime || !shouldValidate(path, trigger)) return;
clearTimeout(runtime.timer);
const debounceMs = runtime.registration.debounceMs ?? 0;
if (trigger === "change" && debounceMs > 0) {
runtime.token += 1;
runtime.timer = setTimeout(() => void runField(path, true), debounceMs);
return;
}
runField(path, true);
};
const clearErrorsUnder = (path, inclusive = false) => {
for (const map of [
fieldErrors,
formErrors,
serverErrors
]) for (const key of map.keys()) if (inclusive ? isUnder(key, path) : key.startsWith(`${path}.`)) map.delete(key);
};
const resetTo = (values) => {
clearRuntimeState();
baseline = values;
form.reset(values);
notify();
};
const emitValueChange = (path, source) => {
for (const listener of valueListeners) listener({
path,
source
});
};
const afterUserChange = (path) => {
touched.add(path);
serverErrors.delete(path);
schedule(path, "change");
for (const [other, runtime] of fields) if (other !== path && runtime.registration.deps?.includes(path) && isTouched(other)) runField(other, true);
notify();
};
const validate = async (paths) => {
const values = getValues();
const formResult = (formValidator ? await formValidator(values) : void 0) ?? {};
const targets = paths ?? [.../* @__PURE__ */ new Set([...fields.keys(), ...Object.keys(formResult)])];
await Promise.all(targets.map((path) => runField(path, false)));
for (const path of targets) setOrDelete(formErrors, path, formResult[path]);
notify();
const errors = {};
for (const path of targets) {
const error = errorOf(path);
if (error !== void 0) errors[path] = error;
}
return {
errors,
valid: Object.keys(errors).length === 0
};
};
const clearRuntimeState = () => {
for (const runtime of fields.values()) {
clearTimeout(runtime.timer);
runtime.token += 1;
}
fieldErrors.clear();
formErrors.clear();
serverErrors.clear();
validating.clear();
touched.clear();
};
const runSubmit = async (onSubmit) => {
submitCount += 1;
serverErrors.clear();
for (const path of fields.keys()) touched.add(path);
let values;
let result;
do {
values = getValues();
result = await validate();
} while (values !== getValues());
if (!result.valid) {
notify();
return result;
}
submitting = true;
notify();
try {
await onSubmit?.(values);
if (serverErrors.size > 0) return {
errors: Object.fromEntries(serverErrors),
valid: false
};
resetTo(getValues());
} finally {
submitting = false;
notify();
}
return result;
};
return {
blurField: (path) => {
touched.add(path);
schedule(path, "blur");
notify();
},
destroy: () => {
for (const runtime of fields.values()) clearTimeout(runtime.timer);
storeSubscription.unsubscribe();
listeners.clear();
valueListeners.clear();
},
getField,
getStatus,
getValue,
getValues,
isTouched,
moveItem: (path, from, to) => {
form.moveFieldValues(toEnginePath(path), from, to);
touched.add(path);
clearErrorsUnder(path);
emitValueChange(path, "user");
notify();
},
onValueChange: (listener) => {
valueListeners.add(listener);
return () => valueListeners.delete(listener);
},
pushItem: (path, item) => {
form.pushFieldValue(toEnginePath(path), item);
touched.add(path);
emitValueChange(path, "user");
notify();
},
registerField: (path, registration) => {
const runtime = {
registration,
token: 0
};
fields.set(path, runtime);
return () => {
if (fields.get(path) !== runtime) return;
clearTimeout(runtime.timer);
fields.delete(path);
fieldErrors.delete(path);
validating.delete(path);
snapshots.delete(path);
notify();
};
},
removeItem: (path, index) => {
form.removeFieldValue(toEnginePath(path), index);
touched.add(path);
clearErrorsUnder(path);
emitValueChange(path, "user");
notify();
},
reset: (values) => resetTo(values ?? baseline),
resetField: (path) => {
for (const [key, runtime] of fields) {
if (!isUnder(key, path)) continue;
clearTimeout(runtime.timer);
runtime.token += 1;
validating.delete(key);
}
for (const entry of touched) if (isUnder(entry, path)) touched.delete(entry);
clearErrorsUnder(path, true);
form.setFieldValue(toEnginePath(path), getBy(baseline, toEnginePath(path)), { dontUpdateMeta: true });
emitValueChange(path, "api");
notify();
},
setErrors: (errors) => {
for (const [path, error] of Object.entries(errors)) setOrDelete(serverErrors, path, error);
notify();
},
setFormValidator: (validator) => {
formValidator = validator;
},
setValue: (path, value, source) => {
form.setFieldValue(toEnginePath(path), value, { dontUpdateMeta: true });
if (source === "api") baseline = setBy(baseline, toEnginePath(path), () => value);
emitValueChange(path, source);
if (source === "user") afterUserChange(path);
},
submit: (onSubmit) => {
inflightSubmit ??= runSubmit(onSubmit).finally(() => {
inflightSubmit = void 0;
});
return inflightSubmit;
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
validate
};
};
//#endregion
export { createTanstackEngine };
//# sourceMappingURL=tanstack.mjs.map