@molecule/app-react 1.7.1 → 1.9.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 CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-21T17:02:37.827Z
6
+ Generated: 2026-10-22T08:55:49.342Z
7
7
  -->
8
8
 
9
9
  # @molecule/app-react
@@ -15,12 +15,38 @@ core interfaces (auth, i18n, theme, routing, state, http, storage, logger,
15
15
  chat, workspace, editor, preview), so framework-agnostic providers plug
16
16
  into React idioms.
17
17
 
18
+ **Editable account/profile forms:** `useProfileForm` composes the bonded form
19
+ and auth clients. It hydrates asynchronous user data without erasing dirty
20
+ fields, snapshots controlled values on native form submission, submits only
21
+ changed fields through an app-supplied adapter, and persists/publishes the
22
+ server's saved user. Use its `onSaved` callback to update other query caches
23
+ from that response before navigating. Configure the endpoint, method and JSON
24
+ envelope once in `AuthClientConfig`; no page-local session store is needed.
25
+
18
26
  > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
19
27
  > It is written to be read by coding agents as much as by people, and is generated from this
20
28
  > package's source — edit `src/index.ts` JSDoc, not this file.
21
29
 
22
30
  ## Quick Start
23
31
 
32
+ ```tsx
33
+ const profile = useProfileForm<User, { name: string; email: string }>({
34
+ defaultValues: { name: '', email: '' },
35
+ getIdentity: (user) => user.id,
36
+ toValues: (user) => ({ name: user.name ?? '', email: user.email ?? '' }),
37
+ toPatch: (changes) => changes,
38
+ onSaved: async (user) => {
39
+ // Set/invalidate the app's profile query keys using the saved response.
40
+ // Navigate only after those writes complete.
41
+ },
42
+ })
43
+ <form {...profile.formProps}>
44
+ <input {...profile.register('name')} />
45
+ <input {...profile.register('email', { email: true })} type="email" />
46
+ <button type="submit" disabled={!profile.ready || profile.isSubmitting}>Save</button>
47
+ </form>
48
+ ```
49
+
24
50
  ```tsx
25
51
  import { MoleculeProvider, useAuth, useTheme, useTranslation } from '@molecule/app-react'
26
52
  import { provider as stateProvider } from '@molecule/app-state-zustand'
@@ -215,6 +241,9 @@ interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShape<TRegi
215
241
  confirmPasswordReset(data: PasswordResetConfirm): Promise<void>;
216
242
  /**
217
243
  * Updates the current user's profile.
244
+ * Returns the saved response. If the session is explicitly replaced or ended
245
+ * while the request is pending, that response does not replace the current
246
+ * session's user or persisted cache.
218
247
  */
219
248
  updateProfile(data: Partial<T>): Promise<T>;
220
249
  /**
@@ -281,8 +310,6 @@ interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShape<TRegi
281
310
  * HTTP client bonded to that provider sends first-paint requests unauthenticated.
282
311
  *
283
312
  * A provider whose restore sends a request THROUGH the gated HTTP client must send that
284
- * request with `skipAuthGate: true` (`RequestConfig` in `@molecule/app-http`): the gate
285
- * waits on this promise, so holding the restore's own request makes the gate wait on
286
313
  // … truncated — the full declaration is in node_modules/@molecule/app-react/dist
287
314
  ```
288
315
 
@@ -365,7 +392,13 @@ interface FormController<T extends Record<string, unknown> = Record<string, unkn
365
392
  */
366
393
  validate(): Promise<boolean>
367
394
  /**
368
- * Resets the form to initial values.
395
+ * Resets the form and clears errors, touched fields and submission state.
396
+ *
397
+ * With no argument it restores the current baseline — the initial
398
+ * `defaultValues`, or the values a previous `reset(values)` established.
399
+ * Passing `values` merges them over the current baseline and makes the
400
+ * result the new baseline, so a form reset to freshly loaded or saved data
401
+ * reads clean (`isDirty === false`).
369
402
  */
370
403
  reset(values?: Partial<T>): void
371
404
  /**
@@ -397,7 +430,8 @@ Form creation options.
397
430
  ```typescript
398
431
  interface FormOptions<T extends Record<string, unknown>> {
399
432
  /**
400
- * Default values.
433
+ * Default values, captured as the form's clean baseline when the controller
434
+ * is created. Mutating this object afterwards does not change the baseline.
401
435
  */
402
436
  defaultValues?: Partial<T>
403
437
  /**
@@ -765,12 +799,18 @@ interface UseDeviceResult {
765
799
 
766
800
  Options for useForm hook.
767
801
 
802
+ Every `FormOptions` field is supported. `createForm` is only an override —
803
+ omit it and the hook creates the controller through the bonded forms
804
+ provider (`createForm` from `@molecule/app-forms`), which uses the built-in
805
+ native provider until the app bonds its own.
806
+
768
807
  ```typescript
769
808
  interface UseFormOptions<T extends Record<string, unknown>> extends FormOptions<T> {
770
809
  /**
771
- * Form provider's createForm function.
810
+ * Explicit form factory for this one form. Defaults to the bonded forms
811
+ * provider; pass it only to pin a specific provider instance.
772
812
  */
773
- createForm: (options: FormOptions<T>) => FormController<T>
813
+ createForm?: (options: FormOptions<T>) => FormController<T>
774
814
  }
775
815
  ```
776
816
 
@@ -822,6 +862,13 @@ Result of useHttp hook.
822
862
 
823
863
  ```typescript
824
864
  interface UseHttpResult<T> extends UseHttpState<T> {
865
+ /**
866
+ * Runs the request and resolves with its data, or `null` on failure.
867
+ *
868
+ * This function is IDENTITY-STABLE for as long as `method`, `url` and the HTTP
869
+ * client do not change, so it is safe in a `useEffect` dependency array. Its
870
+ * request options are read when it is CALLED, from the latest committed render.
871
+ */
825
872
  execute: () => Promise<T | null>
826
873
  reset: () => void
827
874
  }
@@ -831,6 +878,11 @@ interface UseHttpResult<T> extends UseHttpState<T> {
831
878
 
832
879
  Options for useHttp hook.
833
880
 
881
+ Every request option — headers, params, body `data`, `signal`, `onSuccess`,
882
+ `onError` — is read at the moment `execute` runs, not when the hook rendered,
883
+ so an inline object or callback needs no memoizing. Only `immediate` is read
884
+ on mount.
885
+
834
886
  ```typescript
835
887
  interface UseHttpOptions<T> extends RequestConfig {
836
888
  /**
@@ -915,6 +967,71 @@ interface UsePlatformResult {
915
967
  }
916
968
  ```
917
969
 
970
+ #### `UseProfileFormOptions`
971
+
972
+ Configuration for a profile form; transport stays in the auth bond.
973
+
974
+ ```typescript
975
+ interface UseProfileFormOptions<TUser, TValues extends Record<string, unknown>> extends Omit<
976
+ UseFormOptions<TValues>,
977
+ 'defaultValues'
978
+ > {
979
+ /** Complete controlled-field defaults, including blank write-only fields. */
980
+ defaultValues: TValues
981
+ /** Stable account identity; changing accounts discards the previous account's edits. */
982
+ getIdentity: (user: TUser) => string
983
+ /** Maps the current/saved user into controlled values, e.g. nullable text to ''. */
984
+ toValues: (user: TUser) => TValues
985
+ /**
986
+ * Maps ONLY changed fields into an API patch. Omit blank optional credentials
987
+ * here; preserve undefined/absent fields rather than turning them into null.
988
+ * The second argument is the complete value snapshot taken at submit time.
989
+ */
990
+ toPatch: (changes: Partial<TValues>, values: Readonly<TValues>) => Partial<TUser>
991
+ /**
992
+ * Propagates the server's saved user to other caches, then navigates if needed.
993
+ * Awaited before submission finishes. The auth client already persists and
994
+ * publishes the saved user; never seed caches from the submitted patch.
995
+ */
996
+ onSaved?: (user: TUser) => void | Promise<void>
997
+ }
998
+ ```
999
+
1000
+ #### `UseProfileFormResult`
1001
+
1002
+ Controlled profile form state and native form bindings.
1003
+
1004
+ ```typescript
1005
+ interface UseProfileFormResult<TUser, TValues extends Record<string, unknown>> extends Omit<
1006
+ UseFormResult<TValues>,
1007
+ 'register' | 'handleSubmit' | 'reset'
1008
+ > {
1009
+ /** The shared auth user's current profile. */
1010
+ user: TUser | null
1011
+ /** A user has been loaded; disable submission until this is true. */
1012
+ ready: boolean
1013
+ /**
1014
+ * The latest save/propagation failure. Rejected saves retain edits; a failed
1015
+ * onSaved callback can be retried by submit without repeating the mutation.
1016
+ */
1017
+ error: unknown
1018
+ /** Native input/textarea bindings with the field's concrete value type. */
1019
+ register: <K extends keyof TValues>(
1020
+ name: K,
1021
+ options?: Parameters<UseFormResult<TValues>['register']>[1],
1022
+ ) => Omit<FieldRegistration, 'value'> & { value: TValues[K] }
1023
+ /** Bind to the native form, with a type="submit" button inside it. */
1024
+ formProps: { onSubmit: (event: React.FormEvent<HTMLFormElement>) => void }
1025
+ /**
1026
+ * Validates and saves one submit-time snapshot. Concurrent submits share the
1027
+ * same promise. Resolves undefined on validation, save or propagation failure.
1028
+ */
1029
+ submit: () => Promise<TUser | undefined>
1030
+ /** Discards edits and restores the newest loaded/saved profile baseline. */
1031
+ reset: () => void
1032
+ }
1033
+ ```
1034
+
918
1035
  #### `UsePromiseState`
919
1036
 
920
1037
  Extended promise state with actions.
@@ -1938,11 +2055,15 @@ function useEditor(): UseEditorResult
1938
2055
 
1939
2056
  Hook for form state management.
1940
2057
 
2058
+ Creates its controller through the bonded forms provider, so a React app
2059
+ wires a form library once (`setProvider` from `@molecule/app-forms`) and
2060
+ every form picks it up. Passing `createForm` overrides that for one form.
2061
+
1941
2062
  ```typescript
1942
- function useForm(options: UseFormOptions<T>): UseFormResult<T>
2063
+ function useForm(options?: UseFormOptions<T>): UseFormResult<T>
1943
2064
  ```
1944
2065
 
1945
- - `options` — Form options including createForm from a forms provider
2066
+ - `options` — Form options. `defaultValues`, `mode` and the form-level `validate` are forwarded to the provider; omit the options entirely for a form whose fields are registered with their own defaults.
1946
2067
 
1947
2068
  **Returns:** Form state and methods
1948
2069
 
@@ -1986,6 +2107,15 @@ function useHttpClient(): HttpClient
1986
2107
 
1987
2108
  Hook for making HTTP requests with state management.
1988
2109
 
2110
+ `execute` keeps one identity while `method`, `url` and the HTTP client stay the
2111
+ same — across the hook's own loading/data updates, parent rerenders, and inline
2112
+ options or callbacks — so `useEffect(() => { void execute() }, [execute])`
2113
+ runs once per request target instead of looping. The current headers, body,
2114
+ `signal` and callbacks are read when `execute` is called, so a later manual
2115
+ call always sends what the latest committed render passed. Changing `method`, `url` or
2116
+ the client does replace `execute` (it selects a different request) and
2117
+ re-runs such an effect.
2118
+
1989
2119
  ```typescript
1990
2120
  function useHttp(
1991
2121
  method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
@@ -1996,7 +2126,7 @@ function useHttp(
1996
2126
 
1997
2127
  - `method` — HTTP method
1998
2128
  - `url` — Request URL
1999
- - `options` — Request options
2129
+ - `options` — Request options, read at call time
2000
2130
 
2001
2131
  **Returns:** Request state and execute function
2002
2132
 
@@ -2201,6 +2331,27 @@ function usePreview(): UsePreviewResult
2201
2331
 
2202
2332
  **Returns:** Preview state and controls: state (url, isLoading, device, error, isConnected), setUrl, refresh, setDevice, and openExternal.
2203
2333
 
2334
+ #### `useProfileForm(options)`
2335
+
2336
+ Owns load → edit → submit → publish for the signed-in user's profile.
2337
+
2338
+ Same-account background updates refresh untouched fields but preserve dirty
2339
+ controls. Submission snapshots the registered values before asynchronous
2340
+ validation, sends only changed fields through `toPatch`, and uses the auth
2341
+ client's configured updateProfile transport. The saved server response becomes
2342
+ the clean baseline; edits made while saving remain dirty. No request is made
2343
+ for a patch with no fields. A failed request retains the edits and shared user.
2344
+
2345
+ ```typescript
2346
+ function useProfileForm(
2347
+ options: UseProfileFormOptions<TUser, TValues>,
2348
+ ): UseProfileFormResult<TUser, TValues>
2349
+ ```
2350
+
2351
+ - `options` — Controlled defaults, identity and wire adapters, optional cache/navigation callback.
2352
+
2353
+ **Returns:** Form state, typed control bindings, native submit binding and save error.
2354
+
2204
2355
  #### `usePromise(asyncFn)`
2205
2356
 
2206
2357
  Hook that wraps an async function with state tracking.
@@ -2990,6 +3141,23 @@ Peer dependencies:
2990
3141
  - `@molecule/app-version`
2991
3142
  - `react`
2992
3143
 
3144
+ - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
3145
+ and uses the bonded forms provider, just like `useForm`. `defaultValues`
3146
+ and `toValues` must cover every registered control; map nullable text to
3147
+ `''` there. Add optional write-only controls such as `password: ''` to both,
3148
+ then omit a blank password in `toPatch`: `({ password, ...changes }) =>
3149
+ ({ ...changes, ...(password?.trim() ? { password } : {}) })`. `changes`
3150
+ contains only edited fields, so do not fill omitted fields with null or
3151
+ copy every property of the auth user into the request. `reset()` discards
3152
+ edits to the newest baseline. A changed `getIdentity(user)` clears the old
3153
+ account's edits. Field comparisons are shallow, matching form providers.
3154
+ A submission whose account or auth client changes, or whose form unmounts,
3155
+ during asynchronous validation is discarded before any mutation or saved
3156
+ callback. Same-account background hydration does not discard the submit.
3157
+ The hook captures save failures in `error`; render that error with the
3158
+ app's normal API-error presentation. Use native `<form>` submit semantics,
3159
+ and pass the typed `register()` props through custom controls to their
3160
+ actual input/textarea elements.
2993
3161
  - **Every hook throws when its provider is not mounted.** `MoleculeProvider` wires ONLY the
2994
3162
  services you pass as props — it is a convenience wrapper, not a default registry. The map:
2995
3163
  `useAuth`→`auth`, `useTranslation`/`useT`→`i18n`, `useTheme`→`theme`, `useRouter`→`router`,
@@ -6,12 +6,18 @@
6
6
  import type { FieldRegistration, FieldState, FormController, FormOptions, FormState, RegisterOptions } from '@molecule/app-forms';
7
7
  /**
8
8
  * Options for useForm hook.
9
+ *
10
+ * Every `FormOptions` field is supported. `createForm` is only an override —
11
+ * omit it and the hook creates the controller through the bonded forms
12
+ * provider (`createForm` from `@molecule/app-forms`), which uses the built-in
13
+ * native provider until the app bonds its own.
9
14
  */
10
15
  export interface UseFormOptions<T extends Record<string, unknown>> extends FormOptions<T> {
11
16
  /**
12
- * Form provider's createForm function.
17
+ * Explicit form factory for this one form. Defaults to the bonded forms
18
+ * provider; pass it only to pin a specific provider instance.
13
19
  */
14
- createForm: (options: FormOptions<T>) => FormController<T>;
20
+ createForm?: (options: FormOptions<T>) => FormController<T>;
15
21
  }
16
22
  /**
17
23
  * Result of useForm hook.
@@ -36,16 +42,21 @@ export interface UseFormResult<T extends Record<string, unknown>> {
36
42
  /**
37
43
  * Hook for form state management.
38
44
  *
39
- * @param options - Form options including createForm from a forms provider
45
+ * Creates its controller through the bonded forms provider, so a React app
46
+ * wires a form library once (`setProvider` from `@molecule/app-forms`) and
47
+ * every form picks it up. Passing `createForm` overrides that for one form.
48
+ *
49
+ * @param options - Form options. `defaultValues`, `mode` and the form-level
50
+ * `validate` are forwarded to the provider; omit the options entirely for a
51
+ * form whose fields are registered with their own defaults.
40
52
  * @returns Form state and methods
41
53
  *
42
54
  * @example
43
55
  * ```tsx
44
- * import { createForm } from '@molecule/app-forms'
56
+ * import { useForm } from '@molecule/app-react'
45
57
  *
46
58
  * function LoginForm() {
47
59
  * const { register, handleSubmit, formState, isSubmitting } = useForm({
48
- * createForm,
49
60
  * defaultValues: { email: '', password: '' },
50
61
  * })
51
62
  *
@@ -69,7 +80,7 @@ export interface UseFormResult<T extends Record<string, unknown>> {
69
80
  * }
70
81
  * ```
71
82
  */
72
- export declare function useForm<T extends Record<string, unknown>>(options: UseFormOptions<T>): UseFormResult<T>;
83
+ export declare function useForm<T extends Record<string, unknown>>(options?: UseFormOptions<T>): UseFormResult<T>;
73
84
  /**
74
85
  * Hook to watch a specific field value.
75
86
  *
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,WAAW,EACX,SAAS,EACT,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAkC5B;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,CAAC;IACvF;;OAEG;IACH,UAAU,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,CAAC,CAAA;CAC3D;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE9D,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,OAAO,CAAA;IAGrB,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,CAAC,EACb,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KACxD,iBAAiB,CAAA;IACtB,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9C,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAC3D,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,MAAM,GAAG,SAAS,CAAA;IAC/C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAA;IAC5D,WAAW,EAAE,MAAM,IAAI,CAAA;IAGvB,YAAY,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAA;IAClG,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACpC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvD,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GACzB,aAAa,CAAC,CAAC,CAAC,CAwElB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,EAC3E,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,CAAC,GACN,CAAC,CAAC,CAAC,CAAC,CAGN;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7D,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,MAAM,CAAC,GACZ,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAGxB"}
1
+ {"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,WAAW,EACX,SAAS,EACT,eAAe,EAChB,MAAM,qBAAqB,CAAA;AAmC5B;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,CAAC;IACvF;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,cAAc,CAAC,CAAC,CAAC,CAAA;CAC5D;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE9D,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,OAAO,CAAA;IAGrB,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,CAAC,EACb,OAAO,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KACxD,iBAAiB,CAAA;IACtB,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9C,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IAC3D,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,MAAM,GAAG,SAAS,CAAA;IAC/C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAA;IAC5D,WAAW,EAAE,MAAM,IAAI,CAAA;IAGvB,YAAY,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,KAAK,IAAI,CAAA;IAClG,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACpC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACvD,OAAO,GAAE,cAAc,CAAC,CAAC,CAAM,GAC9B,aAAa,CAAC,CAAC,CAAC,CAwElB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,EAC3E,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,CAAC,GACN,CAAC,CAAC,CAAC,CAAC,CAGN;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7D,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,IAAI,EAAE,MAAM,CAAC,GACZ,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAGxB"}
@@ -4,6 +4,7 @@
4
4
  * @module
5
5
  */
6
6
  import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react';
7
+ import { createForm as createBondedForm } from '@molecule/app-forms';
7
8
  /**
8
9
  * Adapt controller notifications to React's stable snapshot contract. Providers
9
10
  * may return fresh objects from their getters; reads between notifications must
@@ -35,16 +36,21 @@ function useFormSnapshot(form, read) {
35
36
  /**
36
37
  * Hook for form state management.
37
38
  *
38
- * @param options - Form options including createForm from a forms provider
39
+ * Creates its controller through the bonded forms provider, so a React app
40
+ * wires a form library once (`setProvider` from `@molecule/app-forms`) and
41
+ * every form picks it up. Passing `createForm` overrides that for one form.
42
+ *
43
+ * @param options - Form options. `defaultValues`, `mode` and the form-level
44
+ * `validate` are forwarded to the provider; omit the options entirely for a
45
+ * form whose fields are registered with their own defaults.
39
46
  * @returns Form state and methods
40
47
  *
41
48
  * @example
42
49
  * ```tsx
43
- * import { createForm } from '@molecule/app-forms'
50
+ * import { useForm } from '@molecule/app-react'
44
51
  *
45
52
  * function LoginForm() {
46
53
  * const { register, handleSubmit, formState, isSubmitting } = useForm({
47
- * createForm,
48
54
  * defaultValues: { email: '', password: '' },
49
55
  * })
50
56
  *
@@ -68,8 +74,8 @@ function useFormSnapshot(form, read) {
68
74
  * }
69
75
  * ```
70
76
  */
71
- export function useForm(options) {
72
- const { createForm, ...formOptions } = options;
77
+ export function useForm(options = {}) {
78
+ const { createForm = createBondedForm, ...formOptions } = options;
73
79
  // Create form controller once
74
80
  const formRef = useRef(null);
75
81
  if (!formRef.current) {
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.js","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useForm.js","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"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"}
@@ -23,11 +23,23 @@ export interface UseHttpState<T> {
23
23
  * Result of useHttp hook.
24
24
  */
25
25
  export interface UseHttpResult<T> extends UseHttpState<T> {
26
+ /**
27
+ * Runs the request and resolves with its data, or `null` on failure.
28
+ *
29
+ * This function is IDENTITY-STABLE for as long as `method`, `url` and the HTTP
30
+ * client do not change, so it is safe in a `useEffect` dependency array. Its
31
+ * request options are read when it is CALLED, from the latest committed render.
32
+ */
26
33
  execute: () => Promise<T | null>;
27
34
  reset: () => void;
28
35
  }
29
36
  /**
30
37
  * Options for useHttp hook.
38
+ *
39
+ * Every request option — headers, params, body `data`, `signal`, `onSuccess`,
40
+ * `onError` — is read at the moment `execute` runs, not when the hook rendered,
41
+ * so an inline object or callback needs no memoizing. Only `immediate` is read
42
+ * on mount.
31
43
  */
32
44
  export interface UseHttpOptions<T> extends RequestConfig {
33
45
  /**
@@ -46,9 +58,18 @@ export interface UseHttpOptions<T> extends RequestConfig {
46
58
  /**
47
59
  * Hook for making HTTP requests with state management.
48
60
  *
61
+ * `execute` keeps one identity while `method`, `url` and the HTTP client stay the
62
+ * same — across the hook's own loading/data updates, parent rerenders, and inline
63
+ * options or callbacks — so `useEffect(() => { void execute() }, [execute])`
64
+ * runs once per request target instead of looping. The current headers, body,
65
+ * `signal` and callbacks are read when `execute` is called, so a later manual
66
+ * call always sends what the latest committed render passed. Changing `method`, `url` or
67
+ * the client does replace `execute` (it selects a different request) and
68
+ * re-runs such an effect.
69
+ *
49
70
  * @param method - HTTP method
50
71
  * @param url - Request URL
51
- * @param options - Request options
72
+ * @param options - Request options, read at call time
52
73
  * @returns Request state and execute function
53
74
  *
54
75
  * @example
@@ -56,8 +77,8 @@ export interface UseHttpOptions<T> extends RequestConfig {
56
77
  * const { data, loading, error, execute } = useHttp<User[]>('GET', '/api/users')
57
78
  *
58
79
  * useEffect(() => {
59
- * execute()
60
- * }, [])
80
+ * void execute()
81
+ * }, [execute])
61
82
  *
62
83
  * if (loading) return <Spinner />
63
84
  * if (error) return <Error message={error.message} />
@@ -1 +1 @@
1
- {"version":3,"file":"useHttp.d.ts","sourceRoot":"","sources":["../../src/hooks/useHttp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAgB,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAKjF;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAU1C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAChC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,aAAa;IACtD;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,EACnD,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAC1B,aAAa,CAAC,CAAC,CAAC,CAqFlB;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEpF;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAErF;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEpF;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvF"}
1
+ {"version":3,"file":"useHttp.d.ts","sourceRoot":"","sources":["../../src/hooks/useHttp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAgB,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAKjF;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAU1C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,GAAG,IAAI,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,CAAE,SAAQ,YAAY,CAAC,CAAC,CAAC;IACvD;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;IAChC,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,CAAE,SAAQ,aAAa;IACtD;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC7B;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,OAAO,CAAC,CAAC,EACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,EACnD,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAC1B,aAAa,CAAC,CAAC,CAAC,CAqGlB;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEpF;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAErF;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEpF;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAEvF"}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { useCallback, useContext, useEffect, useRef, useState } from 'react';
6
+ import { useCallback, useContext, useEffect, useInsertionEffect, useRef, useState } from 'react';
7
7
  import { t } from '@molecule/app-i18n';
8
8
  import { HttpContext } from '../contexts.js';
9
9
  /**
@@ -24,9 +24,18 @@ export function useHttpClient() {
24
24
  /**
25
25
  * Hook for making HTTP requests with state management.
26
26
  *
27
+ * `execute` keeps one identity while `method`, `url` and the HTTP client stay the
28
+ * same — across the hook's own loading/data updates, parent rerenders, and inline
29
+ * options or callbacks — so `useEffect(() => { void execute() }, [execute])`
30
+ * runs once per request target instead of looping. The current headers, body,
31
+ * `signal` and callbacks are read when `execute` is called, so a later manual
32
+ * call always sends what the latest committed render passed. Changing `method`, `url` or
33
+ * the client does replace `execute` (it selects a different request) and
34
+ * re-runs such an effect.
35
+ *
27
36
  * @param method - HTTP method
28
37
  * @param url - Request URL
29
- * @param options - Request options
38
+ * @param options - Request options, read at call time
30
39
  * @returns Request state and execute function
31
40
  *
32
41
  * @example
@@ -34,8 +43,8 @@ export function useHttpClient() {
34
43
  * const { data, loading, error, execute } = useHttp<User[]>('GET', '/api/users')
35
44
  *
36
45
  * useEffect(() => {
37
- * execute()
38
- * }, [])
46
+ * void execute()
47
+ * }, [execute])
39
48
  *
40
49
  * if (loading) return <Spinner />
41
50
  * if (error) return <Error message={error.message} />
@@ -44,12 +53,12 @@ export function useHttpClient() {
44
53
  */
45
54
  export function useHttp(method, url, options) {
46
55
  const client = useHttpClient();
47
- const { immediate = false, onSuccess, onError, ...requestConfig } = options ?? {};
48
- const [state, setState] = useState({
56
+ const immediate = options?.immediate ?? false;
57
+ const [state, setState] = useState(() => ({
49
58
  data: null,
50
59
  loading: immediate,
51
60
  error: null,
52
- });
61
+ }));
53
62
  // Track mounted state to avoid state updates after unmount
54
63
  const mountedRef = useRef(true);
55
64
  useEffect(() => {
@@ -58,9 +67,20 @@ export function useHttp(method, url, options) {
58
67
  mountedRef.current = false;
59
68
  };
60
69
  }, []);
70
+ // Publish options during commit, before consumers' layout effects. A render
71
+ // that suspends or is discarded must not change a mounted action's request.
72
+ // This only updates our own ref: no DOM access or state update is involved.
73
+ const optionsRef = useRef(options);
74
+ useInsertionEffect(() => {
75
+ optionsRef.current = options;
76
+ }, [options]);
61
77
  const execute = useCallback(async () => {
62
78
  if (!mountedRef.current)
63
79
  return null;
80
+ // Deliberately NOT a dependency of this callback: options describe the
81
+ // request being sent, while method/url/client decide WHICH request execute
82
+ // performs. Reading them here keeps this action stable for effect deps.
83
+ const { immediate: _immediate, onSuccess, onError, ...requestConfig } = optionsRef.current ?? {};
64
84
  setState((prev) => ({ ...prev, loading: true, error: null }));
65
85
  try {
66
86
  let response;
@@ -97,11 +117,14 @@ export function useHttp(method, url, options) {
97
117
  }
98
118
  return null;
99
119
  }
100
- }, [client, method, url, requestConfig, onSuccess, onError]);
120
+ }, [client, method, url]);
101
121
  const reset = useCallback(() => {
102
122
  setState({ data: null, loading: false, error: null });
103
123
  }, []);
104
- // Execute immediately if requested
124
+ // Execute immediately if requested — mount only, as it has always been.
125
+ // `immediate` and `execute` are deliberately not dependencies: re-running this
126
+ // would turn a state update into a second request, which is the historical
127
+ // contract of this option (and every useGet(url, { immediate: true })'s).
105
128
  useEffect(() => {
106
129
  if (immediate) {
107
130
  execute();
@@ -1 +1 @@
1
- {"version":3,"file":"useHttp.js","sourceRoot":"","sources":["../../src/hooks/useHttp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useHttp.js","sourceRoot":"","sources":["../../src/hooks/useHttp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Profile editing over the app's bonded form and auth clients.
3
+ *
4
+ * @module
5
+ */
6
+ import type { FieldRegistration } from '@molecule/app-forms';
7
+ import { type UseFormOptions, type UseFormResult } from './useForm.js';
8
+ /** Configuration for a profile form; transport stays in the auth bond. */
9
+ export interface UseProfileFormOptions<TUser, TValues extends Record<string, unknown>> extends Omit<UseFormOptions<TValues>, 'defaultValues'> {
10
+ /** Complete controlled-field defaults, including blank write-only fields. */
11
+ defaultValues: TValues;
12
+ /** Stable account identity; changing accounts discards the previous account's edits. */
13
+ getIdentity: (user: TUser) => string;
14
+ /** Maps the current/saved user into controlled values, e.g. nullable text to ''. */
15
+ toValues: (user: TUser) => TValues;
16
+ /**
17
+ * Maps ONLY changed fields into an API patch. Omit blank optional credentials
18
+ * here; preserve undefined/absent fields rather than turning them into null.
19
+ * The second argument is the complete value snapshot taken at submit time.
20
+ */
21
+ toPatch: (changes: Partial<TValues>, values: Readonly<TValues>) => Partial<TUser>;
22
+ /**
23
+ * Propagates the server's saved user to other caches, then navigates if needed.
24
+ * Awaited before submission finishes. The auth client already persists and
25
+ * publishes the saved user; never seed caches from the submitted patch.
26
+ */
27
+ onSaved?: (user: TUser) => void | Promise<void>;
28
+ }
29
+ /** Controlled profile form state and native form bindings. */
30
+ export interface UseProfileFormResult<TUser, TValues extends Record<string, unknown>> extends Omit<UseFormResult<TValues>, 'register' | 'handleSubmit' | 'reset'> {
31
+ /** The shared auth user's current profile. */
32
+ user: TUser | null;
33
+ /** A user has been loaded; disable submission until this is true. */
34
+ ready: boolean;
35
+ /**
36
+ * The latest save/propagation failure. Rejected saves retain edits; a failed
37
+ * onSaved callback can be retried by submit without repeating the mutation.
38
+ */
39
+ error: unknown;
40
+ /** Native input/textarea bindings with the field's concrete value type. */
41
+ register: <K extends keyof TValues>(name: K, options?: Parameters<UseFormResult<TValues>['register']>[1]) => Omit<FieldRegistration, 'value'> & {
42
+ value: TValues[K];
43
+ };
44
+ /** Bind to the native form, with a type="submit" button inside it. */
45
+ formProps: {
46
+ onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
47
+ };
48
+ /**
49
+ * Validates and saves one submit-time snapshot. Concurrent submits share the
50
+ * same promise. Resolves undefined on validation, save or propagation failure.
51
+ */
52
+ submit: () => Promise<TUser | undefined>;
53
+ /** Discards edits and restores the newest loaded/saved profile baseline. */
54
+ reset: () => void;
55
+ }
56
+ /**
57
+ * Owns load → edit → submit → publish for the signed-in user's profile.
58
+ *
59
+ * Same-account background updates refresh untouched fields but preserve dirty
60
+ * controls. Submission snapshots the registered values before asynchronous
61
+ * validation, sends only changed fields through `toPatch`, and uses the auth
62
+ * client's configured updateProfile transport. The saved server response becomes
63
+ * the clean baseline; edits made while saving remain dirty. No request is made
64
+ * for a patch with no fields. A failed request retains the edits and shared user.
65
+ *
66
+ * @param options - Controlled defaults, identity and wire adapters, optional cache/navigation callback.
67
+ * @returns Form state, typed control bindings, native submit binding and save error.
68
+ */
69
+ export declare function useProfileForm<TUser, TValues extends Record<string, unknown>>(options: UseProfileFormOptions<TUser, TValues>): UseProfileFormResult<TUser, TValues>;
70
+ //# sourceMappingURL=useProfileForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useProfileForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useProfileForm.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,EAAW,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAE/E,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB,CAAC,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,IAAI,CACjG,cAAc,CAAC,OAAO,CAAC,EACvB,eAAe,CAChB;IACC,6EAA6E;IAC7E,aAAa,EAAE,OAAO,CAAA;IACtB,wFAAwF;IACxF,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,CAAA;IACpC,oFAAoF;IACpF,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAA;IAClC;;;;OAIG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,CAAA;IACjF;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD;AAED,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB,CAAC,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,IAAI,CAChG,aAAa,CAAC,OAAO,CAAC,EACtB,UAAU,GAAG,cAAc,GAAG,OAAO,CACtC;IACC,8CAA8C;IAC9C,IAAI,EAAE,KAAK,GAAG,IAAI,CAAA;IAClB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAA;IACd,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EAChC,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,KACxD,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;KAAE,CAAA;IAC7D,sEAAsE;IACtE,SAAS,EAAE;QAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAA;KAAE,CAAA;IAC1E;;;OAGG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAA;IACxC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3E,OAAO,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,GAC7C,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAsLtC"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Profile editing over the app's bonded form and auth clients.
3
+ *
4
+ * @module
5
+ */
6
+ import { useCallback, useInsertionEffect, useLayoutEffect, useRef, useState } from 'react';
7
+ import { useAuth, useAuthClient } from './useAuth.js';
8
+ import { useForm } from './useForm.js';
9
+ /**
10
+ * Owns load → edit → submit → publish for the signed-in user's profile.
11
+ *
12
+ * Same-account background updates refresh untouched fields but preserve dirty
13
+ * controls. Submission snapshots the registered values before asynchronous
14
+ * validation, sends only changed fields through `toPatch`, and uses the auth
15
+ * client's configured updateProfile transport. The saved server response becomes
16
+ * the clean baseline; edits made while saving remain dirty. No request is made
17
+ * for a patch with no fields. A failed request retains the edits and shared user.
18
+ *
19
+ * @param options - Controlled defaults, identity and wire adapters, optional cache/navigation callback.
20
+ * @returns Form state, typed control bindings, native submit binding and save error.
21
+ */
22
+ export function useProfileForm(options) {
23
+ const { defaultValues, getIdentity: _identity, toValues: _values, toPatch: _patch, onSaved: _saved, ...formOptions } = options;
24
+ const form = useForm({ ...formOptions, defaultValues });
25
+ const client = useAuthClient();
26
+ const { user } = useAuth();
27
+ const [error, setError] = useState();
28
+ const [isSaving, setIsSaving] = useState(false);
29
+ const optionsRef = useRef(options);
30
+ const committedClient = useRef(client);
31
+ const baseline = useRef({ ...defaultValues });
32
+ const owner = useRef(null);
33
+ const activeSave = useRef(null);
34
+ const pendingPropagation = useRef(undefined);
35
+ const mounted = useRef(false);
36
+ // Actions use committed configuration, never an abandoned concurrent render.
37
+ useInsertionEffect(() => {
38
+ optionsRef.current = options;
39
+ committedClient.current = client;
40
+ }, [options, client]);
41
+ useLayoutEffect(() => {
42
+ mounted.current = true;
43
+ return () => {
44
+ mounted.current = false;
45
+ };
46
+ }, []);
47
+ const readValues = useCallback(() => {
48
+ const values = { ...baseline.current };
49
+ for (const key of Object.keys(values))
50
+ values[key] = form.getValue(key);
51
+ return values;
52
+ }, [form.getValue]);
53
+ const rebase = useCallback((next, preserveAgainst) => {
54
+ const current = readValues();
55
+ baseline.current = { ...next };
56
+ form.reset(next);
57
+ if (preserveAgainst) {
58
+ for (const key of Object.keys(current)) {
59
+ if (!Object.is(current[key], preserveAgainst[key]))
60
+ form.setValue(key, current[key]);
61
+ }
62
+ }
63
+ }, [readValues, form.reset, form.setValue]);
64
+ useLayoutEffect(() => {
65
+ const config = optionsRef.current;
66
+ if (!user) {
67
+ if (owner.current) {
68
+ owner.current = null;
69
+ pendingPropagation.current = undefined;
70
+ rebase({ ...config.defaultValues });
71
+ setError(undefined);
72
+ }
73
+ return;
74
+ }
75
+ const identity = config.getIdentity(user);
76
+ const previous = owner.current;
77
+ const sameAccount = !previous || (previous.client === client && previous.identity === identity);
78
+ const previousBaseline = baseline.current;
79
+ // Keep this token across same-account hydration, but replace it when the
80
+ // account/client changes. A pending validation belongs to that exact owner.
81
+ if (!previous || !sameAccount)
82
+ owner.current = { client, identity };
83
+ rebase(config.toValues(user), sameAccount ? previousBaseline : undefined);
84
+ if (!sameAccount) {
85
+ pendingPropagation.current = undefined;
86
+ setError(undefined);
87
+ }
88
+ }, [user, client, rebase]);
89
+ const submit = useCallback(() => {
90
+ if (activeSave.current)
91
+ return activeSave.current;
92
+ const currentUser = client.getUser();
93
+ if (!mounted.current || !currentUser)
94
+ return Promise.resolve(undefined);
95
+ const config = optionsRef.current;
96
+ const identity = config.getIdentity(currentUser);
97
+ const submissionOwner = owner.current;
98
+ const ownsProfile = () => {
99
+ const current = client.getUser();
100
+ return (mounted.current &&
101
+ committedClient.current === client &&
102
+ owner.current === submissionOwner &&
103
+ current !== null &&
104
+ config.getIdentity(current) === identity);
105
+ };
106
+ const snapshot = readValues();
107
+ const before = { ...baseline.current };
108
+ const changes = {};
109
+ for (const key of Object.keys(snapshot)) {
110
+ if (!Object.is(snapshot[key], before[key]))
111
+ changes[key] = snapshot[key];
112
+ }
113
+ setError(undefined);
114
+ setIsSaving(true);
115
+ let saved;
116
+ const run = async () => {
117
+ try {
118
+ await form.handleSubmit(async () => {
119
+ // Validation can await arbitrary app work. Check BEFORE toPatch and
120
+ // again immediately before the mutation: its credentials are read by
121
+ // updateProfile now, not when the reader originally clicked Save.
122
+ if (!ownsProfile())
123
+ return;
124
+ const patch = config.toPatch(changes, snapshot);
125
+ if (Object.keys(patch).length > 0) {
126
+ if (!ownsProfile())
127
+ return;
128
+ saved = await client.updateProfile(patch);
129
+ if (!ownsProfile()) {
130
+ saved = undefined;
131
+ return;
132
+ }
133
+ if (mounted.current)
134
+ rebase(config.toValues(saved), snapshot);
135
+ pendingPropagation.current = saved;
136
+ }
137
+ if (pendingPropagation.current !== undefined) {
138
+ if (!ownsProfile()) {
139
+ saved = undefined;
140
+ return;
141
+ }
142
+ const propagated = pendingPropagation.current;
143
+ await config.onSaved?.(propagated);
144
+ pendingPropagation.current = undefined;
145
+ saved = propagated;
146
+ }
147
+ })();
148
+ return saved;
149
+ }
150
+ catch (failure) {
151
+ if (mounted.current)
152
+ setError(failure);
153
+ return undefined;
154
+ }
155
+ finally {
156
+ activeSave.current = null;
157
+ if (mounted.current)
158
+ setIsSaving(false);
159
+ }
160
+ };
161
+ activeSave.current = run();
162
+ return activeSave.current;
163
+ }, [client, readValues, form.handleSubmit, rebase]);
164
+ const register = useCallback((name, registerOptions) => {
165
+ return form.register(name, registerOptions);
166
+ }, [form.register]);
167
+ const reset = useCallback(() => {
168
+ form.reset(baseline.current);
169
+ setError(undefined);
170
+ }, [form.reset]);
171
+ const onSubmit = useCallback((event) => {
172
+ event.preventDefault();
173
+ void submit();
174
+ }, [submit]);
175
+ return {
176
+ ...form,
177
+ formState: { ...form.formState, isSubmitting: isSaving || form.isSubmitting },
178
+ isSubmitting: isSaving || form.isSubmitting,
179
+ user,
180
+ ready: user !== null,
181
+ error,
182
+ register,
183
+ reset,
184
+ submit,
185
+ formProps: { onSubmit },
186
+ };
187
+ }
188
+ //# sourceMappingURL=useProfileForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useProfileForm.js","sourceRoot":"","sources":["../../src/hooks/useProfileForm.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -6,6 +6,33 @@
6
6
  * chat, workspace, editor, preview), so framework-agnostic providers plug
7
7
  * into React idioms.
8
8
  *
9
+ * **Editable account/profile forms:** `useProfileForm` composes the bonded form
10
+ * and auth clients. It hydrates asynchronous user data without erasing dirty
11
+ * fields, snapshots controlled values on native form submission, submits only
12
+ * changed fields through an app-supplied adapter, and persists/publishes the
13
+ * server's saved user. Use its `onSaved` callback to update other query caches
14
+ * from that response before navigating. Configure the endpoint, method and JSON
15
+ * envelope once in `AuthClientConfig`; no page-local session store is needed.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const profile = useProfileForm<User, { name: string; email: string }>({
20
+ * defaultValues: { name: '', email: '' },
21
+ * getIdentity: (user) => user.id,
22
+ * toValues: (user) => ({ name: user.name ?? '', email: user.email ?? '' }),
23
+ * toPatch: (changes) => changes,
24
+ * onSaved: async (user) => {
25
+ * // Set/invalidate the app's profile query keys using the saved response.
26
+ * // Navigate only after those writes complete.
27
+ * },
28
+ * })
29
+ * <form {...profile.formProps}>
30
+ * <input {...profile.register('name')} />
31
+ * <input {...profile.register('email', { email: true })} type="email" />
32
+ * <button type="submit" disabled={!profile.ready || profile.isSubmitting}>Save</button>
33
+ * </form>
34
+ * ```
35
+ *
9
36
  * @example
10
37
  * ```tsx
11
38
  * import { MoleculeProvider, useAuth, useTheme, useTranslation } from '@molecule/app-react'
@@ -48,6 +75,23 @@
48
75
  * ```
49
76
  *
50
77
  * @remarks
78
+ * - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
79
+ * and uses the bonded forms provider, just like `useForm`. `defaultValues`
80
+ * and `toValues` must cover every registered control; map nullable text to
81
+ * `''` there. Add optional write-only controls such as `password: ''` to both,
82
+ * then omit a blank password in `toPatch`: `({ password, ...changes }) =>
83
+ * ({ ...changes, ...(password?.trim() ? { password } : {}) })`. `changes`
84
+ * contains only edited fields, so do not fill omitted fields with null or
85
+ * copy every property of the auth user into the request. `reset()` discards
86
+ * edits to the newest baseline. A changed `getIdentity(user)` clears the old
87
+ * account's edits. Field comparisons are shallow, matching form providers.
88
+ * A submission whose account or auth client changes, or whose form unmounts,
89
+ * during asynchronous validation is discarded before any mutation or saved
90
+ * callback. Same-account background hydration does not discard the submit.
91
+ * The hook captures save failures in `error`; render that error with the
92
+ * app's normal API-error presentation. Use native `<form>` submit semantics,
93
+ * and pass the typed `register()` props through custom controls to their
94
+ * actual input/textarea elements.
51
95
  * - **Every hook throws when its provider is not mounted.** `MoleculeProvider` wires ONLY the
52
96
  * services you pass as props — it is a convenience wrapper, not a default registry. The map:
53
97
  * `useAuth`→`auth`, `useTranslation`/`useT`→`i18n`, `useTheme`→`theme`, `useRouter`→`router`,
@@ -104,6 +148,7 @@
104
148
  export * from './agent-identity.js';
105
149
  export * from './contexts.js';
106
150
  export * from './hooks/index.js';
151
+ export * from './hooks/useProfileForm.js';
107
152
  export * from './providers.js';
108
153
  export * from './types.js';
109
154
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkJG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -6,6 +6,33 @@
6
6
  * chat, workspace, editor, preview), so framework-agnostic providers plug
7
7
  * into React idioms.
8
8
  *
9
+ * **Editable account/profile forms:** `useProfileForm` composes the bonded form
10
+ * and auth clients. It hydrates asynchronous user data without erasing dirty
11
+ * fields, snapshots controlled values on native form submission, submits only
12
+ * changed fields through an app-supplied adapter, and persists/publishes the
13
+ * server's saved user. Use its `onSaved` callback to update other query caches
14
+ * from that response before navigating. Configure the endpoint, method and JSON
15
+ * envelope once in `AuthClientConfig`; no page-local session store is needed.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const profile = useProfileForm<User, { name: string; email: string }>({
20
+ * defaultValues: { name: '', email: '' },
21
+ * getIdentity: (user) => user.id,
22
+ * toValues: (user) => ({ name: user.name ?? '', email: user.email ?? '' }),
23
+ * toPatch: (changes) => changes,
24
+ * onSaved: async (user) => {
25
+ * // Set/invalidate the app's profile query keys using the saved response.
26
+ * // Navigate only after those writes complete.
27
+ * },
28
+ * })
29
+ * <form {...profile.formProps}>
30
+ * <input {...profile.register('name')} />
31
+ * <input {...profile.register('email', { email: true })} type="email" />
32
+ * <button type="submit" disabled={!profile.ready || profile.isSubmitting}>Save</button>
33
+ * </form>
34
+ * ```
35
+ *
9
36
  * @example
10
37
  * ```tsx
11
38
  * import { MoleculeProvider, useAuth, useTheme, useTranslation } from '@molecule/app-react'
@@ -48,6 +75,23 @@
48
75
  * ```
49
76
  *
50
77
  * @remarks
78
+ * - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
79
+ * and uses the bonded forms provider, just like `useForm`. `defaultValues`
80
+ * and `toValues` must cover every registered control; map nullable text to
81
+ * `''` there. Add optional write-only controls such as `password: ''` to both,
82
+ * then omit a blank password in `toPatch`: `({ password, ...changes }) =>
83
+ * ({ ...changes, ...(password?.trim() ? { password } : {}) })`. `changes`
84
+ * contains only edited fields, so do not fill omitted fields with null or
85
+ * copy every property of the auth user into the request. `reset()` discards
86
+ * edits to the newest baseline. A changed `getIdentity(user)` clears the old
87
+ * account's edits. Field comparisons are shallow, matching form providers.
88
+ * A submission whose account or auth client changes, or whose form unmounts,
89
+ * during asynchronous validation is discarded before any mutation or saved
90
+ * callback. Same-account background hydration does not discard the submit.
91
+ * The hook captures save failures in `error`; render that error with the
92
+ * app's normal API-error presentation. Use native `<form>` submit semantics,
93
+ * and pass the typed `register()` props through custom controls to their
94
+ * actual input/textarea elements.
51
95
  * - **Every hook throws when its provider is not mounted.** `MoleculeProvider` wires ONLY the
52
96
  * services you pass as props — it is a convenience wrapper, not a default registry. The map:
53
97
  * `useAuth`→`auth`, `useTranslation`/`useT`→`i18n`, `useTheme`→`theme`, `useRouter`→`router`,
@@ -104,6 +148,7 @@
104
148
  export * from './agent-identity.js';
105
149
  export * from './contexts.js';
106
150
  export * from './hooks/index.js';
151
+ export * from './hooks/useProfileForm.js';
107
152
  export * from './providers.js';
108
153
  export * from './types.js';
109
154
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkJG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/app-react",
3
- "version": "1.7.1",
3
+ "version": "1.9.0",
4
4
  "description": "React framework bindings for molecule.dev",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -58,7 +58,7 @@
58
58
  }
59
59
  },
60
60
  "devDependencies": {
61
- "@molecule/app-ai-models": "1.4.0",
61
+ "@molecule/app-ai-models": "1.4.1",
62
62
  "@molecule/app-bonds-default-react": "1.1.5",
63
63
  "@molecule/app-i18n": "1.0.3",
64
64
  "@testing-library/dom": "10.4.1",
@@ -68,7 +68,7 @@
68
68
  "react": "19.2.8",
69
69
  "react-dom": "19.2.8",
70
70
  "typescript": "6.0.3",
71
- "vitest": "4.1.11"
71
+ "vitest": "4.1.12"
72
72
  },
73
73
  "repository": {
74
74
  "type": "git",