UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

293 lines (292 loc) • 9.1 kB
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