@pyreon/form 0.9.0 → 0.11.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/lib/devtools.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/types/devtools.d.ts.map +1 -1
- package/lib/types/index.d.ts +3 -3
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +14 -7
- package/src/context.ts +7 -12
- package/src/devtools.ts +7 -25
- package/src/index.ts +10 -10
- package/src/tests/devtools.test.ts +53 -53
- package/src/tests/form.test.tsx +375 -395
- package/src/types.ts +3 -3
- package/src/use-field-array.ts +2 -2
- package/src/use-field.ts +8 -13
- package/src/use-form-state.ts +3 -3
- package/src/use-form.ts +24 -49
- package/src/use-watch.ts +11 -20
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Computed, Signal } from
|
|
1
|
+
import type { Computed, Signal } from "@pyreon/reactivity"
|
|
2
2
|
|
|
3
3
|
export type ValidationError = string | undefined
|
|
4
4
|
|
|
@@ -85,7 +85,7 @@ export interface FormState<TValues extends Record<string, unknown>> {
|
|
|
85
85
|
*/
|
|
86
86
|
register: <K extends keyof TValues & string>(
|
|
87
87
|
field: K,
|
|
88
|
-
options?: { type?:
|
|
88
|
+
options?: { type?: "checkbox" | "number" },
|
|
89
89
|
) => FieldRegisterProps<TValues[K]>
|
|
90
90
|
/**
|
|
91
91
|
* Submit handler — runs validation, then calls onSubmit if valid.
|
|
@@ -110,7 +110,7 @@ export interface UseFormOptions<TValues extends Record<string, unknown>> {
|
|
|
110
110
|
/** Schema-level validator (runs after field validators). */
|
|
111
111
|
schema?: SchemaValidateFn<TValues>
|
|
112
112
|
/** When to validate: 'blur' (default), 'change', or 'submit'. */
|
|
113
|
-
validateOn?:
|
|
113
|
+
validateOn?: "blur" | "change" | "submit"
|
|
114
114
|
/** Debounce delay in ms for validators (useful for async validators). */
|
|
115
115
|
debounceMs?: number
|
|
116
116
|
}
|
package/src/use-field-array.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Computed, Signal } from
|
|
2
|
-
import { computed, signal } from
|
|
1
|
+
import type { Computed, Signal } from "@pyreon/reactivity"
|
|
2
|
+
import { computed, signal } from "@pyreon/reactivity"
|
|
3
3
|
|
|
4
4
|
export interface FieldArrayItem<T> {
|
|
5
5
|
/** Stable key for keyed rendering. */
|
package/src/use-field.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import type { Computed, Signal } from
|
|
2
|
-
import { computed } from
|
|
3
|
-
import type {
|
|
4
|
-
FieldRegisterProps,
|
|
5
|
-
FieldState,
|
|
6
|
-
FormState,
|
|
7
|
-
ValidationError,
|
|
8
|
-
} from './types'
|
|
1
|
+
import type { Computed, Signal } from "@pyreon/reactivity"
|
|
2
|
+
import { computed } from "@pyreon/reactivity"
|
|
3
|
+
import type { FieldRegisterProps, FieldState, FormState, ValidationError } from "./types"
|
|
9
4
|
|
|
10
5
|
export interface UseFieldResult<T> {
|
|
11
6
|
/** Current field value (reactive signal). */
|
|
@@ -23,7 +18,7 @@ export interface UseFieldResult<T> {
|
|
|
23
18
|
/** Reset the field to its initial value. */
|
|
24
19
|
reset: () => void
|
|
25
20
|
/** Register props for input binding. */
|
|
26
|
-
register: (opts?: { type?:
|
|
21
|
+
register: (opts?: { type?: "checkbox" }) => FieldRegisterProps<T>
|
|
27
22
|
/** Whether the field has an error (computed). */
|
|
28
23
|
hasError: Computed<boolean>
|
|
29
24
|
/** Whether the field should show its error (touched + has error). */
|
|
@@ -45,10 +40,10 @@ export interface UseFieldResult<T> {
|
|
|
45
40
|
* )
|
|
46
41
|
* }
|
|
47
42
|
*/
|
|
48
|
-
export function useField<
|
|
49
|
-
|
|
50
|
-
K
|
|
51
|
-
|
|
43
|
+
export function useField<TValues extends Record<string, unknown>, K extends keyof TValues & string>(
|
|
44
|
+
form: FormState<TValues>,
|
|
45
|
+
name: K,
|
|
46
|
+
): UseFieldResult<TValues[K]> {
|
|
52
47
|
const fieldState: FieldState<TValues[K]> = form.fields[name]
|
|
53
48
|
|
|
54
49
|
const hasError = computed(() => fieldState.error() !== undefined)
|
package/src/use-form-state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Computed } from
|
|
2
|
-
import { computed } from
|
|
3
|
-
import type { FormState, ValidationError } from
|
|
1
|
+
import type { Computed } from "@pyreon/reactivity"
|
|
2
|
+
import { computed } from "@pyreon/reactivity"
|
|
3
|
+
import type { FormState, ValidationError } from "./types"
|
|
4
4
|
|
|
5
5
|
export interface FormStateSummary<TValues extends Record<string, unknown>> {
|
|
6
6
|
isSubmitting: boolean
|
package/src/use-form.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { onUnmount } from
|
|
2
|
-
import type { Signal } from
|
|
3
|
-
import { computed, effect, signal } from
|
|
1
|
+
import { onUnmount } from "@pyreon/core"
|
|
2
|
+
import type { Signal } from "@pyreon/reactivity"
|
|
3
|
+
import { computed, effect, signal } from "@pyreon/reactivity"
|
|
4
4
|
import type {
|
|
5
5
|
FieldRegisterProps,
|
|
6
6
|
FieldState,
|
|
7
7
|
FormState,
|
|
8
8
|
UseFormOptions,
|
|
9
9
|
ValidationError,
|
|
10
|
-
} from
|
|
10
|
+
} from "./types"
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Create a signal-based form. Returns reactive field states, form-level
|
|
@@ -30,14 +30,7 @@ import type {
|
|
|
30
30
|
export function useForm<TValues extends Record<string, unknown>>(
|
|
31
31
|
options: UseFormOptions<TValues>,
|
|
32
32
|
): FormState<TValues> {
|
|
33
|
-
const {
|
|
34
|
-
initialValues,
|
|
35
|
-
onSubmit,
|
|
36
|
-
validators,
|
|
37
|
-
schema,
|
|
38
|
-
validateOn = 'blur',
|
|
39
|
-
debounceMs,
|
|
40
|
-
} = options
|
|
33
|
+
const { initialValues, onSubmit, validators, schema, validateOn = "blur", debounceMs } = options
|
|
41
34
|
|
|
42
35
|
// Build field states
|
|
43
36
|
const fieldEntries = Object.entries(initialValues) as [
|
|
@@ -48,9 +41,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
48
41
|
const fields = {} as { [K in keyof TValues]: FieldState<TValues[K]> }
|
|
49
42
|
|
|
50
43
|
// Debounce timers per field (only allocated when debounceMs is set)
|
|
51
|
-
const debounceTimers: Partial<
|
|
52
|
-
Record<keyof TValues, ReturnType<typeof setTimeout>>
|
|
53
|
-
> = {}
|
|
44
|
+
const debounceTimers: Partial<Record<keyof TValues, ReturnType<typeof setTimeout>>> = {}
|
|
54
45
|
|
|
55
46
|
// Validation version per field — used to discard stale async results
|
|
56
47
|
const validationVersions: Partial<Record<keyof TValues, number>> = {}
|
|
@@ -60,8 +51,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
60
51
|
const values = {} as TValues
|
|
61
52
|
for (const [name] of fieldEntries) {
|
|
62
53
|
;(values as Record<string, unknown>)[name] =
|
|
63
|
-
fields[name]?.value.peek() ??
|
|
64
|
-
(initialValues as Record<string, unknown>)[name]
|
|
54
|
+
fields[name]?.value.peek() ?? (initialValues as Record<string, unknown>)[name]
|
|
65
55
|
}
|
|
66
56
|
return values
|
|
67
57
|
}
|
|
@@ -127,7 +117,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
127
117
|
: runValidation
|
|
128
118
|
|
|
129
119
|
// Auto-validate on change if configured
|
|
130
|
-
if (validateOn ===
|
|
120
|
+
if (validateOn === "change") {
|
|
131
121
|
effect(() => {
|
|
132
122
|
const v = valueSig()
|
|
133
123
|
validateField(v)
|
|
@@ -146,7 +136,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
146
136
|
},
|
|
147
137
|
setTouched: () => {
|
|
148
138
|
touchedSig.set(true)
|
|
149
|
-
if (validateOn ===
|
|
139
|
+
if (validateOn === "blur") {
|
|
150
140
|
validateField(valueSig.peek())
|
|
151
141
|
}
|
|
152
142
|
},
|
|
@@ -216,18 +206,13 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
216
206
|
validationVersions[name] = (validationVersions[name] ?? 0) + 1
|
|
217
207
|
const currentVersion = validationVersions[name]
|
|
218
208
|
try {
|
|
219
|
-
const error = await fieldValidator(
|
|
220
|
-
fields[name].value.peek(),
|
|
221
|
-
allValues,
|
|
222
|
-
)
|
|
209
|
+
const error = await fieldValidator(fields[name].value.peek(), allValues)
|
|
223
210
|
if (validationVersions[name] === currentVersion) {
|
|
224
211
|
fields[name].error.set(error)
|
|
225
212
|
}
|
|
226
213
|
} catch (err) {
|
|
227
214
|
if (validationVersions[name] === currentVersion) {
|
|
228
|
-
fields[name].error.set(
|
|
229
|
-
err instanceof Error ? err.message : String(err),
|
|
230
|
-
)
|
|
215
|
+
fields[name].error.set(err instanceof Error ? err.message : String(err))
|
|
231
216
|
}
|
|
232
217
|
}
|
|
233
218
|
}
|
|
@@ -241,10 +226,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
241
226
|
const schemaErrors = await schema(allValues)
|
|
242
227
|
for (const [name] of fieldEntries) {
|
|
243
228
|
const schemaError = schemaErrors[name]
|
|
244
|
-
if (
|
|
245
|
-
schemaError !== undefined &&
|
|
246
|
-
fields[name].error.peek() === undefined
|
|
247
|
-
) {
|
|
229
|
+
if (schemaError !== undefined && fields[name].error.peek() === undefined) {
|
|
248
230
|
fields[name].error.set(schemaError)
|
|
249
231
|
}
|
|
250
232
|
}
|
|
@@ -266,7 +248,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
266
248
|
}
|
|
267
249
|
|
|
268
250
|
const handleSubmit = async (e?: Event) => {
|
|
269
|
-
if (e && typeof e.preventDefault ===
|
|
251
|
+
if (e && typeof e.preventDefault === "function") {
|
|
270
252
|
e.preventDefault()
|
|
271
253
|
}
|
|
272
254
|
|
|
@@ -301,13 +283,10 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
301
283
|
submitError.set(undefined)
|
|
302
284
|
}
|
|
303
285
|
|
|
304
|
-
const setFieldValue = <K extends keyof TValues>(
|
|
305
|
-
field: K,
|
|
306
|
-
value: TValues[K],
|
|
307
|
-
) => {
|
|
286
|
+
const setFieldValue = <K extends keyof TValues>(field: K, value: TValues[K]) => {
|
|
308
287
|
if (!fields[field]) {
|
|
309
288
|
throw new Error(
|
|
310
|
-
`[@pyreon/form] Field "${String(field)}" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(
|
|
289
|
+
`[@pyreon/form] Field "${String(field)}" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(", ")}`,
|
|
311
290
|
)
|
|
312
291
|
}
|
|
313
292
|
fields[field].setValue(value)
|
|
@@ -316,15 +295,13 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
316
295
|
const setFieldError = (field: keyof TValues, error: ValidationError) => {
|
|
317
296
|
if (!fields[field]) {
|
|
318
297
|
throw new Error(
|
|
319
|
-
`[@pyreon/form] Field "${String(field)}" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(
|
|
298
|
+
`[@pyreon/form] Field "${String(field)}" does not exist. Available fields: ${fieldEntries.map(([n]) => n).join(", ")}`,
|
|
320
299
|
)
|
|
321
300
|
}
|
|
322
301
|
fields[field].error.set(error)
|
|
323
302
|
}
|
|
324
303
|
|
|
325
|
-
const setErrors = (
|
|
326
|
-
errors: Partial<Record<keyof TValues, ValidationError>>,
|
|
327
|
-
) => {
|
|
304
|
+
const setErrors = (errors: Partial<Record<keyof TValues, ValidationError>>) => {
|
|
328
305
|
for (const [name, error] of Object.entries(errors)) {
|
|
329
306
|
setFieldError(name as keyof TValues, error as ValidationError)
|
|
330
307
|
}
|
|
@@ -347,9 +324,9 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
347
324
|
|
|
348
325
|
const register = <K extends keyof TValues & string>(
|
|
349
326
|
field: K,
|
|
350
|
-
opts?: { type?:
|
|
327
|
+
opts?: { type?: "checkbox" | "number" },
|
|
351
328
|
): FieldRegisterProps<TValues[K]> => {
|
|
352
|
-
const cacheKey = `${field}:${opts?.type ??
|
|
329
|
+
const cacheKey = `${field}:${opts?.type ?? "text"}`
|
|
353
330
|
const cached = registerCache.get(cacheKey)
|
|
354
331
|
if (cached) return cached as FieldRegisterProps<TValues[K]>
|
|
355
332
|
|
|
@@ -358,13 +335,11 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
358
335
|
value: fieldState.value,
|
|
359
336
|
onInput: (e: Event) => {
|
|
360
337
|
const target = e.target as HTMLInputElement
|
|
361
|
-
if (opts?.type ===
|
|
338
|
+
if (opts?.type === "checkbox") {
|
|
362
339
|
fieldState.setValue(target.checked as TValues[K])
|
|
363
|
-
} else if (opts?.type ===
|
|
340
|
+
} else if (opts?.type === "number") {
|
|
364
341
|
const num = target.valueAsNumber
|
|
365
|
-
fieldState.setValue(
|
|
366
|
-
(Number.isNaN(num) ? target.value : num) as TValues[K],
|
|
367
|
-
)
|
|
342
|
+
fieldState.setValue((Number.isNaN(num) ? target.value : num) as TValues[K])
|
|
368
343
|
} else {
|
|
369
344
|
fieldState.setValue(target.value as TValues[K])
|
|
370
345
|
}
|
|
@@ -374,7 +349,7 @@ export function useForm<TValues extends Record<string, unknown>>(
|
|
|
374
349
|
},
|
|
375
350
|
}
|
|
376
351
|
|
|
377
|
-
if (opts?.type ===
|
|
352
|
+
if (opts?.type === "checkbox") {
|
|
378
353
|
props.checked = computed(() => Boolean(fieldState.value()))
|
|
379
354
|
}
|
|
380
355
|
|
|
@@ -420,7 +395,7 @@ function structuredEqual(a: unknown, b: unknown, depth = 0): boolean {
|
|
|
420
395
|
return true
|
|
421
396
|
}
|
|
422
397
|
|
|
423
|
-
if (typeof a ===
|
|
398
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
424
399
|
const aObj = a as Record<string, unknown>
|
|
425
400
|
const bObj = b as Record<string, unknown>
|
|
426
401
|
const aKeys = Object.keys(aObj)
|
package/src/use-watch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Computed, Signal } from
|
|
2
|
-
import { computed } from
|
|
3
|
-
import type { FormState } from
|
|
1
|
+
import type { Computed, Signal } from "@pyreon/reactivity"
|
|
2
|
+
import { computed } from "@pyreon/reactivity"
|
|
3
|
+
import type { FormState } from "./types"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Watch specific field values reactively. Returns a computed signal
|
|
@@ -21,27 +21,21 @@ import type { FormState } from './types'
|
|
|
21
21
|
* const all = useWatch(form)
|
|
22
22
|
* // all() => { email: '...', password: '...' }
|
|
23
23
|
*/
|
|
24
|
-
export function useWatch<
|
|
25
|
-
|
|
26
|
-
K
|
|
27
|
-
|
|
24
|
+
export function useWatch<TValues extends Record<string, unknown>, K extends keyof TValues & string>(
|
|
25
|
+
form: FormState<TValues>,
|
|
26
|
+
name: K,
|
|
27
|
+
): Signal<TValues[K]>
|
|
28
28
|
|
|
29
29
|
export function useWatch<
|
|
30
30
|
TValues extends Record<string, unknown>,
|
|
31
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]> }
|
|
32
|
+
>(form: FormState<TValues>, names: K): { [I in keyof K]: Signal<TValues[K[I] & keyof TValues]> }
|
|
36
33
|
|
|
37
34
|
export function useWatch<TValues extends Record<string, unknown>>(
|
|
38
35
|
form: FormState<TValues>,
|
|
39
36
|
): Computed<TValues>
|
|
40
37
|
|
|
41
|
-
export function useWatch<
|
|
42
|
-
TValues extends Record<string, unknown>,
|
|
43
|
-
K extends keyof TValues & string,
|
|
44
|
-
>(
|
|
38
|
+
export function useWatch<TValues extends Record<string, unknown>, K extends keyof TValues & string>(
|
|
45
39
|
form: FormState<TValues>,
|
|
46
40
|
nameOrNames?: K | K[],
|
|
47
41
|
): Signal<TValues[K]> | Signal<TValues[K]>[] | Computed<TValues> {
|
|
@@ -49,8 +43,7 @@ export function useWatch<
|
|
|
49
43
|
if (nameOrNames === undefined) {
|
|
50
44
|
return computed(() => {
|
|
51
45
|
const result = {} as TValues
|
|
52
|
-
for (const key of Object.keys(form.fields) as (keyof TValues &
|
|
53
|
-
string)[]) {
|
|
46
|
+
for (const key of Object.keys(form.fields) as (keyof TValues & string)[]) {
|
|
54
47
|
;(result as Record<string, unknown>)[key] = form.fields[key].value()
|
|
55
48
|
}
|
|
56
49
|
return result
|
|
@@ -59,9 +52,7 @@ export function useWatch<
|
|
|
59
52
|
|
|
60
53
|
// Watch multiple fields
|
|
61
54
|
if (Array.isArray(nameOrNames)) {
|
|
62
|
-
return nameOrNames.map((name) => form.fields[name].value) as Signal<
|
|
63
|
-
TValues[K]
|
|
64
|
-
>[]
|
|
55
|
+
return nameOrNames.map((name) => form.fields[name].value) as Signal<TValues[K]>[]
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
// Watch single field
|