@pyreon/form 0.0.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/LICENSE +21 -0
- package/README.md +259 -0
- package/lib/analysis/devtools.js.html +5406 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/devtools.js +74 -0
- package/lib/devtools.js.map +1 -0
- package/lib/index.js +451 -0
- package/lib/index.js.map +1 -0
- package/lib/types/devtools.d.ts +67 -0
- package/lib/types/devtools.d.ts.map +1 -0
- package/lib/types/devtools2.d.ts +31 -0
- package/lib/types/devtools2.d.ts.map +1 -0
- package/lib/types/index.d.ts +457 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +291 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/context.ts +67 -0
- package/src/devtools.ts +100 -0
- package/src/index.ts +26 -0
- package/src/tests/devtools.test.ts +206 -0
- package/src/tests/form.test.ts +1860 -0
- package/src/types.ts +116 -0
- package/src/use-field-array.ts +117 -0
- package/src/use-field.ts +69 -0
- package/src/use-form-state.ts +74 -0
- package/src/use-form.ts +436 -0
- package/src/use-watch.ts +69 -0
package/src/use-form.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { onUnmount } from '@pyreon/core'
|
|
2
|
+
import { signal, computed, effect } from '@pyreon/reactivity'
|
|
3
|
+
import type { Signal } from '@pyreon/reactivity'
|
|
4
|
+
import type {
|
|
5
|
+
FieldRegisterProps,
|
|
6
|
+
FieldState,
|
|
7
|
+
FormState,
|
|
8
|
+
UseFormOptions,
|
|
9
|
+
ValidationError,
|
|
10
|
+
} from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a signal-based form. Returns reactive field states, form-level
|
|
14
|
+
* signals, and handlers for submit/reset/validate.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const form = useForm({
|
|
18
|
+
* initialValues: { email: '', password: '', remember: false },
|
|
19
|
+
* validators: {
|
|
20
|
+
* email: (v) => (!v ? 'Required' : undefined),
|
|
21
|
+
* password: (v, all) => (v.length < 8 ? 'Too short' : undefined),
|
|
22
|
+
* },
|
|
23
|
+
* onSubmit: async (values) => { await login(values) },
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* // Bind with register():
|
|
27
|
+
* // h('input', form.register('email'))
|
|
28
|
+
* // h('input', { type: 'checkbox', ...form.register('remember', { type: 'checkbox' }) })
|
|
29
|
+
*/
|
|
30
|
+
export function useForm<TValues extends Record<string, unknown>>(
|
|
31
|
+
options: UseFormOptions<TValues>,
|
|
32
|
+
): FormState<TValues> {
|
|
33
|
+
const {
|
|
34
|
+
initialValues,
|
|
35
|
+
onSubmit,
|
|
36
|
+
validators,
|
|
37
|
+
schema,
|
|
38
|
+
validateOn = 'blur',
|
|
39
|
+
debounceMs,
|
|
40
|
+
} = options
|
|
41
|
+
|
|
42
|
+
// Build field states
|
|
43
|
+
const fieldEntries = Object.entries(initialValues) as [
|
|
44
|
+
keyof TValues & string,
|
|
45
|
+
TValues[keyof TValues],
|
|
46
|
+
][]
|
|
47
|
+
|
|
48
|
+
const fields = {} as { [K in keyof TValues]: FieldState<TValues[K]> }
|
|
49
|
+
|
|
50
|
+
// Debounce timers per field (only allocated when debounceMs is set)
|
|
51
|
+
const debounceTimers: Partial<
|
|
52
|
+
Record<keyof TValues, ReturnType<typeof setTimeout>>
|
|
53
|
+
> = {}
|
|
54
|
+
|
|
55
|
+
// Validation version per field — used to discard stale async results
|
|
56
|
+
const validationVersions: Partial<Record<keyof TValues, number>> = {}
|
|
57
|
+
|
|
58
|
+
// Helper to get all current values (used by cross-field validators)
|
|
59
|
+
const getValues = (): TValues => {
|
|
60
|
+
const values = {} as TValues
|
|
61
|
+
for (const [name] of fieldEntries) {
|
|
62
|
+
;(values as Record<string, unknown>)[name] =
|
|
63
|
+
fields[name]?.value.peek() ??
|
|
64
|
+
(initialValues as Record<string, unknown>)[name]
|
|
65
|
+
}
|
|
66
|
+
return values
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Clear all pending debounce timers
|
|
70
|
+
const clearAllTimers = () => {
|
|
71
|
+
for (const key of Object.keys(debounceTimers)) {
|
|
72
|
+
clearTimeout(debounceTimers[key as keyof TValues])
|
|
73
|
+
delete debounceTimers[key as keyof TValues]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const isValidating = signal(false)
|
|
78
|
+
const submitError = signal<unknown>(undefined)
|
|
79
|
+
|
|
80
|
+
// Track whether the form has been disposed (unmounted)
|
|
81
|
+
let disposed = false
|
|
82
|
+
|
|
83
|
+
for (const [name, initial] of fieldEntries) {
|
|
84
|
+
const valueSig = signal(initial) as Signal<TValues[typeof name]>
|
|
85
|
+
const errorSig = signal<ValidationError>(undefined)
|
|
86
|
+
const touchedSig = signal(false)
|
|
87
|
+
const dirtySig = signal(false)
|
|
88
|
+
|
|
89
|
+
// Initialize validation version
|
|
90
|
+
validationVersions[name] = 0
|
|
91
|
+
|
|
92
|
+
const runValidation = async (value: TValues[typeof name]) => {
|
|
93
|
+
const fieldValidator = validators?.[name]
|
|
94
|
+
if (fieldValidator) {
|
|
95
|
+
// Bump version to track this validation run
|
|
96
|
+
validationVersions[name] = (validationVersions[name] ?? 0) + 1
|
|
97
|
+
const currentVersion = validationVersions[name]
|
|
98
|
+
try {
|
|
99
|
+
const result = await fieldValidator(value, getValues())
|
|
100
|
+
// Only apply result if this is still the latest validation and not disposed
|
|
101
|
+
if (!disposed && validationVersions[name] === currentVersion) {
|
|
102
|
+
errorSig.set(result)
|
|
103
|
+
}
|
|
104
|
+
return result
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Validator threw — treat as error string if possible
|
|
107
|
+
if (!disposed && validationVersions[name] === currentVersion) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
109
|
+
errorSig.set(message)
|
|
110
|
+
}
|
|
111
|
+
return err instanceof Error ? err.message : String(err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
errorSig.set(undefined)
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const validateField = debounceMs
|
|
119
|
+
? (value: TValues[typeof name]) => {
|
|
120
|
+
clearTimeout(debounceTimers[name])
|
|
121
|
+
return new Promise<ValidationError>((resolve) => {
|
|
122
|
+
debounceTimers[name] = setTimeout(async () => {
|
|
123
|
+
resolve(await runValidation(value))
|
|
124
|
+
}, debounceMs)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
: runValidation
|
|
128
|
+
|
|
129
|
+
// Auto-validate on change if configured
|
|
130
|
+
if (validateOn === 'change') {
|
|
131
|
+
effect(() => {
|
|
132
|
+
const v = valueSig()
|
|
133
|
+
validateField(v)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fields[name] = {
|
|
138
|
+
value: valueSig,
|
|
139
|
+
error: errorSig,
|
|
140
|
+
touched: touchedSig,
|
|
141
|
+
dirty: dirtySig,
|
|
142
|
+
setValue: (value: TValues[typeof name]) => {
|
|
143
|
+
valueSig.set(value)
|
|
144
|
+
// Deep comparison for objects/arrays, reference for primitives
|
|
145
|
+
dirtySig.set(!structuredEqual(value, initial))
|
|
146
|
+
},
|
|
147
|
+
setTouched: () => {
|
|
148
|
+
touchedSig.set(true)
|
|
149
|
+
if (validateOn === 'blur') {
|
|
150
|
+
validateField(valueSig.peek())
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
reset: () => {
|
|
154
|
+
valueSig.set(initial as TValues[typeof name])
|
|
155
|
+
errorSig.set(undefined)
|
|
156
|
+
touchedSig.set(false)
|
|
157
|
+
dirtySig.set(false)
|
|
158
|
+
clearTimeout(debounceTimers[name])
|
|
159
|
+
},
|
|
160
|
+
} as FieldState<TValues[typeof name]>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Clean up debounce timers and cancel in-flight validators on unmount
|
|
164
|
+
onUnmount(() => {
|
|
165
|
+
disposed = true
|
|
166
|
+
clearAllTimers()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const isSubmitting = signal(false)
|
|
170
|
+
const submitCount = signal(0)
|
|
171
|
+
|
|
172
|
+
// Form-level computed signals
|
|
173
|
+
const isValid = computed(() => {
|
|
174
|
+
for (const name of fieldEntries.map(([n]) => n)) {
|
|
175
|
+
if (fields[name].error() !== undefined) return false
|
|
176
|
+
}
|
|
177
|
+
return true
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const isDirty = computed(() => {
|
|
181
|
+
for (const name of fieldEntries.map(([n]) => n)) {
|
|
182
|
+
if (fields[name].dirty()) return true
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const getErrors = (): Partial<Record<keyof TValues, ValidationError>> => {
|
|
188
|
+
const errors = {} as Partial<Record<keyof TValues, ValidationError>>
|
|
189
|
+
for (const [name] of fieldEntries) {
|
|
190
|
+
const err = fields[name].error.peek()
|
|
191
|
+
if (err !== undefined) errors[name] = err
|
|
192
|
+
}
|
|
193
|
+
return errors
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const validate = async (): Promise<boolean> => {
|
|
197
|
+
// Cancel any pending debounced validations
|
|
198
|
+
clearAllTimers()
|
|
199
|
+
|
|
200
|
+
isValidating.set(true)
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const allValues = getValues()
|
|
204
|
+
|
|
205
|
+
// Clear all errors before re-validating
|
|
206
|
+
for (const [name] of fieldEntries) {
|
|
207
|
+
fields[name].error.set(undefined)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Run field-level validators with all values for cross-field support
|
|
211
|
+
await Promise.all(
|
|
212
|
+
fieldEntries.map(async ([name]) => {
|
|
213
|
+
const fieldValidator = validators?.[name]
|
|
214
|
+
if (fieldValidator) {
|
|
215
|
+
// Bump version so any in-flight debounced validation is discarded
|
|
216
|
+
validationVersions[name] = (validationVersions[name] ?? 0) + 1
|
|
217
|
+
const currentVersion = validationVersions[name]
|
|
218
|
+
try {
|
|
219
|
+
const error = await fieldValidator(
|
|
220
|
+
fields[name].value.peek(),
|
|
221
|
+
allValues,
|
|
222
|
+
)
|
|
223
|
+
if (validationVersions[name] === currentVersion) {
|
|
224
|
+
fields[name].error.set(error)
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (validationVersions[name] === currentVersion) {
|
|
228
|
+
fields[name].error.set(
|
|
229
|
+
err instanceof Error ? err.message : String(err),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Run schema-level validator — only set schema errors for fields
|
|
238
|
+
// that don't already have a field-level error (field-level wins)
|
|
239
|
+
if (schema) {
|
|
240
|
+
try {
|
|
241
|
+
const schemaErrors = await schema(allValues)
|
|
242
|
+
for (const [name] of fieldEntries) {
|
|
243
|
+
const schemaError = schemaErrors[name]
|
|
244
|
+
if (
|
|
245
|
+
schemaError !== undefined &&
|
|
246
|
+
fields[name].error.peek() === undefined
|
|
247
|
+
) {
|
|
248
|
+
fields[name].error.set(schemaError)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
// Schema validator threw — set as submitError rather than losing it
|
|
253
|
+
submitError.set(err)
|
|
254
|
+
return false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Re-check: any field with an error means invalid
|
|
259
|
+
for (const [name] of fieldEntries) {
|
|
260
|
+
if (fields[name].error.peek() !== undefined) return false
|
|
261
|
+
}
|
|
262
|
+
return true
|
|
263
|
+
} finally {
|
|
264
|
+
isValidating.set(false)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const handleSubmit = async (e?: Event) => {
|
|
269
|
+
if (e && typeof e.preventDefault === 'function') {
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
submitError.set(undefined)
|
|
274
|
+
submitCount.update((n) => n + 1)
|
|
275
|
+
|
|
276
|
+
// Mark all fields as touched
|
|
277
|
+
for (const [name] of fieldEntries) {
|
|
278
|
+
fields[name].touched.set(true)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const valid = await validate()
|
|
282
|
+
if (!valid) return
|
|
283
|
+
|
|
284
|
+
isSubmitting.set(true)
|
|
285
|
+
try {
|
|
286
|
+
await onSubmit(getValues())
|
|
287
|
+
} catch (err) {
|
|
288
|
+
submitError.set(err)
|
|
289
|
+
throw err
|
|
290
|
+
} finally {
|
|
291
|
+
isSubmitting.set(false)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const reset = () => {
|
|
296
|
+
clearAllTimers()
|
|
297
|
+
for (const [name] of fieldEntries) {
|
|
298
|
+
fields[name].reset()
|
|
299
|
+
}
|
|
300
|
+
submitCount.set(0)
|
|
301
|
+
submitError.set(undefined)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const setFieldValue = <K extends keyof TValues>(
|
|
305
|
+
field: K,
|
|
306
|
+
value: TValues[K],
|
|
307
|
+
) => {
|
|
308
|
+
if (!fields[field]) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`[@pyreon/form] Field "${String(field)}" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(', ')}`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
fields[field].setValue(value)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const setFieldError = (field: keyof TValues, error: ValidationError) => {
|
|
317
|
+
if (!fields[field]) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`[@pyreon/form] Field "${String(field)}" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(', ')}`,
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
fields[field].error.set(error)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const setErrors = (
|
|
326
|
+
errors: Partial<Record<keyof TValues, ValidationError>>,
|
|
327
|
+
) => {
|
|
328
|
+
for (const [name, error] of Object.entries(errors)) {
|
|
329
|
+
setFieldError(name as keyof TValues, error as ValidationError)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const clearErrors = () => {
|
|
334
|
+
for (const [name] of fieldEntries) {
|
|
335
|
+
fields[name].error.set(undefined)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const resetField = (field: keyof TValues) => {
|
|
340
|
+
if (fields[field]) {
|
|
341
|
+
fields[field].reset()
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Memoized register props per field+type combo
|
|
346
|
+
const registerCache = new Map<string, FieldRegisterProps<unknown>>()
|
|
347
|
+
|
|
348
|
+
const register = <K extends keyof TValues & string>(
|
|
349
|
+
field: K,
|
|
350
|
+
opts?: { type?: 'checkbox' | 'number' },
|
|
351
|
+
): FieldRegisterProps<TValues[K]> => {
|
|
352
|
+
const cacheKey = `${field}:${opts?.type ?? 'text'}`
|
|
353
|
+
const cached = registerCache.get(cacheKey)
|
|
354
|
+
if (cached) return cached as FieldRegisterProps<TValues[K]>
|
|
355
|
+
|
|
356
|
+
const fieldState = fields[field]
|
|
357
|
+
const props: FieldRegisterProps<TValues[K]> = {
|
|
358
|
+
value: fieldState.value,
|
|
359
|
+
onInput: (e: Event) => {
|
|
360
|
+
const target = e.target as HTMLInputElement
|
|
361
|
+
if (opts?.type === 'checkbox') {
|
|
362
|
+
fieldState.setValue(target.checked as TValues[K])
|
|
363
|
+
} else if (opts?.type === 'number') {
|
|
364
|
+
const num = target.valueAsNumber
|
|
365
|
+
fieldState.setValue(
|
|
366
|
+
(Number.isNaN(num) ? target.value : num) as TValues[K],
|
|
367
|
+
)
|
|
368
|
+
} else {
|
|
369
|
+
fieldState.setValue(target.value as TValues[K])
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
onBlur: () => {
|
|
373
|
+
fieldState.setTouched()
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (opts?.type === 'checkbox') {
|
|
378
|
+
props.checked = computed(() => Boolean(fieldState.value()))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
registerCache.set(cacheKey, props as FieldRegisterProps<unknown>)
|
|
382
|
+
return props
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
fields,
|
|
387
|
+
isSubmitting,
|
|
388
|
+
isValidating,
|
|
389
|
+
isValid,
|
|
390
|
+
isDirty,
|
|
391
|
+
submitCount,
|
|
392
|
+
submitError,
|
|
393
|
+
values: getValues,
|
|
394
|
+
errors: getErrors,
|
|
395
|
+
setFieldValue,
|
|
396
|
+
setFieldError,
|
|
397
|
+
setErrors,
|
|
398
|
+
clearErrors,
|
|
399
|
+
resetField,
|
|
400
|
+
register,
|
|
401
|
+
handleSubmit,
|
|
402
|
+
reset,
|
|
403
|
+
validate,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Deep structural equality with depth limit to guard against circular references. */
|
|
408
|
+
function structuredEqual(a: unknown, b: unknown, depth = 0): boolean {
|
|
409
|
+
if (Object.is(a, b)) return true
|
|
410
|
+
if (a == null || b == null) return false
|
|
411
|
+
if (typeof a !== typeof b) return false
|
|
412
|
+
// Bail at depth 10 — treat as not equal to avoid infinite recursion
|
|
413
|
+
if (depth > 10) return false
|
|
414
|
+
|
|
415
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
416
|
+
if (a.length !== b.length) return false
|
|
417
|
+
for (let i = 0; i < a.length; i++) {
|
|
418
|
+
if (!structuredEqual(a[i], b[i], depth + 1)) return false
|
|
419
|
+
}
|
|
420
|
+
return true
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
424
|
+
const aObj = a as Record<string, unknown>
|
|
425
|
+
const bObj = b as Record<string, unknown>
|
|
426
|
+
const aKeys = Object.keys(aObj)
|
|
427
|
+
const bKeys = Object.keys(bObj)
|
|
428
|
+
if (aKeys.length !== bKeys.length) return false
|
|
429
|
+
for (const key of aKeys) {
|
|
430
|
+
if (!structuredEqual(aObj[key], bObj[key], depth + 1)) return false
|
|
431
|
+
}
|
|
432
|
+
return true
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return false
|
|
436
|
+
}
|
package/src/use-watch.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { computed } from '@pyreon/reactivity'
|
|
2
|
+
import type { Signal, Computed } from '@pyreon/reactivity'
|
|
3
|
+
import type { FormState } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Watch specific field values reactively. Returns a computed signal
|
|
7
|
+
* that re-evaluates when any of the watched fields change.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Watch a single field
|
|
11
|
+
* const email = useWatch(form, 'email')
|
|
12
|
+
* // email() => current email value
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Watch multiple fields
|
|
16
|
+
* const [first, last] = useWatch(form, ['firstName', 'lastName'])
|
|
17
|
+
* // first() => firstName value, last() => lastName value
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // Watch all fields
|
|
21
|
+
* const all = useWatch(form)
|
|
22
|
+
* // all() => { email: '...', password: '...' }
|
|
23
|
+
*/
|
|
24
|
+
export function useWatch<
|
|
25
|
+
TValues extends Record<string, unknown>,
|
|
26
|
+
K extends keyof TValues & string,
|
|
27
|
+
>(form: FormState<TValues>, name: K): Signal<TValues[K]>
|
|
28
|
+
|
|
29
|
+
export function useWatch<
|
|
30
|
+
TValues extends Record<string, unknown>,
|
|
31
|
+
K extends (keyof TValues & string)[],
|
|
32
|
+
>(
|
|
33
|
+
form: FormState<TValues>,
|
|
34
|
+
names: K,
|
|
35
|
+
): { [I in keyof K]: Signal<TValues[K[I] & keyof TValues]> }
|
|
36
|
+
|
|
37
|
+
export function useWatch<TValues extends Record<string, unknown>>(
|
|
38
|
+
form: FormState<TValues>,
|
|
39
|
+
): Computed<TValues>
|
|
40
|
+
|
|
41
|
+
export function useWatch<
|
|
42
|
+
TValues extends Record<string, unknown>,
|
|
43
|
+
K extends keyof TValues & string,
|
|
44
|
+
>(
|
|
45
|
+
form: FormState<TValues>,
|
|
46
|
+
nameOrNames?: K | K[],
|
|
47
|
+
): Signal<TValues[K]> | Signal<TValues[K]>[] | Computed<TValues> {
|
|
48
|
+
// Watch all fields
|
|
49
|
+
if (nameOrNames === undefined) {
|
|
50
|
+
return computed(() => {
|
|
51
|
+
const result = {} as TValues
|
|
52
|
+
for (const key of Object.keys(form.fields) as (keyof TValues &
|
|
53
|
+
string)[]) {
|
|
54
|
+
;(result as Record<string, unknown>)[key] = form.fields[key].value()
|
|
55
|
+
}
|
|
56
|
+
return result
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Watch multiple fields
|
|
61
|
+
if (Array.isArray(nameOrNames)) {
|
|
62
|
+
return nameOrNames.map((name) => form.fields[name].value) as Signal<
|
|
63
|
+
TValues[K]
|
|
64
|
+
>[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Watch single field
|
|
68
|
+
return form.fields[nameOrNames].value
|
|
69
|
+
}
|