@reaxon/hook-form-effector 0.1.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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/index.cjs +121 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +108 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dovran Jorayev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @reaxon/hook-form-effector
|
|
2
|
+
|
|
3
|
+
[Effector](https://effector.dev) bindings for
|
|
4
|
+
[react-hook-form](https://react-hook-form.com)'s headless `createFormControl`.
|
|
5
|
+
Form logic (fields, validation, submit) stays in react-hook-form; this package
|
|
6
|
+
lets Effector models read derived form state and drive the control with
|
|
7
|
+
effects, scope-safe.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @reaxon/hook-form-effector effector react-hook-form
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
### `formState({ form, setup, teardown? })`
|
|
16
|
+
|
|
17
|
+
Mirrors a form control into a store of `{ values, ...FormState }`. `setup`
|
|
18
|
+
starts the subscription (fire it under a scope, e.g. on dialog open);
|
|
19
|
+
`teardown` stops it. The store is seeded on attach, so a `reset` that ran in
|
|
20
|
+
the same wave as `setup` is never missed.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createEvent, fork, allSettled } from 'effector';
|
|
24
|
+
import { createFormControl } from 'react-hook-form';
|
|
25
|
+
import { formState } from '@reaxon/hook-form-effector';
|
|
26
|
+
|
|
27
|
+
const form = createFormControl<{ name: string }>({ defaultValues: { name: '' } });
|
|
28
|
+
const opened = createEvent();
|
|
29
|
+
const $form = formState({ form, setup: opened });
|
|
30
|
+
|
|
31
|
+
const scope = fork();
|
|
32
|
+
await allSettled(opened, { scope });
|
|
33
|
+
scope.getState($form).values.name; // ''
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Effect factories over the control
|
|
37
|
+
|
|
38
|
+
- `formResetApi(form, emptyValues)` — `form.reset(emptyValues)`; pins the
|
|
39
|
+
pristine values because RHF's `reset(values)` replaces `defaultValues`.
|
|
40
|
+
- `formPrefillApi(form)` — `form.reset(payload)`, e.g. an edit prefill.
|
|
41
|
+
- `formSetApi(form)` — `form.setValue(name, value, options)`.
|
|
42
|
+
- `formSetErrorApi(form)` — `form.setError(name, error, options)` for each
|
|
43
|
+
entry of a `FormFieldError[]` payload, e.g. to attach a server's field
|
|
44
|
+
errors after a failed submit.
|
|
45
|
+
|
|
46
|
+
Each accepts either a form control or a `Store` holding one.
|
|
47
|
+
|
|
48
|
+
## Effector plugin setup (SSR / `fork`)
|
|
49
|
+
|
|
50
|
+
Every export of this package is a factory: it creates stores, events and
|
|
51
|
+
effects on each call. For those units to get stable SIDs, which `fork` and
|
|
52
|
+
`serialize` need for SSR and for hydration, list the package in the
|
|
53
|
+
`factories` option of the Effector compiler plugin. Client-only apps that
|
|
54
|
+
never serialize a scope can skip this.
|
|
55
|
+
|
|
56
|
+
Babel (`effector/babel-plugin`):
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"plugins": [
|
|
61
|
+
["effector/babel-plugin", { "factories": ["@reaxon/hook-form-effector"] }]
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
SWC (`@effector/swc-plugin`), e.g. in `.swcrc` or Next.js `experimental.swcPlugins`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
["@effector/swc-plugin", { "factories": ["@reaxon/hook-form-effector"] }]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Vite with `@vitejs/plugin-react`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
react({
|
|
76
|
+
babel: {
|
|
77
|
+
plugins: [
|
|
78
|
+
["effector/babel-plugin", { factories: ["@reaxon/hook-form-effector"] }],
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Peer dependencies
|
|
85
|
+
|
|
86
|
+
- `effector` `^23`
|
|
87
|
+
- `react-hook-form` `^7.72` (`createFormControl` exists since 7.55, but the `form.*` error names and the `isSubmitted` subscription flag this package relies on typecheck from 7.72.0)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let effector = require("effector");
|
|
3
|
+
//#region src/lib.ts
|
|
4
|
+
var storify = (value, config = { serialize: "ignore" }) => effector.is.store(value) ? value : (0, effector.createStore)(value, config);
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/form-state.ts
|
|
7
|
+
var noop = () => {};
|
|
8
|
+
/**
|
|
9
|
+
* Mirror a react-hook-form `createFormControl` instance into an Effector store.
|
|
10
|
+
*
|
|
11
|
+
* The form control itself is a client-only UI controller (held in a
|
|
12
|
+
* `serialize: "ignore"` store). `setup` starts the subscription — fire it under
|
|
13
|
+
* a scope, e.g. on dialog open — and the optional `teardown` stops it. The
|
|
14
|
+
* returned store always holds the latest form state plus current `values`.
|
|
15
|
+
*
|
|
16
|
+
* Form *logic* (fields, validation, submit) stays in the UI via RHF; this store
|
|
17
|
+
* exists so Effector can read derived state such as `isValid`/`isSubmitting`
|
|
18
|
+
* (e.g. to drive a submit button's disabled state) without duplicating logic.
|
|
19
|
+
*/
|
|
20
|
+
var formState = (config) => {
|
|
21
|
+
const $form = storify(config.form, {
|
|
22
|
+
serialize: "ignore",
|
|
23
|
+
name: "form"
|
|
24
|
+
});
|
|
25
|
+
const $unsubscribe = (0, effector.createStore)(noop, { serialize: "ignore" });
|
|
26
|
+
const $formState = (0, effector.createStore)({ values: $form.defaultState.getValues() });
|
|
27
|
+
const updated = (0, effector.createEvent)();
|
|
28
|
+
const subscribeFx = (0, effector.attach)({
|
|
29
|
+
name: "formSubscribeFx",
|
|
30
|
+
source: $unsubscribe,
|
|
31
|
+
effect: (unsubscribe, form) => {
|
|
32
|
+
const onUpdate = (0, effector.scopeBind)(updated, { safe: true });
|
|
33
|
+
unsubscribe();
|
|
34
|
+
queueMicrotask(() => onUpdate({ values: form.getValues() }));
|
|
35
|
+
return form.subscribe({
|
|
36
|
+
formState: {
|
|
37
|
+
values: true,
|
|
38
|
+
errors: true,
|
|
39
|
+
isDirty: true,
|
|
40
|
+
isValid: true,
|
|
41
|
+
isValidating: true,
|
|
42
|
+
isSubmitted: true,
|
|
43
|
+
touchedFields: true,
|
|
44
|
+
dirtyFields: true
|
|
45
|
+
},
|
|
46
|
+
callback: (updates) => queueMicrotask(() => onUpdate(updates))
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const unsubscribeFx = (0, effector.attach)({
|
|
51
|
+
name: "formUnsubscribeFx",
|
|
52
|
+
source: $unsubscribe,
|
|
53
|
+
effect: (unsubscribe) => unsubscribe()
|
|
54
|
+
});
|
|
55
|
+
(0, effector.sample)({
|
|
56
|
+
clock: [config.setup, $form],
|
|
57
|
+
source: $form,
|
|
58
|
+
target: subscribeFx
|
|
59
|
+
});
|
|
60
|
+
(0, effector.sample)({
|
|
61
|
+
clock: subscribeFx.doneData,
|
|
62
|
+
target: $unsubscribe
|
|
63
|
+
});
|
|
64
|
+
(0, effector.sample)({
|
|
65
|
+
clock: updated,
|
|
66
|
+
target: $formState
|
|
67
|
+
});
|
|
68
|
+
if (config.teardown) (0, effector.sample)({
|
|
69
|
+
clock: config.teardown,
|
|
70
|
+
target: unsubscribeFx
|
|
71
|
+
});
|
|
72
|
+
return $formState;
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/form-api.ts
|
|
76
|
+
/**
|
|
77
|
+
* form.reset(emptyValues) — back to the pristine form. The empty values
|
|
78
|
+
* are pinned at creation instead of using a bare form.reset(): RHF's
|
|
79
|
+
* reset(values) REPLACES defaultValues, so after a prefill
|
|
80
|
+
* ([[formPrefillApi]]) a bare reset() would restore the prefilled values,
|
|
81
|
+
* not the pristine form.
|
|
82
|
+
*/
|
|
83
|
+
var formResetApi = (form, emptyValues) => (0, effector.attach)({
|
|
84
|
+
name: "formResetFx",
|
|
85
|
+
source: storify(form),
|
|
86
|
+
effect: (form) => form.reset(emptyValues)
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* form.reset(values) with the payload — e.g. an edit prefill. Replaces
|
|
90
|
+
* RHF defaultValues (RHF semantics), which is why formResetApi exists.
|
|
91
|
+
*/
|
|
92
|
+
var formPrefillApi = (form) => (0, effector.attach)({
|
|
93
|
+
name: "formPrefillFx",
|
|
94
|
+
source: storify(form),
|
|
95
|
+
effect: (form, values) => form.reset(values)
|
|
96
|
+
});
|
|
97
|
+
/** form.setValue(name, value, options) with the payload. */
|
|
98
|
+
var formSetApi = (form) => (0, effector.attach)({
|
|
99
|
+
name: "formSetFx",
|
|
100
|
+
source: storify(form),
|
|
101
|
+
effect: (form, { name, value, options }) => form.setValue(name, value, options)
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* `form.setError` bound to the form: apply a list of field errors, e.g. a
|
|
105
|
+
* server's 422 response mapped to `type: "server"` input errors.
|
|
106
|
+
*/
|
|
107
|
+
var formSetErrorApi = (form) => (0, effector.attach)({
|
|
108
|
+
name: "formSetErrorFx",
|
|
109
|
+
source: storify(form),
|
|
110
|
+
effect: (form, fields) => {
|
|
111
|
+
for (const fieldError of fields) form.setError(fieldError.name, fieldError.error, fieldError.options);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
//#endregion
|
|
115
|
+
exports.formPrefillApi = formPrefillApi;
|
|
116
|
+
exports.formResetApi = formResetApi;
|
|
117
|
+
exports.formSetApi = formSetApi;
|
|
118
|
+
exports.formSetErrorApi = formSetErrorApi;
|
|
119
|
+
exports.formState = formState;
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/lib.ts","../src/form-state.ts","../src/form-api.ts"],"sourcesContent":["import { createStore, is, Json, Store } from \"effector\";\n\nexport const storify = <V, SerializedState extends Json = Json>(\n value: V | Store<V>,\n config: {\n skipVoid?: boolean;\n name?: string;\n sid?: string;\n updateFilter?: (update: V, current: V) => boolean;\n serialize?:\n | \"ignore\"\n | {\n write: (state: V) => SerializedState;\n read: (json: SerializedState) => V;\n };\n } = { serialize: \"ignore\" },\n): Store<V> => (is.store(value) ? value : createStore(value, config));\n","import {\n attach,\n createEvent,\n createStore,\n sample,\n scopeBind,\n type Event,\n type Store\n} from \"effector\";\nimport {\n createFormControl,\n type FieldValues,\n type FormState,\n} from \"react-hook-form\";\nimport { storify } from \"./lib\";\n\n/** Flattens an intersection so hovers and error messages show one object type. */\nexport type Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\nconst noop = () => {};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyFormContext = any;\n\nexport type FormControl<\n TFieldValues extends FieldValues = FieldValues,\n TContext = AnyFormContext,\n TTransformedValues = TFieldValues,\n> = Prettify<\n ReturnType<\n typeof createFormControl<TFieldValues, TContext, TTransformedValues>\n >\n>;\n\nexport type FormStateWithValues<\n TFieldValues extends FieldValues = FieldValues,\n> = Prettify<{ values: TFieldValues } & Partial<FormState<TFieldValues>>>;\n\n/**\n * Mirror a react-hook-form `createFormControl` instance into an Effector store.\n *\n * The form control itself is a client-only UI controller (held in a\n * `serialize: \"ignore\"` store). `setup` starts the subscription — fire it under\n * a scope, e.g. on dialog open — and the optional `teardown` stops it. The\n * returned store always holds the latest form state plus current `values`.\n *\n * Form *logic* (fields, validation, submit) stays in the UI via RHF; this store\n * exists so Effector can read derived state such as `isValid`/`isSubmitting`\n * (e.g. to drive a submit button's disabled state) without duplicating logic.\n */\nexport const formState = <\n TFieldValues extends FieldValues = FieldValues,\n TContext = AnyFormContext,\n TTransformedValues = TFieldValues,\n>(config: {\n form:\n | FormControl<TFieldValues, TContext, TTransformedValues>\n | Store<FormControl<TFieldValues, TContext, TTransformedValues>>;\n setup: Event<unknown>;\n teardown?: Event<unknown>;\n}) => {\n const $form = storify(config.form, { serialize: \"ignore\", name: \"form\" });\n\n const $unsubscribe = createStore(noop, { serialize: \"ignore\" });\n const $formState = createStore<FormStateWithValues<TFieldValues>>({\n values: $form.defaultState.getValues() as TFieldValues,\n });\n\n const updated = createEvent<FormStateWithValues<TFieldValues>>();\n\n const subscribeFx = attach({\n name: \"formSubscribeFx\",\n source: $unsubscribe,\n effect: (\n unsubscribe,\n form: FormControl<TFieldValues, TContext, TTransformedValues>,\n ) => {\n const onUpdate = scopeBind(updated, { safe: true });\n\n unsubscribe();\n\n // Attach snapshot: a reset that ran BEFORE this subscription attached\n // notified nobody — read the current values so the mirror can never\n // start stale. Values only: the control exposes no derived flags;\n // those arrive with RHF's next real notification.\n queueMicrotask(() => onUpdate({ values: form.getValues() }));\n\n return form.subscribe({\n // Opt into the slices we mirror — `subscribe` only invokes the callback\n // for formState parts it is told to track (`values` included).\n formState: {\n values: true,\n errors: true,\n isDirty: true,\n isValid: true,\n isValidating: true,\n isSubmitted: true,\n touchedFields: true,\n dirtyFields: true,\n },\n // RHF notifies subscribers SYNCHRONOUSLY, including from <Controller>\n // field registration which runs during React render. Forwarding into\n // effector right there makes every useUnit subscriber setState during\n // another component's render (React error). One microtask defers the\n // mirror out of the render phase; ordering between updates is kept.\n callback: (updates) => queueMicrotask(() => onUpdate(updates)),\n });\n },\n });\n const unsubscribeFx = attach({\n name: \"formUnsubscribeFx\",\n source: $unsubscribe,\n effect: (unsubscribe) => unsubscribe(),\n });\n\n sample({ clock: [config.setup, $form], source: $form, target: subscribeFx });\n sample({ clock: subscribeFx.doneData, target: $unsubscribe });\n sample({ clock: updated, target: $formState });\n\n if (config.teardown) {\n sample({ clock: config.teardown, target: unsubscribeFx });\n }\n\n return $formState;\n};\n","import { attach, type Store } from \"effector\";\nimport type {\n ErrorOption,\n FieldPath,\n FieldValues,\n Path,\n PathValue,\n SetValueConfig,\n} from \"react-hook-form\";\nimport type { FormControl } from \"./form-state\";\nimport { storify } from \"./lib\";\n\n/**\n * One `formSetApi` write. The name/value pairing is deliberately as loose\n * as RHF's own runtime (the value type is the union over all paths), so\n * callsites that pick the field dynamically stay cast-free.\n */\nexport type SetValuePayload<Values extends FieldValues> = {\n name: Path<Values>;\n value: PathValue<Values, Path<Values>>;\n options?: SetValueConfig;\n};\n\n/*\n * One factory per RHF call models actually make — each returns a single\n * effect over the form control, replacing the per-model\n * `attach({ source: $form, ... })` boilerplate. Granular on purpose: a\n * model declares exactly the effects it uses, under its own names, and\n * two forms in one module never fight over destructure aliases. Extend\n * the set when a new call pattern becomes common.\n */\n\n\n/**\n * form.reset(emptyValues) — back to the pristine form. The empty values\n * are pinned at creation instead of using a bare form.reset(): RHF's\n * reset(values) REPLACES defaultValues, so after a prefill\n * ([[formPrefillApi]]) a bare reset() would restore the prefilled values,\n * not the pristine form.\n */\nexport const formResetApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n emptyValues?: Values,\n) =>\n attach({\n name: \"formResetFx\",\n source: storify(form),\n effect: (form) => form.reset(emptyValues),\n });\n\n/**\n * form.reset(values) with the payload — e.g. an edit prefill. Replaces\n * RHF defaultValues (RHF semantics), which is why formResetApi exists.\n */\nexport const formPrefillApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n) =>\n attach({\n name: \"formPrefillFx\",\n source: storify(form),\n effect: (form, values: Values) => form.reset(values),\n });\n\n/** form.setValue(name, value, options) with the payload. */\nexport const formSetApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n) =>\n attach({\n name: \"formSetFx\",\n source: storify(form),\n effect: (form, { name, value, options }: SetValuePayload<Values>) =>\n form.setValue(name, value, options),\n });\n\n\nexport type FormFieldError<Values extends FieldValues> = {\n name:\n | \"form\"\n | \"root\"\n | `root.${string}`\n | FieldPath<Values>\n | `form.${string}`;\n error: ErrorOption;\n options?: { shouldFocus: boolean };\n};\n\n\n/**\n * `form.setError` bound to the form: apply a list of field errors, e.g. a\n * server's 422 response mapped to `type: \"server\"` input errors.\n */\nexport const formSetErrorApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n) =>\n attach({\n name: \"formSetErrorFx\",\n source: storify(form),\n effect: (form, fields: FormFieldError<Values>[]) => {\n for (const fieldError of fields) {\n form.setError(fieldError.name, fieldError.error, fieldError.options);\n }\n },\n });\n\n"],"mappings":";;;AAEA,IAAa,WACX,OACA,SAWI,EAAE,WAAW,SAAS,MACZ,SAAA,GAAG,MAAM,KAAK,IAAI,SAAA,GAAQ,SAAA,YAAA,CAAY,OAAO,MAAM;;;ACKnE,IAAM,aAAa,CAAC;;;;;;;;;;;;;AA+BpB,IAAa,aAIX,WAMI;CACJ,MAAM,QAAQ,QAAQ,OAAO,MAAM;EAAE,WAAW;EAAU,MAAM;CAAO,CAAC;CAExE,MAAM,gBAAA,GAAe,SAAA,YAAA,CAAY,MAAM,EAAE,WAAW,SAAS,CAAC;CAC9D,MAAM,cAAA,GAAa,SAAA,YAAA,CAA+C,EAChE,QAAQ,MAAM,aAAa,UAAU,EACvC,CAAC;CAED,MAAM,WAAA,GAAU,SAAA,YAAA,CAA+C;CAE/D,MAAM,eAAA,GAAc,SAAA,OAAA,CAAO;EACzB,MAAM;EACN,QAAQ;EACR,SACE,aACA,SACG;GACH,MAAM,YAAA,GAAW,SAAA,UAAA,CAAU,SAAS,EAAE,MAAM,KAAK,CAAC;GAElD,YAAY;GAMZ,qBAAqB,SAAS,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC,CAAC;GAE3D,OAAO,KAAK,UAAU;IAGpB,WAAW;KACT,QAAQ;KACR,QAAQ;KACR,SAAS;KACT,SAAS;KACT,cAAc;KACd,aAAa;KACb,eAAe;KACf,aAAa;IACf;IAMA,WAAW,YAAY,qBAAqB,SAAS,OAAO,CAAC;GAC/D,CAAC;EACH;CACF,CAAC;CACD,MAAM,iBAAA,GAAgB,SAAA,OAAA,CAAO;EAC3B,MAAM;EACN,QAAQ;EACR,SAAS,gBAAgB,YAAY;CACvC,CAAC;CAED,CAAA,GAAA,SAAA,OAAA,CAAO;EAAE,OAAO,CAAC,OAAO,OAAO,KAAK;EAAG,QAAQ;EAAO,QAAQ;CAAY,CAAC;CAC3E,CAAA,GAAA,SAAA,OAAA,CAAO;EAAE,OAAO,YAAY;EAAU,QAAQ;CAAa,CAAC;CAC5D,CAAA,GAAA,SAAA,OAAA,CAAO;EAAE,OAAO;EAAS,QAAQ;CAAW,CAAC;CAE7C,IAAI,OAAO,UACT,CAAA,GAAA,SAAA,OAAA,CAAO;EAAE,OAAO,OAAO;EAAU,QAAQ;CAAc,CAAC;CAG1D,OAAO;AACT;;;;;;;;;;ACtFA,IAAa,gBACX,MACA,iBAAA,GAEA,SAAA,OAAA,CAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,SAAS,KAAK,MAAM,WAAW;AAC1C,CAAC;;;;;AAMH,IAAa,kBACX,UAAA,GAEA,SAAA,OAAA,CAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,MAAM,WAAmB,KAAK,MAAM,MAAM;AACrD,CAAC;;AAGH,IAAa,cACX,UAAA,GAEA,SAAA,OAAA,CAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,MAAM,EAAE,MAAM,OAAO,cAC5B,KAAK,SAAS,MAAM,OAAO,OAAO;AACtC,CAAC;;;;;AAmBH,IAAa,mBACX,UAAA,GAEA,SAAA,OAAA,CAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,MAAM,WAAqC;EAClD,KAAK,MAAM,cAAc,QACvB,KAAK,SAAS,WAAW,MAAM,WAAW,OAAO,WAAW,OAAO;CAEvE;AACF,CAAC"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createFormControl } from 'react-hook-form';
|
|
2
|
+
import { DeepMap } from 'react-hook-form';
|
|
3
|
+
import { DeepPartial } from 'react-hook-form';
|
|
4
|
+
import { Effect } from 'effector';
|
|
5
|
+
import { ErrorOption } from 'react-hook-form';
|
|
6
|
+
import { Event as Event_2 } from 'effector';
|
|
7
|
+
import { FieldErrors } from 'react-hook-form';
|
|
8
|
+
import { FieldPath } from 'react-hook-form';
|
|
9
|
+
import { FieldValues } from 'react-hook-form';
|
|
10
|
+
import { FormState } from 'react-hook-form';
|
|
11
|
+
import { Path } from 'react-hook-form';
|
|
12
|
+
import { PathValue } from 'react-hook-form';
|
|
13
|
+
import { SetValueConfig } from 'react-hook-form';
|
|
14
|
+
import { Store } from 'effector';
|
|
15
|
+
import { StoreWritable } from 'effector';
|
|
16
|
+
|
|
17
|
+
declare type AnyFormContext = any;
|
|
18
|
+
|
|
19
|
+
export declare type FormControl<TFieldValues extends FieldValues = FieldValues, TContext = AnyFormContext, TTransformedValues = TFieldValues> = Prettify<ReturnType<typeof createFormControl<TFieldValues, TContext, TTransformedValues>>>;
|
|
20
|
+
|
|
21
|
+
export declare type FormFieldError<Values extends FieldValues> = {
|
|
22
|
+
name: "form" | "root" | `root.${string}` | FieldPath<Values> | `form.${string}`;
|
|
23
|
+
error: ErrorOption;
|
|
24
|
+
options?: {
|
|
25
|
+
shouldFocus: boolean;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* form.reset(values) with the payload — e.g. an edit prefill. Replaces
|
|
31
|
+
* RHF defaultValues (RHF semantics), which is why formResetApi exists.
|
|
32
|
+
*/
|
|
33
|
+
export declare const formPrefillApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>) => Effect<Values, void, Error>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* form.reset(emptyValues) — back to the pristine form. The empty values
|
|
37
|
+
* are pinned at creation instead of using a bare form.reset(): RHF's
|
|
38
|
+
* reset(values) REPLACES defaultValues, so after a prefill
|
|
39
|
+
* ([[formPrefillApi]]) a bare reset() would restore the prefilled values,
|
|
40
|
+
* not the pristine form.
|
|
41
|
+
*/
|
|
42
|
+
export declare const formResetApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>, emptyValues?: Values) => Effect<void, void, Error>;
|
|
43
|
+
|
|
44
|
+
/** form.setValue(name, value, options) with the payload. */
|
|
45
|
+
export declare const formSetApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>) => Effect<SetValuePayload<Values>, void, Error>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `form.setError` bound to the form: apply a list of field errors, e.g. a
|
|
49
|
+
* server's 422 response mapped to `type: "server"` input errors.
|
|
50
|
+
*/
|
|
51
|
+
export declare const formSetErrorApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>) => Effect<FormFieldError<Values>[], void, Error>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mirror a react-hook-form `createFormControl` instance into an Effector store.
|
|
55
|
+
*
|
|
56
|
+
* The form control itself is a client-only UI controller (held in a
|
|
57
|
+
* `serialize: "ignore"` store). `setup` starts the subscription — fire it under
|
|
58
|
+
* a scope, e.g. on dialog open — and the optional `teardown` stops it. The
|
|
59
|
+
* returned store always holds the latest form state plus current `values`.
|
|
60
|
+
*
|
|
61
|
+
* Form *logic* (fields, validation, submit) stays in the UI via RHF; this store
|
|
62
|
+
* exists so Effector can read derived state such as `isValid`/`isSubmitting`
|
|
63
|
+
* (e.g. to drive a submit button's disabled state) without duplicating logic.
|
|
64
|
+
*/
|
|
65
|
+
export declare const formState: <TFieldValues extends FieldValues = FieldValues, TContext = AnyFormContext, TTransformedValues = TFieldValues>(config: {
|
|
66
|
+
form: FormControl<TFieldValues, TContext, TTransformedValues> | Store<FormControl<TFieldValues, TContext, TTransformedValues>>;
|
|
67
|
+
setup: Event_2<unknown>;
|
|
68
|
+
teardown?: Event_2<unknown>;
|
|
69
|
+
}) => StoreWritable<{
|
|
70
|
+
values: TFieldValues;
|
|
71
|
+
isDirty?: boolean | undefined;
|
|
72
|
+
isLoading?: boolean | undefined;
|
|
73
|
+
isSubmitted?: boolean | undefined;
|
|
74
|
+
isSubmitSuccessful?: boolean | undefined;
|
|
75
|
+
isSubmitting?: boolean | undefined;
|
|
76
|
+
isValidating?: boolean | undefined;
|
|
77
|
+
isValid?: boolean | undefined;
|
|
78
|
+
disabled?: boolean | undefined;
|
|
79
|
+
submitCount?: number | undefined;
|
|
80
|
+
defaultValues?: Readonly< DeepPartial<TFieldValues>> | undefined;
|
|
81
|
+
dirtyFields?: Partial<Readonly< DeepMap<DeepPartial<TFieldValues>, boolean>>> | undefined;
|
|
82
|
+
touchedFields?: Partial<Readonly< DeepMap<DeepPartial<TFieldValues>, boolean>>> | undefined;
|
|
83
|
+
validatingFields?: Partial<Readonly< DeepMap<DeepPartial<TFieldValues>, boolean>>> | undefined;
|
|
84
|
+
errors?: FieldErrors<TFieldValues> | undefined;
|
|
85
|
+
isReady?: boolean | undefined;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
export declare type FormStateWithValues<TFieldValues extends FieldValues = FieldValues> = Prettify<{
|
|
89
|
+
values: TFieldValues;
|
|
90
|
+
} & Partial<FormState<TFieldValues>>>;
|
|
91
|
+
|
|
92
|
+
/** Flattens an intersection so hovers and error messages show one object type. */
|
|
93
|
+
declare type Prettify<T> = {
|
|
94
|
+
[K in keyof T]: T[K];
|
|
95
|
+
} & {};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One `formSetApi` write. The name/value pairing is deliberately as loose
|
|
99
|
+
* as RHF's own runtime (the value type is the union over all paths), so
|
|
100
|
+
* callsites that pick the field dynamically stay cast-free.
|
|
101
|
+
*/
|
|
102
|
+
export declare type SetValuePayload<Values extends FieldValues> = {
|
|
103
|
+
name: Path<Values>;
|
|
104
|
+
value: PathValue<Values, Path<Values>>;
|
|
105
|
+
options?: SetValueConfig;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createFormControl } from 'react-hook-form';
|
|
2
|
+
import { DeepMap } from 'react-hook-form';
|
|
3
|
+
import { DeepPartial } from 'react-hook-form';
|
|
4
|
+
import { Effect } from 'effector';
|
|
5
|
+
import { ErrorOption } from 'react-hook-form';
|
|
6
|
+
import { Event as Event_2 } from 'effector';
|
|
7
|
+
import { FieldErrors } from 'react-hook-form';
|
|
8
|
+
import { FieldPath } from 'react-hook-form';
|
|
9
|
+
import { FieldValues } from 'react-hook-form';
|
|
10
|
+
import { FormState } from 'react-hook-form';
|
|
11
|
+
import { Path } from 'react-hook-form';
|
|
12
|
+
import { PathValue } from 'react-hook-form';
|
|
13
|
+
import { SetValueConfig } from 'react-hook-form';
|
|
14
|
+
import { Store } from 'effector';
|
|
15
|
+
import { StoreWritable } from 'effector';
|
|
16
|
+
|
|
17
|
+
declare type AnyFormContext = any;
|
|
18
|
+
|
|
19
|
+
export declare type FormControl<TFieldValues extends FieldValues = FieldValues, TContext = AnyFormContext, TTransformedValues = TFieldValues> = Prettify<ReturnType<typeof createFormControl<TFieldValues, TContext, TTransformedValues>>>;
|
|
20
|
+
|
|
21
|
+
export declare type FormFieldError<Values extends FieldValues> = {
|
|
22
|
+
name: "form" | "root" | `root.${string}` | FieldPath<Values> | `form.${string}`;
|
|
23
|
+
error: ErrorOption;
|
|
24
|
+
options?: {
|
|
25
|
+
shouldFocus: boolean;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* form.reset(values) with the payload — e.g. an edit prefill. Replaces
|
|
31
|
+
* RHF defaultValues (RHF semantics), which is why formResetApi exists.
|
|
32
|
+
*/
|
|
33
|
+
export declare const formPrefillApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>) => Effect<Values, void, Error>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* form.reset(emptyValues) — back to the pristine form. The empty values
|
|
37
|
+
* are pinned at creation instead of using a bare form.reset(): RHF's
|
|
38
|
+
* reset(values) REPLACES defaultValues, so after a prefill
|
|
39
|
+
* ([[formPrefillApi]]) a bare reset() would restore the prefilled values,
|
|
40
|
+
* not the pristine form.
|
|
41
|
+
*/
|
|
42
|
+
export declare const formResetApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>, emptyValues?: Values) => Effect<void, void, Error>;
|
|
43
|
+
|
|
44
|
+
/** form.setValue(name, value, options) with the payload. */
|
|
45
|
+
export declare const formSetApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>) => Effect<SetValuePayload<Values>, void, Error>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `form.setError` bound to the form: apply a list of field errors, e.g. a
|
|
49
|
+
* server's 422 response mapped to `type: "server"` input errors.
|
|
50
|
+
*/
|
|
51
|
+
export declare const formSetErrorApi: <Values extends FieldValues>(form: FormControl<Values> | Store<FormControl<Values>>) => Effect<FormFieldError<Values>[], void, Error>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mirror a react-hook-form `createFormControl` instance into an Effector store.
|
|
55
|
+
*
|
|
56
|
+
* The form control itself is a client-only UI controller (held in a
|
|
57
|
+
* `serialize: "ignore"` store). `setup` starts the subscription — fire it under
|
|
58
|
+
* a scope, e.g. on dialog open — and the optional `teardown` stops it. The
|
|
59
|
+
* returned store always holds the latest form state plus current `values`.
|
|
60
|
+
*
|
|
61
|
+
* Form *logic* (fields, validation, submit) stays in the UI via RHF; this store
|
|
62
|
+
* exists so Effector can read derived state such as `isValid`/`isSubmitting`
|
|
63
|
+
* (e.g. to drive a submit button's disabled state) without duplicating logic.
|
|
64
|
+
*/
|
|
65
|
+
export declare const formState: <TFieldValues extends FieldValues = FieldValues, TContext = AnyFormContext, TTransformedValues = TFieldValues>(config: {
|
|
66
|
+
form: FormControl<TFieldValues, TContext, TTransformedValues> | Store<FormControl<TFieldValues, TContext, TTransformedValues>>;
|
|
67
|
+
setup: Event_2<unknown>;
|
|
68
|
+
teardown?: Event_2<unknown>;
|
|
69
|
+
}) => StoreWritable<{
|
|
70
|
+
values: TFieldValues;
|
|
71
|
+
isDirty?: boolean | undefined;
|
|
72
|
+
isLoading?: boolean | undefined;
|
|
73
|
+
isSubmitted?: boolean | undefined;
|
|
74
|
+
isSubmitSuccessful?: boolean | undefined;
|
|
75
|
+
isSubmitting?: boolean | undefined;
|
|
76
|
+
isValidating?: boolean | undefined;
|
|
77
|
+
isValid?: boolean | undefined;
|
|
78
|
+
disabled?: boolean | undefined;
|
|
79
|
+
submitCount?: number | undefined;
|
|
80
|
+
defaultValues?: Readonly< DeepPartial<TFieldValues>> | undefined;
|
|
81
|
+
dirtyFields?: Partial<Readonly< DeepMap<DeepPartial<TFieldValues>, boolean>>> | undefined;
|
|
82
|
+
touchedFields?: Partial<Readonly< DeepMap<DeepPartial<TFieldValues>, boolean>>> | undefined;
|
|
83
|
+
validatingFields?: Partial<Readonly< DeepMap<DeepPartial<TFieldValues>, boolean>>> | undefined;
|
|
84
|
+
errors?: FieldErrors<TFieldValues> | undefined;
|
|
85
|
+
isReady?: boolean | undefined;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
export declare type FormStateWithValues<TFieldValues extends FieldValues = FieldValues> = Prettify<{
|
|
89
|
+
values: TFieldValues;
|
|
90
|
+
} & Partial<FormState<TFieldValues>>>;
|
|
91
|
+
|
|
92
|
+
/** Flattens an intersection so hovers and error messages show one object type. */
|
|
93
|
+
declare type Prettify<T> = {
|
|
94
|
+
[K in keyof T]: T[K];
|
|
95
|
+
} & {};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One `formSetApi` write. The name/value pairing is deliberately as loose
|
|
99
|
+
* as RHF's own runtime (the value type is the union over all paths), so
|
|
100
|
+
* callsites that pick the field dynamically stay cast-free.
|
|
101
|
+
*/
|
|
102
|
+
export declare type SetValuePayload<Values extends FieldValues> = {
|
|
103
|
+
name: Path<Values>;
|
|
104
|
+
value: PathValue<Values, Path<Values>>;
|
|
105
|
+
options?: SetValueConfig;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { attach, createEvent, createStore, is, sample, scopeBind } from "effector";
|
|
2
|
+
//#region src/lib.ts
|
|
3
|
+
var storify = (value, config = { serialize: "ignore" }) => is.store(value) ? value : createStore(value, config);
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/form-state.ts
|
|
6
|
+
var noop = () => {};
|
|
7
|
+
/**
|
|
8
|
+
* Mirror a react-hook-form `createFormControl` instance into an Effector store.
|
|
9
|
+
*
|
|
10
|
+
* The form control itself is a client-only UI controller (held in a
|
|
11
|
+
* `serialize: "ignore"` store). `setup` starts the subscription — fire it under
|
|
12
|
+
* a scope, e.g. on dialog open — and the optional `teardown` stops it. The
|
|
13
|
+
* returned store always holds the latest form state plus current `values`.
|
|
14
|
+
*
|
|
15
|
+
* Form *logic* (fields, validation, submit) stays in the UI via RHF; this store
|
|
16
|
+
* exists so Effector can read derived state such as `isValid`/`isSubmitting`
|
|
17
|
+
* (e.g. to drive a submit button's disabled state) without duplicating logic.
|
|
18
|
+
*/
|
|
19
|
+
var formState = (config) => {
|
|
20
|
+
const $form = storify(config.form, {
|
|
21
|
+
serialize: "ignore",
|
|
22
|
+
name: "form"
|
|
23
|
+
});
|
|
24
|
+
const $unsubscribe = createStore(noop, { serialize: "ignore" });
|
|
25
|
+
const $formState = createStore({ values: $form.defaultState.getValues() });
|
|
26
|
+
const updated = createEvent();
|
|
27
|
+
const subscribeFx = attach({
|
|
28
|
+
name: "formSubscribeFx",
|
|
29
|
+
source: $unsubscribe,
|
|
30
|
+
effect: (unsubscribe, form) => {
|
|
31
|
+
const onUpdate = scopeBind(updated, { safe: true });
|
|
32
|
+
unsubscribe();
|
|
33
|
+
queueMicrotask(() => onUpdate({ values: form.getValues() }));
|
|
34
|
+
return form.subscribe({
|
|
35
|
+
formState: {
|
|
36
|
+
values: true,
|
|
37
|
+
errors: true,
|
|
38
|
+
isDirty: true,
|
|
39
|
+
isValid: true,
|
|
40
|
+
isValidating: true,
|
|
41
|
+
isSubmitted: true,
|
|
42
|
+
touchedFields: true,
|
|
43
|
+
dirtyFields: true
|
|
44
|
+
},
|
|
45
|
+
callback: (updates) => queueMicrotask(() => onUpdate(updates))
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const unsubscribeFx = attach({
|
|
50
|
+
name: "formUnsubscribeFx",
|
|
51
|
+
source: $unsubscribe,
|
|
52
|
+
effect: (unsubscribe) => unsubscribe()
|
|
53
|
+
});
|
|
54
|
+
sample({
|
|
55
|
+
clock: [config.setup, $form],
|
|
56
|
+
source: $form,
|
|
57
|
+
target: subscribeFx
|
|
58
|
+
});
|
|
59
|
+
sample({
|
|
60
|
+
clock: subscribeFx.doneData,
|
|
61
|
+
target: $unsubscribe
|
|
62
|
+
});
|
|
63
|
+
sample({
|
|
64
|
+
clock: updated,
|
|
65
|
+
target: $formState
|
|
66
|
+
});
|
|
67
|
+
if (config.teardown) sample({
|
|
68
|
+
clock: config.teardown,
|
|
69
|
+
target: unsubscribeFx
|
|
70
|
+
});
|
|
71
|
+
return $formState;
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/form-api.ts
|
|
75
|
+
/**
|
|
76
|
+
* form.reset(emptyValues) — back to the pristine form. The empty values
|
|
77
|
+
* are pinned at creation instead of using a bare form.reset(): RHF's
|
|
78
|
+
* reset(values) REPLACES defaultValues, so after a prefill
|
|
79
|
+
* ([[formPrefillApi]]) a bare reset() would restore the prefilled values,
|
|
80
|
+
* not the pristine form.
|
|
81
|
+
*/
|
|
82
|
+
var formResetApi = (form, emptyValues) => attach({
|
|
83
|
+
name: "formResetFx",
|
|
84
|
+
source: storify(form),
|
|
85
|
+
effect: (form) => form.reset(emptyValues)
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* form.reset(values) with the payload — e.g. an edit prefill. Replaces
|
|
89
|
+
* RHF defaultValues (RHF semantics), which is why formResetApi exists.
|
|
90
|
+
*/
|
|
91
|
+
var formPrefillApi = (form) => attach({
|
|
92
|
+
name: "formPrefillFx",
|
|
93
|
+
source: storify(form),
|
|
94
|
+
effect: (form, values) => form.reset(values)
|
|
95
|
+
});
|
|
96
|
+
/** form.setValue(name, value, options) with the payload. */
|
|
97
|
+
var formSetApi = (form) => attach({
|
|
98
|
+
name: "formSetFx",
|
|
99
|
+
source: storify(form),
|
|
100
|
+
effect: (form, { name, value, options }) => form.setValue(name, value, options)
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* `form.setError` bound to the form: apply a list of field errors, e.g. a
|
|
104
|
+
* server's 422 response mapped to `type: "server"` input errors.
|
|
105
|
+
*/
|
|
106
|
+
var formSetErrorApi = (form) => attach({
|
|
107
|
+
name: "formSetErrorFx",
|
|
108
|
+
source: storify(form),
|
|
109
|
+
effect: (form, fields) => {
|
|
110
|
+
for (const fieldError of fields) form.setError(fieldError.name, fieldError.error, fieldError.options);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
//#endregion
|
|
114
|
+
export { formPrefillApi, formResetApi, formSetApi, formSetErrorApi, formState };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/lib.ts","../src/form-state.ts","../src/form-api.ts"],"sourcesContent":["import { createStore, is, Json, Store } from \"effector\";\n\nexport const storify = <V, SerializedState extends Json = Json>(\n value: V | Store<V>,\n config: {\n skipVoid?: boolean;\n name?: string;\n sid?: string;\n updateFilter?: (update: V, current: V) => boolean;\n serialize?:\n | \"ignore\"\n | {\n write: (state: V) => SerializedState;\n read: (json: SerializedState) => V;\n };\n } = { serialize: \"ignore\" },\n): Store<V> => (is.store(value) ? value : createStore(value, config));\n","import {\n attach,\n createEvent,\n createStore,\n sample,\n scopeBind,\n type Event,\n type Store\n} from \"effector\";\nimport {\n createFormControl,\n type FieldValues,\n type FormState,\n} from \"react-hook-form\";\nimport { storify } from \"./lib\";\n\n/** Flattens an intersection so hovers and error messages show one object type. */\nexport type Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\nconst noop = () => {};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyFormContext = any;\n\nexport type FormControl<\n TFieldValues extends FieldValues = FieldValues,\n TContext = AnyFormContext,\n TTransformedValues = TFieldValues,\n> = Prettify<\n ReturnType<\n typeof createFormControl<TFieldValues, TContext, TTransformedValues>\n >\n>;\n\nexport type FormStateWithValues<\n TFieldValues extends FieldValues = FieldValues,\n> = Prettify<{ values: TFieldValues } & Partial<FormState<TFieldValues>>>;\n\n/**\n * Mirror a react-hook-form `createFormControl` instance into an Effector store.\n *\n * The form control itself is a client-only UI controller (held in a\n * `serialize: \"ignore\"` store). `setup` starts the subscription — fire it under\n * a scope, e.g. on dialog open — and the optional `teardown` stops it. The\n * returned store always holds the latest form state plus current `values`.\n *\n * Form *logic* (fields, validation, submit) stays in the UI via RHF; this store\n * exists so Effector can read derived state such as `isValid`/`isSubmitting`\n * (e.g. to drive a submit button's disabled state) without duplicating logic.\n */\nexport const formState = <\n TFieldValues extends FieldValues = FieldValues,\n TContext = AnyFormContext,\n TTransformedValues = TFieldValues,\n>(config: {\n form:\n | FormControl<TFieldValues, TContext, TTransformedValues>\n | Store<FormControl<TFieldValues, TContext, TTransformedValues>>;\n setup: Event<unknown>;\n teardown?: Event<unknown>;\n}) => {\n const $form = storify(config.form, { serialize: \"ignore\", name: \"form\" });\n\n const $unsubscribe = createStore(noop, { serialize: \"ignore\" });\n const $formState = createStore<FormStateWithValues<TFieldValues>>({\n values: $form.defaultState.getValues() as TFieldValues,\n });\n\n const updated = createEvent<FormStateWithValues<TFieldValues>>();\n\n const subscribeFx = attach({\n name: \"formSubscribeFx\",\n source: $unsubscribe,\n effect: (\n unsubscribe,\n form: FormControl<TFieldValues, TContext, TTransformedValues>,\n ) => {\n const onUpdate = scopeBind(updated, { safe: true });\n\n unsubscribe();\n\n // Attach snapshot: a reset that ran BEFORE this subscription attached\n // notified nobody — read the current values so the mirror can never\n // start stale. Values only: the control exposes no derived flags;\n // those arrive with RHF's next real notification.\n queueMicrotask(() => onUpdate({ values: form.getValues() }));\n\n return form.subscribe({\n // Opt into the slices we mirror — `subscribe` only invokes the callback\n // for formState parts it is told to track (`values` included).\n formState: {\n values: true,\n errors: true,\n isDirty: true,\n isValid: true,\n isValidating: true,\n isSubmitted: true,\n touchedFields: true,\n dirtyFields: true,\n },\n // RHF notifies subscribers SYNCHRONOUSLY, including from <Controller>\n // field registration which runs during React render. Forwarding into\n // effector right there makes every useUnit subscriber setState during\n // another component's render (React error). One microtask defers the\n // mirror out of the render phase; ordering between updates is kept.\n callback: (updates) => queueMicrotask(() => onUpdate(updates)),\n });\n },\n });\n const unsubscribeFx = attach({\n name: \"formUnsubscribeFx\",\n source: $unsubscribe,\n effect: (unsubscribe) => unsubscribe(),\n });\n\n sample({ clock: [config.setup, $form], source: $form, target: subscribeFx });\n sample({ clock: subscribeFx.doneData, target: $unsubscribe });\n sample({ clock: updated, target: $formState });\n\n if (config.teardown) {\n sample({ clock: config.teardown, target: unsubscribeFx });\n }\n\n return $formState;\n};\n","import { attach, type Store } from \"effector\";\nimport type {\n ErrorOption,\n FieldPath,\n FieldValues,\n Path,\n PathValue,\n SetValueConfig,\n} from \"react-hook-form\";\nimport type { FormControl } from \"./form-state\";\nimport { storify } from \"./lib\";\n\n/**\n * One `formSetApi` write. The name/value pairing is deliberately as loose\n * as RHF's own runtime (the value type is the union over all paths), so\n * callsites that pick the field dynamically stay cast-free.\n */\nexport type SetValuePayload<Values extends FieldValues> = {\n name: Path<Values>;\n value: PathValue<Values, Path<Values>>;\n options?: SetValueConfig;\n};\n\n/*\n * One factory per RHF call models actually make — each returns a single\n * effect over the form control, replacing the per-model\n * `attach({ source: $form, ... })` boilerplate. Granular on purpose: a\n * model declares exactly the effects it uses, under its own names, and\n * two forms in one module never fight over destructure aliases. Extend\n * the set when a new call pattern becomes common.\n */\n\n\n/**\n * form.reset(emptyValues) — back to the pristine form. The empty values\n * are pinned at creation instead of using a bare form.reset(): RHF's\n * reset(values) REPLACES defaultValues, so after a prefill\n * ([[formPrefillApi]]) a bare reset() would restore the prefilled values,\n * not the pristine form.\n */\nexport const formResetApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n emptyValues?: Values,\n) =>\n attach({\n name: \"formResetFx\",\n source: storify(form),\n effect: (form) => form.reset(emptyValues),\n });\n\n/**\n * form.reset(values) with the payload — e.g. an edit prefill. Replaces\n * RHF defaultValues (RHF semantics), which is why formResetApi exists.\n */\nexport const formPrefillApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n) =>\n attach({\n name: \"formPrefillFx\",\n source: storify(form),\n effect: (form, values: Values) => form.reset(values),\n });\n\n/** form.setValue(name, value, options) with the payload. */\nexport const formSetApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n) =>\n attach({\n name: \"formSetFx\",\n source: storify(form),\n effect: (form, { name, value, options }: SetValuePayload<Values>) =>\n form.setValue(name, value, options),\n });\n\n\nexport type FormFieldError<Values extends FieldValues> = {\n name:\n | \"form\"\n | \"root\"\n | `root.${string}`\n | FieldPath<Values>\n | `form.${string}`;\n error: ErrorOption;\n options?: { shouldFocus: boolean };\n};\n\n\n/**\n * `form.setError` bound to the form: apply a list of field errors, e.g. a\n * server's 422 response mapped to `type: \"server\"` input errors.\n */\nexport const formSetErrorApi = <Values extends FieldValues>(\n form: FormControl<Values> | Store<FormControl<Values>>,\n) =>\n attach({\n name: \"formSetErrorFx\",\n source: storify(form),\n effect: (form, fields: FormFieldError<Values>[]) => {\n for (const fieldError of fields) {\n form.setError(fieldError.name, fieldError.error, fieldError.options);\n }\n },\n });\n\n"],"mappings":";;AAEA,IAAa,WACX,OACA,SAWI,EAAE,WAAW,SAAS,MACZ,GAAG,MAAM,KAAK,IAAI,QAAQ,YAAY,OAAO,MAAM;;;ACKnE,IAAM,aAAa,CAAC;;;;;;;;;;;;;AA+BpB,IAAa,aAIX,WAMI;CACJ,MAAM,QAAQ,QAAQ,OAAO,MAAM;EAAE,WAAW;EAAU,MAAM;CAAO,CAAC;CAExE,MAAM,eAAe,YAAY,MAAM,EAAE,WAAW,SAAS,CAAC;CAC9D,MAAM,aAAa,YAA+C,EAChE,QAAQ,MAAM,aAAa,UAAU,EACvC,CAAC;CAED,MAAM,UAAU,YAA+C;CAE/D,MAAM,cAAc,OAAO;EACzB,MAAM;EACN,QAAQ;EACR,SACE,aACA,SACG;GACH,MAAM,WAAW,UAAU,SAAS,EAAE,MAAM,KAAK,CAAC;GAElD,YAAY;GAMZ,qBAAqB,SAAS,EAAE,QAAQ,KAAK,UAAU,EAAE,CAAC,CAAC;GAE3D,OAAO,KAAK,UAAU;IAGpB,WAAW;KACT,QAAQ;KACR,QAAQ;KACR,SAAS;KACT,SAAS;KACT,cAAc;KACd,aAAa;KACb,eAAe;KACf,aAAa;IACf;IAMA,WAAW,YAAY,qBAAqB,SAAS,OAAO,CAAC;GAC/D,CAAC;EACH;CACF,CAAC;CACD,MAAM,gBAAgB,OAAO;EAC3B,MAAM;EACN,QAAQ;EACR,SAAS,gBAAgB,YAAY;CACvC,CAAC;CAED,OAAO;EAAE,OAAO,CAAC,OAAO,OAAO,KAAK;EAAG,QAAQ;EAAO,QAAQ;CAAY,CAAC;CAC3E,OAAO;EAAE,OAAO,YAAY;EAAU,QAAQ;CAAa,CAAC;CAC5D,OAAO;EAAE,OAAO;EAAS,QAAQ;CAAW,CAAC;CAE7C,IAAI,OAAO,UACT,OAAO;EAAE,OAAO,OAAO;EAAU,QAAQ;CAAc,CAAC;CAG1D,OAAO;AACT;;;;;;;;;;ACtFA,IAAa,gBACX,MACA,gBAEA,OAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,SAAS,KAAK,MAAM,WAAW;AAC1C,CAAC;;;;;AAMH,IAAa,kBACX,SAEA,OAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,MAAM,WAAmB,KAAK,MAAM,MAAM;AACrD,CAAC;;AAGH,IAAa,cACX,SAEA,OAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,MAAM,EAAE,MAAM,OAAO,cAC5B,KAAK,SAAS,MAAM,OAAO,OAAO;AACtC,CAAC;;;;;AAmBH,IAAa,mBACX,SAEA,OAAO;CACL,MAAM;CACN,QAAQ,QAAQ,IAAI;CACpB,SAAS,MAAM,WAAqC;EAClD,KAAK,MAAM,cAAc,QACvB,KAAK,SAAS,WAAW,MAAM,WAAW,OAAO,WAAW,OAAO;CAEvE;AACF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reaxon/hook-form-effector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Effector bindings for react-hook-form: mirror form state into stores and drive the form control with effects",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"effector",
|
|
7
|
+
"react-hook-form",
|
|
8
|
+
"forms",
|
|
9
|
+
"state-management"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/dovranJorayev/reaxon.git",
|
|
15
|
+
"directory": "packages/hook-form-effector"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/dovranJorayev/reaxon/tree/main/packages/hook-form-effector#readme",
|
|
18
|
+
"bugs": "https://github.com/dovranJorayev/reaxon/issues",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"main": "./dist/index.cjs",
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"CHANGELOG.md"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"provenance": true
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"effector": "^23",
|
|
51
|
+
"react-hook-form": "^7.72.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/react": "19.2.18",
|
|
55
|
+
"effector": "23.4.4",
|
|
56
|
+
"react": "19.2.8",
|
|
57
|
+
"react-hook-form": "7.79.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "vite build",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:watch": "vitest",
|
|
63
|
+
"typecheck": "tsc --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|