@pyreon/form 0.10.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/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Computed, Signal } from '@pyreon/reactivity'
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?: 'checkbox' | 'number' },
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?: 'blur' | 'change' | 'submit'
113
+ validateOn?: "blur" | "change" | "submit"
114
114
  /** Debounce delay in ms for validators (useful for async validators). */
115
115
  debounceMs?: number
116
116
  }
@@ -1,5 +1,5 @@
1
- import type { Computed, Signal } from '@pyreon/reactivity'
2
- import { computed, signal } from '@pyreon/reactivity'
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 '@pyreon/reactivity'
2
- import { computed } from '@pyreon/reactivity'
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?: 'checkbox' }) => FieldRegisterProps<T>
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
- TValues extends Record<string, unknown>,
50
- K extends keyof TValues & string,
51
- >(form: FormState<TValues>, name: K): UseFieldResult<TValues[K]> {
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)
@@ -1,6 +1,6 @@
1
- import type { Computed } from '@pyreon/reactivity'
2
- import { computed } from '@pyreon/reactivity'
3
- import type { FormState, ValidationError } from './types'
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 '@pyreon/core'
2
- import type { Signal } from '@pyreon/reactivity'
3
- import { computed, effect, signal } from '@pyreon/reactivity'
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 './types'
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 === 'change') {
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 === 'blur') {
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 === 'function') {
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?: 'checkbox' | 'number' },
327
+ opts?: { type?: "checkbox" | "number" },
351
328
  ): FieldRegisterProps<TValues[K]> => {
352
- const cacheKey = `${field}:${opts?.type ?? 'text'}`
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 === 'checkbox') {
338
+ if (opts?.type === "checkbox") {
362
339
  fieldState.setValue(target.checked as TValues[K])
363
- } else if (opts?.type === 'number') {
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 === 'checkbox') {
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 === 'object' && typeof b === 'object') {
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 '@pyreon/reactivity'
2
- import { computed } from '@pyreon/reactivity'
3
- import type { FormState } from './types'
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
- TValues extends Record<string, unknown>,
26
- K extends keyof TValues & string,
27
- >(form: FormState<TValues>, name: K): Signal<TValues[K]>
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