@pyreon/form 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/devtools.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/types/devtools.d.ts.map +1 -1
- package/lib/types/index.d.ts +3 -3
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +14 -7
- package/src/context.ts +7 -12
- package/src/devtools.ts +7 -25
- package/src/index.ts +10 -10
- package/src/tests/devtools.test.ts +53 -53
- package/src/tests/form.test.tsx +375 -395
- package/src/types.ts +3 -3
- package/src/use-field-array.ts +2 -2
- package/src/use-field.ts +8 -13
- package/src/use-form-state.ts +3 -3
- package/src/use-form.ts +24 -49
- package/src/use-watch.ts +11 -20
package/lib/devtools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @pyreon/form devtools introspection API.\n * Import: `import { ... } from \"@pyreon/form/devtools\"`\n */\n\nconst _activeForms = new Map<string, WeakRef<object>>()\nconst _listeners = new Set<() => void>()\n\nfunction _notify(): void {\n for (const listener of _listeners) listener()\n}\n\n/**\n * Register a form instance for devtools inspection.\n *\n * @example\n * const form = useForm({ ... })\n * registerForm(\"login-form\", form)\n */\nexport function registerForm(name: string, form: object): void {\n _activeForms.set(name, new WeakRef(form))\n _notify()\n}\n\n/** Unregister a form instance. */\nexport function unregisterForm(name: string): void {\n _activeForms.delete(name)\n _notify()\n}\n\n/** Get all registered form names. Cleans up garbage-collected instances. */\nexport function getActiveForms(): string[] {\n for (const [name, ref] of _activeForms) {\n if (ref.deref() === undefined) _activeForms.delete(name)\n }\n return [..._activeForms.keys()]\n}\n\n/** Get a form instance by name (or undefined if GC'd or not registered). */\nexport function getFormInstance(name: string): object | undefined {\n const ref = _activeForms.get(name)\n if (!ref) return undefined\n const instance = ref.deref()\n if (!instance) {\n _activeForms.delete(name)\n return undefined\n }\n return instance\n}\n\n/**\n * Get a snapshot of a registered form's current state.\n * Returns values, errors, and form-level status signals.\n */\nexport function getFormSnapshot(
|
|
1
|
+
{"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @pyreon/form devtools introspection API.\n * Import: `import { ... } from \"@pyreon/form/devtools\"`\n */\n\nconst _activeForms = new Map<string, WeakRef<object>>()\nconst _listeners = new Set<() => void>()\n\nfunction _notify(): void {\n for (const listener of _listeners) listener()\n}\n\n/**\n * Register a form instance for devtools inspection.\n *\n * @example\n * const form = useForm({ ... })\n * registerForm(\"login-form\", form)\n */\nexport function registerForm(name: string, form: object): void {\n _activeForms.set(name, new WeakRef(form))\n _notify()\n}\n\n/** Unregister a form instance. */\nexport function unregisterForm(name: string): void {\n _activeForms.delete(name)\n _notify()\n}\n\n/** Get all registered form names. Cleans up garbage-collected instances. */\nexport function getActiveForms(): string[] {\n for (const [name, ref] of _activeForms) {\n if (ref.deref() === undefined) _activeForms.delete(name)\n }\n return [..._activeForms.keys()]\n}\n\n/** Get a form instance by name (or undefined if GC'd or not registered). */\nexport function getFormInstance(name: string): object | undefined {\n const ref = _activeForms.get(name)\n if (!ref) return undefined\n const instance = ref.deref()\n if (!instance) {\n _activeForms.delete(name)\n return undefined\n }\n return instance\n}\n\n/**\n * Get a snapshot of a registered form's current state.\n * Returns values, errors, and form-level status signals.\n */\nexport function getFormSnapshot(name: string): Record<string, unknown> | undefined {\n const form = getFormInstance(name) as Record<string, unknown> | undefined\n if (!form) return undefined\n return {\n values: typeof form.values === \"function\" ? (form.values as () => unknown)() : undefined,\n errors: typeof form.errors === \"function\" ? (form.errors as () => unknown)() : undefined,\n isSubmitting:\n typeof form.isSubmitting === \"function\" ? (form.isSubmitting as () => unknown)() : undefined,\n isValid: typeof form.isValid === \"function\" ? (form.isValid as () => unknown)() : undefined,\n isDirty: typeof form.isDirty === \"function\" ? (form.isDirty as () => unknown)() : undefined,\n submitCount:\n typeof form.submitCount === \"function\" ? (form.submitCount as () => unknown)() : undefined,\n }\n}\n\n/** Subscribe to form registry changes. Returns unsubscribe function. */\nexport function onFormChange(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n\n/** @internal — reset devtools registry (for tests). */\nexport function _resetDevtools(): void {\n _activeForms.clear()\n _listeners.clear()\n}\n"],"mappings":";;;;;AAKA,MAAM,+BAAe,IAAI,KAA8B;AACvD,MAAM,6BAAa,IAAI,KAAiB;AAExC,SAAS,UAAgB;AACvB,MAAK,MAAM,YAAY,WAAY,WAAU;;;;;;;;;AAU/C,SAAgB,aAAa,MAAc,MAAoB;AAC7D,cAAa,IAAI,MAAM,IAAI,QAAQ,KAAK,CAAC;AACzC,UAAS;;;AAIX,SAAgB,eAAe,MAAoB;AACjD,cAAa,OAAO,KAAK;AACzB,UAAS;;;AAIX,SAAgB,iBAA2B;AACzC,MAAK,MAAM,CAAC,MAAM,QAAQ,aACxB,KAAI,IAAI,OAAO,KAAK,OAAW,cAAa,OAAO,KAAK;AAE1D,QAAO,CAAC,GAAG,aAAa,MAAM,CAAC;;;AAIjC,SAAgB,gBAAgB,MAAkC;CAChE,MAAM,MAAM,aAAa,IAAI,KAAK;AAClC,KAAI,CAAC,IAAK,QAAO;CACjB,MAAM,WAAW,IAAI,OAAO;AAC5B,KAAI,CAAC,UAAU;AACb,eAAa,OAAO,KAAK;AACzB;;AAEF,QAAO;;;;;;AAOT,SAAgB,gBAAgB,MAAmD;CACjF,MAAM,OAAO,gBAAgB,KAAK;AAClC,KAAI,CAAC,KAAM,QAAO;AAClB,QAAO;EACL,QAAQ,OAAO,KAAK,WAAW,aAAc,KAAK,QAA0B,GAAG;EAC/E,QAAQ,OAAO,KAAK,WAAW,aAAc,KAAK,QAA0B,GAAG;EAC/E,cACE,OAAO,KAAK,iBAAiB,aAAc,KAAK,cAAgC,GAAG;EACrF,SAAS,OAAO,KAAK,YAAY,aAAc,KAAK,SAA2B,GAAG;EAClF,SAAS,OAAO,KAAK,YAAY,aAAc,KAAK,SAA2B,GAAG;EAClF,aACE,OAAO,KAAK,gBAAgB,aAAc,KAAK,aAA+B,GAAG;EACpF;;;AAIH,SAAgB,aAAa,UAAkC;AAC7D,YAAW,IAAI,SAAS;AACxB,cAAa;AACX,aAAW,OAAO,SAAS;;;;AAK/B,SAAgB,iBAAuB;AACrC,cAAa,OAAO;AACpB,YAAW,OAAO"}
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/use-field.ts","../src/use-field-array.ts","../src/use-form.ts","../src/use-form-state.ts","../src/use-watch.ts"],"sourcesContent":["import type { Props, VNode, VNodeChild } from '@pyreon/core'\nimport { createContext, provide, useContext } from '@pyreon/core'\nimport type { FormState } from './types'\n\nconst FormContext = createContext<FormState<Record<string, unknown>> | null>(\n null,\n)\n\nexport interface FormProviderProps<TValues extends Record<string, unknown>>\n extends Props {\n form: FormState<TValues>\n children?: VNodeChild\n}\n\n/**\n * Provide a form instance to the component tree so nested components\n * can access it via `useFormContext()`.\n *\n * @example\n * const form = useForm({ initialValues: { email: '' }, onSubmit: ... })\n *\n * <FormProvider form={form}>\n * <EmailField />\n * <SubmitButton />\n * </FormProvider>\n */\nexport function FormProvider<TValues extends Record<string, unknown>>(\n props: FormProviderProps<TValues>,\n): VNode {\n provide(FormContext, props.form as FormState<Record<string, unknown>>)\n\n const ch = props.children\n return (typeof ch === 'function' ? (ch as () => VNodeChild)() : ch) as VNode\n}\n\n/**\n * Access the form instance from the nearest `FormProvider`.\n * Must be called within a component tree wrapped by `FormProvider`.\n *\n * @example\n * function EmailField() {\n * const form = useFormContext<{ email: string }>()\n * return <input {...form.register('email')} />\n * }\n */\nexport function useFormContext<\n TValues extends Record<string, unknown> = Record<string, unknown>,\n>(): FormState<TValues> {\n const form = useContext(FormContext)\n if (!form) {\n throw new Error(\n '[@pyreon/form] useFormContext() must be used within a <FormProvider>.',\n )\n }\n // Generic narrowing: context stores FormState<Record<string, unknown>>\n // but callers narrow to their specific TValues at the call site.\n return form as FormState<TValues>\n}\n","import type { Computed, Signal } from '@pyreon/reactivity'\nimport { computed } from '@pyreon/reactivity'\nimport type {\n FieldRegisterProps,\n FieldState,\n FormState,\n ValidationError,\n} from './types'\n\nexport interface UseFieldResult<T> {\n /** Current field value (reactive signal). */\n value: Signal<T>\n /** Field error message (reactive signal). */\n error: Signal<ValidationError>\n /** Whether the field has been touched (reactive signal). */\n touched: Signal<boolean>\n /** Whether the field value differs from initial (reactive signal). */\n dirty: Signal<boolean>\n /** Set the field value. */\n setValue: (value: T) => void\n /** Mark the field as touched. */\n setTouched: () => void\n /** Reset the field to its initial value. */\n reset: () => void\n /** Register props for input binding. */\n register: (opts?: { type?: 'checkbox' }) => FieldRegisterProps<T>\n /** Whether the field has an error (computed). */\n hasError: Computed<boolean>\n /** Whether the field should show its error (touched + has error). */\n showError: Computed<boolean>\n}\n\n/**\n * Extract a single field's state and helpers from a form instance.\n * Useful for building isolated field components.\n *\n * @example\n * function EmailField({ form }: { form: FormState<{ email: string }> }) {\n * const field = useField(form, 'email')\n * return (\n * <>\n * <input {...field.register()} />\n * {field.showError() && <span>{field.error()}</span>}\n * </>\n * )\n * }\n */\nexport function useField<\n TValues extends Record<string, unknown>,\n K extends keyof TValues & string,\n>(form: FormState<TValues>, name: K): UseFieldResult<TValues[K]> {\n const fieldState: FieldState<TValues[K]> = form.fields[name]\n\n const hasError = computed(() => fieldState.error() !== undefined)\n const showError = computed(() => fieldState.touched() && hasError())\n\n return {\n value: fieldState.value,\n error: fieldState.error,\n touched: fieldState.touched,\n dirty: fieldState.dirty,\n setValue: fieldState.setValue,\n setTouched: fieldState.setTouched,\n reset: fieldState.reset,\n register: (opts?) => form.register(name, opts),\n hasError,\n showError,\n }\n}\n","import type { Computed, Signal } from '@pyreon/reactivity'\nimport { computed, signal } from '@pyreon/reactivity'\n\nexport interface FieldArrayItem<T> {\n /** Stable key for keyed rendering. */\n key: number\n /** Reactive value for this item. */\n value: Signal<T>\n}\n\nexport interface UseFieldArrayResult<T> {\n /** Reactive list of items with stable keys. */\n items: Signal<FieldArrayItem<T>[]>\n /** Number of items. */\n length: Computed<number>\n /** Append a new item to the end. */\n append: (value: T) => void\n /** Prepend a new item to the start. */\n prepend: (value: T) => void\n /** Insert an item at the given index. */\n insert: (index: number, value: T) => void\n /** Remove the item at the given index. */\n remove: (index: number) => void\n /** Update the value of an item at the given index. */\n update: (index: number, value: T) => void\n /** Move an item from one index to another. */\n move: (from: number, to: number) => void\n /** Swap two items by index. */\n swap: (indexA: number, indexB: number) => void\n /** Replace all items. */\n replace: (values: T[]) => void\n /** Get all current values as a plain array. */\n values: () => T[]\n}\n\n/**\n * Manage a dynamic array of form fields with stable keys.\n *\n * @example\n * const tags = useFieldArray<string>([])\n * tags.append('typescript')\n * tags.append('pyreon')\n * // tags.items() — array of { key, value } for keyed rendering\n */\nexport function useFieldArray<T>(initial: T[] = []): UseFieldArrayResult<T> {\n let nextKey = 0\n const makeItem = (value: T): FieldArrayItem<T> => ({\n key: nextKey++,\n value: signal(value),\n })\n\n const items = signal<FieldArrayItem<T>[]>(initial.map(makeItem))\n const length = computed(() => items().length)\n\n return {\n items,\n length,\n\n append(value: T) {\n items.update((arr) => [...arr, makeItem(value)])\n },\n\n prepend(value: T) {\n items.update((arr) => [makeItem(value), ...arr])\n },\n\n insert(index: number, value: T) {\n items.update((arr) => {\n const next = [...arr]\n next.splice(index, 0, makeItem(value))\n return next\n })\n },\n\n remove(index: number) {\n items.update((arr) => arr.filter((_, i) => i !== index))\n },\n\n update(index: number, value: T) {\n const current = items.peek()\n const item = current[index]\n if (item) {\n item.value.set(value)\n }\n },\n\n move(from: number, to: number) {\n items.update((arr) => {\n const next = [...arr]\n const [item] = next.splice(from, 1)\n if (item) next.splice(to, 0, item)\n return next\n })\n },\n\n swap(indexA: number, indexB: number) {\n items.update((arr) => {\n const next = [...arr]\n const a = next[indexA]\n const b = next[indexB]\n if (a && b) {\n next[indexA] = b\n next[indexB] = a\n }\n return next\n })\n },\n\n replace(values: T[]) {\n items.set(values.map(makeItem))\n },\n\n values() {\n return items.peek().map((item) => item.value.peek())\n },\n }\n}\n","import { onUnmount } from '@pyreon/core'\nimport type { Signal } from '@pyreon/reactivity'\nimport { computed, effect, signal } from '@pyreon/reactivity'\nimport type {\n FieldRegisterProps,\n FieldState,\n FormState,\n UseFormOptions,\n ValidationError,\n} from './types'\n\n/**\n * Create a signal-based form. Returns reactive field states, form-level\n * signals, and handlers for submit/reset/validate.\n *\n * @example\n * const form = useForm({\n * initialValues: { email: '', password: '', remember: false },\n * validators: {\n * email: (v) => (!v ? 'Required' : undefined),\n * password: (v, all) => (v.length < 8 ? 'Too short' : undefined),\n * },\n * onSubmit: async (values) => { await login(values) },\n * })\n *\n * // Bind with register():\n * // h('input', form.register('email'))\n * // h('input', { type: 'checkbox', ...form.register('remember', { type: 'checkbox' }) })\n */\nexport function useForm<TValues extends Record<string, unknown>>(\n options: UseFormOptions<TValues>,\n): FormState<TValues> {\n const {\n initialValues,\n onSubmit,\n validators,\n schema,\n validateOn = 'blur',\n debounceMs,\n } = options\n\n // Build field states\n const fieldEntries = Object.entries(initialValues) as [\n keyof TValues & string,\n TValues[keyof TValues],\n ][]\n\n const fields = {} as { [K in keyof TValues]: FieldState<TValues[K]> }\n\n // Debounce timers per field (only allocated when debounceMs is set)\n const debounceTimers: Partial<\n Record<keyof TValues, ReturnType<typeof setTimeout>>\n > = {}\n\n // Validation version per field — used to discard stale async results\n const validationVersions: Partial<Record<keyof TValues, number>> = {}\n\n // Helper to get all current values (used by cross-field validators)\n const getValues = (): TValues => {\n const values = {} as TValues\n for (const [name] of fieldEntries) {\n ;(values as Record<string, unknown>)[name] =\n fields[name]?.value.peek() ??\n (initialValues as Record<string, unknown>)[name]\n }\n return values\n }\n\n // Clear all pending debounce timers\n const clearAllTimers = () => {\n for (const key of Object.keys(debounceTimers)) {\n clearTimeout(debounceTimers[key as keyof TValues])\n delete debounceTimers[key as keyof TValues]\n }\n }\n\n const isValidating = signal(false)\n const submitError = signal<unknown>(undefined)\n\n // Track whether the form has been disposed (unmounted)\n let disposed = false\n\n for (const [name, initial] of fieldEntries) {\n const valueSig = signal(initial) as Signal<TValues[typeof name]>\n const errorSig = signal<ValidationError>(undefined)\n const touchedSig = signal(false)\n const dirtySig = signal(false)\n\n // Initialize validation version\n validationVersions[name] = 0\n\n const runValidation = async (value: TValues[typeof name]) => {\n const fieldValidator = validators?.[name]\n if (fieldValidator) {\n // Bump version to track this validation run\n validationVersions[name] = (validationVersions[name] ?? 0) + 1\n const currentVersion = validationVersions[name]\n try {\n const result = await fieldValidator(value, getValues())\n // Only apply result if this is still the latest validation and not disposed\n if (!disposed && validationVersions[name] === currentVersion) {\n errorSig.set(result)\n }\n return result\n } catch (err) {\n // Validator threw — treat as error string if possible\n if (!disposed && validationVersions[name] === currentVersion) {\n const message = err instanceof Error ? err.message : String(err)\n errorSig.set(message)\n }\n return err instanceof Error ? err.message : String(err)\n }\n }\n errorSig.set(undefined)\n return undefined\n }\n\n const validateField = debounceMs\n ? (value: TValues[typeof name]) => {\n clearTimeout(debounceTimers[name])\n return new Promise<ValidationError>((resolve) => {\n debounceTimers[name] = setTimeout(async () => {\n resolve(await runValidation(value))\n }, debounceMs)\n })\n }\n : runValidation\n\n // Auto-validate on change if configured\n if (validateOn === 'change') {\n effect(() => {\n const v = valueSig()\n validateField(v)\n })\n }\n\n fields[name] = {\n value: valueSig,\n error: errorSig,\n touched: touchedSig,\n dirty: dirtySig,\n setValue: (value: TValues[typeof name]) => {\n valueSig.set(value)\n // Deep comparison for objects/arrays, reference for primitives\n dirtySig.set(!structuredEqual(value, initial))\n },\n setTouched: () => {\n touchedSig.set(true)\n if (validateOn === 'blur') {\n validateField(valueSig.peek())\n }\n },\n reset: () => {\n valueSig.set(initial as TValues[typeof name])\n errorSig.set(undefined)\n touchedSig.set(false)\n dirtySig.set(false)\n clearTimeout(debounceTimers[name])\n },\n } as FieldState<TValues[typeof name]>\n }\n\n // Clean up debounce timers and cancel in-flight validators on unmount\n onUnmount(() => {\n disposed = true\n clearAllTimers()\n })\n\n const isSubmitting = signal(false)\n const submitCount = signal(0)\n\n // Form-level computed signals\n const isValid = computed(() => {\n for (const name of fieldEntries.map(([n]) => n)) {\n if (fields[name].error() !== undefined) return false\n }\n return true\n })\n\n const isDirty = computed(() => {\n for (const name of fieldEntries.map(([n]) => n)) {\n if (fields[name].dirty()) return true\n }\n return false\n })\n\n const getErrors = (): Partial<Record<keyof TValues, ValidationError>> => {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const [name] of fieldEntries) {\n const err = fields[name].error.peek()\n if (err !== undefined) errors[name] = err\n }\n return errors\n }\n\n const validate = async (): Promise<boolean> => {\n // Cancel any pending debounced validations\n clearAllTimers()\n\n isValidating.set(true)\n\n try {\n const allValues = getValues()\n\n // Clear all errors before re-validating\n for (const [name] of fieldEntries) {\n fields[name].error.set(undefined)\n }\n\n // Run field-level validators with all values for cross-field support\n await Promise.all(\n fieldEntries.map(async ([name]) => {\n const fieldValidator = validators?.[name]\n if (fieldValidator) {\n // Bump version so any in-flight debounced validation is discarded\n validationVersions[name] = (validationVersions[name] ?? 0) + 1\n const currentVersion = validationVersions[name]\n try {\n const error = await fieldValidator(\n fields[name].value.peek(),\n allValues,\n )\n if (validationVersions[name] === currentVersion) {\n fields[name].error.set(error)\n }\n } catch (err) {\n if (validationVersions[name] === currentVersion) {\n fields[name].error.set(\n err instanceof Error ? err.message : String(err),\n )\n }\n }\n }\n }),\n )\n\n // Run schema-level validator — only set schema errors for fields\n // that don't already have a field-level error (field-level wins)\n if (schema) {\n try {\n const schemaErrors = await schema(allValues)\n for (const [name] of fieldEntries) {\n const schemaError = schemaErrors[name]\n if (\n schemaError !== undefined &&\n fields[name].error.peek() === undefined\n ) {\n fields[name].error.set(schemaError)\n }\n }\n } catch (err) {\n // Schema validator threw — set as submitError rather than losing it\n submitError.set(err)\n return false\n }\n }\n\n // Re-check: any field with an error means invalid\n for (const [name] of fieldEntries) {\n if (fields[name].error.peek() !== undefined) return false\n }\n return true\n } finally {\n isValidating.set(false)\n }\n }\n\n const handleSubmit = async (e?: Event) => {\n if (e && typeof e.preventDefault === 'function') {\n e.preventDefault()\n }\n\n submitError.set(undefined)\n submitCount.update((n) => n + 1)\n\n // Mark all fields as touched\n for (const [name] of fieldEntries) {\n fields[name].touched.set(true)\n }\n\n const valid = await validate()\n if (!valid) return\n\n isSubmitting.set(true)\n try {\n await onSubmit(getValues())\n } catch (err) {\n submitError.set(err)\n throw err\n } finally {\n isSubmitting.set(false)\n }\n }\n\n const reset = () => {\n clearAllTimers()\n for (const [name] of fieldEntries) {\n fields[name].reset()\n }\n submitCount.set(0)\n submitError.set(undefined)\n }\n\n const setFieldValue = <K extends keyof TValues>(\n field: K,\n value: TValues[K],\n ) => {\n if (!fields[field]) {\n throw new Error(\n `[@pyreon/form] Field \"${String(field)}\" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(', ')}`,\n )\n }\n fields[field].setValue(value)\n }\n\n const setFieldError = (field: keyof TValues, error: ValidationError) => {\n if (!fields[field]) {\n throw new Error(\n `[@pyreon/form] Field \"${String(field)}\" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(', ')}`,\n )\n }\n fields[field].error.set(error)\n }\n\n const setErrors = (\n errors: Partial<Record<keyof TValues, ValidationError>>,\n ) => {\n for (const [name, error] of Object.entries(errors)) {\n setFieldError(name as keyof TValues, error as ValidationError)\n }\n }\n\n const clearErrors = () => {\n for (const [name] of fieldEntries) {\n fields[name].error.set(undefined)\n }\n }\n\n const resetField = (field: keyof TValues) => {\n if (fields[field]) {\n fields[field].reset()\n }\n }\n\n // Memoized register props per field+type combo\n const registerCache = new Map<string, FieldRegisterProps<unknown>>()\n\n const register = <K extends keyof TValues & string>(\n field: K,\n opts?: { type?: 'checkbox' | 'number' },\n ): FieldRegisterProps<TValues[K]> => {\n const cacheKey = `${field}:${opts?.type ?? 'text'}`\n const cached = registerCache.get(cacheKey)\n if (cached) return cached as FieldRegisterProps<TValues[K]>\n\n const fieldState = fields[field]\n const props: FieldRegisterProps<TValues[K]> = {\n value: fieldState.value,\n onInput: (e: Event) => {\n const target = e.target as HTMLInputElement\n if (opts?.type === 'checkbox') {\n fieldState.setValue(target.checked as TValues[K])\n } else if (opts?.type === 'number') {\n const num = target.valueAsNumber\n fieldState.setValue(\n (Number.isNaN(num) ? target.value : num) as TValues[K],\n )\n } else {\n fieldState.setValue(target.value as TValues[K])\n }\n },\n onBlur: () => {\n fieldState.setTouched()\n },\n }\n\n if (opts?.type === 'checkbox') {\n props.checked = computed(() => Boolean(fieldState.value()))\n }\n\n registerCache.set(cacheKey, props as FieldRegisterProps<unknown>)\n return props\n }\n\n return {\n fields,\n isSubmitting,\n isValidating,\n isValid,\n isDirty,\n submitCount,\n submitError,\n values: getValues,\n errors: getErrors,\n setFieldValue,\n setFieldError,\n setErrors,\n clearErrors,\n resetField,\n register,\n handleSubmit,\n reset,\n validate,\n }\n}\n\n/** Deep structural equality with depth limit to guard against circular references. */\nfunction structuredEqual(a: unknown, b: unknown, depth = 0): boolean {\n if (Object.is(a, b)) return true\n if (a == null || b == null) return false\n if (typeof a !== typeof b) return false\n // Bail at depth 10 — treat as not equal to avoid infinite recursion\n if (depth > 10) return false\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (!structuredEqual(a[i], b[i], depth + 1)) return false\n }\n return true\n }\n\n if (typeof a === 'object' && typeof b === 'object') {\n const aObj = a as Record<string, unknown>\n const bObj = b as Record<string, unknown>\n const aKeys = Object.keys(aObj)\n const bKeys = Object.keys(bObj)\n if (aKeys.length !== bKeys.length) return false\n for (const key of aKeys) {\n if (!structuredEqual(aObj[key], bObj[key], depth + 1)) return false\n }\n return true\n }\n\n return false\n}\n","import type { Computed } from '@pyreon/reactivity'\nimport { computed } from '@pyreon/reactivity'\nimport type { FormState, ValidationError } from './types'\n\nexport interface FormStateSummary<TValues extends Record<string, unknown>> {\n isSubmitting: boolean\n isValidating: boolean\n isValid: boolean\n isDirty: boolean\n submitCount: number\n submitError: unknown\n touchedFields: Partial<Record<keyof TValues, boolean>>\n dirtyFields: Partial<Record<keyof TValues, boolean>>\n errors: Partial<Record<keyof TValues, ValidationError>>\n}\n\n/**\n * Subscribe to the full form state as a single computed signal.\n * Useful for rendering form-level UI (submit button disabled state,\n * error summaries, progress indicators).\n *\n * @example\n * const state = useFormState(form)\n * // state() => { isSubmitting, isValid, isDirty, errors, ... }\n *\n * @example\n * // Use a selector for fine-grained reactivity\n * const canSubmit = useFormState(form, (s) => s.isValid && !s.isSubmitting)\n */\nexport function useFormState<TValues extends Record<string, unknown>>(\n form: FormState<TValues>,\n): Computed<FormStateSummary<TValues>>\n\nexport function useFormState<TValues extends Record<string, unknown>, R>(\n form: FormState<TValues>,\n selector: (state: FormStateSummary<TValues>) => R,\n): Computed<R>\n\nexport function useFormState<TValues extends Record<string, unknown>, R>(\n form: FormState<TValues>,\n selector?: (state: FormStateSummary<TValues>) => R,\n): Computed<FormStateSummary<TValues>> | Computed<R> {\n const buildSummary = (): FormStateSummary<TValues> => {\n const touchedFields = {} as Partial<Record<keyof TValues, boolean>>\n const dirtyFields = {} as Partial<Record<keyof TValues, boolean>>\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n\n for (const key of Object.keys(form.fields) as (keyof TValues & string)[]) {\n const field = form.fields[key]\n if (field.touched()) touchedFields[key] = true\n if (field.dirty()) dirtyFields[key] = true\n const err = field.error()\n if (err !== undefined) errors[key] = err\n }\n\n return {\n isSubmitting: form.isSubmitting(),\n isValidating: form.isValidating(),\n isValid: form.isValid(),\n isDirty: form.isDirty(),\n submitCount: form.submitCount(),\n submitError: form.submitError(),\n touchedFields,\n dirtyFields,\n errors,\n }\n }\n\n if (selector) {\n return computed(() => selector(buildSummary()))\n }\n\n return computed(buildSummary)\n}\n","import type { Computed, Signal } from '@pyreon/reactivity'\nimport { computed } from '@pyreon/reactivity'\nimport type { FormState } from './types'\n\n/**\n * Watch specific field values reactively. Returns a computed signal\n * that re-evaluates when any of the watched fields change.\n *\n * @example\n * // Watch a single field\n * const email = useWatch(form, 'email')\n * // email() => current email value\n *\n * @example\n * // Watch multiple fields\n * const [first, last] = useWatch(form, ['firstName', 'lastName'])\n * // first() => firstName value, last() => lastName value\n *\n * @example\n * // Watch all fields\n * const all = useWatch(form)\n * // all() => { email: '...', password: '...' }\n */\nexport function useWatch<\n TValues extends Record<string, unknown>,\n K extends keyof TValues & string,\n>(form: FormState<TValues>, name: K): Signal<TValues[K]>\n\nexport function useWatch<\n TValues extends Record<string, unknown>,\n K extends (keyof TValues & string)[],\n>(\n form: FormState<TValues>,\n names: K,\n): { [I in keyof K]: Signal<TValues[K[I] & keyof TValues]> }\n\nexport function useWatch<TValues extends Record<string, unknown>>(\n form: FormState<TValues>,\n): Computed<TValues>\n\nexport function useWatch<\n TValues extends Record<string, unknown>,\n K extends keyof TValues & string,\n>(\n form: FormState<TValues>,\n nameOrNames?: K | K[],\n): Signal<TValues[K]> | Signal<TValues[K]>[] | Computed<TValues> {\n // Watch all fields\n if (nameOrNames === undefined) {\n return computed(() => {\n const result = {} as TValues\n for (const key of Object.keys(form.fields) as (keyof TValues &\n string)[]) {\n ;(result as Record<string, unknown>)[key] = form.fields[key].value()\n }\n return result\n })\n }\n\n // Watch multiple fields\n if (Array.isArray(nameOrNames)) {\n return nameOrNames.map((name) => form.fields[name].value) as Signal<\n TValues[K]\n >[]\n }\n\n // Watch single field\n return form.fields[nameOrNames].value\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/use-field.ts","../src/use-field-array.ts","../src/use-form.ts","../src/use-form-state.ts","../src/use-watch.ts"],"sourcesContent":["import type { Props, VNode, VNodeChild } from \"@pyreon/core\"\nimport { createContext, provide, useContext } from \"@pyreon/core\"\nimport type { FormState } from \"./types\"\n\nconst FormContext = createContext<FormState<Record<string, unknown>> | null>(null)\n\nexport interface FormProviderProps<TValues extends Record<string, unknown>> extends Props {\n form: FormState<TValues>\n children?: VNodeChild\n}\n\n/**\n * Provide a form instance to the component tree so nested components\n * can access it via `useFormContext()`.\n *\n * @example\n * const form = useForm({ initialValues: { email: '' }, onSubmit: ... })\n *\n * <FormProvider form={form}>\n * <EmailField />\n * <SubmitButton />\n * </FormProvider>\n */\nexport function FormProvider<TValues extends Record<string, unknown>>(\n props: FormProviderProps<TValues>,\n): VNode {\n provide(FormContext, props.form as FormState<Record<string, unknown>>)\n\n const ch = props.children\n return (typeof ch === \"function\" ? (ch as () => VNodeChild)() : ch) as VNode\n}\n\n/**\n * Access the form instance from the nearest `FormProvider`.\n * Must be called within a component tree wrapped by `FormProvider`.\n *\n * @example\n * function EmailField() {\n * const form = useFormContext<{ email: string }>()\n * return <input {...form.register('email')} />\n * }\n */\nexport function useFormContext<\n TValues extends Record<string, unknown> = Record<string, unknown>,\n>(): FormState<TValues> {\n const form = useContext(FormContext)\n if (!form) {\n throw new Error(\"[@pyreon/form] useFormContext() must be used within a <FormProvider>.\")\n }\n // Generic narrowing: context stores FormState<Record<string, unknown>>\n // but callers narrow to their specific TValues at the call site.\n return form as FormState<TValues>\n}\n","import type { Computed, Signal } from \"@pyreon/reactivity\"\nimport { computed } from \"@pyreon/reactivity\"\nimport type { FieldRegisterProps, FieldState, FormState, ValidationError } from \"./types\"\n\nexport interface UseFieldResult<T> {\n /** Current field value (reactive signal). */\n value: Signal<T>\n /** Field error message (reactive signal). */\n error: Signal<ValidationError>\n /** Whether the field has been touched (reactive signal). */\n touched: Signal<boolean>\n /** Whether the field value differs from initial (reactive signal). */\n dirty: Signal<boolean>\n /** Set the field value. */\n setValue: (value: T) => void\n /** Mark the field as touched. */\n setTouched: () => void\n /** Reset the field to its initial value. */\n reset: () => void\n /** Register props for input binding. */\n register: (opts?: { type?: \"checkbox\" }) => FieldRegisterProps<T>\n /** Whether the field has an error (computed). */\n hasError: Computed<boolean>\n /** Whether the field should show its error (touched + has error). */\n showError: Computed<boolean>\n}\n\n/**\n * Extract a single field's state and helpers from a form instance.\n * Useful for building isolated field components.\n *\n * @example\n * function EmailField({ form }: { form: FormState<{ email: string }> }) {\n * const field = useField(form, 'email')\n * return (\n * <>\n * <input {...field.register()} />\n * {field.showError() && <span>{field.error()}</span>}\n * </>\n * )\n * }\n */\nexport function useField<TValues extends Record<string, unknown>, K extends keyof TValues & string>(\n form: FormState<TValues>,\n name: K,\n): UseFieldResult<TValues[K]> {\n const fieldState: FieldState<TValues[K]> = form.fields[name]\n\n const hasError = computed(() => fieldState.error() !== undefined)\n const showError = computed(() => fieldState.touched() && hasError())\n\n return {\n value: fieldState.value,\n error: fieldState.error,\n touched: fieldState.touched,\n dirty: fieldState.dirty,\n setValue: fieldState.setValue,\n setTouched: fieldState.setTouched,\n reset: fieldState.reset,\n register: (opts?) => form.register(name, opts),\n hasError,\n showError,\n }\n}\n","import type { Computed, Signal } from \"@pyreon/reactivity\"\nimport { computed, signal } from \"@pyreon/reactivity\"\n\nexport interface FieldArrayItem<T> {\n /** Stable key for keyed rendering. */\n key: number\n /** Reactive value for this item. */\n value: Signal<T>\n}\n\nexport interface UseFieldArrayResult<T> {\n /** Reactive list of items with stable keys. */\n items: Signal<FieldArrayItem<T>[]>\n /** Number of items. */\n length: Computed<number>\n /** Append a new item to the end. */\n append: (value: T) => void\n /** Prepend a new item to the start. */\n prepend: (value: T) => void\n /** Insert an item at the given index. */\n insert: (index: number, value: T) => void\n /** Remove the item at the given index. */\n remove: (index: number) => void\n /** Update the value of an item at the given index. */\n update: (index: number, value: T) => void\n /** Move an item from one index to another. */\n move: (from: number, to: number) => void\n /** Swap two items by index. */\n swap: (indexA: number, indexB: number) => void\n /** Replace all items. */\n replace: (values: T[]) => void\n /** Get all current values as a plain array. */\n values: () => T[]\n}\n\n/**\n * Manage a dynamic array of form fields with stable keys.\n *\n * @example\n * const tags = useFieldArray<string>([])\n * tags.append('typescript')\n * tags.append('pyreon')\n * // tags.items() — array of { key, value } for keyed rendering\n */\nexport function useFieldArray<T>(initial: T[] = []): UseFieldArrayResult<T> {\n let nextKey = 0\n const makeItem = (value: T): FieldArrayItem<T> => ({\n key: nextKey++,\n value: signal(value),\n })\n\n const items = signal<FieldArrayItem<T>[]>(initial.map(makeItem))\n const length = computed(() => items().length)\n\n return {\n items,\n length,\n\n append(value: T) {\n items.update((arr) => [...arr, makeItem(value)])\n },\n\n prepend(value: T) {\n items.update((arr) => [makeItem(value), ...arr])\n },\n\n insert(index: number, value: T) {\n items.update((arr) => {\n const next = [...arr]\n next.splice(index, 0, makeItem(value))\n return next\n })\n },\n\n remove(index: number) {\n items.update((arr) => arr.filter((_, i) => i !== index))\n },\n\n update(index: number, value: T) {\n const current = items.peek()\n const item = current[index]\n if (item) {\n item.value.set(value)\n }\n },\n\n move(from: number, to: number) {\n items.update((arr) => {\n const next = [...arr]\n const [item] = next.splice(from, 1)\n if (item) next.splice(to, 0, item)\n return next\n })\n },\n\n swap(indexA: number, indexB: number) {\n items.update((arr) => {\n const next = [...arr]\n const a = next[indexA]\n const b = next[indexB]\n if (a && b) {\n next[indexA] = b\n next[indexB] = a\n }\n return next\n })\n },\n\n replace(values: T[]) {\n items.set(values.map(makeItem))\n },\n\n values() {\n return items.peek().map((item) => item.value.peek())\n },\n }\n}\n","import { onUnmount } from \"@pyreon/core\"\nimport type { Signal } from \"@pyreon/reactivity\"\nimport { computed, effect, signal } from \"@pyreon/reactivity\"\nimport type {\n FieldRegisterProps,\n FieldState,\n FormState,\n UseFormOptions,\n ValidationError,\n} from \"./types\"\n\n/**\n * Create a signal-based form. Returns reactive field states, form-level\n * signals, and handlers for submit/reset/validate.\n *\n * @example\n * const form = useForm({\n * initialValues: { email: '', password: '', remember: false },\n * validators: {\n * email: (v) => (!v ? 'Required' : undefined),\n * password: (v, all) => (v.length < 8 ? 'Too short' : undefined),\n * },\n * onSubmit: async (values) => { await login(values) },\n * })\n *\n * // Bind with register():\n * // h('input', form.register('email'))\n * // h('input', { type: 'checkbox', ...form.register('remember', { type: 'checkbox' }) })\n */\nexport function useForm<TValues extends Record<string, unknown>>(\n options: UseFormOptions<TValues>,\n): FormState<TValues> {\n const { initialValues, onSubmit, validators, schema, validateOn = \"blur\", debounceMs } = options\n\n // Build field states\n const fieldEntries = Object.entries(initialValues) as [\n keyof TValues & string,\n TValues[keyof TValues],\n ][]\n\n const fields = {} as { [K in keyof TValues]: FieldState<TValues[K]> }\n\n // Debounce timers per field (only allocated when debounceMs is set)\n const debounceTimers: Partial<Record<keyof TValues, ReturnType<typeof setTimeout>>> = {}\n\n // Validation version per field — used to discard stale async results\n const validationVersions: Partial<Record<keyof TValues, number>> = {}\n\n // Helper to get all current values (used by cross-field validators)\n const getValues = (): TValues => {\n const values = {} as TValues\n for (const [name] of fieldEntries) {\n ;(values as Record<string, unknown>)[name] =\n fields[name]?.value.peek() ?? (initialValues as Record<string, unknown>)[name]\n }\n return values\n }\n\n // Clear all pending debounce timers\n const clearAllTimers = () => {\n for (const key of Object.keys(debounceTimers)) {\n clearTimeout(debounceTimers[key as keyof TValues])\n delete debounceTimers[key as keyof TValues]\n }\n }\n\n const isValidating = signal(false)\n const submitError = signal<unknown>(undefined)\n\n // Track whether the form has been disposed (unmounted)\n let disposed = false\n\n for (const [name, initial] of fieldEntries) {\n const valueSig = signal(initial) as Signal<TValues[typeof name]>\n const errorSig = signal<ValidationError>(undefined)\n const touchedSig = signal(false)\n const dirtySig = signal(false)\n\n // Initialize validation version\n validationVersions[name] = 0\n\n const runValidation = async (value: TValues[typeof name]) => {\n const fieldValidator = validators?.[name]\n if (fieldValidator) {\n // Bump version to track this validation run\n validationVersions[name] = (validationVersions[name] ?? 0) + 1\n const currentVersion = validationVersions[name]\n try {\n const result = await fieldValidator(value, getValues())\n // Only apply result if this is still the latest validation and not disposed\n if (!disposed && validationVersions[name] === currentVersion) {\n errorSig.set(result)\n }\n return result\n } catch (err) {\n // Validator threw — treat as error string if possible\n if (!disposed && validationVersions[name] === currentVersion) {\n const message = err instanceof Error ? err.message : String(err)\n errorSig.set(message)\n }\n return err instanceof Error ? err.message : String(err)\n }\n }\n errorSig.set(undefined)\n return undefined\n }\n\n const validateField = debounceMs\n ? (value: TValues[typeof name]) => {\n clearTimeout(debounceTimers[name])\n return new Promise<ValidationError>((resolve) => {\n debounceTimers[name] = setTimeout(async () => {\n resolve(await runValidation(value))\n }, debounceMs)\n })\n }\n : runValidation\n\n // Auto-validate on change if configured\n if (validateOn === \"change\") {\n effect(() => {\n const v = valueSig()\n validateField(v)\n })\n }\n\n fields[name] = {\n value: valueSig,\n error: errorSig,\n touched: touchedSig,\n dirty: dirtySig,\n setValue: (value: TValues[typeof name]) => {\n valueSig.set(value)\n // Deep comparison for objects/arrays, reference for primitives\n dirtySig.set(!structuredEqual(value, initial))\n },\n setTouched: () => {\n touchedSig.set(true)\n if (validateOn === \"blur\") {\n validateField(valueSig.peek())\n }\n },\n reset: () => {\n valueSig.set(initial as TValues[typeof name])\n errorSig.set(undefined)\n touchedSig.set(false)\n dirtySig.set(false)\n clearTimeout(debounceTimers[name])\n },\n } as FieldState<TValues[typeof name]>\n }\n\n // Clean up debounce timers and cancel in-flight validators on unmount\n onUnmount(() => {\n disposed = true\n clearAllTimers()\n })\n\n const isSubmitting = signal(false)\n const submitCount = signal(0)\n\n // Form-level computed signals\n const isValid = computed(() => {\n for (const name of fieldEntries.map(([n]) => n)) {\n if (fields[name].error() !== undefined) return false\n }\n return true\n })\n\n const isDirty = computed(() => {\n for (const name of fieldEntries.map(([n]) => n)) {\n if (fields[name].dirty()) return true\n }\n return false\n })\n\n const getErrors = (): Partial<Record<keyof TValues, ValidationError>> => {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const [name] of fieldEntries) {\n const err = fields[name].error.peek()\n if (err !== undefined) errors[name] = err\n }\n return errors\n }\n\n const validate = async (): Promise<boolean> => {\n // Cancel any pending debounced validations\n clearAllTimers()\n\n isValidating.set(true)\n\n try {\n const allValues = getValues()\n\n // Clear all errors before re-validating\n for (const [name] of fieldEntries) {\n fields[name].error.set(undefined)\n }\n\n // Run field-level validators with all values for cross-field support\n await Promise.all(\n fieldEntries.map(async ([name]) => {\n const fieldValidator = validators?.[name]\n if (fieldValidator) {\n // Bump version so any in-flight debounced validation is discarded\n validationVersions[name] = (validationVersions[name] ?? 0) + 1\n const currentVersion = validationVersions[name]\n try {\n const error = await fieldValidator(fields[name].value.peek(), allValues)\n if (validationVersions[name] === currentVersion) {\n fields[name].error.set(error)\n }\n } catch (err) {\n if (validationVersions[name] === currentVersion) {\n fields[name].error.set(err instanceof Error ? err.message : String(err))\n }\n }\n }\n }),\n )\n\n // Run schema-level validator — only set schema errors for fields\n // that don't already have a field-level error (field-level wins)\n if (schema) {\n try {\n const schemaErrors = await schema(allValues)\n for (const [name] of fieldEntries) {\n const schemaError = schemaErrors[name]\n if (schemaError !== undefined && fields[name].error.peek() === undefined) {\n fields[name].error.set(schemaError)\n }\n }\n } catch (err) {\n // Schema validator threw — set as submitError rather than losing it\n submitError.set(err)\n return false\n }\n }\n\n // Re-check: any field with an error means invalid\n for (const [name] of fieldEntries) {\n if (fields[name].error.peek() !== undefined) return false\n }\n return true\n } finally {\n isValidating.set(false)\n }\n }\n\n const handleSubmit = async (e?: Event) => {\n if (e && typeof e.preventDefault === \"function\") {\n e.preventDefault()\n }\n\n submitError.set(undefined)\n submitCount.update((n) => n + 1)\n\n // Mark all fields as touched\n for (const [name] of fieldEntries) {\n fields[name].touched.set(true)\n }\n\n const valid = await validate()\n if (!valid) return\n\n isSubmitting.set(true)\n try {\n await onSubmit(getValues())\n } catch (err) {\n submitError.set(err)\n throw err\n } finally {\n isSubmitting.set(false)\n }\n }\n\n const reset = () => {\n clearAllTimers()\n for (const [name] of fieldEntries) {\n fields[name].reset()\n }\n submitCount.set(0)\n submitError.set(undefined)\n }\n\n const setFieldValue = <K extends keyof TValues>(field: K, value: TValues[K]) => {\n if (!fields[field]) {\n throw new Error(\n `[@pyreon/form] Field \"${String(field)}\" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(\", \")}`,\n )\n }\n fields[field].setValue(value)\n }\n\n const setFieldError = (field: keyof TValues, error: ValidationError) => {\n if (!fields[field]) {\n throw new Error(\n `[@pyreon/form] Field \"${String(field)}\" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(\", \")}`,\n )\n }\n fields[field].error.set(error)\n }\n\n const setErrors = (errors: Partial<Record<keyof TValues, ValidationError>>) => {\n for (const [name, error] of Object.entries(errors)) {\n setFieldError(name as keyof TValues, error as ValidationError)\n }\n }\n\n const clearErrors = () => {\n for (const [name] of fieldEntries) {\n fields[name].error.set(undefined)\n }\n }\n\n const resetField = (field: keyof TValues) => {\n if (fields[field]) {\n fields[field].reset()\n }\n }\n\n // Memoized register props per field+type combo\n const registerCache = new Map<string, FieldRegisterProps<unknown>>()\n\n const register = <K extends keyof TValues & string>(\n field: K,\n opts?: { type?: \"checkbox\" | \"number\" },\n ): FieldRegisterProps<TValues[K]> => {\n const cacheKey = `${field}:${opts?.type ?? \"text\"}`\n const cached = registerCache.get(cacheKey)\n if (cached) return cached as FieldRegisterProps<TValues[K]>\n\n const fieldState = fields[field]\n const props: FieldRegisterProps<TValues[K]> = {\n value: fieldState.value,\n onInput: (e: Event) => {\n const target = e.target as HTMLInputElement\n if (opts?.type === \"checkbox\") {\n fieldState.setValue(target.checked as TValues[K])\n } else if (opts?.type === \"number\") {\n const num = target.valueAsNumber\n fieldState.setValue((Number.isNaN(num) ? target.value : num) as TValues[K])\n } else {\n fieldState.setValue(target.value as TValues[K])\n }\n },\n onBlur: () => {\n fieldState.setTouched()\n },\n }\n\n if (opts?.type === \"checkbox\") {\n props.checked = computed(() => Boolean(fieldState.value()))\n }\n\n registerCache.set(cacheKey, props as FieldRegisterProps<unknown>)\n return props\n }\n\n return {\n fields,\n isSubmitting,\n isValidating,\n isValid,\n isDirty,\n submitCount,\n submitError,\n values: getValues,\n errors: getErrors,\n setFieldValue,\n setFieldError,\n setErrors,\n clearErrors,\n resetField,\n register,\n handleSubmit,\n reset,\n validate,\n }\n}\n\n/** Deep structural equality with depth limit to guard against circular references. */\nfunction structuredEqual(a: unknown, b: unknown, depth = 0): boolean {\n if (Object.is(a, b)) return true\n if (a == null || b == null) return false\n if (typeof a !== typeof b) return false\n // Bail at depth 10 — treat as not equal to avoid infinite recursion\n if (depth > 10) return false\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (!structuredEqual(a[i], b[i], depth + 1)) return false\n }\n return true\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>\n const bObj = b as Record<string, unknown>\n const aKeys = Object.keys(aObj)\n const bKeys = Object.keys(bObj)\n if (aKeys.length !== bKeys.length) return false\n for (const key of aKeys) {\n if (!structuredEqual(aObj[key], bObj[key], depth + 1)) return false\n }\n return true\n }\n\n return false\n}\n","import type { Computed } from \"@pyreon/reactivity\"\nimport { computed } from \"@pyreon/reactivity\"\nimport type { FormState, ValidationError } from \"./types\"\n\nexport interface FormStateSummary<TValues extends Record<string, unknown>> {\n isSubmitting: boolean\n isValidating: boolean\n isValid: boolean\n isDirty: boolean\n submitCount: number\n submitError: unknown\n touchedFields: Partial<Record<keyof TValues, boolean>>\n dirtyFields: Partial<Record<keyof TValues, boolean>>\n errors: Partial<Record<keyof TValues, ValidationError>>\n}\n\n/**\n * Subscribe to the full form state as a single computed signal.\n * Useful for rendering form-level UI (submit button disabled state,\n * error summaries, progress indicators).\n *\n * @example\n * const state = useFormState(form)\n * // state() => { isSubmitting, isValid, isDirty, errors, ... }\n *\n * @example\n * // Use a selector for fine-grained reactivity\n * const canSubmit = useFormState(form, (s) => s.isValid && !s.isSubmitting)\n */\nexport function useFormState<TValues extends Record<string, unknown>>(\n form: FormState<TValues>,\n): Computed<FormStateSummary<TValues>>\n\nexport function useFormState<TValues extends Record<string, unknown>, R>(\n form: FormState<TValues>,\n selector: (state: FormStateSummary<TValues>) => R,\n): Computed<R>\n\nexport function useFormState<TValues extends Record<string, unknown>, R>(\n form: FormState<TValues>,\n selector?: (state: FormStateSummary<TValues>) => R,\n): Computed<FormStateSummary<TValues>> | Computed<R> {\n const buildSummary = (): FormStateSummary<TValues> => {\n const touchedFields = {} as Partial<Record<keyof TValues, boolean>>\n const dirtyFields = {} as Partial<Record<keyof TValues, boolean>>\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n\n for (const key of Object.keys(form.fields) as (keyof TValues & string)[]) {\n const field = form.fields[key]\n if (field.touched()) touchedFields[key] = true\n if (field.dirty()) dirtyFields[key] = true\n const err = field.error()\n if (err !== undefined) errors[key] = err\n }\n\n return {\n isSubmitting: form.isSubmitting(),\n isValidating: form.isValidating(),\n isValid: form.isValid(),\n isDirty: form.isDirty(),\n submitCount: form.submitCount(),\n submitError: form.submitError(),\n touchedFields,\n dirtyFields,\n errors,\n }\n }\n\n if (selector) {\n return computed(() => selector(buildSummary()))\n }\n\n return computed(buildSummary)\n}\n","import type { Computed, Signal } from \"@pyreon/reactivity\"\nimport { computed } from \"@pyreon/reactivity\"\nimport type { FormState } from \"./types\"\n\n/**\n * Watch specific field values reactively. Returns a computed signal\n * that re-evaluates when any of the watched fields change.\n *\n * @example\n * // Watch a single field\n * const email = useWatch(form, 'email')\n * // email() => current email value\n *\n * @example\n * // Watch multiple fields\n * const [first, last] = useWatch(form, ['firstName', 'lastName'])\n * // first() => firstName value, last() => lastName value\n *\n * @example\n * // Watch all fields\n * const all = useWatch(form)\n * // all() => { email: '...', password: '...' }\n */\nexport function useWatch<TValues extends Record<string, unknown>, K extends keyof TValues & string>(\n form: FormState<TValues>,\n name: K,\n): Signal<TValues[K]>\n\nexport function useWatch<\n TValues extends Record<string, unknown>,\n K extends (keyof TValues & string)[],\n>(form: FormState<TValues>, names: K): { [I in keyof K]: Signal<TValues[K[I] & keyof TValues]> }\n\nexport function useWatch<TValues extends Record<string, unknown>>(\n form: FormState<TValues>,\n): Computed<TValues>\n\nexport function useWatch<TValues extends Record<string, unknown>, K extends keyof TValues & string>(\n form: FormState<TValues>,\n nameOrNames?: K | K[],\n): Signal<TValues[K]> | Signal<TValues[K]>[] | Computed<TValues> {\n // Watch all fields\n if (nameOrNames === undefined) {\n return computed(() => {\n const result = {} as TValues\n for (const key of Object.keys(form.fields) as (keyof TValues & string)[]) {\n ;(result as Record<string, unknown>)[key] = form.fields[key].value()\n }\n return result\n })\n }\n\n // Watch multiple fields\n if (Array.isArray(nameOrNames)) {\n return nameOrNames.map((name) => form.fields[name].value) as Signal<TValues[K]>[]\n }\n\n // Watch single field\n return form.fields[nameOrNames].value\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools2.d.ts","names":[],"sources":["../../../src/devtools.ts"],"mappings":";;AAmBA;;;;;AAMA;;;;;iBANgB,YAAA,CAAa,IAAA,UAAc,IAAA;;iBAM3B,cAAA,CAAe,IAAA;;iBAMf,cAAA,CAAA;AAQhB;AAAA,iBAAgB,eAAA,CAAgB,IAAA;;;;AAehC;iBAAgB,eAAA,
|
|
1
|
+
{"version":3,"file":"devtools2.d.ts","names":[],"sources":["../../../src/devtools.ts"],"mappings":";;AAmBA;;;;;AAMA;;;;;iBANgB,YAAA,CAAa,IAAA,UAAc,IAAA;;iBAM3B,cAAA,CAAe,IAAA;;iBAMf,cAAA,CAAA;AAQhB;AAAA,iBAAgB,eAAA,CAAgB,IAAA;;;;AAehC;iBAAgB,eAAA,CAAgB,IAAA,WAAe,MAAA;;iBAgB/B,YAAA,CAAa,QAAA;;iBAQb,cAAA,CAAA"}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ interface FormState<TValues extends Record<string, unknown>> {
|
|
|
73
73
|
* For numbers: pass `{ type: 'number' }` to use `valueAsNumber` on input.
|
|
74
74
|
*/
|
|
75
75
|
register: <K extends keyof TValues & string>(field: K, options?: {
|
|
76
|
-
type?:
|
|
76
|
+
type?: "checkbox" | "number";
|
|
77
77
|
}) => FieldRegisterProps<TValues[K]>;
|
|
78
78
|
/**
|
|
79
79
|
* Submit handler — runs validation, then calls onSubmit if valid.
|
|
@@ -95,7 +95,7 @@ interface UseFormOptions<TValues extends Record<string, unknown>> {
|
|
|
95
95
|
/** Schema-level validator (runs after field validators). */
|
|
96
96
|
schema?: SchemaValidateFn<TValues>;
|
|
97
97
|
/** When to validate: 'blur' (default), 'change', or 'submit'. */
|
|
98
|
-
validateOn?:
|
|
98
|
+
validateOn?: "blur" | "change" | "submit";
|
|
99
99
|
/** Debounce delay in ms for validators (useful for async validators). */
|
|
100
100
|
debounceMs?: number;
|
|
101
101
|
}
|
|
@@ -148,7 +148,7 @@ interface UseFieldResult<T> {
|
|
|
148
148
|
reset: () => void;
|
|
149
149
|
/** Register props for input binding. */
|
|
150
150
|
register: (opts?: {
|
|
151
|
-
type?:
|
|
151
|
+
type?: "checkbox";
|
|
152
152
|
}) => FieldRegisterProps<T>;
|
|
153
153
|
/** Whether the field has an error (computed). */
|
|
154
154
|
hasError: Computed<boolean>;
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/context.ts","../../../src/use-field.ts","../../../src/use-field-array.ts","../../../src/use-form.ts","../../../src/use-form-state.ts","../../../src/use-watch.ts"],"mappings":";;;;KAEY,eAAA;;;AAAZ;;KAMY,QAAA,MAAc,MAAA,CAAO,CAAA,IAAK,QAAA,CAAS,CAAA;;;AAA/C;;KAMY,UAAA,cAAwB,MAAA,sBAClC,KAAA,EAAO,CAAA,EACP,SAAA,EAAW,OAAA,KACR,eAAA,GAAkB,OAAA,CAAQ,eAAA;AAAA,KAEnB,gBAAA,aACV,MAAA,EAAQ,OAAA,KAEN,OAAA,CAAQ,MAAA,OAAa,OAAA,EAAS,eAAA,KAC9B,OAAA,CAAQ,OAAA,CAAQ,MAAA,OAAa,OAAA,EAAS,eAAA;AAAA,UAEzB,UAAA;EAjB8B;EAmB7C,KAAA,EAAO,MAAA,CAAO,CAAA;EAnB8B;EAqB5C,KAAA,EAAO,MAAA,CAAO,eAAA;EArBK;EAuBnB,OAAA,EAAS,MAAA;EAvBsB;EAyB/B,KAAA,EAAO,MAAA;EAzBsC;EA2B7C,QAAA,GAAW,KAAA,EAAO,CAAA;EA3B4B;EA6B9C,UAAA;EAvBoB;EAyBpB,KAAA;AAAA;;UAIe,kBAAA;EACf,KAAA,EAAO,MAAA,CAAO,CAAA;EACd,OAAA,GAAU,CAAA,EAAG,KAAA;EACb,MAAA;EACA,OAAA,GAAU,QAAA;AAAA;AAAA,UAGK,SAAA,iBAA0B,MAAA;EApCjB;EAsCxB,MAAA,gBAAsB,OAAA,GAAU,UAAA,CAAW,OAAA,CAAQ,CAAA;EApCxC;EAsCX,YAAA,EAAc,MAAA;EArCX;EAuCH,YAAA,EAAc,MAAA;EAvCe;EAyC7B,OAAA,EAAS,QAAA;EAzCmC;EA2C5C,OAAA,EAAS,QAAA;EAzCiB;EA2C1B,WAAA,EAAa,MAAA;EA1CL;EA4CR,WAAA,EAAa,MAAA;EA1CmB;EA4ChC,MAAA,QAAc,OAAA;EA5CZ;EA8CF,MAAA,QAAc,OAAA,CAAQ,MAAA,OAAa,OAAA,EAAS,eAAA;EA7CJ;EA+CxC,aAAA,mBAAgC,OAAA,EAAS,KAAA,EAAO,CAAA,EAAG,KAAA,EAAO,OAAA,CAAQ,CAAA;EA/CxD;EAiDV,aAAA,GAAgB,KAAA,QAAa,OAAA,EAAS,KAAA,EAAO,eAAA;EAjDpC;EAmDT,SAAA,GAAY,MAAA,EAAQ,OAAA,CAAQ,MAAA,OAAa,OAAA,EAAS,eAAA;EAvDvB;EAyD3B,WAAA;EAxDA;EA0DA,UAAA,GAAa,KAAA,QAAa,OAAA;EAxDhB;;;;;;EA+DV,QAAA,mBAA2B,OAAA,WACzB,KAAA,EAAO,CAAA,EACP,OAAA;IAAY,IAAA;EAAA,MACT,kBAAA,CAAmB,OAAA,CAAQ,CAAA;EAjEuB;AAEzD;;;EAoEE,YAAA,GAAe,CAAA,GAAI,KAAA,KAAU,OAAA;EAlEtB;EAoEP,KAAA;EAlEO;EAoEP,QAAA,QAAgB,OAAA;AAAA;AAAA,UAGD,cAAA,iBAA+B,MAAA;EAjE3B;EAmEnB,aAAA,EAAe,OAAA;EA7EW;EA+E1B,QAAA,GAAW,MAAA,EAAQ,OAAA,YAAmB,OAAA;EA7E/B;EA+EP,UAAA,GAAa,OAAA,eACC,OAAA,GAAU,UAAA,CAAW,OAAA,CAAQ,CAAA,GAAI,OAAA;EA9EjC;EAiFd,MAAA,GAAS,gBAAA,CAAiB,OAAA;EA/EjB;EAiFT,UAAA;EA/EO;EAiFP,UAAA;AAAA;;;
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/context.ts","../../../src/use-field.ts","../../../src/use-field-array.ts","../../../src/use-form.ts","../../../src/use-form-state.ts","../../../src/use-watch.ts"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/form",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Signal-based form management for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/pyreon/
|
|
9
|
-
"directory": "packages/form"
|
|
8
|
+
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
|
+
"directory": "packages/fundamentals/form"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/form#readme",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/
|
|
13
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
@@ -42,10 +42,17 @@
|
|
|
42
42
|
"build": "vl_rolldown_build",
|
|
43
43
|
"dev": "vl_rolldown_build-watch",
|
|
44
44
|
"test": "vitest run",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "biome check ."
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"@pyreon/core": "
|
|
49
|
-
"@pyreon/reactivity": "
|
|
49
|
+
"@pyreon/core": "^0.11.0",
|
|
50
|
+
"@pyreon/reactivity": "^0.11.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@happy-dom/global-registrator": "^20.8.3",
|
|
54
|
+
"@pyreon/core": "^0.11.0",
|
|
55
|
+
"@pyreon/reactivity": "^0.11.0",
|
|
56
|
+
"@pyreon/runtime-dom": "^0.11.0"
|
|
50
57
|
}
|
|
51
58
|
}
|
package/src/context.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import type { Props, VNode, VNodeChild } from
|
|
2
|
-
import { createContext, provide, useContext } from
|
|
3
|
-
import type { FormState } from
|
|
1
|
+
import type { Props, VNode, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { createContext, provide, useContext } from "@pyreon/core"
|
|
3
|
+
import type { FormState } from "./types"
|
|
4
4
|
|
|
5
|
-
const FormContext = createContext<FormState<Record<string, unknown>> | null>(
|
|
6
|
-
null,
|
|
7
|
-
)
|
|
5
|
+
const FormContext = createContext<FormState<Record<string, unknown>> | null>(null)
|
|
8
6
|
|
|
9
|
-
export interface FormProviderProps<TValues extends Record<string, unknown>>
|
|
10
|
-
extends Props {
|
|
7
|
+
export interface FormProviderProps<TValues extends Record<string, unknown>> extends Props {
|
|
11
8
|
form: FormState<TValues>
|
|
12
9
|
children?: VNodeChild
|
|
13
10
|
}
|
|
@@ -30,7 +27,7 @@ export function FormProvider<TValues extends Record<string, unknown>>(
|
|
|
30
27
|
provide(FormContext, props.form as FormState<Record<string, unknown>>)
|
|
31
28
|
|
|
32
29
|
const ch = props.children
|
|
33
|
-
return (typeof ch ===
|
|
30
|
+
return (typeof ch === "function" ? (ch as () => VNodeChild)() : ch) as VNode
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
/**
|
|
@@ -48,9 +45,7 @@ export function useFormContext<
|
|
|
48
45
|
>(): FormState<TValues> {
|
|
49
46
|
const form = useContext(FormContext)
|
|
50
47
|
if (!form) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'[@pyreon/form] useFormContext() must be used within a <FormProvider>.',
|
|
53
|
-
)
|
|
48
|
+
throw new Error("[@pyreon/form] useFormContext() must be used within a <FormProvider>.")
|
|
54
49
|
}
|
|
55
50
|
// Generic narrowing: context stores FormState<Record<string, unknown>>
|
|
56
51
|
// but callers narrow to their specific TValues at the call site.
|
package/src/devtools.ts
CHANGED
|
@@ -52,36 +52,18 @@ export function getFormInstance(name: string): object | undefined {
|
|
|
52
52
|
* Get a snapshot of a registered form's current state.
|
|
53
53
|
* Returns values, errors, and form-level status signals.
|
|
54
54
|
*/
|
|
55
|
-
export function getFormSnapshot(
|
|
56
|
-
name: string,
|
|
57
|
-
): Record<string, unknown> | undefined {
|
|
55
|
+
export function getFormSnapshot(name: string): Record<string, unknown> | undefined {
|
|
58
56
|
const form = getFormInstance(name) as Record<string, unknown> | undefined
|
|
59
57
|
if (!form) return undefined
|
|
60
58
|
return {
|
|
61
|
-
values:
|
|
62
|
-
|
|
63
|
-
? (form.values as () => unknown)()
|
|
64
|
-
: undefined,
|
|
65
|
-
errors:
|
|
66
|
-
typeof form.errors === 'function'
|
|
67
|
-
? (form.errors as () => unknown)()
|
|
68
|
-
: undefined,
|
|
59
|
+
values: typeof form.values === "function" ? (form.values as () => unknown)() : undefined,
|
|
60
|
+
errors: typeof form.errors === "function" ? (form.errors as () => unknown)() : undefined,
|
|
69
61
|
isSubmitting:
|
|
70
|
-
typeof form.isSubmitting ===
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
isValid:
|
|
74
|
-
typeof form.isValid === 'function'
|
|
75
|
-
? (form.isValid as () => unknown)()
|
|
76
|
-
: undefined,
|
|
77
|
-
isDirty:
|
|
78
|
-
typeof form.isDirty === 'function'
|
|
79
|
-
? (form.isDirty as () => unknown)()
|
|
80
|
-
: undefined,
|
|
62
|
+
typeof form.isSubmitting === "function" ? (form.isSubmitting as () => unknown)() : undefined,
|
|
63
|
+
isValid: typeof form.isValid === "function" ? (form.isValid as () => unknown)() : undefined,
|
|
64
|
+
isDirty: typeof form.isDirty === "function" ? (form.isDirty as () => unknown)() : undefined,
|
|
81
65
|
submitCount:
|
|
82
|
-
typeof form.submitCount ===
|
|
83
|
-
? (form.submitCount as () => unknown)()
|
|
84
|
-
: undefined,
|
|
66
|
+
typeof form.submitCount === "function" ? (form.submitCount as () => unknown)() : undefined,
|
|
85
67
|
}
|
|
86
68
|
}
|
|
87
69
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { FormProvider, useFormContext } from
|
|
1
|
+
export { FormProvider, useFormContext } from "./context"
|
|
2
2
|
export type {
|
|
3
3
|
Accessor,
|
|
4
4
|
FieldRegisterProps,
|
|
@@ -8,15 +8,15 @@ export type {
|
|
|
8
8
|
UseFormOptions,
|
|
9
9
|
ValidateFn,
|
|
10
10
|
ValidationError,
|
|
11
|
-
} from
|
|
12
|
-
export type { UseFieldResult } from
|
|
13
|
-
export { useField } from
|
|
11
|
+
} from "./types"
|
|
12
|
+
export type { UseFieldResult } from "./use-field"
|
|
13
|
+
export { useField } from "./use-field"
|
|
14
14
|
export type {
|
|
15
15
|
FieldArrayItem,
|
|
16
16
|
UseFieldArrayResult,
|
|
17
|
-
} from
|
|
18
|
-
export { useFieldArray } from
|
|
19
|
-
export { useForm } from
|
|
20
|
-
export type { FormStateSummary } from
|
|
21
|
-
export { useFormState } from
|
|
22
|
-
export { useWatch } from
|
|
17
|
+
} from "./use-field-array"
|
|
18
|
+
export { useFieldArray } from "./use-field-array"
|
|
19
|
+
export { useForm } from "./use-form"
|
|
20
|
+
export type { FormStateSummary } from "./use-form-state"
|
|
21
|
+
export { useFormState } from "./use-form-state"
|
|
22
|
+
export { useWatch } from "./use-watch"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computed, signal } from
|
|
1
|
+
import { computed, signal } from "@pyreon/reactivity"
|
|
2
2
|
import {
|
|
3
3
|
_resetDevtools,
|
|
4
4
|
getActiveForms,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
onFormChange,
|
|
8
8
|
registerForm,
|
|
9
9
|
unregisterForm,
|
|
10
|
-
} from
|
|
10
|
+
} from "../devtools"
|
|
11
11
|
|
|
12
12
|
// Minimal form-like object for testing (avoids needing the full useForm + DOM)
|
|
13
13
|
function createMockForm(values: Record<string, unknown>) {
|
|
@@ -28,40 +28,40 @@ function createMockForm(values: Record<string, unknown>) {
|
|
|
28
28
|
|
|
29
29
|
afterEach(() => _resetDevtools())
|
|
30
30
|
|
|
31
|
-
describe(
|
|
32
|
-
test(
|
|
31
|
+
describe("form devtools", () => {
|
|
32
|
+
test("getActiveForms returns empty initially", () => {
|
|
33
33
|
expect(getActiveForms()).toEqual([])
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
test(
|
|
37
|
-
const form = createMockForm({ email:
|
|
38
|
-
registerForm(
|
|
39
|
-
expect(getActiveForms()).toEqual([
|
|
36
|
+
test("registerForm makes form visible", () => {
|
|
37
|
+
const form = createMockForm({ email: "" })
|
|
38
|
+
registerForm("login", form)
|
|
39
|
+
expect(getActiveForms()).toEqual(["login"])
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
test(
|
|
43
|
-
const form = createMockForm({ email:
|
|
44
|
-
registerForm(
|
|
45
|
-
expect(getFormInstance(
|
|
42
|
+
test("getFormInstance returns the registered form", () => {
|
|
43
|
+
const form = createMockForm({ email: "" })
|
|
44
|
+
registerForm("login", form)
|
|
45
|
+
expect(getFormInstance("login")).toBe(form)
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
test(
|
|
49
|
-
expect(getFormInstance(
|
|
48
|
+
test("getFormInstance returns undefined for unregistered name", () => {
|
|
49
|
+
expect(getFormInstance("nope")).toBeUndefined()
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
test(
|
|
53
|
-
const form = createMockForm({ email:
|
|
54
|
-
registerForm(
|
|
55
|
-
unregisterForm(
|
|
52
|
+
test("unregisterForm removes the form", () => {
|
|
53
|
+
const form = createMockForm({ email: "" })
|
|
54
|
+
registerForm("login", form)
|
|
55
|
+
unregisterForm("login")
|
|
56
56
|
expect(getActiveForms()).toEqual([])
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
test(
|
|
60
|
-
const form = createMockForm({ email:
|
|
61
|
-
registerForm(
|
|
62
|
-
const snapshot = getFormSnapshot(
|
|
59
|
+
test("getFormSnapshot returns current form state", () => {
|
|
60
|
+
const form = createMockForm({ email: "test@test.com" })
|
|
61
|
+
registerForm("login", form)
|
|
62
|
+
const snapshot = getFormSnapshot("login")
|
|
63
63
|
expect(snapshot).toBeDefined()
|
|
64
|
-
expect(snapshot!.values).toEqual({ email:
|
|
64
|
+
expect(snapshot!.values).toEqual({ email: "test@test.com" })
|
|
65
65
|
expect(snapshot!.errors).toEqual({})
|
|
66
66
|
expect(snapshot!.isSubmitting).toBe(false)
|
|
67
67
|
expect(snapshot!.isValid).toBe(true)
|
|
@@ -69,19 +69,19 @@ describe('form devtools', () => {
|
|
|
69
69
|
expect(snapshot!.submitCount).toBe(0)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
test(
|
|
72
|
+
test("getFormSnapshot handles form with non-function properties", () => {
|
|
73
73
|
// Register a plain object where properties are NOT functions
|
|
74
74
|
// This covers the false branches of typeof checks in getFormSnapshot
|
|
75
75
|
const plainForm = {
|
|
76
|
-
values:
|
|
76
|
+
values: "not-a-function",
|
|
77
77
|
errors: 42,
|
|
78
78
|
isSubmitting: true,
|
|
79
79
|
isValid: null,
|
|
80
80
|
isDirty: undefined,
|
|
81
|
-
submitCount:
|
|
81
|
+
submitCount: "five",
|
|
82
82
|
}
|
|
83
|
-
registerForm(
|
|
84
|
-
const snapshot = getFormSnapshot(
|
|
83
|
+
registerForm("plain", plainForm)
|
|
84
|
+
const snapshot = getFormSnapshot("plain")
|
|
85
85
|
expect(snapshot).toBeDefined()
|
|
86
86
|
expect(snapshot!.values).toBeUndefined()
|
|
87
87
|
expect(snapshot!.errors).toBeUndefined()
|
|
@@ -91,51 +91,51 @@ describe('form devtools', () => {
|
|
|
91
91
|
expect(snapshot!.submitCount).toBeUndefined()
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
test(
|
|
95
|
-
expect(getFormSnapshot(
|
|
94
|
+
test("getFormSnapshot returns undefined for unregistered name", () => {
|
|
95
|
+
expect(getFormSnapshot("nope")).toBeUndefined()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
test(
|
|
98
|
+
test("onFormChange fires on register", () => {
|
|
99
99
|
const calls: number[] = []
|
|
100
100
|
const unsub = onFormChange(() => calls.push(1))
|
|
101
101
|
|
|
102
|
-
registerForm(
|
|
102
|
+
registerForm("login", createMockForm({}))
|
|
103
103
|
expect(calls.length).toBe(1)
|
|
104
104
|
|
|
105
105
|
unsub()
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
test(
|
|
109
|
-
registerForm(
|
|
108
|
+
test("onFormChange fires on unregister", () => {
|
|
109
|
+
registerForm("login", createMockForm({}))
|
|
110
110
|
|
|
111
111
|
const calls: number[] = []
|
|
112
112
|
const unsub = onFormChange(() => calls.push(1))
|
|
113
|
-
unregisterForm(
|
|
113
|
+
unregisterForm("login")
|
|
114
114
|
expect(calls.length).toBe(1)
|
|
115
115
|
|
|
116
116
|
unsub()
|
|
117
117
|
})
|
|
118
118
|
|
|
119
|
-
test(
|
|
119
|
+
test("onFormChange unsubscribe stops notifications", () => {
|
|
120
120
|
const calls: number[] = []
|
|
121
121
|
const unsub = onFormChange(() => calls.push(1))
|
|
122
122
|
unsub()
|
|
123
123
|
|
|
124
|
-
registerForm(
|
|
124
|
+
registerForm("login", createMockForm({}))
|
|
125
125
|
expect(calls.length).toBe(0)
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
-
test(
|
|
129
|
-
registerForm(
|
|
130
|
-
registerForm(
|
|
131
|
-
expect(getActiveForms().sort()).toEqual([
|
|
128
|
+
test("multiple forms are tracked", () => {
|
|
129
|
+
registerForm("login", createMockForm({}))
|
|
130
|
+
registerForm("signup", createMockForm({}))
|
|
131
|
+
expect(getActiveForms().sort()).toEqual(["login", "signup"])
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
-
test(
|
|
134
|
+
test("getActiveForms cleans up garbage-collected WeakRefs", () => {
|
|
135
135
|
// Simulate a WeakRef whose target has been GC'd by replacing
|
|
136
136
|
// the internal map entry with a WeakRef that returns undefined from deref()
|
|
137
|
-
registerForm(
|
|
138
|
-
expect(getActiveForms()).toEqual([
|
|
137
|
+
registerForm("gc-form", createMockForm({}))
|
|
138
|
+
expect(getActiveForms()).toEqual(["gc-form"])
|
|
139
139
|
|
|
140
140
|
// Overwrite with a WeakRef-like object that always returns undefined (simulates GC)
|
|
141
141
|
// We do this by registering and then manipulating the internal state
|
|
@@ -150,7 +150,7 @@ describe('form devtools', () => {
|
|
|
150
150
|
// the WeakRef naturally. Instead, let's create a real WeakRef to a short-lived object:
|
|
151
151
|
;(() => {
|
|
152
152
|
let tempObj: object | null = { tmp: true }
|
|
153
|
-
registerForm(
|
|
153
|
+
registerForm("temp-form", tempObj)
|
|
154
154
|
tempObj = null // Allow GC
|
|
155
155
|
})()
|
|
156
156
|
|
|
@@ -165,11 +165,11 @@ describe('form devtools', () => {
|
|
|
165
165
|
_resetDevtools()
|
|
166
166
|
})
|
|
167
167
|
|
|
168
|
-
test(
|
|
168
|
+
test("getFormInstance cleans up and returns undefined when WeakRef is dead", () => {
|
|
169
169
|
// Register a form, then simulate GC by replacing the map entry
|
|
170
|
-
const form = createMockForm({ email:
|
|
171
|
-
registerForm(
|
|
172
|
-
expect(getFormInstance(
|
|
170
|
+
const form = createMockForm({ email: "" })
|
|
171
|
+
registerForm("dying-form", form)
|
|
172
|
+
expect(getFormInstance("dying-form")).toBe(form)
|
|
173
173
|
|
|
174
174
|
// Now we need to make the WeakRef deref return undefined.
|
|
175
175
|
// We can't directly access _activeForms, but we can test the
|
|
@@ -189,15 +189,15 @@ describe('form devtools', () => {
|
|
|
189
189
|
globalThis.WeakRef = MockWeakRef as any
|
|
190
190
|
|
|
191
191
|
_resetDevtools()
|
|
192
|
-
registerForm(
|
|
193
|
-
expect(getFormInstance(
|
|
192
|
+
registerForm("mock-form", form)
|
|
193
|
+
expect(getFormInstance("mock-form")).toBe(form)
|
|
194
194
|
|
|
195
195
|
// Now simulate GC
|
|
196
196
|
mockDerefResult = undefined
|
|
197
|
-
expect(getFormInstance(
|
|
197
|
+
expect(getFormInstance("mock-form")).toBeUndefined()
|
|
198
198
|
|
|
199
199
|
// getActiveForms should also clean it up
|
|
200
|
-
registerForm(
|
|
200
|
+
registerForm("mock-form2", form)
|
|
201
201
|
mockDerefResult = undefined
|
|
202
202
|
expect(getActiveForms()).toEqual([])
|
|
203
203
|
|