@cleverbrush/react-form 4.4.3 → 4.5.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/README.md +29 -2
- package/dist/FormStore.d.ts +16 -6
- package/dist/components.d.ts +1 -1
- package/dist/contexts.d.ts +1 -0
- package/dist/helpers.d.ts +1 -1
- package/dist/hooks.d.ts +15 -25
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/system.d.ts +69 -0
- package/dist/types.d.ts +30 -10
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -54,6 +54,21 @@ npm install @cleverbrush/react-form
|
|
|
54
54
|
|
|
55
55
|
## Quick Start
|
|
56
56
|
|
|
57
|
+
For typed renderer props/variants and managed submission, see the new
|
|
58
|
+
[consumer examples and migration guide](../../docs/cache-form-migration.md).
|
|
59
|
+
The provider-based APIs below remain supported.
|
|
60
|
+
|
|
61
|
+
Typed fields and headless `form.useField()` accept properties with schema
|
|
62
|
+
defaults, including enums, booleans, arrays, and nullable values. A default
|
|
63
|
+
does not erase the property's inferred type. Defaults are applied during
|
|
64
|
+
validation; call `form.reset(values)` to establish a visible clean baseline.
|
|
65
|
+
`reset()` still clears the store rather than restoring schema defaults.
|
|
66
|
+
|
|
67
|
+
Shared UI packages can directly export `createFormSystem(...)` results and
|
|
68
|
+
`system.Field` while emitting TypeScript declarations. The named
|
|
69
|
+
`TypedFormSystem` and `TypedFieldComponent` types preserve the registry's
|
|
70
|
+
field/variant/props checks across package boundaries.
|
|
71
|
+
|
|
57
72
|
```tsx
|
|
58
73
|
import { object, string, number } from '@cleverbrush/schema';
|
|
59
74
|
import { useSchemaForm, FormSystemProvider, Field } from '@cleverbrush/react-form';
|
|
@@ -316,9 +331,21 @@ const form = useSchemaForm(UserSchema, {
|
|
|
316
331
|
| `form.useField(forProperty)` | Bind a field by PropertyDescriptor selector |
|
|
317
332
|
| `form.submit()` | Validate and return `ValidationResult` (includes `result.object` on success) |
|
|
318
333
|
| `form.validate()` | Run validation, propagate errors to fields |
|
|
319
|
-
| `form.reset(values?)` |
|
|
334
|
+
| `form.reset(values?)` | Clear values, or establish supplied values as a clean baseline; synchronize mounted fields and clear errors/touched/dirty/validation |
|
|
335
|
+
| `form.handleSubmit(onValid, options?)` | Awaitable event handler: validate, prevent duplicate submits, handle success/failure results |
|
|
336
|
+
| `form.submitting` | Reactive, read-only pending state from validation through callbacks |
|
|
337
|
+
| `form.error` | Reactive, read-only submission error; cleared on reset or the next attempt |
|
|
320
338
|
| `form.getValue()` | Get current form values as plain object |
|
|
321
|
-
| `form.setValue(values)` |
|
|
339
|
+
| `form.setValue(values)` | Shallow-merge values and synchronize mounted fields without marking touched |
|
|
340
|
+
|
|
341
|
+
Form snapshots and dirty checks use `deepClone` and `deepEqual` from
|
|
342
|
+
[`@cleverbrush/deep`](../deep/README.md). Plain objects, arrays and Dates are cloned
|
|
343
|
+
on reset/setters; changing caller-owned input cannot change their baseline. Cycles,
|
|
344
|
+
shared references, sparse arrays and null prototypes are preserved. Replacing a
|
|
345
|
+
value with structurally equal data clears dirty, even with different sharing of
|
|
346
|
+
child references. Files and other opaque objects retain identity: a different File
|
|
347
|
+
is dirty even if its name/content match. Treat returned snapshots and opaque values
|
|
348
|
+
as read-only; update through setters rather than mutating them in place.
|
|
322
349
|
|
|
323
350
|
## useField
|
|
324
351
|
|
package/dist/FormStore.d.ts
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
|
-
import type { FieldState } from './types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { FieldState, FormSubmissionState } from './types.js';
|
|
2
|
+
type Binding = {
|
|
3
|
+
getValue: (values: any) => {
|
|
4
|
+
success: boolean;
|
|
5
|
+
value?: any;
|
|
6
|
+
};
|
|
7
|
+
setValue: (values: any, value: any, options: any) => boolean;
|
|
8
|
+
};
|
|
9
|
+
/** Internal immutable snapshots shared by all bindings of a form. */
|
|
6
10
|
export declare function createFormStore(initialValues: any): {
|
|
11
|
+
registerField: (path: string, binding: Binding) => FieldState;
|
|
7
12
|
getFieldState: (path: string) => FieldState;
|
|
8
13
|
updateFieldState: (path: string, patch: Partial<FieldState>) => void;
|
|
9
14
|
subscribe: (path: string, listener: () => void) => () => void;
|
|
10
15
|
subscribeGlobal: (listener: () => void) => () => void;
|
|
11
16
|
getValues: () => any;
|
|
12
17
|
setValues: (newValues: any) => void;
|
|
18
|
+
setFieldValue: (path: string, value: any, createMissingStructure: boolean) => void;
|
|
13
19
|
resetAll: (newInitialValues?: any) => void;
|
|
14
|
-
notifyAll: () => void;
|
|
15
20
|
getAllFieldPaths: () => string[];
|
|
21
|
+
getRevision: () => number;
|
|
22
|
+
getSubmissionState: () => FormSubmissionState;
|
|
23
|
+
setSubmissionState: (patch: Partial<FormSubmissionState>) => void;
|
|
24
|
+
notifyAll: () => void;
|
|
16
25
|
};
|
|
17
26
|
export type FormStore = ReturnType<typeof createFormStore>;
|
|
27
|
+
export {};
|
package/dist/components.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export declare function FormProvider<TSchema extends ObjectSchemaBuilder<any, an
|
|
|
45
45
|
* const name = useField<typeof MySchema>((t) => t.name);
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
|
-
export declare function useField<TSchema extends ObjectSchemaBuilder<any, any, any>, TPropertySchema extends SchemaBuilder<any, any, any> =
|
|
48
|
+
export declare function useField<TSchema extends ObjectSchemaBuilder<any, any, any>, TPropertySchema extends SchemaBuilder<any, any, any, any> = any>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>): UseFieldResult<InferType<TPropertySchema>>;
|
|
49
49
|
/**
|
|
50
50
|
* Hook to access FormSystem configuration.
|
|
51
51
|
*/
|
package/dist/contexts.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type FormContextValue = {
|
|
|
15
15
|
schema: ObjectSchemaBuilder<any, any, any>;
|
|
16
16
|
options: UseSchemaFormOptions;
|
|
17
17
|
pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;
|
|
18
|
+
triggerValidation?: (markTouched: boolean) => Promise<unknown>;
|
|
18
19
|
};
|
|
19
20
|
/**
|
|
20
21
|
* Context for per-form data — allows useField to work via context.
|
package/dist/helpers.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare function getDescriptorPath(inner: PropertyDescriptorInner<any, an
|
|
|
12
12
|
/**
|
|
13
13
|
* Returns the schema type string (e.g. "string", "number", "object").
|
|
14
14
|
*/
|
|
15
|
-
export declare function getSchemaType(schema: SchemaBuilder<any, any, any>): string;
|
|
15
|
+
export declare function getSchemaType(schema: SchemaBuilder<any, any, any, any>): string;
|
|
16
16
|
/**
|
|
17
17
|
* Builds a selector function from a dot-separated path string.
|
|
18
18
|
* The selector traverses the PropertyDescriptorTree by property access.
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,36 +1,26 @@
|
|
|
1
1
|
import type { InferType, PropertyDescriptor, PropertyDescriptorTree, SchemaBuilder, ValidationResult } from '@cleverbrush/schema';
|
|
2
2
|
import { ObjectSchemaBuilder } from '@cleverbrush/schema';
|
|
3
3
|
import type { FormContextValue } from './contexts.js';
|
|
4
|
-
import type { FieldRenderer, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
|
|
5
|
-
/**
|
|
6
|
-
* Return type for useSchemaForm — fully typed for IntelliSense.
|
|
7
|
-
* The `useField` method infers the field value type from the schema via PropertyDescriptor.
|
|
8
|
-
*/
|
|
4
|
+
import type { FieldRenderer, FormSubmissionState, FormSubmitHandler, FormSubmitOptions, FormSubmitResult, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
|
|
5
|
+
/** A stable form controller with reactive field and submission subscriptions. */
|
|
9
6
|
export type SchemaFormInstance<TSchema extends ObjectSchemaBuilder<any, any, any>> = {
|
|
10
|
-
useField: <TPropertySchema extends SchemaBuilder<any, any, any>>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>) => UseFieldResult<InferType<TPropertySchema>>;
|
|
7
|
+
useField: <TPropertySchema extends SchemaBuilder<any, any, any, any>>(forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => PropertyDescriptor<TSchema, TPropertySchema, any>) => UseFieldResult<InferType<TPropertySchema>>;
|
|
11
8
|
submit: () => Promise<ValidationResult<InferType<TSchema>>>;
|
|
12
9
|
validate: () => Promise<ValidationResult<InferType<TSchema>>>;
|
|
10
|
+
/** Clear values, or establish supplied values as the new clean baseline. */
|
|
13
11
|
reset: (values?: Partial<InferType<TSchema>>) => void;
|
|
12
|
+
/** Read the current snapshot. Use setters rather than mutating it. */
|
|
14
13
|
getValue: () => InferType<TSchema>;
|
|
14
|
+
/** Shallow-merge values without marking fields touched. */
|
|
15
15
|
setValue: (values: Partial<InferType<TSchema>>) => void;
|
|
16
|
-
/**
|
|
16
|
+
/** Validate and submit once; repeated calls while pending are ignored. */
|
|
17
|
+
handleSubmit: <TData = void>(onValid: (values: InferType<TSchema>) => FormSubmitResult<TData> | Promise<FormSubmitResult<TData>>, options?: FormSubmitOptions<InferType<TSchema>, TData>) => FormSubmitHandler;
|
|
18
|
+
/** @internal — Used by FormProvider and Field to access internal context. */
|
|
17
19
|
_getFormContext: () => FormContextValue;
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Hook that binds a schema to a form instance.
|
|
21
|
-
* Provides field binding API, form-level validation, submit, reset.
|
|
22
|
-
*/
|
|
20
|
+
} & FormSubmissionState;
|
|
21
|
+
/** Bind a schema to independently subscribed fields and a stable form instance. */
|
|
23
22
|
export declare function useSchemaForm<TSchema extends ObjectSchemaBuilder<any, any, any>>(schema: TSchema, options?: UseSchemaFormOptions): SchemaFormInstance<TSchema>;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
export declare function useFieldFromContext(formContext: FormContextValue, forProperty: (tree: any) => any, triggerValidation?: (markTouched: boolean) => Promise<any>): UseFieldResult;
|
|
29
|
-
/**
|
|
30
|
-
* Resolves a renderer from the FormSystem config based on schema type and optional variant.
|
|
31
|
-
*
|
|
32
|
-
* When `variant` is provided the registry is checked for `"type:variant"` first
|
|
33
|
-
* (e.g. `"string:password"`). If no match is found it falls back to the base
|
|
34
|
-
* `"type"` key (e.g. `"string"`).
|
|
35
|
-
*/
|
|
36
|
-
export declare function resolveRenderer(config: FormSystemConfig | null, schema: SchemaBuilder<any, any, any>, variant?: string): FieldRenderer | undefined;
|
|
23
|
+
/** Internal field binding shared by direct and context-based hooks. */
|
|
24
|
+
export declare function useFieldFromContext(formContext: FormContextValue, forProperty: (tree: any) => any, triggerValidation?: ((markTouched: boolean) => Promise<unknown>) | undefined): UseFieldResult;
|
|
25
|
+
/** Resolve type:variant first, falling back to the base type. */
|
|
26
|
+
export declare function resolveRenderer(config: FormSystemConfig | null, schema: SchemaBuilder<any, any, any, any>, variant?: string): FieldRenderer | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,6 @@ export type { FieldProps, FormProviderProps, FormSystemProviderProps } from './c
|
|
|
2
2
|
export { Field, FormProvider, FormSystemProvider, useField, useFormSystem } from './components.js';
|
|
3
3
|
export type { SchemaFormInstance } from './hooks.js';
|
|
4
4
|
export { useSchemaForm } from './hooks.js';
|
|
5
|
-
export type {
|
|
5
|
+
export type { TypedFieldComponent, TypedFieldProps, TypedFieldRenderer, TypedFormSystem, TypedRendererRegistry } from './system.js';
|
|
6
|
+
export { createFormSystem, defineFieldRenderer } from './system.js';
|
|
7
|
+
export type { FieldRenderer, FieldRenderProps, FieldState, FormSubmissionState, FormSubmitHandler, FormSubmitOptions, FormSubmitResult, FormSystemConfig, UseFieldResult, UseSchemaFormOptions } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useContext as
|
|
1
|
+
import{useContext as H,useMemo as ye}from"react";import{createContext as X}from"react";var N=X(null),G=X(null);import{ObjectSchemaBuilder as ee,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as Z}from"@cleverbrush/schema";function J(n,o){let i=new Map,e=o.introspect();if(!e.properties)return i;for(let s of Object.keys(e.properties)){let a=n[s];if(!a||!(Z in a))continue;let m=a[Z];i.set(m,s);let r=e.properties[s];if(r instanceof ee){let p=J(a,r);for(let[y,l]of p)i.set(y,`${s}.${l}`)}}return i}function O(n){return n.introspect()?.type??"unknown"}function W(n,o){let i=o.introspect();if(!i.properties)return n??{};let e=n!=null?{...n}:{};for(let s of Object.keys(i.properties)){let a=i.properties[s];a instanceof ee&&(e[s]=W(e[s],a))}return e}import{ObjectSchemaBuilder as me,SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as le}from"@cleverbrush/schema";import{useCallback as f,useEffect as ne,useMemo as oe,useRef as h,useSyncExternalStore as se}from"react";import{deepClone as w,deepEqual as re}from"@cleverbrush/deep";function te(n){let o=w(n??{}),i=o,e=0,s={submitting:!1,error:void 0},a=new Map,m=new Map,r=new Map,p=new Set;function y(t,u){let d=t?.getValue(u);return d?.success?d.value:void 0}function l(t){if(!m.has(t)){let u=a.get(t),d=y(u,o),c=y(u,i);m.set(t,{value:d,initialValue:c,dirty:!re(d,c),touched:!1,error:void 0,validating:!1})}return m.get(t)}function F(t,u){return a.has(t)||a.set(t,u),l(t)}function g(t){for(let u of r.get(t)??[])u()}function b(){for(let t of r.keys())g(t);for(let t of p)t()}function v(t,u){let d=l(t);Object.entries(u).every(([c,S])=>Object.is(d[c],S))||(m.set(t,{...d,...u}),g(t))}function T(t){for(let[u,d]of a){let c=y(d,o),S=y(d,i);v(u,{value:c,initialValue:S,dirty:!re(c,S),validating:!1,...t?{touched:!1,error:void 0}:{}})}}function P(t,u){let d=r.get(t);return d||(d=new Set,r.set(t,d)),d.add(u),()=>{d.delete(u),d.size===0&&r.delete(t)}}function C(t){return p.add(t),()=>{p.delete(t)}}function E(){return s}function I(t){let u={...s,...t};if(!(u.submitting===s.submitting&&u.error===s.error)){s=u;for(let d of p)d()}}function k(){return o}function _(){return e}function B(t){o=w(t??{}),e++,T(!1)}function A(t,u,d){let c=w(o);a.get(t)?.setValue(c,w(u),{createMissingStructure:d})&&B(c)}function U(t){o=w(t??{}),i=o,e++,T(!0),I({error:void 0})}function j(){return Array.from(a.keys())}return{registerField:F,getFieldState:l,updateFieldState:v,subscribe:P,subscribeGlobal:C,getValues:k,setValues:B,setFieldValue:A,resetAll:U,getAllFieldPaths:j,getRevision:_,getSubmissionState:E,setSubmissionState:I,notifyAll:b}}function pe(n,o){let i=h(null);i.current||(i.current=te({}));let e=i.current,s=h(null);s.current||(s.current=me.getPropertiesFor(n));let a=s.current,m=h(null);m.current||(m.current=J(a,n));let r=m.current;for(let[c]of r)e.registerField(c.toJsonPointer(),c);let p=h(n),y=h({});y.current={createMissingStructure:!0,...o};let l=h(!0),F=h(0),g=h(0),b=h(!1),v=h(null),T=f(()=>{v.current!==null&&clearTimeout(v.current),v.current=null},[]);ne(()=>(l.current=!0,()=>{l.current=!1,F.current++,g.current++,T()}),[T]);let P=f(async c=>{T();let S=++g.current,Y=e.getRevision();for(let x of e.getAllFieldPaths())e.updateFieldState(x,{validating:!0});let K=W(e.getValues(),p.current),R;try{R=await p.current.validateAsync(K,{doNotStopOnFirstError:!0})}catch{R={valid:!1}}if(!l.current||S!==g.current||Y!==e.getRevision())return R;for(let x of e.getAllFieldPaths())e.updateFieldState(x,{error:void 0,validating:!1,...c?{touched:!0}:{}});let $=R.getErrorsFor;if(typeof $=="function")for(let[x]of r){let M=x.toJsonPointer(),V=M.slice(1).split("/").map(D=>D.replace(/~1/g,"/").replace(/~0/g,"~"));try{let D=$(L=>V.reduce((ue,de)=>ue?.[de],L));Array.isArray(D?.errors)&&D.errors.length&&e.updateFieldState(M,{error:D.errors[0]})}catch{}}return R},[T,e,r]),C=f(()=>P(!0),[P]),E=f(()=>C(),[C]),I=f(c=>{F.current++,g.current++,T(),e.resetAll(c)},[e,T]),k=f(()=>e.getValues(),[e]),_=f(c=>{T(),e.setValues({...e.getValues(),...c})},[e,T]),B=f(c=>{T();let S=y.current.validationDebounceMs;return S!=null&&S>0?(v.current=setTimeout(()=>{v.current=null,P(c)},S),Promise.resolve()):P(c)},[P,T]),A=f((c,S)=>async Y=>{if(Y?.preventDefault(),b.current||!l.current)return;b.current=!0;let K=F.current,R=()=>l.current&&K===F.current;e.setSubmissionState({submitting:!0,error:void 0});try{let $=e.getRevision(),x=await P(!0);if(!R()||$!==e.getRevision()||!x.valid||x.object===void 0)return;let M=x.object,V;try{V=await c(M)}catch(D){if(!R())return;if(!S?.onError)throw D;let L=await S.onError(D);R()&&e.setSubmissionState({error:L});return}if(!R())return;if(V&&!V.ok){e.setSubmissionState({error:V.error});return}await S?.onSuccess?.(V?.data,M)}finally{b.current=!1,l.current&&e.setSubmissionState({submitting:!1})}},[P,e]),U=oe(()=>({store:e,descriptorTree:a,schema:p.current,pathMap:r,get options(){return y.current},triggerValidation:B}),[e,a,r,B]),j=h(U);j.current=U;let t=f(()=>j.current,[]),u=f(c=>q(j.current,c),[]),d=h(o?.validateOnMount);return ne(()=>{d.current&&P(!0)},[P]),se(e.subscribeGlobal,e.getSubmissionState,e.getSubmissionState),oe(()=>({useField:u,submit:E,validate:C,reset:I,getValue:k,setValue:_,handleSubmit:A,_getFormContext:t,get submitting(){return e.getSubmissionState().submitting},get error(){return e.getSubmissionState().error}}),[u,E,C,I,k,_,A,t,e])}function q(n,o,i=n.triggerValidation){let{store:e,descriptorTree:s,options:a}=n,m=o(s)[le],r=m.toJsonPointer();e.registerField(r,m);let p=f(b=>e.subscribe(r,b),[e,r]),y=f(()=>e.getFieldState(r),[e,r]),l=se(p,y,y),F=f(b=>{e.setFieldValue(r,b,a.createMissingStructure!==!1),i?.(!1)},[e,r,a.createMissingStructure,i]),g=f(()=>e.updateFieldState(r,{touched:!0}),[e,r]);return{...l,onChange:F,onBlur:g,setValue:F,schema:m.getSchema()}}function ie(n,o,i){if(!n?.renderers)return;let e=O(o);return(i?n.renderers[`${e}:${i}`]:void 0)??n.renderers[e]}import{jsx as ae}from"react/jsx-runtime";function z({renderers:n,config:o,children:i}){let e=H(N),s=ye(()=>{let a={...o,renderers:{...o?.renderers,...n}};return e?{...e,...a,renderers:{...e.renderers,...a.renderers}}:a},[e,o,n]);return ae(N.Provider,{value:s,children:i})}function fe({form:n,children:o}){let i=n._getFormContext();return ae(G.Provider,{value:i,children:o})}function Se(n){let o=H(G);if(!o)throw new Error("useField must be used within a FormProvider. Wrap your component tree with <FormProvider form={form}>.");return q(o,n)}function Te(){let n=H(N);if(!n)throw new Error("useFormSystem must be used within a FormSystemProvider");return n}function Q({forProperty:n,form:o,renderer:i,variant:e,label:s,name:a,fieldProps:m}){let r=o.useField(n),p=H(N),y=i??ie(p,r.schema,e);if(!y){let l=O(r.schema),F=e?`"${l}:${e}" or "${l}"`:`"${l}"`;throw new Error(`No renderer found for schema type ${F}. Provide a renderer prop or configure one in FormSystemProvider.`)}return y({value:r.value,initialValue:r.initialValue,dirty:r.dirty,touched:r.touched,error:r.error,validating:r.validating,onChange:r.onChange,onBlur:r.onBlur,setValue:r.setValue,schema:r.schema,variant:e,label:s,name:a,fieldProps:m})}import{SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR as he}from"@cleverbrush/schema";import{jsx as ce}from"react/jsx-runtime";function Fe(n){return n}function Pe(n){let o=Object.freeze({...n.renderers});function i(s){let a=s.forProperty(s.form._getFormContext().descriptorTree),m=O(a[he].getSchema()),r=s.variant?`${m}:${s.variant}`:m,p=o[r];if(!p)throw new Error(`No renderer registered for "${r}" in this form system.`);return ce(Q,{...s,renderer:p})}function e({children:s}){return ce(z,{renderers:o,children:s})}return{Field:i,Provider:e,renderers:o}}export{Q as Field,fe as FormProvider,z as FormSystemProvider,Pe as createFormSystem,Fe as defineFieldRenderer,Se as useField,Te as useFormSystem,pe as useSchemaForm};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components.tsx","../src/contexts.ts","../src/helpers.ts","../src/hooks.ts","../src/debounce.ts","../src/FormStore.ts"],"sourcesContent":["import type {\n InferType,\n ObjectSchemaBuilder,\n PropertyDescriptor,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type React from 'react';\nimport { useContext, useMemo } from 'react';\nimport { FormContext, FormSystemContext } from './contexts.js';\nimport { getSchemaType } from './helpers.js';\nimport type { SchemaFormInstance } from './hooks.js';\nimport { resolveRenderer, useFieldFromContext } from './hooks.js';\nimport type {\n FieldRenderer,\n FormSystemConfig,\n UseFieldResult\n} from './types.js';\n\n// ─── FormSystemProvider ──────────────────────────────────────────────────────\n\nexport type FormSystemProviderProps = {\n /**\n * Renderer registry mapping schema types to renderer functions.\n * Example: `{ string: (props) => <input .../>, number: (props) => <input type=\"number\" .../> }`\n */\n renderers?: Record<string, FieldRenderer>;\n /**\n * Full configuration object (for future extensibility).\n * If both `renderers` and `config.renderers` are provided, `renderers` takes precedence.\n */\n config?: FormSystemConfig;\n children: React.ReactNode;\n};\n\n/**\n * Provides global renderer configuration via React Context.\n * Supports nesting — inner provider overrides outer.\n *\n * @example\n * ```tsx\n * <FormSystemProvider renderers={htmlRenderers}>\n * <App />\n * </FormSystemProvider>\n * ```\n */\nexport function FormSystemProvider({\n renderers,\n config,\n children\n}: FormSystemProviderProps): React.ReactNode {\n const parentConfig = useContext(FormSystemContext);\n\n const resolvedConfig: FormSystemConfig = useMemo(() => {\n const current: FormSystemConfig = {\n ...config,\n renderers: {\n ...config?.renderers,\n ...renderers\n }\n };\n if (!parentConfig) return current;\n return {\n ...parentConfig,\n ...current,\n renderers: {\n ...parentConfig.renderers,\n ...current.renderers\n }\n };\n }, [parentConfig, config, renderers]);\n\n return (\n <FormSystemContext.Provider value={resolvedConfig}>\n {children}\n </FormSystemContext.Provider>\n );\n}\n\n// ─── FormProvider ────────────────────────────────────────────────────────────\n\nexport type FormProviderProps<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n form: SchemaFormInstance<TSchema>;\n children: React.ReactNode;\n};\n\n/**\n * Provides form context to children so they can use the context-based useField.\n */\nexport function FormProvider<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>({ form, children }: FormProviderProps<TSchema>): React.ReactNode {\n const formContext = form._getFormContext();\n return (\n <FormContext.Provider value={formContext}>\n {children}\n </FormContext.Provider>\n );\n}\n\n// ─── Context-based useField ──────────────────────────────────────────────────\n\n/**\n * Context-based useField — can be used inside a FormProvider.\n * For best IntelliSense, prefer `form.useField()` which infers field types from the schema.\n * When using this context-based version, specify the schema type explicitly:\n *\n * @example\n * ```tsx\n * const name = useField<typeof MySchema>((t) => t.name);\n * ```\n */\nexport function useField<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TPropertySchema extends SchemaBuilder<any, any, any> = SchemaBuilder<\n any,\n any,\n any\n >\n>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n): UseFieldResult<InferType<TPropertySchema>> {\n const formContext = useContext(FormContext);\n if (!formContext) {\n throw new Error(\n 'useField must be used within a FormProvider. ' +\n 'Wrap your component tree with <FormProvider form={form}>.'\n );\n }\n return useFieldFromContext(formContext, forProperty) as UseFieldResult<\n InferType<TPropertySchema>\n >;\n}\n\n// ─── useFormSystem ───────────────────────────────────────────────────────────\n\n/**\n * Hook to access FormSystem configuration.\n */\nexport function useFormSystem(): FormSystemConfig {\n const config = useContext(FormSystemContext);\n if (!config) {\n throw new Error(\n 'useFormSystem must be used within a FormSystemProvider'\n );\n }\n return config;\n}\n\n// ─── Field Component ─────────────────────────────────────────────────────────\n\nexport type FieldProps<TSchema extends ObjectSchemaBuilder<any, any, any>> = {\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, any, any>;\n form: SchemaFormInstance<TSchema>;\n renderer?: FieldRenderer;\n /**\n * Rendering variant hint. Participates in renderer resolution:\n * the registry is checked for `\"type:variant\"` (e.g. `\"string:password\"`)\n * before falling back to the base `\"type\"` key.\n * Also forwarded to the renderer via `FieldRenderProps.variant`.\n */\n variant?: string;\n /** Visible label text forwarded to the renderer via `FieldRenderProps.label`. */\n label?: string;\n /** HTML `name` attribute forwarded to the renderer via `FieldRenderProps.name`. */\n name?: string;\n /**\n * Bag of extra renderer-specific props forwarded to the renderer via\n * `FieldRenderProps.fieldProps` (e.g. `placeholder`, `autoComplete`).\n */\n fieldProps?: Record<string, unknown>;\n};\n\n/**\n * UI-agnostic Field component.\n *\n * Resolves a renderer from:\n * 1. Explicit `renderer` prop\n * 2. FormSystemProvider registry — checked for `\"type:variant\"` first,\n * then for the base `\"type\"`\n *\n * All rendering hints (`variant`, `label`, `name`, `fieldProps`) are\n * forwarded to the resolved renderer via `FieldRenderProps`.\n *\n * @example\n * ```tsx\n * <Field forProperty={(t) => t.password} form={form}\n * variant=\"password\" label=\"Password\"\n * fieldProps={{ placeholder: \"Enter password\", autoComplete: \"current-password\" }} />\n * ```\n */\nexport function Field<TSchema extends ObjectSchemaBuilder<any, any, any>>({\n forProperty,\n form,\n renderer,\n variant,\n label,\n name,\n fieldProps\n}: FieldProps<TSchema>): React.ReactNode {\n const fieldResult = form.useField(forProperty);\n const systemConfig = useContext(FormSystemContext);\n\n const resolvedRenderer =\n renderer ?? resolveRenderer(systemConfig, fieldResult.schema, variant);\n\n if (!resolvedRenderer) {\n const schemaType = getSchemaType(fieldResult.schema);\n const tried = variant\n ? `\"${schemaType}:${variant}\" or \"${schemaType}\"`\n : `\"${schemaType}\"`;\n throw new Error(\n `No renderer found for schema type ${tried}. ` +\n 'Provide a renderer prop or configure one in FormSystemProvider.'\n );\n }\n\n return resolvedRenderer({\n value: fieldResult.value,\n initialValue: fieldResult.initialValue,\n dirty: fieldResult.dirty,\n touched: fieldResult.touched,\n error: fieldResult.error,\n validating: fieldResult.validating,\n onChange: fieldResult.onChange,\n onBlur: fieldResult.onBlur,\n setValue: fieldResult.setValue,\n schema: fieldResult.schema,\n variant,\n label,\n name,\n fieldProps\n });\n}\n","import type {\n ObjectSchemaBuilder,\n PropertyDescriptorInner,\n PropertyDescriptorTree\n} from '@cleverbrush/schema';\nimport { createContext } from 'react';\nimport type { FormStore } from './FormStore.js';\nimport type { FormSystemConfig, UseSchemaFormOptions } from './types.js';\n\n/**\n * Context for global form system configuration (renderers, etc).\n */\nexport const FormSystemContext = createContext<FormSystemConfig | null>(null);\n\n/**\n * Internal form context value — used to bridge useSchemaForm store\n * to the context-based useField and Field component.\n */\nexport type FormContextValue = {\n store: FormStore;\n descriptorTree: PropertyDescriptorTree<any, any>;\n schema: ObjectSchemaBuilder<any, any, any>;\n options: UseSchemaFormOptions;\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;\n};\n\n/**\n * Context for per-form data — allows useField to work via context.\n */\nexport const FormContext = createContext<FormContextValue | null>(null);\n","import type {\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\n\n/**\n * Builds a map from PropertyDescriptorInner identity → dot-separated path\n * by traversing the descriptor tree recursively.\n */\nexport function buildDescriptorPathMap(\n tree: PropertyDescriptorTree<any, any>,\n schema: ObjectSchemaBuilder<any, any, any>\n): Map<PropertyDescriptorInner<any, any, any>, string> {\n const map = new Map<PropertyDescriptorInner<any, any, any>, string>();\n const introspected = schema.introspect();\n if (!introspected.properties) return map;\n\n for (const propName of Object.keys(introspected.properties)) {\n const propDescriptor = (tree as any)[propName];\n if (\n !propDescriptor ||\n !(SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR in propDescriptor)\n ) {\n continue;\n }\n const inner = propDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ] as PropertyDescriptorInner<any, any, any>;\n map.set(inner, propName);\n\n // Recurse into nested objects\n const propSchema = introspected.properties[propName];\n if (propSchema instanceof ObjectSchemaBuilder) {\n const childMap = buildDescriptorPathMap(propDescriptor, propSchema);\n for (const [childInner, childPath] of childMap) {\n map.set(childInner, `${propName}.${childPath}`);\n }\n }\n }\n\n return map;\n}\n\n/**\n * Looks up the path for a given descriptor from the pre-built map.\n */\nexport function getDescriptorPath(\n inner: PropertyDescriptorInner<any, any, any>,\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>\n): string {\n return pathMap.get(inner) ?? '';\n}\n\n/**\n * Returns the schema type string (e.g. \"string\", \"number\", \"object\").\n */\nexport function getSchemaType(schema: SchemaBuilder<any, any, any>): string {\n const introspected = schema.introspect();\n return introspected?.type ?? 'unknown';\n}\n\n/**\n * Builds a selector function from a dot-separated path string.\n * The selector traverses the PropertyDescriptorTree by property access.\n * Example: \"customer.address.city\" → (tree) => tree.customer.address.city\n */\nexport function buildSelectorFromPath(path: string): (tree: any) => any {\n const parts = path.split('.');\n return (tree: any) => {\n let current = tree;\n for (const part of parts) {\n if (current == null) return undefined;\n current = current[part];\n }\n return current;\n };\n}\n\n/**\n * Checks whether a validation error path matches a field's expected error path.\n * Matches exact path, path with nested suffix, or path with validator suffix.\n * Example: isErrorPathMatch(\"$.user.name\", \"$.user.name\") → true\n * isErrorPathMatch(\"$.user.name($validators[0])\", \"$.user.name\") → true\n */\nexport function isErrorPathMatch(\n errPath: string,\n fieldErrorPath: string\n): boolean {\n return (\n errPath === fieldErrorPath ||\n errPath.startsWith(fieldErrorPath + '.') ||\n errPath.startsWith(fieldErrorPath + '(')\n );\n}\n\n/**\n * Ensures all nested object structures exist in the values object\n * by traversing the schema and creating empty objects where needed.\n * This prevents ObjectSchemaBuilder.validate() from throwing when\n * nested object properties are undefined.\n *\n * Example: ensureNestedStructure({}, OrderSchema)\n * → { customer: { address: {} } }\n */\nexport function ensureNestedStructure(\n values: any,\n schema: ObjectSchemaBuilder<any, any, any>\n): any {\n const introspected = schema.introspect();\n if (!introspected.properties) return values != null ? values : {};\n\n const result = values != null ? { ...values } : {};\n for (const key of Object.keys(introspected.properties)) {\n const propSchema = introspected.properties[key];\n if (propSchema instanceof ObjectSchemaBuilder) {\n result[key] = ensureNestedStructure(result[key], propSchema);\n }\n }\n return result;\n}\n","import type {\n InferType,\n PropertyDescriptor,\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder,\n ValidationResult\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { FormContextValue } from './contexts.js';\nimport { debounce } from './debounce.js';\nimport type { FormStore } from './FormStore.js';\nimport { createFormStore } from './FormStore.js';\nimport {\n buildDescriptorPathMap,\n buildSelectorFromPath,\n ensureNestedStructure,\n getDescriptorPath,\n getSchemaType\n} from './helpers.js';\nimport type {\n FieldRenderer,\n FormSystemConfig,\n UseFieldResult,\n UseSchemaFormOptions\n} from './types.js';\n\n// ─── SchemaFormInstance ──────────────────────────────────────────────────────\n\n/**\n * Return type for useSchemaForm — fully typed for IntelliSense.\n * The `useField` method infers the field value type from the schema via PropertyDescriptor.\n */\nexport type SchemaFormInstance<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n useField: <TPropertySchema extends SchemaBuilder<any, any, any>>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ) => UseFieldResult<InferType<TPropertySchema>>;\n submit: () => Promise<ValidationResult<InferType<TSchema>>>;\n validate: () => Promise<ValidationResult<InferType<TSchema>>>;\n reset: (values?: Partial<InferType<TSchema>>) => void;\n getValue: () => InferType<TSchema>;\n setValue: (values: Partial<InferType<TSchema>>) => void;\n /** @internal — Used by FormProvider and Field to access internal context */\n _getFormContext: () => FormContextValue;\n};\n\n// ─── useSchemaForm ───────────────────────────────────────────────────────────\n\n/**\n * Hook that binds a schema to a form instance.\n * Provides field binding API, form-level validation, submit, reset.\n */\nexport function useSchemaForm<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>(\n schema: TSchema,\n options?: UseSchemaFormOptions\n): SchemaFormInstance<TSchema> {\n const resolvedOptions: UseSchemaFormOptions = {\n createMissingStructure: true,\n ...options\n };\n\n const storeRef = useRef<FormStore | null>(null);\n if (!storeRef.current) {\n storeRef.current = createFormStore({});\n }\n const store = storeRef.current;\n\n const descriptorTreeRef = useRef<PropertyDescriptorTree<\n TSchema,\n TSchema\n > | null>(null);\n if (!descriptorTreeRef.current) {\n descriptorTreeRef.current = ObjectSchemaBuilder.getPropertiesFor(\n schema\n ) as PropertyDescriptorTree<TSchema, TSchema>;\n }\n const descriptorTree = descriptorTreeRef.current;\n\n const pathMapRef = useRef<Map<\n PropertyDescriptorInner<any, any, any>,\n string\n > | null>(null);\n if (!pathMapRef.current) {\n pathMapRef.current = buildDescriptorPathMap(descriptorTree, schema);\n }\n const pathMap = pathMapRef.current;\n\n const schemaRef = useRef(schema);\n const optionsRef = useRef(resolvedOptions);\n optionsRef.current = resolvedOptions;\n\n const formContextValue = useMemo<FormContextValue>(\n () => ({\n store,\n descriptorTree,\n schema: schemaRef.current,\n options: optionsRef.current,\n pathMap\n }),\n [store, descriptorTree, pathMap]\n );\n\n const formContextRef = useRef(formContextValue);\n formContextRef.current = formContextValue;\n\n // Generation counter to discard stale validation results from concurrent runs\n const validationGenRef = useRef(0);\n\n /**\n * Runs full schema validation using getErrorsFor to extract per-field errors.\n * Optionally marks all fields as touched (used by submit/explicit validate).\n */\n const runValidation = useCallback(\n async (\n markTouched: boolean\n ): Promise<ValidationResult<InferType<TSchema>>> => {\n const gen = ++validationGenRef.current;\n const values = store.getValues();\n // Ensure all nested object structures exist to prevent\n // ObjectSchemaBuilder.validateAsync() from throwing on undefined nested objects\n const safeValues = ensureNestedStructure(values, schemaRef.current);\n let result: ValidationResult<InferType<TSchema>>;\n try {\n result = (await schemaRef.current.validateAsync(safeValues, {\n doNotStopOnFirstError: true\n })) as ValidationResult<InferType<TSchema>>;\n } catch {\n // If validation itself throws, treat as invalid\n return { valid: false } as ValidationResult<InferType<TSchema>>;\n }\n\n // Discard results if a newer validation has started since this one began\n if (gen !== validationGenRef.current) {\n return result as ValidationResult<InferType<TSchema>>;\n }\n\n // Clear all existing field errors\n const allPaths = store.getAllFieldPaths();\n for (const p of allPaths) {\n const patch: Partial<{\n error: string | undefined;\n touched: boolean;\n }> = { error: undefined };\n if (markTouched) {\n patch.touched = true;\n }\n store.updateFieldState(p, patch);\n }\n\n // Use getErrorsFor to extract per-field errors via tree selectors\n const resultWithErrors = result as ValidationResult<\n InferType<TSchema>\n > & {\n getErrorsFor?: (selector: (t: any) => any) => {\n errors: ReadonlyArray<string>;\n isValid: boolean;\n };\n errors?: ReadonlyArray<{ message: string; path?: string }>;\n };\n\n // Build a map of errors found via getErrorsFor so we can detect gaps\n const fieldsWithErrors = new Set<string>();\n\n if (typeof resultWithErrors.getErrorsFor === 'function') {\n const getErrorsFor = resultWithErrors.getErrorsFor;\n\n // Extract per-field errors by building selectors from field paths\n for (const [, path] of pathMap) {\n try {\n const selector = buildSelectorFromPath(path);\n const fieldResult = getErrorsFor(selector);\n if (\n fieldResult &&\n Array.isArray(fieldResult.errors) &&\n fieldResult.errors.length > 0\n ) {\n const errorMessage = fieldResult.errors[0];\n const patch: Partial<{\n error: string | undefined;\n touched: boolean;\n }> = { error: errorMessage };\n if (markTouched) {\n patch.touched = true;\n }\n store.updateFieldState(path, patch);\n fieldsWithErrors.add(path);\n }\n } catch {\n // If getErrorsFor fails for this path, skip\n }\n }\n }\n\n return result as ValidationResult<InferType<TSchema>>;\n },\n [store, pathMap]\n );\n\n const validate = useCallback(async (): Promise<\n ValidationResult<InferType<TSchema>>\n > => {\n return runValidation(true);\n }, [runValidation]);\n\n const submit = useCallback(async (): Promise<\n ValidationResult<InferType<TSchema>>\n > => {\n return validate();\n }, [validate]);\n\n const reset = useCallback(\n (values?: Partial<InferType<TSchema>>) => {\n const newValues = values ?? {};\n store.resetAll(newValues);\n },\n [store]\n );\n\n const getValue = useCallback((): InferType<TSchema> => {\n return store.getValues();\n }, [store]);\n\n const setValueFn = useCallback(\n (values: Partial<InferType<TSchema>>) => {\n const currentValues = store.getValues();\n const merged = { ...currentValues, ...values };\n store.setValues(merged);\n store.notifyAll();\n },\n [store]\n );\n\n const _getFormContext = useCallback(() => formContextRef.current, []);\n\n // Validate on mount when requested — runs once after first render\n const validateOnMountRef = useRef(resolvedOptions.validateOnMount);\n // biome-ignore lint/correctness/useExhaustiveDependencies: We only want to check validateOnMount on the initial mount, ignoring changes to it after that\n useEffect(() => {\n if (validateOnMountRef.current) {\n runValidation(true);\n }\n }, []);\n\n // Create a debounced version of runValidation for onChange triggers.\n // validate(), submit(), and validateOnMount always use runValidation directly.\n const debouncedValidationRef = useRef<\n ((markTouched: boolean) => void) | null\n >(null);\n if (\n resolvedOptions.validationDebounceMs != null &&\n resolvedOptions.validationDebounceMs > 0 &&\n !debouncedValidationRef.current\n ) {\n debouncedValidationRef.current = debounce((markTouched: boolean) => {\n runValidation(markTouched);\n }, resolvedOptions.validationDebounceMs);\n }\n\n const triggerValidation = useCallback(\n (markTouched: boolean) => {\n if (debouncedValidationRef.current) {\n debouncedValidationRef.current(markTouched);\n return Promise.resolve(\n undefined as unknown as ValidationResult<InferType<TSchema>>\n );\n }\n return runValidation(markTouched);\n },\n [runValidation]\n );\n\n const useFieldHook = useCallback(\n <TPropertySchema extends SchemaBuilder<any, any, any>>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ): UseFieldResult<InferType<TPropertySchema>> => {\n return useFieldFromContext(\n formContextRef.current,\n forProperty,\n triggerValidation\n ) as UseFieldResult<InferType<TPropertySchema>>;\n },\n [triggerValidation]\n );\n\n return useMemo(\n () => ({\n useField: useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValue: setValueFn,\n _getFormContext\n }),\n [\n useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValueFn,\n _getFormContext\n ]\n );\n}\n\n// ─── useField (from context) ─────────────────────────────────────────────────\n\n/**\n * Internal useField implementation, requires FormContextValue.\n * Returns UseFieldResult with untyped values — callers should cast to the proper generic type.\n */\nexport function useFieldFromContext(\n formContext: FormContextValue,\n forProperty: (tree: any) => any,\n triggerValidation?: (markTouched: boolean) => Promise<any>\n): UseFieldResult {\n const { store, descriptorTree, options, pathMap } = formContext;\n\n const descriptor = forProperty(descriptorTree as any);\n const inner = descriptor[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR];\n const path = getDescriptorPath(inner, pathMap);\n const fieldSchema = inner.getSchema();\n\n // Initialize field state from current values on first access\n const initializedRef = useRef<string | null>(null);\n\n if (initializedRef.current !== path) {\n const values = store.getValues();\n const { success, value } = inner.getValue(values);\n const currentState = store.getFieldState(path);\n if (currentState.initialValue === undefined && !currentState.touched) {\n const resolvedValue = success ? value : undefined;\n store.updateFieldState(path, {\n value: resolvedValue,\n initialValue: resolvedValue,\n dirty: false\n });\n }\n initializedRef.current = path;\n }\n\n const [, setRenderTick] = useState(0);\n\n // Subscribe to field changes with proper cleanup on unmount/path change\n useEffect(() => {\n const unsub = store.subscribe(path, () => {\n setRenderTick(c => c + 1);\n });\n return unsub;\n }, [store, path]);\n\n const onChange = useCallback(\n (value: any) => {\n const values = store.getValues();\n inner.setValue(values, value, {\n createMissingStructure: options.createMissingStructure !== false\n });\n store.setValues(values);\n const currentState = store.getFieldState(path);\n store.updateFieldState(path, {\n value,\n dirty: value !== currentState.initialValue\n });\n // Run validation on every field change (without marking all fields touched)\n if (triggerValidation) {\n triggerValidation(false);\n }\n },\n [store, inner, path, options, triggerValidation]\n );\n\n const onBlur = useCallback(() => {\n store.updateFieldState(path, { touched: true });\n }, [store, path]);\n\n const setValue = useCallback(\n (value: any) => {\n onChange(value);\n },\n [onChange]\n );\n\n const fieldState = store.getFieldState(path);\n\n return {\n value: fieldState.value,\n initialValue: fieldState.initialValue,\n dirty: fieldState.dirty,\n touched: fieldState.touched,\n error: fieldState.error,\n validating: fieldState.validating,\n onChange,\n onBlur,\n setValue,\n schema: fieldSchema\n };\n}\n\n/**\n * Resolves a renderer from the FormSystem config based on schema type and optional variant.\n *\n * When `variant` is provided the registry is checked for `\"type:variant\"` first\n * (e.g. `\"string:password\"`). If no match is found it falls back to the base\n * `\"type\"` key (e.g. `\"string\"`).\n */\nexport function resolveRenderer(\n config: FormSystemConfig | null,\n schema: SchemaBuilder<any, any, any>,\n variant?: string\n): FieldRenderer | undefined {\n if (!config?.renderers) return undefined;\n const type = getSchemaType(schema);\n if (variant) {\n const variantRenderer = config.renderers[`${type}:${variant}`];\n if (variantRenderer) return variantRenderer;\n }\n return config.renderers[type];\n}\n","/**\n * A debounce function ensures that a function is not called too frequently.\n * It only allows the function to be executed after a specified delay has passed since the last call.\n * @param func The function to debounce.\n * @param wait The delay in ms before the function is called.\n */\nexport function debounce<T extends (...args: any[]) => void>(\n func: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeout: ReturnType<typeof setTimeout> | null = null;\n\n return (...args: Parameters<T>) => {\n if (timeout !== null) {\n clearTimeout(timeout);\n }\n timeout = setTimeout(() => {\n func(...args);\n }, wait);\n };\n}\n","import type { FieldState } from './types.js';\n\n/**\n * Internal form store — manages field state and notification.\n * Does not depend on React; used by hooks for state management.\n */\nexport function createFormStore(initialValues: any) {\n let values: any = initialValues != null ? { ...initialValues } : {};\n const fieldStates = new Map<string, FieldState>();\n const listeners = new Map<string, Set<() => void>>();\n const globalListeners = new Set<() => void>();\n\n function ensureFieldState(path: string): FieldState {\n if (!fieldStates.has(path)) {\n fieldStates.set(path, {\n value: undefined,\n initialValue: undefined,\n dirty: false,\n touched: false,\n error: undefined,\n validating: false\n });\n }\n return fieldStates.get(path)!;\n }\n\n function getFieldState(path: string): FieldState {\n return ensureFieldState(path);\n }\n\n function updateFieldState(path: string, patch: Partial<FieldState>) {\n const current = ensureFieldState(path);\n const updated = { ...current, ...patch };\n fieldStates.set(path, updated);\n notifyPath(path);\n }\n\n function notifyPath(path: string) {\n const pathListeners = listeners.get(path);\n if (pathListeners) {\n for (const listener of pathListeners) {\n listener();\n }\n }\n }\n\n function notifyAll() {\n for (const [, pathListeners] of listeners) {\n for (const listener of pathListeners) {\n listener();\n }\n }\n for (const listener of globalListeners) {\n listener();\n }\n }\n\n function subscribe(path: string, listener: () => void): () => void {\n if (!listeners.has(path)) {\n listeners.set(path, new Set());\n }\n listeners.get(path)!.add(listener);\n return () => {\n listeners.get(path)?.delete(listener);\n };\n }\n\n function subscribeGlobal(listener: () => void): () => void {\n globalListeners.add(listener);\n return () => {\n globalListeners.delete(listener);\n };\n }\n\n function getValues(): any {\n return values;\n }\n\n function setValues(newValues: any) {\n values = newValues != null ? { ...newValues } : {};\n }\n\n function resetAll(newInitialValues?: any) {\n values = newInitialValues != null ? { ...newInitialValues } : {};\n fieldStates.clear();\n notifyAll();\n }\n\n function getAllFieldPaths(): string[] {\n return Array.from(fieldStates.keys());\n }\n\n return {\n getFieldState,\n updateFieldState,\n subscribe,\n subscribeGlobal,\n getValues,\n setValues,\n resetAll,\n notifyAll,\n getAllFieldPaths\n };\n}\n\nexport type FormStore = ReturnType<typeof createFormStore>;\n"],"mappings":"AAQA,OAAS,cAAAA,EAAY,WAAAC,OAAe,QCHpC,OAAS,iBAAAC,MAAqB,QAOvB,IAAMC,EAAoBD,EAAuC,IAAI,EAiB/DE,EAAcF,EAAuC,IAAI,ECxBtE,OACI,uBAAAG,EACA,qCAAAC,MACG,sBAMA,SAASC,EACZC,EACAC,EACmD,CACnD,IAAMC,EAAM,IAAI,IACVC,EAAeF,EAAO,WAAW,EACvC,GAAI,CAACE,EAAa,WAAY,OAAOD,EAErC,QAAWE,KAAY,OAAO,KAAKD,EAAa,UAAU,EAAG,CACzD,IAAME,EAAkBL,EAAaI,CAAQ,EAC7C,GACI,CAACC,GACD,EAAEP,KAAqCO,GAEvC,SAEJ,IAAMC,EAAQD,EACVP,CACJ,EACAI,EAAI,IAAII,EAAOF,CAAQ,EAGvB,IAAMG,EAAaJ,EAAa,WAAWC,CAAQ,EACnD,GAAIG,aAAsBV,EAAqB,CAC3C,IAAMW,EAAWT,EAAuBM,EAAgBE,CAAU,EAClE,OAAW,CAACE,EAAYC,CAAS,IAAKF,EAClCN,EAAI,IAAIO,EAAY,GAAGL,CAAQ,IAAIM,CAAS,EAAE,CAEtD,CACJ,CAEA,OAAOR,CACX,CAKO,SAASS,EACZL,EACAM,EACM,CACN,OAAOA,EAAQ,IAAIN,CAAK,GAAK,EACjC,CAKO,SAASO,EAAcZ,EAA8C,CAExE,OADqBA,EAAO,WAAW,GAClB,MAAQ,SACjC,CAOO,SAASa,EAAsBC,EAAkC,CACpE,IAAMC,EAAQD,EAAK,MAAM,GAAG,EAC5B,OAAQf,GAAc,CAClB,IAAIiB,EAAUjB,EACd,QAAWkB,KAAQF,EAAO,CACtB,GAAIC,GAAW,KAAM,OACrBA,EAAUA,EAAQC,CAAI,CAC1B,CACA,OAAOD,CACX,CACJ,CA4BO,SAASE,EACZC,EACAC,EACG,CACH,IAAMC,EAAeD,EAAO,WAAW,EACvC,GAAI,CAACC,EAAa,WAAY,OAAOF,GAA0B,CAAC,EAEhE,IAAMG,EAASH,GAAU,KAAO,CAAE,GAAGA,CAAO,EAAI,CAAC,EACjD,QAAWI,KAAO,OAAO,KAAKF,EAAa,UAAU,EAAG,CACpD,IAAMG,EAAaH,EAAa,WAAWE,CAAG,EAC1CC,aAAsBC,IACtBH,EAAOC,CAAG,EAAIL,EAAsBI,EAAOC,CAAG,EAAGC,CAAU,EAEnE,CACA,OAAOF,CACX,CCpHA,OACI,uBAAAI,GACA,qCAAAC,OACG,sBACP,OAAS,eAAAC,EAAa,aAAAC,EAAW,WAAAC,EAAS,UAAAC,EAAQ,YAAAC,OAAgB,QCN3D,SAASC,EACZC,EACAC,EACgC,CAChC,IAAIC,EAAgD,KAEpD,MAAO,IAAIC,IAAwB,CAC3BD,IAAY,MACZ,aAAaA,CAAO,EAExBA,EAAU,WAAW,IAAM,CACvBF,EAAK,GAAGG,CAAI,CAChB,EAAGF,CAAI,CACX,CACJ,CCdO,SAASG,EAAgBC,EAAoB,CAChD,IAAIC,EAAcD,GAAiB,KAAO,CAAE,GAAGA,CAAc,EAAI,CAAC,EAC5DE,EAAc,IAAI,IAClBC,EAAY,IAAI,IAChBC,EAAkB,IAAI,IAE5B,SAASC,EAAiBC,EAA0B,CAChD,OAAKJ,EAAY,IAAII,CAAI,GACrBJ,EAAY,IAAII,EAAM,CAClB,MAAO,OACP,aAAc,OACd,MAAO,GACP,QAAS,GACT,MAAO,OACP,WAAY,EAChB,CAAC,EAEEJ,EAAY,IAAII,CAAI,CAC/B,CAEA,SAASC,EAAcD,EAA0B,CAC7C,OAAOD,EAAiBC,CAAI,CAChC,CAEA,SAASE,EAAiBF,EAAcG,EAA4B,CAEhE,IAAMC,EAAU,CAAE,GADFL,EAAiBC,CAAI,EACP,GAAGG,CAAM,EACvCP,EAAY,IAAII,EAAMI,CAAO,EAC7BC,EAAWL,CAAI,CACnB,CAEA,SAASK,EAAWL,EAAc,CAC9B,IAAMM,EAAgBT,EAAU,IAAIG,CAAI,EACxC,GAAIM,EACA,QAAWC,KAAYD,EACnBC,EAAS,CAGrB,CAEA,SAASC,GAAY,CACjB,OAAW,CAAC,CAAEF,CAAa,IAAKT,EAC5B,QAAWU,KAAYD,EACnBC,EAAS,EAGjB,QAAWA,KAAYT,EACnBS,EAAS,CAEjB,CAEA,SAASE,EAAUT,EAAcO,EAAkC,CAC/D,OAAKV,EAAU,IAAIG,CAAI,GACnBH,EAAU,IAAIG,EAAM,IAAI,GAAK,EAEjCH,EAAU,IAAIG,CAAI,EAAG,IAAIO,CAAQ,EAC1B,IAAM,CACTV,EAAU,IAAIG,CAAI,GAAG,OAAOO,CAAQ,CACxC,CACJ,CAEA,SAASG,EAAgBH,EAAkC,CACvD,OAAAT,EAAgB,IAAIS,CAAQ,EACrB,IAAM,CACTT,EAAgB,OAAOS,CAAQ,CACnC,CACJ,CAEA,SAASI,GAAiB,CACtB,OAAOhB,CACX,CAEA,SAASiB,EAAUC,EAAgB,CAC/BlB,EAASkB,GAAa,KAAO,CAAE,GAAGA,CAAU,EAAI,CAAC,CACrD,CAEA,SAASC,EAASC,EAAwB,CACtCpB,EAASoB,GAAoB,KAAO,CAAE,GAAGA,CAAiB,EAAI,CAAC,EAC/DnB,EAAY,MAAM,EAClBY,EAAU,CACd,CAEA,SAASQ,GAA6B,CAClC,OAAO,MAAM,KAAKpB,EAAY,KAAK,CAAC,CACxC,CAEA,MAAO,CACH,cAAAK,EACA,iBAAAC,EACA,UAAAO,EACA,gBAAAC,EACA,UAAAC,EACA,UAAAC,EACA,SAAAE,EACA,UAAAN,EACA,iBAAAQ,CACJ,CACJ,CF3CO,SAASC,GAGZC,EACAC,EAC2B,CAC3B,IAAMC,EAAwC,CAC1C,uBAAwB,GACxB,GAAGD,CACP,EAEME,EAAWC,EAAyB,IAAI,EACzCD,EAAS,UACVA,EAAS,QAAUE,EAAgB,CAAC,CAAC,GAEzC,IAAMC,EAAQH,EAAS,QAEjBI,EAAoBH,EAGhB,IAAI,EACTG,EAAkB,UACnBA,EAAkB,QAAUC,GAAoB,iBAC5CR,CACJ,GAEJ,IAAMS,EAAiBF,EAAkB,QAEnCG,EAAaN,EAGT,IAAI,EACTM,EAAW,UACZA,EAAW,QAAUC,EAAuBF,EAAgBT,CAAM,GAEtE,IAAMY,EAAUF,EAAW,QAErBG,EAAYT,EAAOJ,CAAM,EACzBc,EAAaV,EAAOF,CAAe,EACzCY,EAAW,QAAUZ,EAErB,IAAMa,EAAmBC,EACrB,KAAO,CACH,MAAAV,EACA,eAAAG,EACA,OAAQI,EAAU,QAClB,QAASC,EAAW,QACpB,QAAAF,CACJ,GACA,CAACN,EAAOG,EAAgBG,CAAO,CACnC,EAEMK,EAAiBb,EAAOW,CAAgB,EAC9CE,EAAe,QAAUF,EAGzB,IAAMG,EAAmBd,EAAO,CAAC,EAM3Be,EAAgBC,EAClB,MACIC,GACgD,CAChD,IAAMC,EAAM,EAAEJ,EAAiB,QACzBK,EAASjB,EAAM,UAAU,EAGzBkB,GAAaC,EAAsBF,EAAQV,EAAU,OAAO,EAC9Da,EACJ,GAAI,CACAA,EAAU,MAAMb,EAAU,QAAQ,cAAcW,GAAY,CACxD,sBAAuB,EAC3B,CAAC,CACL,MAAQ,CAEJ,MAAO,CAAE,MAAO,EAAM,CAC1B,CAGA,GAAIF,IAAQJ,EAAiB,QACzB,OAAOQ,EAIX,IAAMC,GAAWrB,EAAM,iBAAiB,EACxC,QAAWsB,KAAKD,GAAU,CACtB,IAAME,EAGD,CAAE,MAAO,MAAU,EACpBR,IACAQ,EAAM,QAAU,IAEpBvB,EAAM,iBAAiBsB,EAAGC,CAAK,CACnC,CAGA,IAAMC,EAAmBJ,EAWnBK,GAAmB,IAAI,IAE7B,GAAI,OAAOD,EAAiB,cAAiB,WAAY,CACrD,IAAME,EAAeF,EAAiB,aAGtC,OAAW,CAAC,CAAEG,CAAI,IAAKrB,EACnB,GAAI,CACA,IAAMsB,GAAWC,EAAsBF,CAAI,EACrCG,EAAcJ,EAAaE,EAAQ,EACzC,GACIE,GACA,MAAM,QAAQA,EAAY,MAAM,GAChCA,EAAY,OAAO,OAAS,EAC9B,CAEE,IAAMP,EAGD,CAAE,MAJcO,EAAY,OAAO,CAAC,CAId,EACvBf,IACAQ,EAAM,QAAU,IAEpBvB,EAAM,iBAAiB2B,EAAMJ,CAAK,EAClCE,GAAiB,IAAIE,CAAI,CAC7B,CACJ,MAAQ,CAER,CAER,CAEA,OAAOP,CACX,EACA,CAACpB,EAAOM,CAAO,CACnB,EAEMyB,EAAWjB,EAAY,SAGlBD,EAAc,EAAI,EAC1B,CAACA,CAAa,CAAC,EAEZmB,EAASlB,EAAY,SAGhBiB,EAAS,EACjB,CAACA,CAAQ,CAAC,EAEPE,EAAQnB,EACTG,GAAyC,CACtC,IAAMiB,EAAYjB,GAAU,CAAC,EAC7BjB,EAAM,SAASkC,CAAS,CAC5B,EACA,CAAClC,CAAK,CACV,EAEMmC,EAAWrB,EAAY,IAClBd,EAAM,UAAU,EACxB,CAACA,CAAK,CAAC,EAEJoC,EAAatB,EACdG,GAAwC,CAErC,IAAMoB,EAAS,CAAE,GADKrC,EAAM,UAAU,EACH,GAAGiB,CAAO,EAC7CjB,EAAM,UAAUqC,CAAM,EACtBrC,EAAM,UAAU,CACpB,EACA,CAACA,CAAK,CACV,EAEMsC,EAAkBxB,EAAY,IAAMH,EAAe,QAAS,CAAC,CAAC,EAG9D4B,EAAqBzC,EAAOF,EAAgB,eAAe,EAEjE4C,EAAU,IAAM,CACRD,EAAmB,SACnB1B,EAAc,EAAI,CAE1B,EAAG,CAAC,CAAC,EAIL,IAAM4B,EAAyB3C,EAE7B,IAAI,EAEFF,EAAgB,sBAAwB,MACxCA,EAAgB,qBAAuB,GACvC,CAAC6C,EAAuB,UAExBA,EAAuB,QAAUC,EAAU3B,GAAyB,CAChEF,EAAcE,CAAW,CAC7B,EAAGnB,EAAgB,oBAAoB,GAG3C,IAAM+C,EAAoB7B,EACrBC,GACO0B,EAAuB,SACvBA,EAAuB,QAAQ1B,CAAW,EACnC,QAAQ,QACX,MACJ,GAEGF,EAAcE,CAAW,EAEpC,CAACF,CAAa,CAClB,EAEM+B,EAAe9B,EAEb+B,GAIOC,EACHnC,EAAe,QACfkC,EACAF,CACJ,EAEJ,CAACA,CAAiB,CACtB,EAEA,OAAOjC,EACH,KAAO,CACH,SAAUkC,EACV,OAAAZ,EACA,SAAAD,EACA,MAAAE,EACA,SAAAE,EACA,SAAUC,EACV,gBAAAE,CACJ,GACA,CACIM,EACAZ,EACAD,EACAE,EACAE,EACAC,EACAE,CACJ,CACJ,CACJ,CAQO,SAASQ,EACZC,EACAF,EACAF,EACc,CACd,GAAM,CAAE,MAAA3C,EAAO,eAAAG,EAAgB,QAAAR,EAAS,QAAAW,CAAQ,EAAIyC,EAG9CC,EADaH,EAAY1C,CAAqB,EAC3B8C,EAAiC,EACpDtB,EAAOuB,EAAkBF,EAAO1C,CAAO,EACvC6C,EAAcH,EAAM,UAAU,EAG9BI,EAAiBtD,EAAsB,IAAI,EAEjD,GAAIsD,EAAe,UAAYzB,EAAM,CACjC,IAAMV,EAASjB,EAAM,UAAU,EACzB,CAAE,QAAAqD,EAAS,MAAAC,CAAM,EAAIN,EAAM,SAAS/B,CAAM,EAC1CsC,EAAevD,EAAM,cAAc2B,CAAI,EAC7C,GAAI4B,EAAa,eAAiB,QAAa,CAACA,EAAa,QAAS,CAClE,IAAMC,EAAgBH,EAAUC,EAAQ,OACxCtD,EAAM,iBAAiB2B,EAAM,CACzB,MAAO6B,EACP,aAAcA,EACd,MAAO,EACX,CAAC,CACL,CACAJ,EAAe,QAAUzB,CAC7B,CAEA,GAAM,CAAC,CAAE8B,CAAa,EAAIC,GAAS,CAAC,EAGpClB,EAAU,IACQxC,EAAM,UAAU2B,EAAM,IAAM,CACtC8B,EAAcE,GAAKA,EAAI,CAAC,CAC5B,CAAC,EAEF,CAAC3D,EAAO2B,CAAI,CAAC,EAEhB,IAAMiC,EAAW9C,EACZwC,GAAe,CACZ,IAAMrC,EAASjB,EAAM,UAAU,EAC/BgD,EAAM,SAAS/B,EAAQqC,EAAO,CAC1B,uBAAwB3D,EAAQ,yBAA2B,EAC/D,CAAC,EACDK,EAAM,UAAUiB,CAAM,EACtB,IAAMsC,EAAevD,EAAM,cAAc2B,CAAI,EAC7C3B,EAAM,iBAAiB2B,EAAM,CACzB,MAAA2B,EACA,MAAOA,IAAUC,EAAa,YAClC,CAAC,EAEGZ,GACAA,EAAkB,EAAK,CAE/B,EACA,CAAC3C,EAAOgD,EAAOrB,EAAMhC,EAASgD,CAAiB,CACnD,EAEMkB,EAAS/C,EAAY,IAAM,CAC7Bd,EAAM,iBAAiB2B,EAAM,CAAE,QAAS,EAAK,CAAC,CAClD,EAAG,CAAC3B,EAAO2B,CAAI,CAAC,EAEVmC,EAAWhD,EACZwC,GAAe,CACZM,EAASN,CAAK,CAClB,EACA,CAACM,CAAQ,CACb,EAEMG,EAAa/D,EAAM,cAAc2B,CAAI,EAE3C,MAAO,CACH,MAAOoC,EAAW,MAClB,aAAcA,EAAW,aACzB,MAAOA,EAAW,MAClB,QAASA,EAAW,QACpB,MAAOA,EAAW,MAClB,WAAYA,EAAW,WACvB,SAAAH,EACA,OAAAC,EACA,SAAAC,EACA,OAAQX,CACZ,CACJ,CASO,SAASa,EACZC,EACAvE,EACAwE,EACyB,CACzB,GAAI,CAACD,GAAQ,UAAW,OACxB,IAAME,EAAOC,EAAc1E,CAAM,EACjC,GAAIwE,EAAS,CACT,IAAMG,EAAkBJ,EAAO,UAAU,GAAGE,CAAI,IAAID,CAAO,EAAE,EAC7D,GAAIG,EAAiB,OAAOA,CAChC,CACA,OAAOJ,EAAO,UAAUE,CAAI,CAChC,CHrWQ,cAAAG,MAAA,oBA3BD,SAASC,GAAmB,CAC/B,UAAAC,EACA,OAAAC,EACA,SAAAC,CACJ,EAA6C,CACzC,IAAMC,EAAeC,EAAWC,CAAiB,EAE3CC,EAAmCC,GAAQ,IAAM,CACnD,IAAMC,EAA4B,CAC9B,GAAGP,EACH,UAAW,CACP,GAAGA,GAAQ,UACX,GAAGD,CACP,CACJ,EACA,OAAKG,EACE,CACH,GAAGA,EACH,GAAGK,EACH,UAAW,CACP,GAAGL,EAAa,UAChB,GAAGK,EAAQ,SACf,CACJ,EAR0BA,CAS9B,EAAG,CAACL,EAAcF,EAAQD,CAAS,CAAC,EAEpC,OACIF,EAACO,EAAkB,SAAlB,CAA2B,MAAOC,EAC9B,SAAAJ,EACL,CAER,CAcO,SAASO,GAEd,CAAE,KAAAC,EAAM,SAAAR,CAAS,EAAgD,CAC/D,IAAMS,EAAcD,EAAK,gBAAgB,EACzC,OACIZ,EAACc,EAAY,SAAZ,CAAqB,MAAOD,EACxB,SAAAT,EACL,CAER,CAcO,SAASW,GAQZC,EAG0C,CAC1C,IAAMH,EAAcP,EAAWQ,CAAW,EAC1C,GAAI,CAACD,EACD,MAAM,IAAI,MACN,wGAEJ,EAEJ,OAAOI,EAAoBJ,EAAaG,CAAW,CAGvD,CAOO,SAASE,IAAkC,CAC9C,IAAMf,EAASG,EAAWC,CAAiB,EAC3C,GAAI,CAACJ,EACD,MAAM,IAAI,MACN,wDACJ,EAEJ,OAAOA,CACX,CA8CO,SAASgB,GAA0D,CACtE,YAAAH,EACA,KAAAJ,EACA,SAAAQ,EACA,QAAAC,EACA,MAAAC,EACA,KAAAC,EACA,WAAAC,CACJ,EAAyC,CACrC,IAAMC,EAAcb,EAAK,SAASI,CAAW,EACvCU,EAAepB,EAAWC,CAAiB,EAE3CoB,EACFP,GAAYQ,EAAgBF,EAAcD,EAAY,OAAQJ,CAAO,EAEzE,GAAI,CAACM,EAAkB,CACnB,IAAME,EAAaC,EAAcL,EAAY,MAAM,EAC7CM,EAAQV,EACR,IAAIQ,CAAU,IAAIR,CAAO,SAASQ,CAAU,IAC5C,IAAIA,CAAU,IACpB,MAAM,IAAI,MACN,qCAAqCE,CAAK,mEAE9C,CACJ,CAEA,OAAOJ,EAAiB,CACpB,MAAOF,EAAY,MACnB,aAAcA,EAAY,aAC1B,MAAOA,EAAY,MACnB,QAASA,EAAY,QACrB,MAAOA,EAAY,MACnB,WAAYA,EAAY,WACxB,SAAUA,EAAY,SACtB,OAAQA,EAAY,OACpB,SAAUA,EAAY,SACtB,OAAQA,EAAY,OACpB,QAAAJ,EACA,MAAAC,EACA,KAAAC,EACA,WAAAC,CACJ,CAAC,CACL","names":["useContext","useMemo","createContext","FormSystemContext","FormContext","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","buildDescriptorPathMap","tree","schema","map","introspected","propName","propDescriptor","inner","propSchema","childMap","childInner","childPath","getDescriptorPath","pathMap","getSchemaType","buildSelectorFromPath","path","parts","current","part","ensureNestedStructure","values","schema","introspected","result","key","propSchema","ObjectSchemaBuilder","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","useCallback","useEffect","useMemo","useRef","useState","debounce","func","wait","timeout","args","createFormStore","initialValues","values","fieldStates","listeners","globalListeners","ensureFieldState","path","getFieldState","updateFieldState","patch","updated","notifyPath","pathListeners","listener","notifyAll","subscribe","subscribeGlobal","getValues","setValues","newValues","resetAll","newInitialValues","getAllFieldPaths","useSchemaForm","schema","options","resolvedOptions","storeRef","useRef","createFormStore","store","descriptorTreeRef","ObjectSchemaBuilder","descriptorTree","pathMapRef","buildDescriptorPathMap","pathMap","schemaRef","optionsRef","formContextValue","useMemo","formContextRef","validationGenRef","runValidation","useCallback","markTouched","gen","values","safeValues","ensureNestedStructure","result","allPaths","p","patch","resultWithErrors","fieldsWithErrors","getErrorsFor","path","selector","buildSelectorFromPath","fieldResult","validate","submit","reset","newValues","getValue","setValueFn","merged","_getFormContext","validateOnMountRef","useEffect","debouncedValidationRef","debounce","triggerValidation","useFieldHook","forProperty","useFieldFromContext","formContext","inner","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","getDescriptorPath","fieldSchema","initializedRef","success","value","currentState","resolvedValue","setRenderTick","useState","c","onChange","onBlur","setValue","fieldState","resolveRenderer","config","variant","type","getSchemaType","variantRenderer","jsx","FormSystemProvider","renderers","config","children","parentConfig","useContext","FormSystemContext","resolvedConfig","useMemo","current","FormProvider","form","formContext","FormContext","useField","forProperty","useFieldFromContext","useFormSystem","Field","renderer","variant","label","name","fieldProps","fieldResult","systemConfig","resolvedRenderer","resolveRenderer","schemaType","getSchemaType","tried"]}
|
|
1
|
+
{"version":3,"sources":["../src/components.tsx","../src/contexts.ts","../src/helpers.ts","../src/hooks.ts","../src/FormStore.ts","../src/system.tsx"],"sourcesContent":["import type {\n InferType,\n ObjectSchemaBuilder,\n PropertyDescriptor,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport type React from 'react';\nimport { useContext, useMemo } from 'react';\nimport { FormContext, FormSystemContext } from './contexts.js';\nimport { getSchemaType } from './helpers.js';\nimport type { SchemaFormInstance } from './hooks.js';\nimport { resolveRenderer, useFieldFromContext } from './hooks.js';\nimport type {\n FieldRenderer,\n FormSystemConfig,\n UseFieldResult\n} from './types.js';\n\n// ─── FormSystemProvider ──────────────────────────────────────────────────────\n\nexport type FormSystemProviderProps = {\n /**\n * Renderer registry mapping schema types to renderer functions.\n * Example: `{ string: (props) => <input .../>, number: (props) => <input type=\"number\" .../> }`\n */\n renderers?: Record<string, FieldRenderer>;\n /**\n * Full configuration object (for future extensibility).\n * If both `renderers` and `config.renderers` are provided, `renderers` takes precedence.\n */\n config?: FormSystemConfig;\n children: React.ReactNode;\n};\n\n/**\n * Provides global renderer configuration via React Context.\n * Supports nesting — inner provider overrides outer.\n *\n * @example\n * ```tsx\n * <FormSystemProvider renderers={htmlRenderers}>\n * <App />\n * </FormSystemProvider>\n * ```\n */\nexport function FormSystemProvider({\n renderers,\n config,\n children\n}: FormSystemProviderProps): React.ReactNode {\n const parentConfig = useContext(FormSystemContext);\n\n const resolvedConfig: FormSystemConfig = useMemo(() => {\n const current: FormSystemConfig = {\n ...config,\n renderers: {\n ...config?.renderers,\n ...renderers\n }\n };\n if (!parentConfig) return current;\n return {\n ...parentConfig,\n ...current,\n renderers: {\n ...parentConfig.renderers,\n ...current.renderers\n }\n };\n }, [parentConfig, config, renderers]);\n\n return (\n <FormSystemContext.Provider value={resolvedConfig}>\n {children}\n </FormSystemContext.Provider>\n );\n}\n\n// ─── FormProvider ────────────────────────────────────────────────────────────\n\nexport type FormProviderProps<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n form: SchemaFormInstance<TSchema>;\n children: React.ReactNode;\n};\n\n/**\n * Provides form context to children so they can use the context-based useField.\n */\nexport function FormProvider<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>({ form, children }: FormProviderProps<TSchema>): React.ReactNode {\n const formContext = form._getFormContext();\n return (\n <FormContext.Provider value={formContext}>\n {children}\n </FormContext.Provider>\n );\n}\n\n// ─── Context-based useField ──────────────────────────────────────────────────\n\n/**\n * Context-based useField — can be used inside a FormProvider.\n * For best IntelliSense, prefer `form.useField()` which infers field types from the schema.\n * When using this context-based version, specify the schema type explicitly:\n *\n * @example\n * ```tsx\n * const name = useField<typeof MySchema>((t) => t.name);\n * ```\n */\nexport function useField<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TPropertySchema extends SchemaBuilder<any, any, any, any> = any\n>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n): UseFieldResult<InferType<TPropertySchema>> {\n const formContext = useContext(FormContext);\n if (!formContext) {\n throw new Error(\n 'useField must be used within a FormProvider. ' +\n 'Wrap your component tree with <FormProvider form={form}>.'\n );\n }\n return useFieldFromContext(formContext, forProperty) as UseFieldResult<\n InferType<TPropertySchema>\n >;\n}\n\n// ─── useFormSystem ───────────────────────────────────────────────────────────\n\n/**\n * Hook to access FormSystem configuration.\n */\nexport function useFormSystem(): FormSystemConfig {\n const config = useContext(FormSystemContext);\n if (!config) {\n throw new Error(\n 'useFormSystem must be used within a FormSystemProvider'\n );\n }\n return config;\n}\n\n// ─── Field Component ─────────────────────────────────────────────────────────\n\nexport type FieldProps<TSchema extends ObjectSchemaBuilder<any, any, any>> = {\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, any, any>;\n form: SchemaFormInstance<TSchema>;\n renderer?: FieldRenderer;\n /**\n * Rendering variant hint. Participates in renderer resolution:\n * the registry is checked for `\"type:variant\"` (e.g. `\"string:password\"`)\n * before falling back to the base `\"type\"` key.\n * Also forwarded to the renderer via `FieldRenderProps.variant`.\n */\n variant?: string;\n /** Visible label text forwarded to the renderer via `FieldRenderProps.label`. */\n label?: string;\n /** HTML `name` attribute forwarded to the renderer via `FieldRenderProps.name`. */\n name?: string;\n /**\n * Bag of extra renderer-specific props forwarded to the renderer via\n * `FieldRenderProps.fieldProps` (e.g. `placeholder`, `autoComplete`).\n */\n fieldProps?: Record<string, unknown>;\n};\n\n/**\n * UI-agnostic Field component.\n *\n * Resolves a renderer from:\n * 1. Explicit `renderer` prop\n * 2. FormSystemProvider registry — checked for `\"type:variant\"` first,\n * then for the base `\"type\"`\n *\n * All rendering hints (`variant`, `label`, `name`, `fieldProps`) are\n * forwarded to the resolved renderer via `FieldRenderProps`.\n *\n * @example\n * ```tsx\n * <Field forProperty={(t) => t.password} form={form}\n * variant=\"password\" label=\"Password\"\n * fieldProps={{ placeholder: \"Enter password\", autoComplete: \"current-password\" }} />\n * ```\n */\nexport function Field<TSchema extends ObjectSchemaBuilder<any, any, any>>({\n forProperty,\n form,\n renderer,\n variant,\n label,\n name,\n fieldProps\n}: FieldProps<TSchema>): React.ReactNode {\n const fieldResult = form.useField(forProperty);\n const systemConfig = useContext(FormSystemContext);\n\n const resolvedRenderer =\n renderer ?? resolveRenderer(systemConfig, fieldResult.schema, variant);\n\n if (!resolvedRenderer) {\n const schemaType = getSchemaType(fieldResult.schema);\n const tried = variant\n ? `\"${schemaType}:${variant}\" or \"${schemaType}\"`\n : `\"${schemaType}\"`;\n throw new Error(\n `No renderer found for schema type ${tried}. ` +\n 'Provide a renderer prop or configure one in FormSystemProvider.'\n );\n }\n\n return resolvedRenderer({\n value: fieldResult.value,\n initialValue: fieldResult.initialValue,\n dirty: fieldResult.dirty,\n touched: fieldResult.touched,\n error: fieldResult.error,\n validating: fieldResult.validating,\n onChange: fieldResult.onChange,\n onBlur: fieldResult.onBlur,\n setValue: fieldResult.setValue,\n schema: fieldResult.schema,\n variant,\n label,\n name,\n fieldProps\n });\n}\n","import type {\n ObjectSchemaBuilder,\n PropertyDescriptorInner,\n PropertyDescriptorTree\n} from '@cleverbrush/schema';\nimport { createContext } from 'react';\nimport type { FormStore } from './FormStore.js';\nimport type { FormSystemConfig, UseSchemaFormOptions } from './types.js';\n\n/**\n * Context for global form system configuration (renderers, etc).\n */\nexport const FormSystemContext = createContext<FormSystemConfig | null>(null);\n\n/**\n * Internal form context value — used to bridge useSchemaForm store\n * to the context-based useField and Field component.\n */\nexport type FormContextValue = {\n store: FormStore;\n descriptorTree: PropertyDescriptorTree<any, any>;\n schema: ObjectSchemaBuilder<any, any, any>;\n options: UseSchemaFormOptions;\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>;\n triggerValidation?: (markTouched: boolean) => Promise<unknown>;\n};\n\n/**\n * Context for per-form data — allows useField to work via context.\n */\nexport const FormContext = createContext<FormContextValue | null>(null);\n","import type {\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\n\n/**\n * Builds a map from PropertyDescriptorInner identity → dot-separated path\n * by traversing the descriptor tree recursively.\n */\nexport function buildDescriptorPathMap(\n tree: PropertyDescriptorTree<any, any>,\n schema: ObjectSchemaBuilder<any, any, any>\n): Map<PropertyDescriptorInner<any, any, any>, string> {\n const map = new Map<PropertyDescriptorInner<any, any, any>, string>();\n const introspected = schema.introspect();\n if (!introspected.properties) return map;\n\n for (const propName of Object.keys(introspected.properties)) {\n const propDescriptor = (tree as any)[propName];\n if (\n !propDescriptor ||\n !(SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR in propDescriptor)\n ) {\n continue;\n }\n const inner = propDescriptor[\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n ] as PropertyDescriptorInner<any, any, any>;\n map.set(inner, propName);\n\n // Recurse into nested objects\n const propSchema = introspected.properties[propName];\n if (propSchema instanceof ObjectSchemaBuilder) {\n const childMap = buildDescriptorPathMap(propDescriptor, propSchema);\n for (const [childInner, childPath] of childMap) {\n map.set(childInner, `${propName}.${childPath}`);\n }\n }\n }\n\n return map;\n}\n\n/**\n * Looks up the path for a given descriptor from the pre-built map.\n */\nexport function getDescriptorPath(\n inner: PropertyDescriptorInner<any, any, any>,\n pathMap: Map<PropertyDescriptorInner<any, any, any>, string>\n): string {\n return pathMap.get(inner) ?? '';\n}\n\n/**\n * Returns the schema type string (e.g. \"string\", \"number\", \"object\").\n */\nexport function getSchemaType(\n schema: SchemaBuilder<any, any, any, any>\n): string {\n const introspected = schema.introspect();\n return introspected?.type ?? 'unknown';\n}\n\n/**\n * Builds a selector function from a dot-separated path string.\n * The selector traverses the PropertyDescriptorTree by property access.\n * Example: \"customer.address.city\" → (tree) => tree.customer.address.city\n */\nexport function buildSelectorFromPath(path: string): (tree: any) => any {\n const parts = path.split('.');\n return (tree: any) => {\n let current = tree;\n for (const part of parts) {\n if (current == null) return undefined;\n current = current[part];\n }\n return current;\n };\n}\n\n/**\n * Checks whether a validation error path matches a field's expected error path.\n * Matches exact path, path with nested suffix, or path with validator suffix.\n * Example: isErrorPathMatch(\"$.user.name\", \"$.user.name\") → true\n * isErrorPathMatch(\"$.user.name($validators[0])\", \"$.user.name\") → true\n */\nexport function isErrorPathMatch(\n errPath: string,\n fieldErrorPath: string\n): boolean {\n return (\n errPath === fieldErrorPath ||\n errPath.startsWith(fieldErrorPath + '.') ||\n errPath.startsWith(fieldErrorPath + '(')\n );\n}\n\n/**\n * Ensures all nested object structures exist in the values object\n * by traversing the schema and creating empty objects where needed.\n * This prevents ObjectSchemaBuilder.validate() from throwing when\n * nested object properties are undefined.\n *\n * Example: ensureNestedStructure({}, OrderSchema)\n * → { customer: { address: {} } }\n */\nexport function ensureNestedStructure(\n values: any,\n schema: ObjectSchemaBuilder<any, any, any>\n): any {\n const introspected = schema.introspect();\n if (!introspected.properties) return values != null ? values : {};\n\n const result = values != null ? { ...values } : {};\n for (const key of Object.keys(introspected.properties)) {\n const propSchema = introspected.properties[key];\n if (propSchema instanceof ObjectSchemaBuilder) {\n result[key] = ensureNestedStructure(result[key], propSchema);\n }\n }\n return result;\n}\n","import type {\n InferType,\n PropertyDescriptor,\n PropertyDescriptorInner,\n PropertyDescriptorTree,\n SchemaBuilder,\n ValidationResult\n} from '@cleverbrush/schema';\nimport {\n ObjectSchemaBuilder,\n SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR\n} from '@cleverbrush/schema';\nimport {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore\n} from 'react';\nimport type { FormContextValue } from './contexts.js';\nimport type { FormStore } from './FormStore.js';\nimport { createFormStore } from './FormStore.js';\nimport {\n buildDescriptorPathMap,\n ensureNestedStructure,\n getSchemaType\n} from './helpers.js';\nimport type {\n FieldRenderer,\n FormSubmissionState,\n FormSubmitHandler,\n FormSubmitOptions,\n FormSubmitResult,\n FormSystemConfig,\n UseFieldResult,\n UseSchemaFormOptions\n} from './types.js';\n\n/** A stable form controller with reactive field and submission subscriptions. */\nexport type SchemaFormInstance<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n> = {\n useField: <TPropertySchema extends SchemaBuilder<any, any, any, any>>(\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ) => UseFieldResult<InferType<TPropertySchema>>;\n submit: () => Promise<ValidationResult<InferType<TSchema>>>;\n validate: () => Promise<ValidationResult<InferType<TSchema>>>;\n /** Clear values, or establish supplied values as the new clean baseline. */\n reset: (values?: Partial<InferType<TSchema>>) => void;\n /** Read the current snapshot. Use setters rather than mutating it. */\n getValue: () => InferType<TSchema>;\n /** Shallow-merge values without marking fields touched. */\n setValue: (values: Partial<InferType<TSchema>>) => void;\n /** Validate and submit once; repeated calls while pending are ignored. */\n handleSubmit: <TData = void>(\n onValid: (\n values: InferType<TSchema>\n ) => FormSubmitResult<TData> | Promise<FormSubmitResult<TData>>,\n options?: FormSubmitOptions<InferType<TSchema>, TData>\n ) => FormSubmitHandler;\n /** @internal — Used by FormProvider and Field to access internal context. */\n _getFormContext: () => FormContextValue;\n} & FormSubmissionState;\n\n/** Bind a schema to independently subscribed fields and a stable form instance. */\nexport function useSchemaForm<\n TSchema extends ObjectSchemaBuilder<any, any, any>\n>(\n schema: TSchema,\n options?: UseSchemaFormOptions\n): SchemaFormInstance<TSchema> {\n const storeRef = useRef<FormStore | null>(null);\n if (!storeRef.current) storeRef.current = createFormStore({});\n const store = storeRef.current;\n const descriptorTreeRef = useRef<PropertyDescriptorTree<\n TSchema,\n TSchema\n > | null>(null);\n if (!descriptorTreeRef.current) {\n descriptorTreeRef.current = ObjectSchemaBuilder.getPropertiesFor(\n schema\n ) as PropertyDescriptorTree<TSchema, TSchema>;\n }\n const descriptorTree = descriptorTreeRef.current;\n const pathMapRef = useRef<Map<\n PropertyDescriptorInner<any, any, any>,\n string\n > | null>(null);\n if (!pathMapRef.current) {\n pathMapRef.current = buildDescriptorPathMap(descriptorTree, schema);\n }\n const pathMap = pathMapRef.current;\n for (const [descriptor] of pathMap) {\n store.registerField(descriptor.toJsonPointer(), descriptor);\n }\n const schemaRef = useRef(schema);\n const optionsRef = useRef<UseSchemaFormOptions>({});\n optionsRef.current = { createMissingStructure: true, ...options };\n const mountedRef = useRef(true);\n const epochRef = useRef(0);\n const validationGenRef = useRef(0);\n const submissionLockRef = useRef(false);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const cancelScheduledValidation = useCallback(() => {\n if (timerRef.current !== null) clearTimeout(timerRef.current);\n timerRef.current = null;\n }, []);\n useEffect(() => {\n mountedRef.current = true;\n return () => {\n mountedRef.current = false;\n epochRef.current++;\n validationGenRef.current++;\n cancelScheduledValidation();\n };\n }, [cancelScheduledValidation]);\n\n const runValidation = useCallback(\n async (\n markTouched: boolean\n ): Promise<ValidationResult<InferType<TSchema>>> => {\n cancelScheduledValidation();\n const gen = ++validationGenRef.current;\n const revision = store.getRevision();\n for (const path of store.getAllFieldPaths()) {\n store.updateFieldState(path, { validating: true });\n }\n const safeValues = ensureNestedStructure(\n store.getValues(),\n schemaRef.current\n );\n let result: ValidationResult<InferType<TSchema>>;\n try {\n result = (await schemaRef.current.validateAsync(safeValues, {\n doNotStopOnFirstError: true\n })) as ValidationResult<InferType<TSchema>>;\n } catch {\n result = { valid: false } as ValidationResult<\n InferType<TSchema>\n >;\n }\n // A value update invalidates validation immediately, even while the next\n // validation is only scheduled (debounced) and has not started yet.\n if (\n !mountedRef.current ||\n gen !== validationGenRef.current ||\n revision !== store.getRevision()\n )\n return result;\n\n for (const path of store.getAllFieldPaths()) {\n store.updateFieldState(path, {\n error: undefined,\n validating: false,\n ...(markTouched ? { touched: true } : {})\n });\n }\n const getErrorsFor = (result as any).getErrorsFor;\n if (typeof getErrorsFor === 'function') {\n for (const [descriptor] of pathMap) {\n const path = descriptor.toJsonPointer();\n const parts = path\n .slice(1)\n .split('/')\n .map(part =>\n part.replace(/~1/g, '/').replace(/~0/g, '~')\n );\n try {\n const field = getErrorsFor((tree: any) =>\n parts.reduce(\n (value: any, key) => value?.[key],\n tree\n )\n );\n if (\n Array.isArray(field?.errors) &&\n field.errors.length\n ) {\n store.updateFieldState(path, {\n error: field.errors[0]\n });\n }\n } catch {\n // A missing error selector must not prevent other fields\n // from receiving their validation results.\n }\n }\n }\n return result;\n },\n [cancelScheduledValidation, store, pathMap]\n );\n\n const validate = useCallback(() => runValidation(true), [runValidation]);\n const submit = useCallback(() => validate(), [validate]);\n const reset = useCallback(\n (values?: Partial<InferType<TSchema>>) => {\n epochRef.current++;\n validationGenRef.current++;\n cancelScheduledValidation();\n store.resetAll(values);\n },\n [store, cancelScheduledValidation]\n );\n const getValue = useCallback(\n (): InferType<TSchema> => store.getValues(),\n [store]\n );\n const setValue = useCallback(\n (values: Partial<InferType<TSchema>>) => {\n cancelScheduledValidation();\n store.setValues({ ...store.getValues(), ...values });\n },\n [store, cancelScheduledValidation]\n );\n const triggerValidation = useCallback(\n (markTouched: boolean) => {\n cancelScheduledValidation();\n const delay = optionsRef.current.validationDebounceMs;\n if (delay != null && delay > 0) {\n timerRef.current = setTimeout(() => {\n timerRef.current = null;\n void runValidation(markTouched);\n }, delay);\n return Promise.resolve();\n }\n return runValidation(markTouched);\n },\n [runValidation, cancelScheduledValidation]\n );\n\n const handleSubmit = useCallback(\n <TData>(\n onValid: (\n values: InferType<TSchema>\n ) => FormSubmitResult<TData> | Promise<FormSubmitResult<TData>>,\n submissionOptions?: FormSubmitOptions<InferType<TSchema>, TData>\n ): FormSubmitHandler =>\n async event => {\n event?.preventDefault();\n if (submissionLockRef.current || !mountedRef.current) return;\n submissionLockRef.current = true;\n const epoch = epochRef.current;\n const current = () =>\n mountedRef.current && epoch === epochRef.current;\n store.setSubmissionState({\n submitting: true,\n error: undefined\n });\n try {\n const revision = store.getRevision();\n const result = await runValidation(true);\n if (\n !current() ||\n revision !== store.getRevision() ||\n !result.valid ||\n result.object === undefined\n )\n return;\n const values = result.object;\n let outcome: FormSubmitResult<TData>;\n try {\n outcome = await onValid(values);\n } catch (error) {\n if (!current()) return;\n if (!submissionOptions?.onError) throw error;\n const message = await submissionOptions.onError(error);\n if (current())\n store.setSubmissionState({ error: message });\n return;\n }\n if (!current()) return;\n if (outcome && !outcome.ok) {\n store.setSubmissionState({ error: outcome.error });\n return;\n }\n // Errors from success callbacks (including redirects) propagate;\n // they are not translated as failures of a completed submission.\n await submissionOptions?.onSuccess?.(outcome?.data, values);\n } finally {\n submissionLockRef.current = false;\n if (mountedRef.current)\n store.setSubmissionState({ submitting: false });\n }\n },\n [runValidation, store]\n );\n\n const formContextValue = useMemo<FormContextValue>(\n () => ({\n store,\n descriptorTree,\n schema: schemaRef.current,\n pathMap,\n get options() {\n return optionsRef.current;\n },\n triggerValidation\n }),\n [store, descriptorTree, pathMap, triggerValidation]\n );\n const formContextRef = useRef(formContextValue);\n formContextRef.current = formContextValue;\n const _getFormContext = useCallback(() => formContextRef.current, []);\n const useFieldHook = useCallback(\n <TPropertySchema extends SchemaBuilder<any, any, any, any>>(\n selector: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => PropertyDescriptor<TSchema, TPropertySchema, any>\n ): UseFieldResult<InferType<TPropertySchema>> =>\n useFieldFromContext(formContextRef.current, selector),\n []\n );\n const validateOnMountRef = useRef(options?.validateOnMount);\n useEffect(() => {\n if (validateOnMountRef.current) void runValidation(true);\n }, [runValidation]);\n\n // Subscribe to submission state without changing the controller's identity.\n useSyncExternalStore(\n store.subscribeGlobal,\n store.getSubmissionState,\n store.getSubmissionState\n );\n return useMemo(\n () => ({\n useField: useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValue,\n handleSubmit,\n _getFormContext,\n get submitting() {\n return store.getSubmissionState().submitting;\n },\n get error() {\n return store.getSubmissionState().error;\n }\n }),\n [\n useFieldHook,\n submit,\n validate,\n reset,\n getValue,\n setValue,\n handleSubmit,\n _getFormContext,\n store\n ]\n );\n}\n\n/** Internal field binding shared by direct and context-based hooks. */\nexport function useFieldFromContext(\n formContext: FormContextValue,\n forProperty: (tree: any) => any,\n triggerValidation = formContext.triggerValidation\n): UseFieldResult {\n const { store, descriptorTree, options } = formContext;\n const inner =\n forProperty(descriptorTree)[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR];\n const path = inner.toJsonPointer();\n store.registerField(path, inner);\n const subscribe = useCallback(\n (listener: () => void) => store.subscribe(path, listener),\n [store, path]\n );\n const getSnapshot = useCallback(\n () => store.getFieldState(path),\n [store, path]\n );\n const fieldState = useSyncExternalStore(\n subscribe,\n getSnapshot,\n getSnapshot\n );\n const onChange = useCallback(\n (value: any) => {\n store.setFieldValue(\n path,\n value,\n options.createMissingStructure !== false\n );\n void triggerValidation?.(false);\n },\n [store, path, options.createMissingStructure, triggerValidation]\n );\n const onBlur = useCallback(\n () => store.updateFieldState(path, { touched: true }),\n [store, path]\n );\n return {\n ...fieldState,\n onChange,\n onBlur,\n setValue: onChange,\n schema: inner.getSchema()\n };\n}\n\n/** Resolve type:variant first, falling back to the base type. */\nexport function resolveRenderer(\n config: FormSystemConfig | null,\n schema: SchemaBuilder<any, any, any, any>,\n variant?: string\n): FieldRenderer | undefined {\n if (!config?.renderers) return undefined;\n const type = getSchemaType(schema);\n return (\n (variant ? config.renderers[`${type}:${variant}`] : undefined) ??\n config.renderers[type]\n );\n}\n","import { deepClone, deepEqual } from '@cleverbrush/deep';\nimport type { FieldState, FormSubmissionState } from './types.js';\n\ntype Binding = {\n getValue: (values: any) => { success: boolean; value?: any };\n setValue: (values: any, value: any, options: any) => boolean;\n};\n\n/** Internal immutable snapshots shared by all bindings of a form. */\nexport function createFormStore(initialValues: any) {\n let values = deepClone(initialValues ?? {});\n let baseline = values;\n let revision = 0;\n let submission: FormSubmissionState = {\n submitting: false,\n error: undefined\n };\n const bindings = new Map<string, Binding>();\n const fieldStates = new Map<string, FieldState>();\n const listeners = new Map<string, Set<() => void>>();\n const globalListeners = new Set<() => void>();\n\n function read(binding: Binding | undefined, source: any) {\n const result = binding?.getValue(source);\n return result?.success ? result.value : undefined;\n }\n function getFieldState(path: string): FieldState {\n if (!fieldStates.has(path)) {\n const binding = bindings.get(path);\n const value = read(binding, values);\n const initialValue = read(binding, baseline);\n fieldStates.set(path, {\n value,\n initialValue,\n dirty: !deepEqual(value, initialValue),\n touched: false,\n error: undefined,\n validating: false\n });\n }\n return fieldStates.get(path)!;\n }\n function registerField(path: string, binding: Binding) {\n if (!bindings.has(path)) bindings.set(path, binding);\n return getFieldState(path);\n }\n function notifyPath(path: string) {\n for (const listener of listeners.get(path) ?? []) listener();\n }\n function notifyAll() {\n for (const path of listeners.keys()) notifyPath(path);\n for (const listener of globalListeners) listener();\n }\n function updateFieldState(path: string, patch: Partial<FieldState>) {\n const current = getFieldState(path);\n if (\n Object.entries(patch).every(([key, value]) =>\n Object.is(current[key as keyof FieldState], value)\n )\n )\n return;\n fieldStates.set(path, { ...current, ...patch });\n notifyPath(path);\n }\n function syncFields(reset: boolean) {\n for (const [path, binding] of bindings) {\n const value = read(binding, values);\n const initialValue = read(binding, baseline);\n updateFieldState(path, {\n value,\n initialValue,\n dirty: !deepEqual(value, initialValue),\n validating: false,\n ...(reset ? { touched: false, error: undefined } : {})\n });\n }\n }\n function subscribe(path: string, listener: () => void): () => void {\n let set = listeners.get(path);\n if (!set) {\n set = new Set();\n listeners.set(path, set);\n }\n set.add(listener);\n return () => {\n set.delete(listener);\n if (set.size === 0) listeners.delete(path);\n };\n }\n function subscribeGlobal(listener: () => void): () => void {\n globalListeners.add(listener);\n return () => {\n globalListeners.delete(listener);\n };\n }\n function getSubmissionState() {\n return submission;\n }\n function setSubmissionState(patch: Partial<FormSubmissionState>) {\n const next = { ...submission, ...patch };\n if (\n next.submitting === submission.submitting &&\n next.error === submission.error\n )\n return;\n submission = next;\n for (const listener of globalListeners) listener();\n }\n function getValues() {\n return values;\n }\n function getRevision() {\n return revision;\n }\n function setValues(newValues: any) {\n values = deepClone(newValues ?? {});\n revision++;\n syncFields(false);\n }\n function setFieldValue(\n path: string,\n value: any,\n createMissingStructure: boolean\n ) {\n const next = deepClone(values);\n if (\n bindings\n .get(path)\n ?.setValue(next, deepClone(value), { createMissingStructure })\n ) {\n setValues(next);\n }\n }\n function resetAll(newInitialValues?: any) {\n values = deepClone(newInitialValues ?? {});\n baseline = values;\n revision++;\n syncFields(true);\n setSubmissionState({ error: undefined });\n }\n function getAllFieldPaths() {\n return Array.from(bindings.keys());\n }\n\n return {\n registerField,\n getFieldState,\n updateFieldState,\n subscribe,\n subscribeGlobal,\n getValues,\n setValues,\n setFieldValue,\n resetAll,\n getAllFieldPaths,\n getRevision,\n getSubmissionState,\n setSubmissionState,\n notifyAll\n };\n}\n\nexport type FormStore = ReturnType<typeof createFormStore>;\n","import type {\n InferType,\n ObjectSchemaBuilder,\n PropertyDescriptorTree,\n SchemaBuilder\n} from '@cleverbrush/schema';\nimport { SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR } from '@cleverbrush/schema';\nimport type { ReactNode } from 'react';\nimport { Field, type FieldProps, FormSystemProvider } from './components.js';\nimport { getSchemaType } from './helpers.js';\nimport type { SchemaFormInstance } from './hooks.js';\nimport type { FieldRenderer } from './types.js';\n\ndeclare const rendererTypes: unique symbol;\n\n/** A renderer whose accepted values and custom props survive registration. */\nexport type TypedFieldRenderer<TValue, TProps> = FieldRenderer<\n TValue,\n TProps\n> & {\n readonly [rendererTypes]: { value: TValue; props: TProps };\n};\n\n/**\n * Attach type information without adding a UI dependency or runtime wrapper.\n * Include null in TValue when the renderer supports nullable fields.\n */\nexport function defineFieldRenderer<TValue, TProps = Record<string, never>>(\n renderer: FieldRenderer<TValue, TProps>\n): TypedFieldRenderer<TValue, TProps> {\n return renderer as TypedFieldRenderer<TValue, TProps>;\n}\n\nexport type TypedRendererRegistry = Readonly<\n Record<string, TypedFieldRenderer<any, any>>\n>;\n\ntype ValueOf<T> = T extends TypedFieldRenderer<infer V, any> ? V : never;\ntype PropsOf<T> = T extends TypedFieldRenderer<any, infer P> ? P : never;\ntype KindOf<T> = 0 extends 1 & T\n ? string\n : [NonNullable<T>] extends [string]\n ? 'string'\n : [NonNullable<T>] extends [number]\n ? 'number'\n : [NonNullable<T>] extends [boolean]\n ? 'boolean'\n : [NonNullable<T>] extends [Date]\n ? 'date'\n : [NonNullable<T>] extends [readonly unknown[]]\n ? 'array'\n : [NonNullable<T>] extends [object]\n ? 'object'\n : never;\ntype CustomProps<P> = {} extends P ? { fieldProps?: P } : { fieldProps: P };\ntype RendererChoice<R extends TypedRendererRegistry, V> = {\n [K in keyof R & string]: [Exclude<V, undefined>] extends [ValueOf<R[K]>]\n ? K extends `${KindOf<V>}:${infer Variant}`\n ? { variant: Variant } & CustomProps<PropsOf<R[K]>>\n : K extends KindOf<V>\n ? { variant?: undefined } & CustomProps<PropsOf<R[K]>>\n : never\n : never;\n}[keyof R & string];\n\ntype FieldDescriptor = {\n readonly [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {\n getSchema: () => SchemaBuilder<any, any, any, any>;\n };\n};\ntype DescriptorValue<D extends FieldDescriptor> = InferType<\n ReturnType<D[typeof SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]['getSchema']>\n>;\n\n/** Props are inferred from the selected schema property and registered variant. */\nexport type TypedFieldProps<\n R extends TypedRendererRegistry,\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TDescriptor extends FieldDescriptor\n> = {\n form: SchemaFormInstance<TSchema>;\n forProperty: (\n tree: PropertyDescriptorTree<TSchema, TSchema>\n ) => TDescriptor;\n label?: string;\n name?: string;\n} & RendererChoice<R, NoInfer<DescriptorValue<TDescriptor>>>;\n\n/** Named callable type also supports exporting system.Field from UI packages. */\nexport type TypedFieldComponent<R extends TypedRendererRegistry> = <\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TDescriptor extends FieldDescriptor\n>(\n props: TypedFieldProps<R, TSchema, TDescriptor>\n) => ReactNode;\n\n/** Named return type keeps exported consumer registries declaration-safe. */\nexport type TypedFormSystem<R extends TypedRendererRegistry> = {\n Field: TypedFieldComponent<R>;\n Provider: (props: { children: ReactNode }) => ReactNode;\n renderers: Readonly<R>;\n};\n\n/**\n * Create a typed Field and a provider for an application's renderer registry.\n * The typed Field resolves from this factory's closed registry, so an untyped\n * ancestor provider cannot silently replace a renderer with incompatible props.\n * The Provider also configures legacy Field consumers and supports nesting.\n * Extend a system with createFormSystem({ renderers: { ...system.renderers,\n * 'string:custom': customRenderer } }). Existing global APIs remain available.\n */\nexport function createFormSystem<\n const R extends TypedRendererRegistry\n>(config: { renderers: R }): TypedFormSystem<R> {\n const renderers = Object.freeze({ ...config.renderers });\n function TypedField<\n TSchema extends ObjectSchemaBuilder<any, any, any>,\n TDescriptor extends FieldDescriptor\n >(props: TypedFieldProps<R, TSchema, TDescriptor>): ReactNode {\n const descriptor = props.forProperty(\n props.form._getFormContext()\n .descriptorTree as PropertyDescriptorTree<TSchema, TSchema>\n );\n const type = getSchemaType(\n descriptor[SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR].getSchema()\n );\n const key = props.variant ? `${type}:${props.variant}` : type;\n const renderer = renderers[key];\n if (!renderer)\n throw new Error(\n `No renderer registered for \"${key}\" in this form system.`\n );\n // Field's legacy signature erases descriptor parent types. The factory\n // has already checked the selected value and renderer-specific props.\n return (\n <Field\n {...(props as unknown as FieldProps<TSchema>)}\n renderer={renderer}\n />\n );\n }\n function Provider({ children }: { children: ReactNode }): ReactNode {\n return (\n <FormSystemProvider renderers={renderers}>\n {children}\n </FormSystemProvider>\n );\n }\n return { Field: TypedField, Provider, renderers };\n}\n"],"mappings":"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","names":["useContext","useMemo","createContext","FormSystemContext","FormContext","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","buildDescriptorPathMap","tree","schema","map","introspected","propName","propDescriptor","inner","propSchema","childMap","childInner","childPath","getSchemaType","schema","ensureNestedStructure","values","schema","introspected","result","key","propSchema","ObjectSchemaBuilder","ObjectSchemaBuilder","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","useCallback","useEffect","useMemo","useRef","useSyncExternalStore","deepClone","deepEqual","createFormStore","initialValues","values","baseline","revision","submission","bindings","fieldStates","listeners","globalListeners","read","binding","source","result","getFieldState","path","value","initialValue","registerField","notifyPath","listener","notifyAll","updateFieldState","patch","current","key","syncFields","reset","subscribe","set","subscribeGlobal","getSubmissionState","setSubmissionState","next","getValues","getRevision","setValues","newValues","setFieldValue","createMissingStructure","resetAll","newInitialValues","getAllFieldPaths","useSchemaForm","schema","options","storeRef","useRef","createFormStore","store","descriptorTreeRef","ObjectSchemaBuilder","descriptorTree","pathMapRef","buildDescriptorPathMap","pathMap","descriptor","schemaRef","optionsRef","mountedRef","epochRef","validationGenRef","submissionLockRef","timerRef","cancelScheduledValidation","useCallback","useEffect","runValidation","markTouched","gen","revision","path","safeValues","ensureNestedStructure","result","getErrorsFor","parts","part","field","tree","value","key","validate","submit","reset","values","getValue","setValue","triggerValidation","delay","handleSubmit","onValid","submissionOptions","event","epoch","current","outcome","error","message","formContextValue","useMemo","formContextRef","_getFormContext","useFieldHook","selector","useFieldFromContext","validateOnMountRef","useSyncExternalStore","formContext","forProperty","inner","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","subscribe","listener","getSnapshot","fieldState","onChange","onBlur","resolveRenderer","config","variant","type","getSchemaType","jsx","FormSystemProvider","renderers","config","children","parentConfig","useContext","FormSystemContext","resolvedConfig","useMemo","current","FormProvider","form","formContext","FormContext","useField","forProperty","useFieldFromContext","useFormSystem","Field","renderer","variant","label","name","fieldProps","fieldResult","systemConfig","resolvedRenderer","resolveRenderer","schemaType","getSchemaType","tried","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","jsx","defineFieldRenderer","renderer","createFormSystem","config","renderers","TypedField","props","descriptor","type","getSchemaType","SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR","key","Field","Provider","children","FormSystemProvider"]}
|
package/dist/system.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { InferType, ObjectSchemaBuilder, PropertyDescriptorTree, SchemaBuilder } from '@cleverbrush/schema';
|
|
2
|
+
import { SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR } from '@cleverbrush/schema';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { SchemaFormInstance } from './hooks.js';
|
|
5
|
+
import type { FieldRenderer } from './types.js';
|
|
6
|
+
declare const rendererTypes: unique symbol;
|
|
7
|
+
/** A renderer whose accepted values and custom props survive registration. */
|
|
8
|
+
export type TypedFieldRenderer<TValue, TProps> = FieldRenderer<TValue, TProps> & {
|
|
9
|
+
readonly [rendererTypes]: {
|
|
10
|
+
value: TValue;
|
|
11
|
+
props: TProps;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Attach type information without adding a UI dependency or runtime wrapper.
|
|
16
|
+
* Include null in TValue when the renderer supports nullable fields.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defineFieldRenderer<TValue, TProps = Record<string, never>>(renderer: FieldRenderer<TValue, TProps>): TypedFieldRenderer<TValue, TProps>;
|
|
19
|
+
export type TypedRendererRegistry = Readonly<Record<string, TypedFieldRenderer<any, any>>>;
|
|
20
|
+
type ValueOf<T> = T extends TypedFieldRenderer<infer V, any> ? V : never;
|
|
21
|
+
type PropsOf<T> = T extends TypedFieldRenderer<any, infer P> ? P : never;
|
|
22
|
+
type KindOf<T> = 0 extends 1 & T ? string : [NonNullable<T>] extends [string] ? 'string' : [NonNullable<T>] extends [number] ? 'number' : [NonNullable<T>] extends [boolean] ? 'boolean' : [NonNullable<T>] extends [Date] ? 'date' : [NonNullable<T>] extends [readonly unknown[]] ? 'array' : [NonNullable<T>] extends [object] ? 'object' : never;
|
|
23
|
+
type CustomProps<P> = {} extends P ? {
|
|
24
|
+
fieldProps?: P;
|
|
25
|
+
} : {
|
|
26
|
+
fieldProps: P;
|
|
27
|
+
};
|
|
28
|
+
type RendererChoice<R extends TypedRendererRegistry, V> = {
|
|
29
|
+
[K in keyof R & string]: [Exclude<V, undefined>] extends [ValueOf<R[K]>] ? K extends `${KindOf<V>}:${infer Variant}` ? {
|
|
30
|
+
variant: Variant;
|
|
31
|
+
} & CustomProps<PropsOf<R[K]>> : K extends KindOf<V> ? {
|
|
32
|
+
variant?: undefined;
|
|
33
|
+
} & CustomProps<PropsOf<R[K]>> : never : never;
|
|
34
|
+
}[keyof R & string];
|
|
35
|
+
type FieldDescriptor = {
|
|
36
|
+
readonly [SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]: {
|
|
37
|
+
getSchema: () => SchemaBuilder<any, any, any, any>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
type DescriptorValue<D extends FieldDescriptor> = InferType<ReturnType<D[typeof SYMBOL_SCHEMA_PROPERTY_DESCRIPTOR]['getSchema']>>;
|
|
41
|
+
/** Props are inferred from the selected schema property and registered variant. */
|
|
42
|
+
export type TypedFieldProps<R extends TypedRendererRegistry, TSchema extends ObjectSchemaBuilder<any, any, any>, TDescriptor extends FieldDescriptor> = {
|
|
43
|
+
form: SchemaFormInstance<TSchema>;
|
|
44
|
+
forProperty: (tree: PropertyDescriptorTree<TSchema, TSchema>) => TDescriptor;
|
|
45
|
+
label?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
} & RendererChoice<R, NoInfer<DescriptorValue<TDescriptor>>>;
|
|
48
|
+
/** Named callable type also supports exporting system.Field from UI packages. */
|
|
49
|
+
export type TypedFieldComponent<R extends TypedRendererRegistry> = <TSchema extends ObjectSchemaBuilder<any, any, any>, TDescriptor extends FieldDescriptor>(props: TypedFieldProps<R, TSchema, TDescriptor>) => ReactNode;
|
|
50
|
+
/** Named return type keeps exported consumer registries declaration-safe. */
|
|
51
|
+
export type TypedFormSystem<R extends TypedRendererRegistry> = {
|
|
52
|
+
Field: TypedFieldComponent<R>;
|
|
53
|
+
Provider: (props: {
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
}) => ReactNode;
|
|
56
|
+
renderers: Readonly<R>;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Create a typed Field and a provider for an application's renderer registry.
|
|
60
|
+
* The typed Field resolves from this factory's closed registry, so an untyped
|
|
61
|
+
* ancestor provider cannot silently replace a renderer with incompatible props.
|
|
62
|
+
* The Provider also configures legacy Field consumers and supports nesting.
|
|
63
|
+
* Extend a system with createFormSystem({ renderers: { ...system.renderers,
|
|
64
|
+
* 'string:custom': customRenderer } }). Existing global APIs remain available.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createFormSystem<const R extends TypedRendererRegistry>(config: {
|
|
67
|
+
renderers: R;
|
|
68
|
+
}): TypedFormSystem<R>;
|
|
69
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import type { SchemaBuilder } from '@cleverbrush/schema';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
2
|
+
import type { FormEvent, ReactNode } from 'react';
|
|
3
3
|
/**
|
|
4
4
|
* A renderer function that receives field state and returns a React node.
|
|
5
5
|
*/
|
|
6
|
-
export type FieldRenderer = (props: FieldRenderProps) => ReactNode;
|
|
6
|
+
export type FieldRenderer<TValue = any, TProps = Record<string, unknown>> = (props: FieldRenderProps<TValue, TProps>) => ReactNode;
|
|
7
7
|
/**
|
|
8
8
|
* Props passed to a field renderer.
|
|
9
9
|
*/
|
|
10
|
-
export type FieldRenderProps = {
|
|
11
|
-
value:
|
|
12
|
-
initialValue:
|
|
10
|
+
export type FieldRenderProps<TValue = any, TProps = Record<string, unknown>> = {
|
|
11
|
+
value: TValue | undefined;
|
|
12
|
+
initialValue: TValue | undefined;
|
|
13
13
|
dirty: boolean;
|
|
14
14
|
touched: boolean;
|
|
15
15
|
error: string | undefined;
|
|
16
16
|
validating: boolean;
|
|
17
|
-
onChange: (value:
|
|
17
|
+
onChange: (value: TValue) => void;
|
|
18
18
|
onBlur: () => void;
|
|
19
|
-
setValue: (value:
|
|
20
|
-
schema: SchemaBuilder<any, any, any>;
|
|
19
|
+
setValue: (value: TValue) => void;
|
|
20
|
+
schema: SchemaBuilder<any, any, any, any>;
|
|
21
21
|
/**
|
|
22
22
|
* Rendering variant hint passed from the `Field` component.
|
|
23
23
|
* Used by renderers to select a sub-variant of the base schema type
|
|
@@ -67,7 +67,7 @@ export type FieldRenderProps = {
|
|
|
67
67
|
* />
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
|
-
fieldProps?:
|
|
70
|
+
fieldProps?: TProps;
|
|
71
71
|
};
|
|
72
72
|
/**
|
|
73
73
|
* Configuration for FormSystemProvider.
|
|
@@ -100,7 +100,7 @@ export type UseFieldResult<T = any> = {
|
|
|
100
100
|
onChange: (value: T) => void;
|
|
101
101
|
onBlur: () => void;
|
|
102
102
|
setValue: (value: T) => void;
|
|
103
|
-
schema: SchemaBuilder<any, any, any>;
|
|
103
|
+
schema: SchemaBuilder<any, any, any, any>;
|
|
104
104
|
};
|
|
105
105
|
/**
|
|
106
106
|
* Options for useSchemaForm.
|
|
@@ -130,3 +130,23 @@ export type UseSchemaFormOptions = {
|
|
|
130
130
|
*/
|
|
131
131
|
validationDebounceMs?: number;
|
|
132
132
|
};
|
|
133
|
+
/** Reactive state for handleSubmit; independent of any UI kit. */
|
|
134
|
+
export type FormSubmissionState = {
|
|
135
|
+
readonly submitting: boolean;
|
|
136
|
+
readonly error: string | undefined;
|
|
137
|
+
};
|
|
138
|
+
/** Void denotes success without data; explicit failures preserve user input. */
|
|
139
|
+
export type FormSubmitResult<TData = void> = void | {
|
|
140
|
+
ok: true;
|
|
141
|
+
data?: TData;
|
|
142
|
+
} | {
|
|
143
|
+
ok: false;
|
|
144
|
+
error: string;
|
|
145
|
+
};
|
|
146
|
+
/** Application-owned success UI and exception translation. Rethrows propagate. */
|
|
147
|
+
export type FormSubmitOptions<TValues, TData = void> = {
|
|
148
|
+
onSuccess?: (data: TData | undefined, values: TValues) => void | Promise<void>;
|
|
149
|
+
onError?: (error: unknown) => string | Promise<string>;
|
|
150
|
+
};
|
|
151
|
+
/** Awaitable handler for a form event or a programmatic invocation. */
|
|
152
|
+
export type FormSubmitHandler = (event?: Pick<FormEvent, 'preventDefault'>) => Promise<void>;
|
package/package.json
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"email": "andrew_zol@cleverbrush.com"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@cleverbrush/
|
|
8
|
+
"@cleverbrush/deep": "^4.5.0",
|
|
9
|
+
"@cleverbrush/schema": "^4.5.0"
|
|
9
10
|
},
|
|
10
11
|
"devDependencies": {
|
|
11
12
|
"@types/react": "^19.0.0",
|
|
@@ -47,5 +48,5 @@
|
|
|
47
48
|
},
|
|
48
49
|
"type": "module",
|
|
49
50
|
"types": "./dist/index.d.ts",
|
|
50
|
-
"version": "4.
|
|
51
|
+
"version": "4.5.0"
|
|
51
52
|
}
|