@formbar/react 0.8.0 → 0.12.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/dist/index.cjs CHANGED
@@ -73,7 +73,7 @@ function useFormSelector(form, selector, equalityFn) {
73
73
  prevRef.current = { value: next, initialized: true };
74
74
  return next;
75
75
  }, [form]);
76
- return react.useSyncExternalStore(subscribe, getSnapshot);
76
+ return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
77
77
  }
78
78
 
79
79
  // src/use-field.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/a11y.ts","../src/use-form-selector.ts","../src/use-field.ts","../src/core-form-options.ts","../src/use-form.ts","../src/use-expression-props.ts"],"names":["useRef","useCallback","useSyncExternalStore","useMemo","createForm","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ValidationIssue } from \"@formbar/core\";\n\nconst DEFAULT_FIELD_PREFIX = \"field\";\n\n/** ARIA props for a form field */\nexport interface FieldA11yProps {\n\treadonly id: string;\n\treadonly \"aria-invalid\"?: boolean;\n\treadonly \"aria-describedby\"?: string;\n\treadonly \"aria-required\"?: boolean;\n\treadonly \"aria-errormessage\"?: string;\n}\n\n/** Label props for semantic association */\nexport interface LabelA11yProps {\n\treadonly htmlFor: string;\n}\n\n/** Description/error message props */\nexport interface DescriptionA11yProps {\n\treadonly id: string;\n\treadonly role?: \"alert\";\n}\n\n/** Generate a deterministic field ID from path */\nexport function fieldId(path: string, prefix: string = DEFAULT_FIELD_PREFIX): string {\n\treturn `${prefix}-${path\n\t\t.replace(/[.[\\]/]/g, \"-\")\n\t\t.replace(/-+/g, \"-\")\n\t\t.replace(/-$/, \"\")}`;\n}\n\n/** Generate description element ID */\nexport function descriptionId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-description`;\n}\n\n/** Generate error element ID */\nexport function errorId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-error`;\n}\n\n/** Get ARIA props for a field input element */\nexport function getFieldProps(\n\tpath: string,\n\toptions?: {\n\t\treadonly issues?: readonly ValidationIssue[];\n\t\treadonly required?: boolean;\n\t\treadonly hasDescription?: boolean;\n\t},\n): FieldA11yProps {\n\tconst id = fieldId(path);\n\tconst hasErrors = options?.issues?.some((i) => i.severity === \"error\") ?? false;\n\n\tconst describedBy: string[] = [];\n\tif (options?.hasDescription) describedBy.push(descriptionId(path));\n\tif (hasErrors) describedBy.push(errorId(path));\n\n\tconst props: FieldA11yProps = {\n\t\tid,\n\t\t...(hasErrors ? { \"aria-invalid\": true as const } : {}),\n\t\t...(describedBy.length > 0 ? { \"aria-describedby\": describedBy.join(\" \") } : {}),\n\t\t...(options?.required ? { \"aria-required\": true as const } : {}),\n\t\t...(hasErrors ? { \"aria-errormessage\": errorId(path) } : {}),\n\t};\n\n\treturn props;\n}\n\n/** Get label props for semantic association */\nexport function getLabelProps(path: string): LabelA11yProps {\n\treturn { htmlFor: fieldId(path) };\n}\n\n/** Get description element props */\nexport function getDescriptionProps(path: string): DescriptionA11yProps {\n\treturn { id: descriptionId(path) };\n}\n\n/** Get error message props */\nexport function getErrorProps(path: string): DescriptionA11yProps {\n\treturn { id: errorId(path), role: \"alert\" };\n}\n\n/** Find the first field path with errors (for focus management) */\nexport function findFirstErrorPath(issues: readonly ValidationIssue[]): string | undefined {\n\tconst firstError = issues.find((i) => i.severity === \"error\");\n\tif (!firstError) return undefined;\n\treturn firstError.path.segments.join(\".\");\n}\n\n/** Focus the first error field after submit (browser-only) */\nexport function focusFirstError(issues: readonly ValidationIssue[]): boolean {\n\tconst path = findFirstErrorPath(issues);\n\tif (!path) return false;\n\n\tif (typeof document === \"undefined\") return false;\n\n\tconst id = fieldId(path);\n\tconst element = document.getElementById(id);\n\tif (element) {\n\t\telement.focus();\n\t\treturn true;\n\t}\n\treturn false;\n}\n","import type { FormApi, FormState } from \"@formbar/core\";\nimport { useCallback, useRef, useSyncExternalStore } from \"react\";\n\n/**\n * Subscribe to a derived value from form state with fine-grained reactivity.\n * Only triggers re-render when the selected value changes (by equality function).\n *\n * @param form - The {@link FormApi} instance.\n * @param selector - Function that extracts a value from the full form state.\n * @param equalityFn - Optional equality comparator (defaults to `Object.is`).\n * @returns The current selected value, updated reactively.\n *\n * @example\n * ```typescript\n * const isValid = useFormSelector(form, (state) => state.issues.length === 0);\n * const submitCount = useFormSelector(form, (state) => state.meta.submitted);\n * ```\n */\nexport function useFormSelector<TData, TUi, T>(\n\tform: FormApi<TData, TUi>,\n\tselector: (state: FormState<TData, TUi>) => T,\n\tequalityFn?: (prev: T, next: T) => boolean,\n): T {\n\tconst eqRef = useRef(equalityFn ?? Object.is);\n\teqRef.current = equalityFn ?? Object.is;\n\n\tconst selectorRef = useRef(selector);\n\tselectorRef.current = selector;\n\n\tconst prevRef = useRef<{ readonly value: T; readonly initialized: boolean }>({\n\t\tvalue: undefined as T,\n\t\tinitialized: false,\n\t});\n\n\tconst subscribe = useCallback((onStoreChange: () => void) => form.subscribe(onStoreChange), [form]);\n\n\tconst getSnapshot = useCallback((): T => {\n\t\tconst next = selectorRef.current(form.getState());\n\t\tif (prevRef.current.initialized && eqRef.current(prevRef.current.value, next)) {\n\t\t\treturn prevRef.current.value;\n\t\t}\n\t\tprevRef.current = { value: next, initialized: true };\n\t\treturn next;\n\t}, [form]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot);\n}\n","import type { FieldApi, FieldConfig, FieldMetaEntry, FormApi } from \"@formbar/core\";\nimport { useMemo, useRef } from \"react\";\nimport { useFormSelector } from \"./use-form-selector.js\";\n\ninterface FieldSnapshot {\n\treadonly value: unknown;\n\treadonly meta: FieldMetaEntry | undefined;\n}\n\nfunction fieldSnapshotEqual(a: FieldSnapshot, b: FieldSnapshot): boolean {\n\treturn a.value === b.value && a.meta === b.meta;\n}\n\n/**\n * React hook that subscribes to a specific field with fine-grained re-rendering.\n * Only re-renders when the field's value or metadata actually changes.\n *\n * @param form - The {@link FormApi} instance (from useForm or createForm).\n * @param path - Dot-path to the field (e.g., `\"user.email\"`).\n * @param config - Optional field configuration (label, validators, triggers).\n * @returns A {@link FieldApi} with reactive get/set, validation, and touch tracking.\n *\n * @example\n * ```typescript\n * function EmailField({ form }) {\n * const field = useField(form, \"email\");\n * return (\n * <div>\n * <input\n * value={field.get() ?? \"\"}\n * onChange={e => field.handleChange(e.target.value)}\n * onBlur={() => field.handleBlur()}\n * />\n * {field.issues().map(i => <span key={i.code}>{i.message}</span>)}\n * </div>\n * );\n * }\n * ```\n */\nexport function useField<TData, TUi, P extends string>(\n\tform: FormApi<TData, TUi>,\n\tpath: P,\n\tconfig?: FieldConfig,\n): FieldApi<TData, TUi, P> {\n\t// Stabilize config reference — inline objects create new references every render\n\tconst configRef = useRef(config);\n\tconst stableConfig = useMemo(() => {\n\t\tconst prev = configRef.current;\n\t\tif (prev === config) return prev;\n\t\tif (prev && config && JSON.stringify(prev) === JSON.stringify(config)) return prev;\n\t\tconfigRef.current = config;\n\t\treturn config;\n\t}, [config]);\n\n\tconst field = useMemo(() => form.fieldDynamic(path, stableConfig), [form, path, stableConfig]);\n\n\t// Subscribe to field value and touched state to trigger re-renders\n\tuseFormSelector(\n\t\tform,\n\t\t() => {\n\t\t\tconst state = form.getState();\n\t\t\tconst pathKey = field.path.segments.join(\".\");\n\t\t\tconst meta = (state.fieldMeta as Readonly<Record<string, FieldMetaEntry>>)[pathKey];\n\t\t\treturn { value: field.get(), meta } as FieldSnapshot;\n\t\t},\n\t\tfieldSnapshotEqual,\n\t);\n\n\treturn field as FieldApi<TData, TUi, P>;\n}\n","import type { CreateFormOptions } from \"@formbar/core\";\nimport type { UseFormOptions } from \"./use-form.js\";\n\nexport function getCoreFormOptions<TData, TUi>(\n\toptions: UseFormOptions<TData, TUi> | undefined,\n): CreateFormOptions<TData, TUi> | undefined {\n\tif (options === undefined) return undefined;\n\ttry {\n\t\tif (!Object.hasOwn(options, \"autoFocusOnError\")) return options;\n\t\tconst { autoFocusOnError: _reactOption, ...coreDescriptors } = Object.getOwnPropertyDescriptors(options);\n\t\treturn Object.create(Object.getPrototypeOf(options), coreDescriptors) as CreateFormOptions<TData, TUi>;\n\t} catch {\n\t\t// Option diagnostics must not make useForm less resilient than createForm.\n\t\treturn options;\n\t}\n}\n","import type { CreateFormOptions, FormApi, SubmitResult } from \"@formbar/core\";\nimport { createForm } from \"@formbar/core\";\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { focusFirstError } from \"./a11y.js\";\nimport { getCoreFormOptions } from \"./core-form-options.js\";\n\n/** Options for useForm, extending core CreateFormOptions with React-specific behavior */\nexport interface UseFormOptions<TData, TUi> extends CreateFormOptions<TData, TUi> {\n\t/** Auto-focus the first error field on submit failure (default: true) */\n\treadonly autoFocusOnError?: boolean;\n}\n\n/**\n * React hook that creates and manages a form instance with automatic cleanup.\n * The form is created once on mount and disposed on unmount (StrictMode-safe).\n *\n * @param options - Form configuration (same as {@link createForm} options).\n * @returns A stable {@link FormApi} reference that persists across re-renders.\n *\n * @example\n * ```typescript\n * function ContactForm() {\n * const form = useForm({\n * initialData: { name: \"\", email: \"\" },\n * onSubmit: async ({ payload }) => {\n * await saveContact(payload);\n * return { ok: true, submitId: \"1\" };\n * },\n * });\n *\n * return <input value={form.field(\"name\").get()} onChange={e => form.field(\"name\").set(e.target.value)} />;\n * }\n * ```\n */\nexport function useForm<TData, TUi>(options?: UseFormOptions<TData, TUi>): FormApi<TData, TUi> {\n\tconst autoFocus = options?.autoFocusOnError ?? true;\n\tconst formRef = useRef<FormApi<TData, TUi> | null>(null);\n\tconst disposeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\tif (formRef.current === null) {\n\t\tformRef.current = createForm<TData, TUi>(getCoreFormOptions(options));\n\t}\n\n\tconst form = formRef.current;\n\n\t// Adapt form.subscribe (which passes state) to useSyncExternalStore's expected signature\n\tconst subscribe = useRef((onStoreChange: () => void) => {\n\t\treturn form.subscribe(onStoreChange);\n\t}).current;\n\n\tuseSyncExternalStore(subscribe, () => form.getState());\n\n\t// Deferred disposal: schedule dispose in a macrotask so StrictMode remount can cancel it\n\tuseEffect(() => {\n\t\tif (disposeTimerRef.current !== null) {\n\t\t\tclearTimeout(disposeTimerRef.current);\n\t\t\tdisposeTimerRef.current = null;\n\t\t}\n\t\treturn () => {\n\t\t\tdisposeTimerRef.current = setTimeout(() => {\n\t\t\t\tformRef.current?.dispose();\n\t\t\t}, 0);\n\t\t};\n\t}, []);\n\n\t// Wrap the form API to auto-focus on submit errors (ADR §12)\n\tconst wrappedApi = useMemo((): FormApi<TData, TUi> => {\n\t\tif (!autoFocus) return form;\n\n\t\treturn {\n\t\t\t...form,\n\t\t\tsubmit: async (...args: Parameters<FormApi<TData, TUi>[\"submit\"]>): Promise<SubmitResult> => {\n\t\t\t\tconst result = await form.submit(...args);\n\t\t\t\tif (!result.ok && result.fieldIssues?.length) {\n\t\t\t\t\tfocusFirstError(result.fieldIssues);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t};\n\t}, [form, autoFocus]);\n\n\treturn wrappedApi;\n}\n","import type { ExpressionService, PropDefinitions, ResolvedProps } from \"@formbar/expressions\";\nimport { useMemo, useSyncExternalStore } from \"react\";\n\n/** Keep definitions stable with useMemo; subscription cleanup also invalidates retained setters. */\nexport function useExpressionProps(service: ExpressionService, definitions: PropDefinitions): ResolvedProps {\n\tconst binding = useMemo(() => service.resolveProps(definitions), [service, definitions]);\n\treturn useSyncExternalStore(binding.subscribe, binding.getSnapshot, binding.getSnapshot);\n}\n"]}
1
+ {"version":3,"sources":["../src/a11y.ts","../src/use-form-selector.ts","../src/use-field.ts","../src/core-form-options.ts","../src/use-form.ts","../src/use-expression-props.ts"],"names":["useRef","useCallback","useSyncExternalStore","useMemo","createForm","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ValidationIssue } from \"@formbar/core\";\n\nconst DEFAULT_FIELD_PREFIX = \"field\";\n\n/** ARIA props for a form field */\nexport interface FieldA11yProps {\n\treadonly id: string;\n\treadonly \"aria-invalid\"?: boolean;\n\treadonly \"aria-describedby\"?: string;\n\treadonly \"aria-required\"?: boolean;\n\treadonly \"aria-errormessage\"?: string;\n}\n\n/** Label props for semantic association */\nexport interface LabelA11yProps {\n\treadonly htmlFor: string;\n}\n\n/** Description/error message props */\nexport interface DescriptionA11yProps {\n\treadonly id: string;\n\treadonly role?: \"alert\";\n}\n\n/** Generate a deterministic field ID from path */\nexport function fieldId(path: string, prefix: string = DEFAULT_FIELD_PREFIX): string {\n\treturn `${prefix}-${path\n\t\t.replace(/[.[\\]/]/g, \"-\")\n\t\t.replace(/-+/g, \"-\")\n\t\t.replace(/-$/, \"\")}`;\n}\n\n/** Generate description element ID */\nexport function descriptionId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-description`;\n}\n\n/** Generate error element ID */\nexport function errorId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-error`;\n}\n\n/** Get ARIA props for a field input element */\nexport function getFieldProps(\n\tpath: string,\n\toptions?: {\n\t\treadonly issues?: readonly ValidationIssue[];\n\t\treadonly required?: boolean;\n\t\treadonly hasDescription?: boolean;\n\t},\n): FieldA11yProps {\n\tconst id = fieldId(path);\n\tconst hasErrors = options?.issues?.some((i) => i.severity === \"error\") ?? false;\n\n\tconst describedBy: string[] = [];\n\tif (options?.hasDescription) describedBy.push(descriptionId(path));\n\tif (hasErrors) describedBy.push(errorId(path));\n\n\tconst props: FieldA11yProps = {\n\t\tid,\n\t\t...(hasErrors ? { \"aria-invalid\": true as const } : {}),\n\t\t...(describedBy.length > 0 ? { \"aria-describedby\": describedBy.join(\" \") } : {}),\n\t\t...(options?.required ? { \"aria-required\": true as const } : {}),\n\t\t...(hasErrors ? { \"aria-errormessage\": errorId(path) } : {}),\n\t};\n\n\treturn props;\n}\n\n/** Get label props for semantic association */\nexport function getLabelProps(path: string): LabelA11yProps {\n\treturn { htmlFor: fieldId(path) };\n}\n\n/** Get description element props */\nexport function getDescriptionProps(path: string): DescriptionA11yProps {\n\treturn { id: descriptionId(path) };\n}\n\n/** Get error message props */\nexport function getErrorProps(path: string): DescriptionA11yProps {\n\treturn { id: errorId(path), role: \"alert\" };\n}\n\n/** Find the first field path with errors (for focus management) */\nexport function findFirstErrorPath(issues: readonly ValidationIssue[]): string | undefined {\n\tconst firstError = issues.find((i) => i.severity === \"error\");\n\tif (!firstError) return undefined;\n\treturn firstError.path.segments.join(\".\");\n}\n\n/** Focus the first error field after submit (browser-only) */\nexport function focusFirstError(issues: readonly ValidationIssue[]): boolean {\n\tconst path = findFirstErrorPath(issues);\n\tif (!path) return false;\n\n\tif (typeof document === \"undefined\") return false;\n\n\tconst id = fieldId(path);\n\tconst element = document.getElementById(id);\n\tif (element) {\n\t\telement.focus();\n\t\treturn true;\n\t}\n\treturn false;\n}\n","import type { FormApi, FormState } from \"@formbar/core\";\nimport { useCallback, useRef, useSyncExternalStore } from \"react\";\n\n/**\n * Subscribe to a derived value from form state with fine-grained reactivity.\n * Only triggers re-render when the selected value changes (by equality function).\n *\n * @param form - The {@link FormApi} instance.\n * @param selector - Function that extracts a value from the full form state.\n * @param equalityFn - Optional equality comparator (defaults to `Object.is`).\n * @returns The current selected value, updated reactively.\n *\n * @example\n * ```typescript\n * const isValid = useFormSelector(form, (state) => state.issues.length === 0);\n * const submitCount = useFormSelector(form, (state) => state.meta.submitted);\n * ```\n */\nexport function useFormSelector<TData, TUi, T>(\n\tform: FormApi<TData, TUi>,\n\tselector: (state: FormState<TData, TUi>) => T,\n\tequalityFn?: (prev: T, next: T) => boolean,\n): T {\n\tconst eqRef = useRef(equalityFn ?? Object.is);\n\teqRef.current = equalityFn ?? Object.is;\n\n\tconst selectorRef = useRef(selector);\n\tselectorRef.current = selector;\n\n\tconst prevRef = useRef<{ readonly value: T; readonly initialized: boolean }>({\n\t\tvalue: undefined as T,\n\t\tinitialized: false,\n\t});\n\n\tconst subscribe = useCallback((onStoreChange: () => void) => form.subscribe(onStoreChange), [form]);\n\n\tconst getSnapshot = useCallback((): T => {\n\t\tconst next = selectorRef.current(form.getState());\n\t\tif (prevRef.current.initialized && eqRef.current(prevRef.current.value, next)) {\n\t\t\treturn prevRef.current.value;\n\t\t}\n\t\tprevRef.current = { value: next, initialized: true };\n\t\treturn next;\n\t}, [form]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n","import type { FieldApi, FieldConfig, FieldMetaEntry, FormApi } from \"@formbar/core\";\nimport { useMemo, useRef } from \"react\";\nimport { useFormSelector } from \"./use-form-selector.js\";\n\ninterface FieldSnapshot {\n\treadonly value: unknown;\n\treadonly meta: FieldMetaEntry | undefined;\n}\n\nfunction fieldSnapshotEqual(a: FieldSnapshot, b: FieldSnapshot): boolean {\n\treturn a.value === b.value && a.meta === b.meta;\n}\n\n/**\n * React hook that subscribes to a specific field with fine-grained re-rendering.\n * Only re-renders when the field's value or metadata actually changes.\n *\n * @param form - The {@link FormApi} instance (from useForm or createForm).\n * @param path - Dot-path to the field (e.g., `\"user.email\"`).\n * @param config - Optional field configuration (label, validators, triggers).\n * @returns A {@link FieldApi} with reactive get/set, validation, and touch tracking.\n *\n * @example\n * ```typescript\n * function EmailField({ form }) {\n * const field = useField(form, \"email\");\n * return (\n * <div>\n * <input\n * value={field.get() ?? \"\"}\n * onChange={e => field.handleChange(e.target.value)}\n * onBlur={() => field.handleBlur()}\n * />\n * {field.issues().map(i => <span key={i.code}>{i.message}</span>)}\n * </div>\n * );\n * }\n * ```\n */\nexport function useField<TData, TUi, P extends string>(\n\tform: FormApi<TData, TUi>,\n\tpath: P,\n\tconfig?: FieldConfig,\n): FieldApi<TData, TUi, P> {\n\t// Stabilize config reference — inline objects create new references every render\n\tconst configRef = useRef(config);\n\tconst stableConfig = useMemo(() => {\n\t\tconst prev = configRef.current;\n\t\tif (prev === config) return prev;\n\t\tif (prev && config && JSON.stringify(prev) === JSON.stringify(config)) return prev;\n\t\tconfigRef.current = config;\n\t\treturn config;\n\t}, [config]);\n\n\tconst field = useMemo(() => form.fieldDynamic(path, stableConfig), [form, path, stableConfig]);\n\n\t// Subscribe to field value and touched state to trigger re-renders\n\tuseFormSelector(\n\t\tform,\n\t\t() => {\n\t\t\tconst state = form.getState();\n\t\t\tconst pathKey = field.path.segments.join(\".\");\n\t\t\tconst meta = (state.fieldMeta as Readonly<Record<string, FieldMetaEntry>>)[pathKey];\n\t\t\treturn { value: field.get(), meta } as FieldSnapshot;\n\t\t},\n\t\tfieldSnapshotEqual,\n\t);\n\n\treturn field as FieldApi<TData, TUi, P>;\n}\n","import type { CreateFormOptions } from \"@formbar/core\";\nimport type { UseFormOptions } from \"./use-form.js\";\n\nexport function getCoreFormOptions<TData, TUi>(\n\toptions: UseFormOptions<TData, TUi> | undefined,\n): CreateFormOptions<TData, TUi> | undefined {\n\tif (options === undefined) return undefined;\n\ttry {\n\t\tif (!Object.hasOwn(options, \"autoFocusOnError\")) return options;\n\t\tconst { autoFocusOnError: _reactOption, ...coreDescriptors } = Object.getOwnPropertyDescriptors(options);\n\t\treturn Object.create(Object.getPrototypeOf(options), coreDescriptors) as CreateFormOptions<TData, TUi>;\n\t} catch {\n\t\t// Option diagnostics must not make useForm less resilient than createForm.\n\t\treturn options;\n\t}\n}\n","import type { CreateFormOptions, FormApi, SubmitResult } from \"@formbar/core\";\nimport { createForm } from \"@formbar/core\";\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { focusFirstError } from \"./a11y.js\";\nimport { getCoreFormOptions } from \"./core-form-options.js\";\n\n/** Options for useForm, extending core CreateFormOptions with React-specific behavior */\nexport interface UseFormOptions<TData, TUi> extends CreateFormOptions<TData, TUi> {\n\t/** Auto-focus the first error field on submit failure (default: true) */\n\treadonly autoFocusOnError?: boolean;\n}\n\n/**\n * React hook that creates and manages a form instance with automatic cleanup.\n * The form is created once on mount and disposed on unmount (StrictMode-safe).\n *\n * @param options - Form configuration (same as {@link createForm} options).\n * @returns A stable {@link FormApi} reference that persists across re-renders.\n *\n * @example\n * ```typescript\n * function ContactForm() {\n * const form = useForm({\n * initialData: { name: \"\", email: \"\" },\n * onSubmit: async ({ payload }) => {\n * await saveContact(payload);\n * return { ok: true, submitId: \"1\" };\n * },\n * });\n *\n * return <input value={form.field(\"name\").get()} onChange={e => form.field(\"name\").set(e.target.value)} />;\n * }\n * ```\n */\nexport function useForm<TData, TUi>(options?: UseFormOptions<TData, TUi>): FormApi<TData, TUi> {\n\tconst autoFocus = options?.autoFocusOnError ?? true;\n\tconst formRef = useRef<FormApi<TData, TUi> | null>(null);\n\tconst disposeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\tif (formRef.current === null) {\n\t\tformRef.current = createForm<TData, TUi>(getCoreFormOptions(options));\n\t}\n\n\tconst form = formRef.current;\n\n\t// Adapt form.subscribe (which passes state) to useSyncExternalStore's expected signature\n\tconst subscribe = useRef((onStoreChange: () => void) => {\n\t\treturn form.subscribe(onStoreChange);\n\t}).current;\n\n\tuseSyncExternalStore(subscribe, () => form.getState());\n\n\t// Deferred disposal: schedule dispose in a macrotask so StrictMode remount can cancel it\n\tuseEffect(() => {\n\t\tif (disposeTimerRef.current !== null) {\n\t\t\tclearTimeout(disposeTimerRef.current);\n\t\t\tdisposeTimerRef.current = null;\n\t\t}\n\t\treturn () => {\n\t\t\tdisposeTimerRef.current = setTimeout(() => {\n\t\t\t\tformRef.current?.dispose();\n\t\t\t}, 0);\n\t\t};\n\t}, []);\n\n\t// Wrap the form API to auto-focus on submit errors (ADR §12)\n\tconst wrappedApi = useMemo((): FormApi<TData, TUi> => {\n\t\tif (!autoFocus) return form;\n\n\t\treturn {\n\t\t\t...form,\n\t\t\tsubmit: async (...args: Parameters<FormApi<TData, TUi>[\"submit\"]>): Promise<SubmitResult> => {\n\t\t\t\tconst result = await form.submit(...args);\n\t\t\t\tif (!result.ok && result.fieldIssues?.length) {\n\t\t\t\t\tfocusFirstError(result.fieldIssues);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t};\n\t}, [form, autoFocus]);\n\n\treturn wrappedApi;\n}\n","import type { ExpressionService, PropDefinitions, ResolvedProps } from \"@formbar/expressions\";\nimport { useMemo, useSyncExternalStore } from \"react\";\n\n/** Keep definitions stable with useMemo; subscription cleanup also invalidates retained setters. */\nexport function useExpressionProps(service: ExpressionService, definitions: PropDefinitions): ResolvedProps {\n\tconst binding = useMemo(() => service.resolveProps(definitions), [service, definitions]);\n\treturn useSyncExternalStore(binding.subscribe, binding.getSnapshot, binding.getSnapshot);\n}\n"]}
package/dist/index.js CHANGED
@@ -71,7 +71,7 @@ function useFormSelector(form, selector, equalityFn) {
71
71
  prevRef.current = { value: next, initialized: true };
72
72
  return next;
73
73
  }, [form]);
74
- return useSyncExternalStore(subscribe, getSnapshot);
74
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
75
75
  }
76
76
 
77
77
  // src/use-field.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/a11y.ts","../src/use-form-selector.ts","../src/use-field.ts","../src/core-form-options.ts","../src/use-form.ts","../src/use-expression-props.ts"],"names":["useRef","useSyncExternalStore","useMemo"],"mappings":"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","file":"index.js","sourcesContent":["import type { ValidationIssue } from \"@formbar/core\";\n\nconst DEFAULT_FIELD_PREFIX = \"field\";\n\n/** ARIA props for a form field */\nexport interface FieldA11yProps {\n\treadonly id: string;\n\treadonly \"aria-invalid\"?: boolean;\n\treadonly \"aria-describedby\"?: string;\n\treadonly \"aria-required\"?: boolean;\n\treadonly \"aria-errormessage\"?: string;\n}\n\n/** Label props for semantic association */\nexport interface LabelA11yProps {\n\treadonly htmlFor: string;\n}\n\n/** Description/error message props */\nexport interface DescriptionA11yProps {\n\treadonly id: string;\n\treadonly role?: \"alert\";\n}\n\n/** Generate a deterministic field ID from path */\nexport function fieldId(path: string, prefix: string = DEFAULT_FIELD_PREFIX): string {\n\treturn `${prefix}-${path\n\t\t.replace(/[.[\\]/]/g, \"-\")\n\t\t.replace(/-+/g, \"-\")\n\t\t.replace(/-$/, \"\")}`;\n}\n\n/** Generate description element ID */\nexport function descriptionId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-description`;\n}\n\n/** Generate error element ID */\nexport function errorId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-error`;\n}\n\n/** Get ARIA props for a field input element */\nexport function getFieldProps(\n\tpath: string,\n\toptions?: {\n\t\treadonly issues?: readonly ValidationIssue[];\n\t\treadonly required?: boolean;\n\t\treadonly hasDescription?: boolean;\n\t},\n): FieldA11yProps {\n\tconst id = fieldId(path);\n\tconst hasErrors = options?.issues?.some((i) => i.severity === \"error\") ?? false;\n\n\tconst describedBy: string[] = [];\n\tif (options?.hasDescription) describedBy.push(descriptionId(path));\n\tif (hasErrors) describedBy.push(errorId(path));\n\n\tconst props: FieldA11yProps = {\n\t\tid,\n\t\t...(hasErrors ? { \"aria-invalid\": true as const } : {}),\n\t\t...(describedBy.length > 0 ? { \"aria-describedby\": describedBy.join(\" \") } : {}),\n\t\t...(options?.required ? { \"aria-required\": true as const } : {}),\n\t\t...(hasErrors ? { \"aria-errormessage\": errorId(path) } : {}),\n\t};\n\n\treturn props;\n}\n\n/** Get label props for semantic association */\nexport function getLabelProps(path: string): LabelA11yProps {\n\treturn { htmlFor: fieldId(path) };\n}\n\n/** Get description element props */\nexport function getDescriptionProps(path: string): DescriptionA11yProps {\n\treturn { id: descriptionId(path) };\n}\n\n/** Get error message props */\nexport function getErrorProps(path: string): DescriptionA11yProps {\n\treturn { id: errorId(path), role: \"alert\" };\n}\n\n/** Find the first field path with errors (for focus management) */\nexport function findFirstErrorPath(issues: readonly ValidationIssue[]): string | undefined {\n\tconst firstError = issues.find((i) => i.severity === \"error\");\n\tif (!firstError) return undefined;\n\treturn firstError.path.segments.join(\".\");\n}\n\n/** Focus the first error field after submit (browser-only) */\nexport function focusFirstError(issues: readonly ValidationIssue[]): boolean {\n\tconst path = findFirstErrorPath(issues);\n\tif (!path) return false;\n\n\tif (typeof document === \"undefined\") return false;\n\n\tconst id = fieldId(path);\n\tconst element = document.getElementById(id);\n\tif (element) {\n\t\telement.focus();\n\t\treturn true;\n\t}\n\treturn false;\n}\n","import type { FormApi, FormState } from \"@formbar/core\";\nimport { useCallback, useRef, useSyncExternalStore } from \"react\";\n\n/**\n * Subscribe to a derived value from form state with fine-grained reactivity.\n * Only triggers re-render when the selected value changes (by equality function).\n *\n * @param form - The {@link FormApi} instance.\n * @param selector - Function that extracts a value from the full form state.\n * @param equalityFn - Optional equality comparator (defaults to `Object.is`).\n * @returns The current selected value, updated reactively.\n *\n * @example\n * ```typescript\n * const isValid = useFormSelector(form, (state) => state.issues.length === 0);\n * const submitCount = useFormSelector(form, (state) => state.meta.submitted);\n * ```\n */\nexport function useFormSelector<TData, TUi, T>(\n\tform: FormApi<TData, TUi>,\n\tselector: (state: FormState<TData, TUi>) => T,\n\tequalityFn?: (prev: T, next: T) => boolean,\n): T {\n\tconst eqRef = useRef(equalityFn ?? Object.is);\n\teqRef.current = equalityFn ?? Object.is;\n\n\tconst selectorRef = useRef(selector);\n\tselectorRef.current = selector;\n\n\tconst prevRef = useRef<{ readonly value: T; readonly initialized: boolean }>({\n\t\tvalue: undefined as T,\n\t\tinitialized: false,\n\t});\n\n\tconst subscribe = useCallback((onStoreChange: () => void) => form.subscribe(onStoreChange), [form]);\n\n\tconst getSnapshot = useCallback((): T => {\n\t\tconst next = selectorRef.current(form.getState());\n\t\tif (prevRef.current.initialized && eqRef.current(prevRef.current.value, next)) {\n\t\t\treturn prevRef.current.value;\n\t\t}\n\t\tprevRef.current = { value: next, initialized: true };\n\t\treturn next;\n\t}, [form]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot);\n}\n","import type { FieldApi, FieldConfig, FieldMetaEntry, FormApi } from \"@formbar/core\";\nimport { useMemo, useRef } from \"react\";\nimport { useFormSelector } from \"./use-form-selector.js\";\n\ninterface FieldSnapshot {\n\treadonly value: unknown;\n\treadonly meta: FieldMetaEntry | undefined;\n}\n\nfunction fieldSnapshotEqual(a: FieldSnapshot, b: FieldSnapshot): boolean {\n\treturn a.value === b.value && a.meta === b.meta;\n}\n\n/**\n * React hook that subscribes to a specific field with fine-grained re-rendering.\n * Only re-renders when the field's value or metadata actually changes.\n *\n * @param form - The {@link FormApi} instance (from useForm or createForm).\n * @param path - Dot-path to the field (e.g., `\"user.email\"`).\n * @param config - Optional field configuration (label, validators, triggers).\n * @returns A {@link FieldApi} with reactive get/set, validation, and touch tracking.\n *\n * @example\n * ```typescript\n * function EmailField({ form }) {\n * const field = useField(form, \"email\");\n * return (\n * <div>\n * <input\n * value={field.get() ?? \"\"}\n * onChange={e => field.handleChange(e.target.value)}\n * onBlur={() => field.handleBlur()}\n * />\n * {field.issues().map(i => <span key={i.code}>{i.message}</span>)}\n * </div>\n * );\n * }\n * ```\n */\nexport function useField<TData, TUi, P extends string>(\n\tform: FormApi<TData, TUi>,\n\tpath: P,\n\tconfig?: FieldConfig,\n): FieldApi<TData, TUi, P> {\n\t// Stabilize config reference — inline objects create new references every render\n\tconst configRef = useRef(config);\n\tconst stableConfig = useMemo(() => {\n\t\tconst prev = configRef.current;\n\t\tif (prev === config) return prev;\n\t\tif (prev && config && JSON.stringify(prev) === JSON.stringify(config)) return prev;\n\t\tconfigRef.current = config;\n\t\treturn config;\n\t}, [config]);\n\n\tconst field = useMemo(() => form.fieldDynamic(path, stableConfig), [form, path, stableConfig]);\n\n\t// Subscribe to field value and touched state to trigger re-renders\n\tuseFormSelector(\n\t\tform,\n\t\t() => {\n\t\t\tconst state = form.getState();\n\t\t\tconst pathKey = field.path.segments.join(\".\");\n\t\t\tconst meta = (state.fieldMeta as Readonly<Record<string, FieldMetaEntry>>)[pathKey];\n\t\t\treturn { value: field.get(), meta } as FieldSnapshot;\n\t\t},\n\t\tfieldSnapshotEqual,\n\t);\n\n\treturn field as FieldApi<TData, TUi, P>;\n}\n","import type { CreateFormOptions } from \"@formbar/core\";\nimport type { UseFormOptions } from \"./use-form.js\";\n\nexport function getCoreFormOptions<TData, TUi>(\n\toptions: UseFormOptions<TData, TUi> | undefined,\n): CreateFormOptions<TData, TUi> | undefined {\n\tif (options === undefined) return undefined;\n\ttry {\n\t\tif (!Object.hasOwn(options, \"autoFocusOnError\")) return options;\n\t\tconst { autoFocusOnError: _reactOption, ...coreDescriptors } = Object.getOwnPropertyDescriptors(options);\n\t\treturn Object.create(Object.getPrototypeOf(options), coreDescriptors) as CreateFormOptions<TData, TUi>;\n\t} catch {\n\t\t// Option diagnostics must not make useForm less resilient than createForm.\n\t\treturn options;\n\t}\n}\n","import type { CreateFormOptions, FormApi, SubmitResult } from \"@formbar/core\";\nimport { createForm } from \"@formbar/core\";\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { focusFirstError } from \"./a11y.js\";\nimport { getCoreFormOptions } from \"./core-form-options.js\";\n\n/** Options for useForm, extending core CreateFormOptions with React-specific behavior */\nexport interface UseFormOptions<TData, TUi> extends CreateFormOptions<TData, TUi> {\n\t/** Auto-focus the first error field on submit failure (default: true) */\n\treadonly autoFocusOnError?: boolean;\n}\n\n/**\n * React hook that creates and manages a form instance with automatic cleanup.\n * The form is created once on mount and disposed on unmount (StrictMode-safe).\n *\n * @param options - Form configuration (same as {@link createForm} options).\n * @returns A stable {@link FormApi} reference that persists across re-renders.\n *\n * @example\n * ```typescript\n * function ContactForm() {\n * const form = useForm({\n * initialData: { name: \"\", email: \"\" },\n * onSubmit: async ({ payload }) => {\n * await saveContact(payload);\n * return { ok: true, submitId: \"1\" };\n * },\n * });\n *\n * return <input value={form.field(\"name\").get()} onChange={e => form.field(\"name\").set(e.target.value)} />;\n * }\n * ```\n */\nexport function useForm<TData, TUi>(options?: UseFormOptions<TData, TUi>): FormApi<TData, TUi> {\n\tconst autoFocus = options?.autoFocusOnError ?? true;\n\tconst formRef = useRef<FormApi<TData, TUi> | null>(null);\n\tconst disposeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\tif (formRef.current === null) {\n\t\tformRef.current = createForm<TData, TUi>(getCoreFormOptions(options));\n\t}\n\n\tconst form = formRef.current;\n\n\t// Adapt form.subscribe (which passes state) to useSyncExternalStore's expected signature\n\tconst subscribe = useRef((onStoreChange: () => void) => {\n\t\treturn form.subscribe(onStoreChange);\n\t}).current;\n\n\tuseSyncExternalStore(subscribe, () => form.getState());\n\n\t// Deferred disposal: schedule dispose in a macrotask so StrictMode remount can cancel it\n\tuseEffect(() => {\n\t\tif (disposeTimerRef.current !== null) {\n\t\t\tclearTimeout(disposeTimerRef.current);\n\t\t\tdisposeTimerRef.current = null;\n\t\t}\n\t\treturn () => {\n\t\t\tdisposeTimerRef.current = setTimeout(() => {\n\t\t\t\tformRef.current?.dispose();\n\t\t\t}, 0);\n\t\t};\n\t}, []);\n\n\t// Wrap the form API to auto-focus on submit errors (ADR §12)\n\tconst wrappedApi = useMemo((): FormApi<TData, TUi> => {\n\t\tif (!autoFocus) return form;\n\n\t\treturn {\n\t\t\t...form,\n\t\t\tsubmit: async (...args: Parameters<FormApi<TData, TUi>[\"submit\"]>): Promise<SubmitResult> => {\n\t\t\t\tconst result = await form.submit(...args);\n\t\t\t\tif (!result.ok && result.fieldIssues?.length) {\n\t\t\t\t\tfocusFirstError(result.fieldIssues);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t};\n\t}, [form, autoFocus]);\n\n\treturn wrappedApi;\n}\n","import type { ExpressionService, PropDefinitions, ResolvedProps } from \"@formbar/expressions\";\nimport { useMemo, useSyncExternalStore } from \"react\";\n\n/** Keep definitions stable with useMemo; subscription cleanup also invalidates retained setters. */\nexport function useExpressionProps(service: ExpressionService, definitions: PropDefinitions): ResolvedProps {\n\tconst binding = useMemo(() => service.resolveProps(definitions), [service, definitions]);\n\treturn useSyncExternalStore(binding.subscribe, binding.getSnapshot, binding.getSnapshot);\n}\n"]}
1
+ {"version":3,"sources":["../src/a11y.ts","../src/use-form-selector.ts","../src/use-field.ts","../src/core-form-options.ts","../src/use-form.ts","../src/use-expression-props.ts"],"names":["useRef","useSyncExternalStore","useMemo"],"mappings":"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","file":"index.js","sourcesContent":["import type { ValidationIssue } from \"@formbar/core\";\n\nconst DEFAULT_FIELD_PREFIX = \"field\";\n\n/** ARIA props for a form field */\nexport interface FieldA11yProps {\n\treadonly id: string;\n\treadonly \"aria-invalid\"?: boolean;\n\treadonly \"aria-describedby\"?: string;\n\treadonly \"aria-required\"?: boolean;\n\treadonly \"aria-errormessage\"?: string;\n}\n\n/** Label props for semantic association */\nexport interface LabelA11yProps {\n\treadonly htmlFor: string;\n}\n\n/** Description/error message props */\nexport interface DescriptionA11yProps {\n\treadonly id: string;\n\treadonly role?: \"alert\";\n}\n\n/** Generate a deterministic field ID from path */\nexport function fieldId(path: string, prefix: string = DEFAULT_FIELD_PREFIX): string {\n\treturn `${prefix}-${path\n\t\t.replace(/[.[\\]/]/g, \"-\")\n\t\t.replace(/-+/g, \"-\")\n\t\t.replace(/-$/, \"\")}`;\n}\n\n/** Generate description element ID */\nexport function descriptionId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-description`;\n}\n\n/** Generate error element ID */\nexport function errorId(path: string, prefix?: string): string {\n\treturn `${fieldId(path, prefix)}-error`;\n}\n\n/** Get ARIA props for a field input element */\nexport function getFieldProps(\n\tpath: string,\n\toptions?: {\n\t\treadonly issues?: readonly ValidationIssue[];\n\t\treadonly required?: boolean;\n\t\treadonly hasDescription?: boolean;\n\t},\n): FieldA11yProps {\n\tconst id = fieldId(path);\n\tconst hasErrors = options?.issues?.some((i) => i.severity === \"error\") ?? false;\n\n\tconst describedBy: string[] = [];\n\tif (options?.hasDescription) describedBy.push(descriptionId(path));\n\tif (hasErrors) describedBy.push(errorId(path));\n\n\tconst props: FieldA11yProps = {\n\t\tid,\n\t\t...(hasErrors ? { \"aria-invalid\": true as const } : {}),\n\t\t...(describedBy.length > 0 ? { \"aria-describedby\": describedBy.join(\" \") } : {}),\n\t\t...(options?.required ? { \"aria-required\": true as const } : {}),\n\t\t...(hasErrors ? { \"aria-errormessage\": errorId(path) } : {}),\n\t};\n\n\treturn props;\n}\n\n/** Get label props for semantic association */\nexport function getLabelProps(path: string): LabelA11yProps {\n\treturn { htmlFor: fieldId(path) };\n}\n\n/** Get description element props */\nexport function getDescriptionProps(path: string): DescriptionA11yProps {\n\treturn { id: descriptionId(path) };\n}\n\n/** Get error message props */\nexport function getErrorProps(path: string): DescriptionA11yProps {\n\treturn { id: errorId(path), role: \"alert\" };\n}\n\n/** Find the first field path with errors (for focus management) */\nexport function findFirstErrorPath(issues: readonly ValidationIssue[]): string | undefined {\n\tconst firstError = issues.find((i) => i.severity === \"error\");\n\tif (!firstError) return undefined;\n\treturn firstError.path.segments.join(\".\");\n}\n\n/** Focus the first error field after submit (browser-only) */\nexport function focusFirstError(issues: readonly ValidationIssue[]): boolean {\n\tconst path = findFirstErrorPath(issues);\n\tif (!path) return false;\n\n\tif (typeof document === \"undefined\") return false;\n\n\tconst id = fieldId(path);\n\tconst element = document.getElementById(id);\n\tif (element) {\n\t\telement.focus();\n\t\treturn true;\n\t}\n\treturn false;\n}\n","import type { FormApi, FormState } from \"@formbar/core\";\nimport { useCallback, useRef, useSyncExternalStore } from \"react\";\n\n/**\n * Subscribe to a derived value from form state with fine-grained reactivity.\n * Only triggers re-render when the selected value changes (by equality function).\n *\n * @param form - The {@link FormApi} instance.\n * @param selector - Function that extracts a value from the full form state.\n * @param equalityFn - Optional equality comparator (defaults to `Object.is`).\n * @returns The current selected value, updated reactively.\n *\n * @example\n * ```typescript\n * const isValid = useFormSelector(form, (state) => state.issues.length === 0);\n * const submitCount = useFormSelector(form, (state) => state.meta.submitted);\n * ```\n */\nexport function useFormSelector<TData, TUi, T>(\n\tform: FormApi<TData, TUi>,\n\tselector: (state: FormState<TData, TUi>) => T,\n\tequalityFn?: (prev: T, next: T) => boolean,\n): T {\n\tconst eqRef = useRef(equalityFn ?? Object.is);\n\teqRef.current = equalityFn ?? Object.is;\n\n\tconst selectorRef = useRef(selector);\n\tselectorRef.current = selector;\n\n\tconst prevRef = useRef<{ readonly value: T; readonly initialized: boolean }>({\n\t\tvalue: undefined as T,\n\t\tinitialized: false,\n\t});\n\n\tconst subscribe = useCallback((onStoreChange: () => void) => form.subscribe(onStoreChange), [form]);\n\n\tconst getSnapshot = useCallback((): T => {\n\t\tconst next = selectorRef.current(form.getState());\n\t\tif (prevRef.current.initialized && eqRef.current(prevRef.current.value, next)) {\n\t\t\treturn prevRef.current.value;\n\t\t}\n\t\tprevRef.current = { value: next, initialized: true };\n\t\treturn next;\n\t}, [form]);\n\n\treturn useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n","import type { FieldApi, FieldConfig, FieldMetaEntry, FormApi } from \"@formbar/core\";\nimport { useMemo, useRef } from \"react\";\nimport { useFormSelector } from \"./use-form-selector.js\";\n\ninterface FieldSnapshot {\n\treadonly value: unknown;\n\treadonly meta: FieldMetaEntry | undefined;\n}\n\nfunction fieldSnapshotEqual(a: FieldSnapshot, b: FieldSnapshot): boolean {\n\treturn a.value === b.value && a.meta === b.meta;\n}\n\n/**\n * React hook that subscribes to a specific field with fine-grained re-rendering.\n * Only re-renders when the field's value or metadata actually changes.\n *\n * @param form - The {@link FormApi} instance (from useForm or createForm).\n * @param path - Dot-path to the field (e.g., `\"user.email\"`).\n * @param config - Optional field configuration (label, validators, triggers).\n * @returns A {@link FieldApi} with reactive get/set, validation, and touch tracking.\n *\n * @example\n * ```typescript\n * function EmailField({ form }) {\n * const field = useField(form, \"email\");\n * return (\n * <div>\n * <input\n * value={field.get() ?? \"\"}\n * onChange={e => field.handleChange(e.target.value)}\n * onBlur={() => field.handleBlur()}\n * />\n * {field.issues().map(i => <span key={i.code}>{i.message}</span>)}\n * </div>\n * );\n * }\n * ```\n */\nexport function useField<TData, TUi, P extends string>(\n\tform: FormApi<TData, TUi>,\n\tpath: P,\n\tconfig?: FieldConfig,\n): FieldApi<TData, TUi, P> {\n\t// Stabilize config reference — inline objects create new references every render\n\tconst configRef = useRef(config);\n\tconst stableConfig = useMemo(() => {\n\t\tconst prev = configRef.current;\n\t\tif (prev === config) return prev;\n\t\tif (prev && config && JSON.stringify(prev) === JSON.stringify(config)) return prev;\n\t\tconfigRef.current = config;\n\t\treturn config;\n\t}, [config]);\n\n\tconst field = useMemo(() => form.fieldDynamic(path, stableConfig), [form, path, stableConfig]);\n\n\t// Subscribe to field value and touched state to trigger re-renders\n\tuseFormSelector(\n\t\tform,\n\t\t() => {\n\t\t\tconst state = form.getState();\n\t\t\tconst pathKey = field.path.segments.join(\".\");\n\t\t\tconst meta = (state.fieldMeta as Readonly<Record<string, FieldMetaEntry>>)[pathKey];\n\t\t\treturn { value: field.get(), meta } as FieldSnapshot;\n\t\t},\n\t\tfieldSnapshotEqual,\n\t);\n\n\treturn field as FieldApi<TData, TUi, P>;\n}\n","import type { CreateFormOptions } from \"@formbar/core\";\nimport type { UseFormOptions } from \"./use-form.js\";\n\nexport function getCoreFormOptions<TData, TUi>(\n\toptions: UseFormOptions<TData, TUi> | undefined,\n): CreateFormOptions<TData, TUi> | undefined {\n\tif (options === undefined) return undefined;\n\ttry {\n\t\tif (!Object.hasOwn(options, \"autoFocusOnError\")) return options;\n\t\tconst { autoFocusOnError: _reactOption, ...coreDescriptors } = Object.getOwnPropertyDescriptors(options);\n\t\treturn Object.create(Object.getPrototypeOf(options), coreDescriptors) as CreateFormOptions<TData, TUi>;\n\t} catch {\n\t\t// Option diagnostics must not make useForm less resilient than createForm.\n\t\treturn options;\n\t}\n}\n","import type { CreateFormOptions, FormApi, SubmitResult } from \"@formbar/core\";\nimport { createForm } from \"@formbar/core\";\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { focusFirstError } from \"./a11y.js\";\nimport { getCoreFormOptions } from \"./core-form-options.js\";\n\n/** Options for useForm, extending core CreateFormOptions with React-specific behavior */\nexport interface UseFormOptions<TData, TUi> extends CreateFormOptions<TData, TUi> {\n\t/** Auto-focus the first error field on submit failure (default: true) */\n\treadonly autoFocusOnError?: boolean;\n}\n\n/**\n * React hook that creates and manages a form instance with automatic cleanup.\n * The form is created once on mount and disposed on unmount (StrictMode-safe).\n *\n * @param options - Form configuration (same as {@link createForm} options).\n * @returns A stable {@link FormApi} reference that persists across re-renders.\n *\n * @example\n * ```typescript\n * function ContactForm() {\n * const form = useForm({\n * initialData: { name: \"\", email: \"\" },\n * onSubmit: async ({ payload }) => {\n * await saveContact(payload);\n * return { ok: true, submitId: \"1\" };\n * },\n * });\n *\n * return <input value={form.field(\"name\").get()} onChange={e => form.field(\"name\").set(e.target.value)} />;\n * }\n * ```\n */\nexport function useForm<TData, TUi>(options?: UseFormOptions<TData, TUi>): FormApi<TData, TUi> {\n\tconst autoFocus = options?.autoFocusOnError ?? true;\n\tconst formRef = useRef<FormApi<TData, TUi> | null>(null);\n\tconst disposeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n\tif (formRef.current === null) {\n\t\tformRef.current = createForm<TData, TUi>(getCoreFormOptions(options));\n\t}\n\n\tconst form = formRef.current;\n\n\t// Adapt form.subscribe (which passes state) to useSyncExternalStore's expected signature\n\tconst subscribe = useRef((onStoreChange: () => void) => {\n\t\treturn form.subscribe(onStoreChange);\n\t}).current;\n\n\tuseSyncExternalStore(subscribe, () => form.getState());\n\n\t// Deferred disposal: schedule dispose in a macrotask so StrictMode remount can cancel it\n\tuseEffect(() => {\n\t\tif (disposeTimerRef.current !== null) {\n\t\t\tclearTimeout(disposeTimerRef.current);\n\t\t\tdisposeTimerRef.current = null;\n\t\t}\n\t\treturn () => {\n\t\t\tdisposeTimerRef.current = setTimeout(() => {\n\t\t\t\tformRef.current?.dispose();\n\t\t\t}, 0);\n\t\t};\n\t}, []);\n\n\t// Wrap the form API to auto-focus on submit errors (ADR §12)\n\tconst wrappedApi = useMemo((): FormApi<TData, TUi> => {\n\t\tif (!autoFocus) return form;\n\n\t\treturn {\n\t\t\t...form,\n\t\t\tsubmit: async (...args: Parameters<FormApi<TData, TUi>[\"submit\"]>): Promise<SubmitResult> => {\n\t\t\t\tconst result = await form.submit(...args);\n\t\t\t\tif (!result.ok && result.fieldIssues?.length) {\n\t\t\t\t\tfocusFirstError(result.fieldIssues);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t},\n\t\t};\n\t}, [form, autoFocus]);\n\n\treturn wrappedApi;\n}\n","import type { ExpressionService, PropDefinitions, ResolvedProps } from \"@formbar/expressions\";\nimport { useMemo, useSyncExternalStore } from \"react\";\n\n/** Keep definitions stable with useMemo; subscription cleanup also invalidates retained setters. */\nexport function useExpressionProps(service: ExpressionService, definitions: PropDefinitions): ResolvedProps {\n\tconst binding = useMemo(() => service.resolveProps(definitions), [service, definitions]);\n\treturn useSyncExternalStore(binding.subscribe, binding.getSnapshot, binding.getSnapshot);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formbar/react",
3
- "version": "0.8.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "React hooks and a11y utilities for @formbar/core",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@formbar/expressions": "^0.4.0",
31
- "@formbar/core": "^0.8.0"
31
+ "@formbar/core": "^0.12.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": ">=18.0.0"
@@ -1,3 +1,6 @@
1
+ import { createForm } from "@formbar/core";
2
+ import { createElement } from "react";
3
+ import { renderToString } from "react-dom/server";
1
4
  import { describe, expect, test } from "vitest";
2
5
  import { useField, useFormSelector } from "../index.js";
3
6
 
@@ -5,6 +8,19 @@ describe("useFormSelector", () => {
5
8
  test("is exported as a function", () => {
6
9
  expect(typeof useFormSelector).toBe("function");
7
10
  });
11
+
12
+ test("provides a server snapshot", () => {
13
+ const form = createForm({ initialData: { value: "server" } });
14
+ function Selected() {
15
+ return createElement(
16
+ "span",
17
+ null,
18
+ useFormSelector(form, (state) => state.data.value),
19
+ );
20
+ }
21
+ expect(renderToString(createElement(Selected))).toContain("server");
22
+ form.dispose();
23
+ });
8
24
  });
9
25
 
10
26
  describe("useField", () => {
@@ -43,5 +43,5 @@ export function useFormSelector<TData, TUi, T>(
43
43
  return next;
44
44
  }, [form]);
45
45
 
46
- return useSyncExternalStore(subscribe, getSnapshot);
46
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
47
47
  }