react-store-input 0.2.5 → 0.4.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +179 -201
  3. package/dist/index.d.mts +128 -11
  4. package/dist/index.d.ts +128 -11
  5. package/dist/index.js +559 -249
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +552 -247
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/text-editor.d.mts +11 -0
  10. package/dist/text-editor.d.ts +11 -0
  11. package/dist/{text_editor.js → text-editor.js} +31 -51
  12. package/dist/text-editor.js.map +1 -0
  13. package/dist/text-editor.mjs +90 -0
  14. package/dist/text-editor.mjs.map +1 -0
  15. package/package.json +78 -18
  16. package/dist/create_render.d.mts +0 -9
  17. package/dist/create_render.d.ts +0 -9
  18. package/dist/create_render.js +0 -51
  19. package/dist/create_render.mjs +0 -25
  20. package/dist/text_editor.d.mts +0 -13
  21. package/dist/text_editor.d.ts +0 -13
  22. package/dist/text_editor.mjs +0 -112
  23. package/dist/use_form_store.d.mts +0 -16
  24. package/dist/use_form_store.d.ts +0 -16
  25. package/dist/use_form_store.js +0 -365
  26. package/dist/use_form_store.mjs +0 -340
  27. package/dist/use_store_component.d.mts +0 -23
  28. package/dist/use_store_component.d.ts +0 -23
  29. package/dist/use_store_component.js +0 -337
  30. package/dist/use_store_component.mjs +0 -311
  31. package/dist/use_store_controller.d.mts +0 -11
  32. package/dist/use_store_controller.d.ts +0 -11
  33. package/dist/use_store_controller.js +0 -57
  34. package/dist/use_store_controller.mjs +0 -32
  35. package/dist/use_store_input.d.mts +0 -22
  36. package/dist/use_store_input.d.ts +0 -22
  37. package/dist/use_store_input.js +0 -151
  38. package/dist/use_store_input.mjs +0 -126
  39. package/dist/use_store_input_with_name.d.mts +0 -17
  40. package/dist/use_store_input_with_name.d.ts +0 -17
  41. package/dist/use_store_input_with_name.js +0 -177
  42. package/dist/use_store_input_with_name.mjs +0 -150
  43. package/tsup.config.ts +0 -9
package/CHANGELOG.md ADDED
@@ -0,0 +1,114 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project uses [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] - 2026-07-22
11
+
12
+ ### Added
13
+
14
+ - Typed `Lens`, `Codec`, and `InputBinding` primitives for custom controls.
15
+ - `stateLens` for generating a getter and setter from one typed state path.
16
+ - `gw-result@0.3.0`-based codec parsing with typed failure metadata from
17
+ `useStoreInput`.
18
+ - `assertLensLaws` and `assertCodecLaws` helpers for testing mapping and
19
+ round-trip invariants.
20
+ - Top-level `ok`, `err`, and `Result` re-exports for codec implementations.
21
+
22
+ ### Changed
23
+
24
+ - Removed the deprecated `getter`, `setter`, `toInputValue`, and
25
+ `toStateValue` APIs. Nested and converted controls now use bindings.
26
+ - Reimplemented named controls internally with a generated Lens and default
27
+ Codec while preserving built-in conversion and reset behavior.
28
+ - Expanded the custom currency example to demonstrate validation failures and
29
+ accessible error feedback.
30
+
31
+ ### Fixed
32
+
33
+ - Preserved invalid raw control input while unrelated store fields update.
34
+ - Cleared binding parse errors and restored the initial domain value on native
35
+ form reset.
36
+
37
+ ## [0.3.0] - 2026-07-22
38
+
39
+ ### Added
40
+
41
+ - Standalone `Input`, `Select`, and `Textarea` components with typed named or
42
+ custom getter/setter bindings.
43
+ - `useStoreInput`, `useStoreController`, `createRender`, and complete
44
+ `gw-store` API re-exports.
45
+ - An optional `react-store-input/text-editor` entry point that keeps editor
46
+ dependencies out of the core bundle.
47
+ - Automatic native form reset synchronization, including batched store
48
+ notifications and multiple-select, date, checkbox, radio, and file values.
49
+ - Runtime, package-consumer, type, and example build tests.
50
+ - A complete interactive example covering the public components, hooks, and
51
+ supported input value types.
52
+
53
+ ### Changed
54
+
55
+ - Updated the store integration to use exactly `gw-store@0.2.0`.
56
+ - Published separate CommonJS and ESM entry points with matching declaration
57
+ files.
58
+ - Made `gw-react-text-editor` an optional peer dependency.
59
+ - Expanded React peer support to React 18 and 19.
60
+ - Organized package source, examples, styles, and runtime tests by
61
+ responsibility.
62
+
63
+ ### Fixed
64
+
65
+ - Prevented bundled duplicate React copies from causing invalid hook calls.
66
+ - Preserved numeric radio values and mapped empty numeric inputs to
67
+ `undefined`.
68
+ - Stored all selected values from multiple selects and normalized empty file
69
+ inputs to `null`.
70
+ - Kept rendered JSON synchronized after a native form reset.
71
+ - Reduced reset notifications to one batch and eliminated notifications for
72
+ semantically unchanged arrays and `Date` values.
73
+
74
+ ## [0.2.6] - 2026-04-28
75
+
76
+ ### Added
77
+
78
+ - Re-exported the `gw-store` API.
79
+
80
+ ## [0.2.5] - 2026-04-27
81
+
82
+ ### Changed
83
+
84
+ - Added `gw-store` as a package dependency.
85
+
86
+ ## [0.2.4] - 2026-03-16
87
+
88
+ ### Fixed
89
+
90
+ - Improved store value comparison behavior.
91
+
92
+ ## [0.2.3] - 2026-03-09
93
+
94
+ ### Changed
95
+
96
+ - Applied package maintenance updates.
97
+
98
+ ## [0.2.2] - 2026-03-09
99
+
100
+ ### Changed
101
+
102
+ - Added compatible React 18 type and peer dependency ranges.
103
+
104
+ ## [0.2.1] - 2026-01-21
105
+
106
+ ### Changed
107
+
108
+ - Updated the JSX runtime configuration.
109
+
110
+ ## [0.2.0] - 2026-01-13
111
+
112
+ ### Fixed
113
+
114
+ - Corrected forwarded ref behavior.
package/README.md CHANGED
@@ -1,285 +1,263 @@
1
1
  # React Store Input
2
2
 
3
- The goal of this package is to make state management easier when using input elements in React.
3
+ Typed, store-backed form controls for React 18 and 19. The package uses
4
+ [`gw-store`](https://www.npmjs.com/package/gw-store) 0.2.0, so state snapshots
5
+ are immutable and updates are made through Immer recipes.
4
6
 
5
- It eliminates repetitive code required to implement state changes and subscriptions for input elements, and provides a simple interface.
7
+ ## Install
6
8
 
7
- At the same time, it allows you to use all the attributes originally provided by the input tag as-is, without needing to learn this package.
8
-
9
- ## Get Started
9
+ ```sh
10
+ npm install react-store-input
11
+ ```
10
12
 
11
- This is a simple example of how to use this package.
13
+ ## Quick start
12
14
 
13
15
  ```tsx
14
- import { useFormStore } from "dn-react-input";
16
+ import { useFormStore } from "react-store-input";
15
17
 
16
- export default function App() {
17
- const store = useFormStore({
18
+ export default function LoginForm() {
19
+ const form = useFormStore({
18
20
  email: "",
19
21
  password: "",
22
+ rememberMe: false,
20
23
  });
21
24
 
22
- const submit = async () => {
23
- const { email, password } = store.state;
24
-
25
- alert(`Email: ${email}\nPassword: ${password}`);
26
- };
27
-
28
25
  return (
29
26
  <form
30
- onSubmit={(e) => {
31
- e.preventDefault();
32
- submit();
27
+ onSubmit={(event) => {
28
+ event.preventDefault();
29
+ console.log(form.state);
33
30
  }}
34
31
  >
35
- <store.input name="email" type="email" />
36
- <store.input name="password" type="password" />
37
- <button type="submit">Submit</button>
32
+ <form.input name="email" type="email" />
33
+ <form.input name="password" type="password" />
34
+ <form.input name="rememberMe" type="checkbox" />
35
+ <button type="submit">Sign in</button>
38
36
  </form>
39
37
  );
40
38
  }
41
39
  ```
42
40
 
43
- ## How to define state?
41
+ `useFormStore` returns the complete `gw-store` API (`state`, `dispatch`,
42
+ `batch`, and `subscribe`) together with stable `input`, `select`, `textarea`,
43
+ and `render` helpers.
44
+
45
+ ## Components
44
46
 
45
- You can define any state you want as an object when calling `useStore`.
47
+ You can use the standalone components when the store is passed from elsewhere:
46
48
 
47
49
  ```tsx
48
- function Component() {
49
- ...
50
+ import { Input, Select, Textarea, useStore } from "react-store-input";
50
51
 
51
- const store = useStore({
52
- email: "",
53
- password: "",
54
- rememberMe: false,
55
- });
52
+ const store = useStore({ role: "user", bio: "" });
56
53
 
57
- ...
58
- }
54
+ <Input store={store} name="role" type="radio" value="admin" />;
55
+ <Input store={store} name="role" type="radio" value="user" />;
56
+ <Select store={store} name="role">
57
+ <option value="admin">Admin</option>
58
+ <option value="user">User</option>
59
+ </Select>;
60
+ <Textarea store={store} name="bio" rows={5} />;
59
61
  ```
60
62
 
61
- It's a single source of truth for your form state.
63
+ A named `Input`, `Select`, or `Textarea` requires a valid top-level state key.
64
+ Use `useStoreInput` with a typed binding for nested or converted values.
62
65
 
63
- ## How to get input values?
66
+ ## Value conversion
64
67
 
65
- You can access the current values of the input elements through the `state` property of the store.
68
+ The default conversions are:
66
69
 
67
- ```tsx
68
- function Component() {
69
- ...
70
+ - checkbox → `boolean`
71
+ - radio → the original `value` prop, preserving numbers and strings
72
+ - number/range → `number`, or `undefined` when empty
73
+ - datetime-local → `Date`, or `undefined` when empty or invalid
74
+ - multiple select → `string[]`
75
+ - file → `FileList | null`
76
+ - other controls → `string`
70
77
 
71
- const submit = () => {
72
- const { email, password, rememberMe } = store.state;
73
- };
78
+ Domain-specific conversions use a Codec as described below.
74
79
 
75
- ...
76
- }
77
- ```
80
+ The controls are store-backed uncontrolled inputs. An explicit `value` or
81
+ `checked` prop is respected as an externally controlled value and is not
82
+ overwritten by store subscriptions. Native form reset is synchronized back to
83
+ the store.
78
84
 
79
- ## How to add input elements?
85
+ ## Lens, codec, and binding
80
86
 
81
- You can add input elements using the `Input` component provided by the store. There are 'Select' and 'Textarea' components as well.
87
+ A custom control is defined from three small pieces:
82
88
 
83
- ```tsx
84
- import { Input } from "dn-react-input";
85
-
86
- function Component() {
87
- ...
88
-
89
- return (
90
- <form>
91
- <Input store={store} name="email" type="email" />
92
- <Input store={store} name="password" type="password" />
93
- <Input store={store} name="rememberMe" type="checkbox" />
94
- </form>
95
- );
96
- }
97
- ```
89
+ - A `Lens<TState, TValue>` selects and updates one domain value in the store.
90
+ - A `Codec<TValue, TInput, TError>` formats that domain value for the control
91
+ and parses input back to a `gw-result` `Result`.
92
+ - An `InputBinding` combines a lens and codec whose `TValue` types must match.
98
93
 
99
- If you want to avoid passing the store to each input component, use `useStoreComponent`. This hook provides input components that are already connected to the store.
94
+ Keeping the lens and codec separate lets one state field use different UI
95
+ representations, and lets one codec be reused for the same domain type in
96
+ different stores.
100
97
 
101
98
  ```tsx
102
- import { useStoreComponent } from "dn-react-input";
103
-
104
- function Component() {
105
- ...
106
- const component = useStoreComponent(store);
107
-
108
- return (
109
- <form>
110
- <component.input name="email" type="email" />
111
- <component.input name="password" type="password" />
112
- <component.input name="rememberMe" type="checkbox" />
113
- </form>
114
- );
115
- }
99
+ import {
100
+ defineBinding,
101
+ defineCodec,
102
+ err,
103
+ ok,
104
+ stateLens,
105
+ } from "react-store-input";
106
+
107
+ type FormState = { profile: { budget?: number } };
108
+ type BudgetError = { code: "INVALID_BUDGET"; input: string };
109
+
110
+ const budgetBinding = defineBinding({
111
+ lens: stateLens<FormState>().prop("profile").prop("budget"),
112
+ codec: defineCodec<number | undefined, string, BudgetError>({
113
+ format: (value) => value?.toString() ?? "",
114
+ parse: (input) => {
115
+ if (input === "") return ok(undefined);
116
+
117
+ const value = Number(input);
118
+ return Number.isFinite(value)
119
+ ? ok(value)
120
+ : err({ code: "INVALID_BUDGET", input });
121
+ },
122
+ }),
123
+ });
116
124
  ```
117
125
 
118
- `useFormStore` is a facade that combines `useStore` and `useStoreInput` for convenience.
126
+ `stateLens().prop(...)` creates `get` and `set` from the same typed path, so
127
+ they cannot accidentally target different fields. `defineLens` is also
128
+ available for computed or otherwise non-path mappings.
119
129
 
120
- ```tsx
121
- import { useFormStore } from "dn-react-input";
122
-
123
- function Component() {
124
- ...
125
- const store = useFormStore({
126
- email: "",
127
- password: "",
128
- rememberMe: false,
129
- });
130
-
131
- return (
132
- <form>
133
- <store.input name="email" type="email" />
134
- <store.input name="password" type="password" />
135
- <store.input name="rememberMe" type="checkbox" />
136
- </form>
137
- );
138
- }
139
- ```
130
+ `ok`, `err`, and the `Result` type are re-exported from `gw-result@0.3.0` for
131
+ codec implementations.
140
132
 
141
- ## How to render components on state changes?
133
+ ## Rendering selected state
142
134
 
143
- If you want to render a component only when specific parts of the state change, use the `useSelector` hook.
135
+ `useSelector`, `shallowEqual`, and the rest of `gw-store@0.2.0` are re-exported.
144
136
 
145
137
  ```tsx
146
- import { useSelector } from "dn-react-input";
138
+ import { createRender, useSelector } from "react-store-input";
147
139
 
148
- function Component() {
149
- ...
150
- const email = useSelector(store, (state) => state.email);
140
+ const email = useSelector(store, (state) => state.email);
151
141
 
152
- return <div>Your email is: {email}</div>;
153
- }
142
+ return (
143
+ <>
144
+ <p>{email}</p>
145
+ {createRender(store, (state) => <p>{state.password.length} characters</p>)}
146
+ {form.render((state) => <p>{state.rememberMe ? "Remember" : "Forget"}</p>)}
147
+ </>
148
+ );
154
149
  ```
155
150
 
156
- If you want to render components in an inline manner, use the `createRender` function. By using this, you can avoid creating separate components for each part of the state you want to track.
151
+ ## Custom controls
157
152
 
158
- ```tsx
159
- import { createRender } from "dn-react-input";
160
-
161
- function Component() {
162
- ...
163
- return (
164
- <div>
165
- {createRender(store, (state) => <p>{state.email}</p>)}
166
- {createRender(store, (state) => <p>{state.password}</p>)}
167
- </div>
168
- );
169
- }
170
- ```
171
-
172
- `Store.render` is a shortcut for `createRender` when you use `useFormStore`.
153
+ Use `useStoreInput` with a binding for custom elements that expose a normal
154
+ form-control DOM node. The ref is deliberately explicit. A parse failure keeps
155
+ the last valid store value, preserves the user's raw input, and exposes the
156
+ typed error through `meta`.
173
157
 
174
158
  ```tsx
175
- function Component() {
176
- const store = useFormStore({
177
- email: "",
178
- password: "",
179
- });
159
+ import { useRef } from "react";
160
+ import { useStoreInput, type Store } from "react-store-input";
161
+
162
+ function BudgetInput({ store }: { store: Store<FormState> }) {
163
+ const ref = useRef<HTMLInputElement>(null);
164
+ const field = useStoreInput(ref, store, budgetBinding, { type: "text" });
180
165
 
181
166
  return (
182
- <div>
183
- {store.render((state) => (
184
- <p>{state.email}</p>
185
- ))}
186
- {store.render((state) => (
187
- <p>{state.password}</p>
188
- ))}
189
- </div>
167
+ <label>
168
+ Budget
169
+ <input
170
+ ref={ref}
171
+ type="text"
172
+ inputMode="decimal"
173
+ aria-invalid={!field.meta.valid}
174
+ {...field.inputProps}
175
+ />
176
+ {!field.meta.valid && <span>{field.meta.error.code}</span>}
177
+ </label>
190
178
  );
191
179
  }
192
180
  ```
193
181
 
194
- ## How to subscribe to state changes?
182
+ Generated lenses and codecs can be checked with the exported law assertions in
183
+ unit tests:
195
184
 
196
- You can subscribe to state changes using the `subscribe` method of the store.
185
+ ```ts
186
+ assertLensLaws(budgetBinding.lens, {
187
+ state: { profile: { budget: 10 } },
188
+ values: [undefined, 0, 25],
189
+ });
197
190
 
198
- ```tsx
199
- function Component() {
200
- ...
201
- useEffect(() => {
202
- const unsubscribe = store.subscribe((state) => {
203
- console.log(`State changed`, state);
204
- });
205
-
206
- return () => {
207
- unsubscribe();
208
- };
209
- }, []);
210
-
211
- ...
212
- }
191
+ assertCodecLaws(budgetBinding.codec, {
192
+ values: [undefined, 0, 25],
193
+ inputs: ["", "0", "25"],
194
+ });
213
195
  ```
214
196
 
215
- ## How to update state manually?
197
+ The lens assertions verify get-after-set, set-current-value, and last-set-wins.
198
+ The codec assertion verifies `parse(format(value))` for representative domain
199
+ values, plus `format(parse(input).value)` for successful canonical inputs when
200
+ `inputs` are supplied. Normalizing or lossy codecs may supply domain-specific
201
+ `equals` and `equalsInput` functions.
216
202
 
217
- You can update the state manually using the `dispatch` method of the store.
203
+ For non-input controllers, call the returned `dispatch` when the controller
204
+ changes:
218
205
 
219
206
  ```tsx
220
- function Component() {
221
- ...
222
- const updateEmail = () => {
223
- store.dispatch({ email: "ohjinsu98@icloud.com" });
224
- };
207
+ import { useStoreController } from "react-store-input";
225
208
 
226
- return <button onClick={updateEmail}>Update Email</button>;
209
+ function Counter({ store }: { store: Store<{ count: number }> }) {
210
+ const { dispatch } = useStoreController(store, {
211
+ onSubscribe: () => {},
212
+ onDispatch: (state) => {
213
+ state.count += 1;
214
+ },
215
+ });
216
+
217
+ return <button onClick={dispatch}>Increment</button>;
227
218
  }
228
219
  ```
229
220
 
230
- The `dispatch` method uses immerjs internally to update the state, so you can also use a function to update the state based on the previous state.
221
+ ## Optional text editor
231
222
 
232
- ```tsx
233
- function Component() {
234
- ...
223
+ The ProseMirror-based editor is a separate entry point so normal forms do not
224
+ download or bundle editor dependencies. It requires React 19 and an explicit
225
+ optional peer installation:
235
226
 
236
- const updateEmail = () => {
237
- store.dispatch((state) => {
238
- state.email = "ohjinsu98@icloud.com";
239
- });
240
- };
241
-
242
- return <button onClick={updateEmail}>Update Email</button>;
243
- }
227
+ ```sh
228
+ npm install gw-react-text-editor
244
229
  ```
245
230
 
246
- ## How to create custom input components?
247
-
248
- You can create custom input components using the `useStoreInput` hook. This hook provides the necessary props to connect your custom input component to the store: `name`, `value`, `defaultValue`, `defaultChecked`, `onChange`, and `ref` which already subscribed to the store.
249
-
250
231
  ```tsx
251
- import { useStoreInput } from "dn-react-input";
252
-
253
- function CustomInput({ store }: { store: Store<{ email: string }> }) {
254
- const inputProps = useStoreInput(store, {
255
- name: "email",
256
- });
232
+ import { TextEditor } from "react-store-input/text-editor";
257
233
 
258
- return <input {...inputProps} />;
259
- }
234
+ <TextEditor store={store} name="content" />;
260
235
  ```
261
236
 
262
- ## How to creatre custom controller components?
237
+ ## Development
263
238
 
264
- If your custom component is not an html input element, you can use the `useStoreController` hook. This hook provides the necessary props to connect your custom controller component to the store: `ref`, `onSubscribe`, and `onDispatch`.
239
+ Release history is tracked in [CHANGELOG.md](./CHANGELOG.md). The manual
240
+ pre-release checks are documented in [docs/PUBLISHING.md](./docs/PUBLISHING.md).
265
241
 
266
- ```tsx
267
- import { useStoreController } from "dn-react-input";
242
+ Source code is grouped by responsibility:
268
243
 
269
- type State = {
270
- count: number;
271
- };
244
+ ```text
245
+ src/
246
+ ├─ binding/ Lens, Codec, Binding, and law assertions
247
+ ├─ input/ DOM value conversion, reset coordination, and input hooks
248
+ ├─ form/ bound components and useFormStore composition
249
+ ├─ store/ controller and render helpers
250
+ └─ editor/ optional text-editor integration
272
251
 
273
- function CustomController({ store }: { store: Store<State> }) {
274
- const controllerProps = useStoreController<HTMLDivElement, State>(store, {
275
- onSubscribe: (state, element) => {
276
- element.textContent = `Count: ${state.count}`;
277
- },
278
- onDispatch: (state, element) => {
279
- state.count += Number(element.textContent.replace("Count: ", ""));
280
- },
281
- });
252
+ example/src/
253
+ ├─ components/ reusable demo UI
254
+ ├─ sections/ one catalog section per capability group
255
+ ├─ demo/ state model and initial data
256
+ └─ styles/ layout, fields, toolbar, state panel, and responsive rules
257
+ ```
282
258
 
283
- return <div {...controllerProps} />;
284
- }
259
+ ```sh
260
+ npm run typecheck
261
+ npm test
262
+ npm pack --dry-run
285
263
  ```