@bquery/bquery 1.7.0 → 1.8.1
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 +760 -716
- package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +1 -1
- package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
- package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +86 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +28 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/shared.d.ts +6 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -47
- package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +1 -1
- package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts.map +1 -1
- package/dist/forms/index.d.ts +3 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/types.d.ts +46 -0
- package/dist/forms/types.d.ts.map +1 -1
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +25 -0
- package/dist/forms/validators.d.ts.map +1 -1
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +14 -12
- package/dist/full.d.ts +17 -26
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +206 -181
- package/dist/full.iife.js +33 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +33 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.es.mjs +251 -228
- package/dist/media/breakpoints.d.ts.map +1 -1
- package/dist/media/types.d.ts +2 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +1 -1
- package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +1 -1
- package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
- package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +3 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -19
- package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
- package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
- package/dist/router/constraints.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +9 -8
- package/dist/ssr/hydrate.d.ts.map +1 -1
- package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +1 -1
- package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
- package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts.map +1 -1
- package/dist/view.es.mjs +4 -4
- package/package.json +177 -177
- package/src/a11y/announce.ts +131 -131
- package/src/a11y/audit.ts +314 -314
- package/src/a11y/index.ts +68 -68
- package/src/a11y/media-preferences.ts +255 -255
- package/src/a11y/roving-tab-index.ts +164 -164
- package/src/a11y/skip-link.ts +255 -255
- package/src/a11y/trap-focus.ts +184 -184
- package/src/a11y/types.ts +183 -183
- package/src/component/component.ts +599 -599
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -52
- package/src/component/library.ts +540 -542
- package/src/component/scope.ts +212 -212
- package/src/component/types.ts +310 -310
- package/src/core/collection.ts +876 -707
- package/src/core/element.ts +1015 -981
- package/src/core/env.ts +60 -60
- package/src/core/index.ts +49 -49
- package/src/core/shared.ts +77 -62
- package/src/core/utils/index.ts +148 -148
- package/src/devtools/devtools.ts +410 -410
- package/src/devtools/index.ts +48 -48
- package/src/devtools/types.ts +104 -104
- package/src/dnd/draggable.ts +296 -296
- package/src/dnd/droppable.ts +228 -228
- package/src/dnd/index.ts +62 -62
- package/src/dnd/sortable.ts +307 -307
- package/src/dnd/types.ts +293 -293
- package/src/forms/create-form.ts +320 -278
- package/src/forms/index.ts +70 -65
- package/src/forms/types.ts +203 -154
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -265
- package/src/full.ts +554 -480
- package/src/i18n/formatting.ts +67 -67
- package/src/i18n/i18n.ts +200 -200
- package/src/i18n/index.ts +67 -67
- package/src/i18n/translate.ts +182 -182
- package/src/i18n/types.ts +171 -171
- package/src/index.ts +108 -108
- package/src/media/battery.ts +116 -116
- package/src/media/breakpoints.ts +129 -131
- package/src/media/clipboard.ts +80 -80
- package/src/media/device-sensors.ts +158 -158
- package/src/media/geolocation.ts +119 -119
- package/src/media/index.ts +76 -76
- package/src/media/media-query.ts +92 -92
- package/src/media/network.ts +115 -115
- package/src/media/types.ts +177 -177
- package/src/media/viewport.ts +84 -84
- package/src/motion/index.ts +57 -57
- package/src/motion/morph.ts +151 -151
- package/src/motion/parallax.ts +120 -120
- package/src/motion/reduced-motion.ts +66 -66
- package/src/motion/types.ts +271 -271
- package/src/motion/typewriter.ts +164 -164
- package/src/plugin/index.ts +37 -37
- package/src/plugin/registry.ts +284 -269
- package/src/plugin/types.ts +137 -137
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +144 -130
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -279
- package/src/router/constraints.ts +204 -201
- package/src/router/index.ts +49 -49
- package/src/router/match.ts +312 -312
- package/src/router/path-pattern.ts +52 -52
- package/src/router/query.ts +38 -38
- package/src/router/router.ts +421 -402
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +139 -139
- package/src/router/use-route.ts +68 -68
- package/src/router/utils.ts +157 -157
- package/src/security/index.ts +12 -12
- package/src/ssr/hydrate.ts +84 -82
- package/src/ssr/index.ts +70 -70
- package/src/ssr/render.ts +508 -508
- package/src/ssr/serialize.ts +296 -296
- package/src/ssr/types.ts +81 -81
- package/src/store/create-store.ts +467 -467
- package/src/store/index.ts +27 -27
- package/src/store/persisted.ts +245 -249
- package/src/store/types.ts +247 -247
- package/src/store/utils.ts +135 -135
- package/src/storybook/index.ts +480 -480
- package/src/testing/index.ts +42 -42
- package/src/testing/testing.ts +593 -593
- package/src/testing/types.ts +170 -170
- package/src/view/custom-directives.ts +28 -30
- package/src/view/evaluate.ts +292 -292
- package/src/view/process.ts +108 -108
- package/dist/a11y-C5QOVvRn.js.map +0 -1
- package/dist/component-CuuTijA6.js.map +0 -1
- package/dist/constraints-3lV9yyBw.js.map +0 -1
- package/dist/core-Cjl7GUu8.js.map +0 -1
- package/dist/core-DnlyjbF2.js +0 -112
- package/dist/core-DnlyjbF2.js.map +0 -1
- package/dist/custom-directives-7wAShnnd.js.map +0 -1
- package/dist/devtools-D2fQLhDN.js.map +0 -1
- package/dist/dnd-B8EgyzaI.js.map +0 -1
- package/dist/env-NeVmr4Gf.js.map +0 -1
- package/dist/forms-C3yovgH9.js +0 -141
- package/dist/forms-C3yovgH9.js.map +0 -1
- package/dist/i18n-BnnhTFOS.js.map +0 -1
- package/dist/media-Di2Ta22s.js.map +0 -1
- package/dist/motion-qPj_TYGv.js.map +0 -1
- package/dist/mount-SM07RUa6.js.map +0 -1
- package/dist/plugin-cPoOHFLY.js.map +0 -1
- package/dist/reactive-Cfv0RK6x.js +0 -233
- package/dist/reactive-Cfv0RK6x.js.map +0 -1
- package/dist/router-BrthaP_z.js +0 -473
- package/dist/router-BrthaP_z.js.map +0 -1
- package/dist/ssr-B2qd_WBB.js.map +0 -1
- package/dist/store-DWpyH6p5.js.map +0 -1
- package/dist/testing-CsqjNUyy.js.map +0 -1
- package/dist/untrack-DJVQQ2WM.js +0 -33
- package/dist/untrack-DJVQQ2WM.js.map +0 -1
package/src/forms/create-form.ts
CHANGED
|
@@ -1,278 +1,320 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reactive form creation and management.
|
|
3
|
-
*
|
|
4
|
-
* @module bquery/forms
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { computed, signal } from '../reactive/index';
|
|
8
|
-
import {
|
|
9
|
-
import type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* @
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* values
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* @
|
|
105
|
-
* @
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* },
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* console.log(form.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(
|
|
149
|
-
(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
string
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
isSubmitting.value
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Reactive form creation and management.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/forms
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computed, signal } from '../reactive/index';
|
|
8
|
+
import { isPrototypePollutionKey } from '../core/utils/object';
|
|
9
|
+
import { isPromise } from '../core/utils/type-guards';
|
|
10
|
+
import type {
|
|
11
|
+
CrossFieldValidator,
|
|
12
|
+
FieldConfig,
|
|
13
|
+
Form,
|
|
14
|
+
FormConfig,
|
|
15
|
+
FormErrors,
|
|
16
|
+
FormField,
|
|
17
|
+
FormFields,
|
|
18
|
+
ValidationResult,
|
|
19
|
+
Validator,
|
|
20
|
+
} from './types';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Determines whether a validator returned a valid result.
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
const isValid = (result: ValidationResult): boolean => result === true || result === undefined;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Runs a single validator, normalising sync and async results.
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
const runValidator = async <T>(validator: Validator<T>, value: T): Promise<string | undefined> => {
|
|
33
|
+
const result = validator(value);
|
|
34
|
+
const resolved = isPromise(result) ? await result : result;
|
|
35
|
+
return isValid(resolved) ? undefined : (resolved as string);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates a reactive form field from its configuration.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
const createField = <T>(config: FieldConfig<T>): FormField<T> => {
|
|
43
|
+
const initial = config.initialValue;
|
|
44
|
+
const value = signal<T>(initial);
|
|
45
|
+
const error = signal('');
|
|
46
|
+
const isTouched = signal(false);
|
|
47
|
+
|
|
48
|
+
const isDirty = computed(() => !Object.is(value.value, initial));
|
|
49
|
+
const isPristine = computed(() => !isDirty.value);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
value,
|
|
53
|
+
error,
|
|
54
|
+
isDirty,
|
|
55
|
+
isTouched,
|
|
56
|
+
isPristine,
|
|
57
|
+
touch: () => {
|
|
58
|
+
isTouched.value = true;
|
|
59
|
+
},
|
|
60
|
+
reset: () => {
|
|
61
|
+
value.value = initial;
|
|
62
|
+
error.value = '';
|
|
63
|
+
isTouched.value = false;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validates a single field against its validators.
|
|
70
|
+
* Sets the field's error signal.
|
|
71
|
+
*
|
|
72
|
+
* @returns The first error message, or an empty string if valid.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
const validateSingleField = async <T>(
|
|
76
|
+
field: FormField<T>,
|
|
77
|
+
validators: Validator<T>[] | undefined
|
|
78
|
+
): Promise<string> => {
|
|
79
|
+
if (!validators || validators.length === 0) {
|
|
80
|
+
field.error.value = '';
|
|
81
|
+
return '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const validator of validators) {
|
|
85
|
+
const errorMsg = await runValidator(validator, field.value.value);
|
|
86
|
+
if (errorMsg) {
|
|
87
|
+
field.error.value = errorMsg;
|
|
88
|
+
return errorMsg;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
field.error.value = '';
|
|
93
|
+
return '';
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a fully reactive form with field-level validation,
|
|
98
|
+
* dirty/touched tracking, cross-field validation, and submission handling.
|
|
99
|
+
*
|
|
100
|
+
* Each field's `value`, `error`, `isDirty`, `isTouched`, and `isPristine`
|
|
101
|
+
* are reactive signals/computed values that can be used in effects, computed
|
|
102
|
+
* values, or directly read/written.
|
|
103
|
+
*
|
|
104
|
+
* @template T - Shape of the form values (e.g. `{ name: string; age: number }`)
|
|
105
|
+
* @param config - Form configuration with field definitions, validators, and submit handler
|
|
106
|
+
* @returns A reactive {@link Form} instance
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* import { createForm, required, email, min } from '@bquery/bquery/forms';
|
|
111
|
+
*
|
|
112
|
+
* const form = createForm({
|
|
113
|
+
* fields: {
|
|
114
|
+
* name: { initialValue: '', validators: [required()] },
|
|
115
|
+
* email: { initialValue: '', validators: [required(), email()] },
|
|
116
|
+
* age: { initialValue: 0, validators: [min(18, 'Must be 18+')] },
|
|
117
|
+
* },
|
|
118
|
+
* onSubmit: async (values) => {
|
|
119
|
+
* await fetch('/api/register', {
|
|
120
|
+
* method: 'POST',
|
|
121
|
+
* body: JSON.stringify(values),
|
|
122
|
+
* });
|
|
123
|
+
* },
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* // Read reactive state
|
|
127
|
+
* console.log(form.isValid.value); // true (initially, before validation runs)
|
|
128
|
+
* console.log(form.fields.name.value.value); // ''
|
|
129
|
+
*
|
|
130
|
+
* // Update a field
|
|
131
|
+
* form.fields.name.value.value = 'Ada';
|
|
132
|
+
*
|
|
133
|
+
* // Validate and submit
|
|
134
|
+
* await form.handleSubmit();
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export const createForm = <T extends Record<string, unknown>>(config: FormConfig<T>): Form<T> => {
|
|
138
|
+
// Build reactive field objects
|
|
139
|
+
const fieldEntries = Object.entries(config.fields) as [
|
|
140
|
+
keyof T & string,
|
|
141
|
+
FieldConfig<T[keyof T]>,
|
|
142
|
+
][];
|
|
143
|
+
|
|
144
|
+
const fields = {} as FormFields<T>;
|
|
145
|
+
const errors = {} as FormErrors<T>;
|
|
146
|
+
|
|
147
|
+
for (const [name, fieldConfig] of fieldEntries) {
|
|
148
|
+
const field = createField(fieldConfig as FieldConfig<T[typeof name]>);
|
|
149
|
+
(fields as Record<string, FormField>)[name] = field;
|
|
150
|
+
(errors as Record<string, typeof field.error>)[name] = field.error;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isSubmitting = signal(false);
|
|
154
|
+
|
|
155
|
+
// Computed: form is valid when all error signals are empty
|
|
156
|
+
const isFormValid = computed(() => {
|
|
157
|
+
for (const name of Object.keys(fields)) {
|
|
158
|
+
if ((fields as Record<string, FormField>)[name].error.value !== '') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Computed: form is dirty when any field is dirty
|
|
166
|
+
const isFormDirty = computed(() => {
|
|
167
|
+
for (const name of Object.keys(fields)) {
|
|
168
|
+
if ((fields as Record<string, FormField>)[name].isDirty.value) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate a single field by name.
|
|
177
|
+
*/
|
|
178
|
+
const validateField = async (name: keyof T & string): Promise<void> => {
|
|
179
|
+
const field = (fields as Record<string, FormField>)[name];
|
|
180
|
+
const fieldConfig = (config.fields as Record<string, FieldConfig>)[name];
|
|
181
|
+
if (!field || !fieldConfig) return;
|
|
182
|
+
await validateSingleField(field, fieldConfig.validators);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Validate all fields (per-field + cross-field).
|
|
187
|
+
* Returns `true` if the entire form is valid.
|
|
188
|
+
*/
|
|
189
|
+
const validate = async (): Promise<boolean> => {
|
|
190
|
+
let hasError = false;
|
|
191
|
+
|
|
192
|
+
// Per-field validation
|
|
193
|
+
for (const [name, fieldConfig] of fieldEntries) {
|
|
194
|
+
const field = (fields as Record<string, FormField>)[name];
|
|
195
|
+
const error = await validateSingleField(field, (fieldConfig as FieldConfig).validators);
|
|
196
|
+
if (error) hasError = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Cross-field validation
|
|
200
|
+
if (config.crossValidators && config.crossValidators.length > 0) {
|
|
201
|
+
const values = getValues();
|
|
202
|
+
for (const crossValidator of config.crossValidators as CrossFieldValidator<T>[]) {
|
|
203
|
+
const crossErrors = await crossValidator(values);
|
|
204
|
+
if (crossErrors) {
|
|
205
|
+
for (const [fieldName, errorMsg] of Object.entries(crossErrors) as [
|
|
206
|
+
string,
|
|
207
|
+
string | undefined,
|
|
208
|
+
][]) {
|
|
209
|
+
if (errorMsg) {
|
|
210
|
+
const field = (fields as Record<string, FormField>)[fieldName];
|
|
211
|
+
if (field) {
|
|
212
|
+
// Only set cross-field error if no per-field error exists
|
|
213
|
+
if (field.error.value === '') {
|
|
214
|
+
field.error.value = errorMsg;
|
|
215
|
+
}
|
|
216
|
+
hasError = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return !hasError;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Validate all fields and, if valid, invoke the onSubmit handler.
|
|
229
|
+
* Prevents concurrent submissions by setting isSubmitting before validation.
|
|
230
|
+
*/
|
|
231
|
+
const handleSubmit = async (): Promise<void> => {
|
|
232
|
+
if (isSubmitting.value) return;
|
|
233
|
+
isSubmitting.value = true;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const valid = await validate();
|
|
237
|
+
if (!valid) return;
|
|
238
|
+
|
|
239
|
+
if (config.onSubmit) {
|
|
240
|
+
await config.onSubmit(getValues());
|
|
241
|
+
}
|
|
242
|
+
} finally {
|
|
243
|
+
isSubmitting.value = false;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Reset every field to its initial value and clear all errors.
|
|
249
|
+
*/
|
|
250
|
+
const reset = (): void => {
|
|
251
|
+
for (const name of Object.keys(fields)) {
|
|
252
|
+
(fields as Record<string, FormField>)[name].reset();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Return a plain object snapshot of all current field values.
|
|
258
|
+
*/
|
|
259
|
+
const getValues = (): T => {
|
|
260
|
+
const values = {} as Record<string, unknown>;
|
|
261
|
+
for (const name of Object.keys(fields)) {
|
|
262
|
+
values[name] = (fields as Record<string, FormField>)[name].value.value;
|
|
263
|
+
}
|
|
264
|
+
return values as T;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Bulk-set field values from a partial object.
|
|
269
|
+
* Only fields present in the object are updated; missing keys are left unchanged.
|
|
270
|
+
*/
|
|
271
|
+
const setValues = (values: Partial<T>): void => {
|
|
272
|
+
for (const [name, val] of Object.entries(values)) {
|
|
273
|
+
// Ignore inherited keys and prototype-pollution vectors before mutating field state.
|
|
274
|
+
if (isPrototypePollutionKey(name) || !Object.prototype.hasOwnProperty.call(fields, name)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const field = (fields as Record<string, FormField>)[name];
|
|
279
|
+
if (!field) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
field.value.value = val;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Bulk-set field error messages from a partial object.
|
|
288
|
+
* Useful for applying server-side validation errors.
|
|
289
|
+
* Only fields present in the object are updated; missing keys are left unchanged.
|
|
290
|
+
*/
|
|
291
|
+
const setErrors = (errorMap: Partial<Record<keyof T & string, string>>): void => {
|
|
292
|
+
for (const [name, msg] of Object.entries(errorMap)) {
|
|
293
|
+
// Ignore inherited keys and prototype-pollution vectors before mutating field state.
|
|
294
|
+
if (isPrototypePollutionKey(name) || !Object.prototype.hasOwnProperty.call(fields, name)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const field = (fields as Record<string, FormField>)[name];
|
|
299
|
+
if (!field) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
field.error.value = (msg as string) ?? '';
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
fields,
|
|
308
|
+
errors,
|
|
309
|
+
isValid: isFormValid,
|
|
310
|
+
isDirty: isFormDirty,
|
|
311
|
+
isSubmitting,
|
|
312
|
+
handleSubmit,
|
|
313
|
+
validateField,
|
|
314
|
+
validate,
|
|
315
|
+
reset,
|
|
316
|
+
getValues,
|
|
317
|
+
setValues,
|
|
318
|
+
setErrors,
|
|
319
|
+
};
|
|
320
|
+
};
|