@octanejs/formisch 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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/UPSTREAM.md +95 -0
  4. package/package.json +65 -0
  5. package/src/components/Field/Field.tsrx +22 -0
  6. package/src/components/Field/Field.tsrx.d.ts +17 -0
  7. package/src/components/Field/index.ts +1 -0
  8. package/src/components/FieldArray/FieldArray.tsrx +22 -0
  9. package/src/components/FieldArray/FieldArray.tsrx.d.ts +18 -0
  10. package/src/components/FieldArray/index.ts +1 -0
  11. package/src/components/Form/Form.tsrx +28 -0
  12. package/src/components/Form/Form.tsrx.d.ts +15 -0
  13. package/src/components/Form/index.ts +1 -0
  14. package/src/components/index.ts +3 -0
  15. package/src/core/array/copyItemState/copyItemState.ts +101 -0
  16. package/src/core/array/copyItemState/index.ts +1 -0
  17. package/src/core/array/index.ts +3 -0
  18. package/src/core/array/resetItemState/index.ts +1 -0
  19. package/src/core/array/resetItemState/resetItemState.ts +172 -0
  20. package/src/core/array/swapItemState/index.ts +1 -0
  21. package/src/core/array/swapItemState/swapItemState.ts +138 -0
  22. package/src/core/field/focusFieldElement/focusFieldElement.ts +32 -0
  23. package/src/core/field/focusFieldElement/index.ts +1 -0
  24. package/src/core/field/getDirtyFieldInput/getDirtyFieldInput.ts +66 -0
  25. package/src/core/field/getDirtyFieldInput/index.ts +1 -0
  26. package/src/core/field/getElementInput/getElementInput.ts +78 -0
  27. package/src/core/field/getElementInput/index.ts +1 -0
  28. package/src/core/field/getFieldBool/getFieldBool.ts +22 -0
  29. package/src/core/field/getFieldBool/index.ts +1 -0
  30. package/src/core/field/getFieldInput/getFieldInput.ts +52 -0
  31. package/src/core/field/getFieldInput/index.ts +1 -0
  32. package/src/core/field/getFieldStore/getFieldStore.ts +34 -0
  33. package/src/core/field/getFieldStore/index.ts +1 -0
  34. package/src/core/field/index.ts +11 -0
  35. package/src/core/field/initializeFieldStore/index.ts +1 -0
  36. package/src/core/field/initializeFieldStore/initializeFieldStore.ts +325 -0
  37. package/src/core/field/setFieldBool/index.ts +1 -0
  38. package/src/core/field/setFieldBool/setFieldBool.ts +29 -0
  39. package/src/core/field/setFieldInput/index.ts +1 -0
  40. package/src/core/field/setFieldInput/setFieldInput.ts +180 -0
  41. package/src/core/field/setInitialFieldInput/index.ts +1 -0
  42. package/src/core/field/setInitialFieldInput/setInitialFieldInput.ts +99 -0
  43. package/src/core/field/walkFieldStore/index.ts +1 -0
  44. package/src/core/field/walkFieldStore/walkFieldStore.ts +49 -0
  45. package/src/core/form/createFormStore/createFormStore.ts +56 -0
  46. package/src/core/form/createFormStore/index.ts +1 -0
  47. package/src/core/form/decodeFormData/decodeFormData.ts +436 -0
  48. package/src/core/form/decodeFormData/index.ts +1 -0
  49. package/src/core/form/index.ts +4 -0
  50. package/src/core/form/validateFormInput/index.ts +1 -0
  51. package/src/core/form/validateFormInput/validateFormInput.ts +138 -0
  52. package/src/core/form/validateIfRequired/index.ts +1 -0
  53. package/src/core/form/validateIfRequired/validateIfRequired.ts +31 -0
  54. package/src/core/framework/index.ts +80 -0
  55. package/src/core/index.ts +6 -0
  56. package/src/core/types/field/field.ts +201 -0
  57. package/src/core/types/field/index.ts +1 -0
  58. package/src/core/types/form/form.ts +140 -0
  59. package/src/core/types/form/index.ts +1 -0
  60. package/src/core/types/index.ts +6 -0
  61. package/src/core/types/path/index.ts +10 -0
  62. package/src/core/types/path/path.ts +301 -0
  63. package/src/core/types/schema/index.ts +1 -0
  64. package/src/core/types/schema/schema.ts +18 -0
  65. package/src/core/types/signal/index.ts +1 -0
  66. package/src/core/types/signal/signal.ts +23 -0
  67. package/src/core/types/utils/index.ts +1 -0
  68. package/src/core/types/utils/utils.ts +46 -0
  69. package/src/core/values.ts +4 -0
  70. package/src/hooks/index.ts +3 -0
  71. package/src/hooks/useField/index.ts +1 -0
  72. package/src/hooks/useField/useField.ts +114 -0
  73. package/src/hooks/useFieldArray/index.ts +1 -0
  74. package/src/hooks/useFieldArray/useFieldArray.ts +63 -0
  75. package/src/hooks/useForm/index.ts +1 -0
  76. package/src/hooks/useForm/useForm.ts +68 -0
  77. package/src/hooks/useSignals/index.ts +1 -0
  78. package/src/hooks/useSignals/useSignals.ts +31 -0
  79. package/src/index.ts +19 -0
  80. package/src/internal.ts +32 -0
  81. package/src/methods/focus/focus.ts +35 -0
  82. package/src/methods/focus/index.ts +1 -0
  83. package/src/methods/getDeepErrorEntries/getDeepErrorEntries.ts +108 -0
  84. package/src/methods/getDeepErrorEntries/index.ts +1 -0
  85. package/src/methods/getDeepErrors/getDeepErrors.ts +90 -0
  86. package/src/methods/getDeepErrors/index.ts +1 -0
  87. package/src/methods/getDirtyInput/getDirtyInput.ts +87 -0
  88. package/src/methods/getDirtyInput/index.ts +1 -0
  89. package/src/methods/getDirtyPaths/getDirtyPaths.ts +123 -0
  90. package/src/methods/getDirtyPaths/index.ts +1 -0
  91. package/src/methods/getErrors/getErrors.ts +70 -0
  92. package/src/methods/getErrors/index.ts +1 -0
  93. package/src/methods/getInput/getInput.ts +75 -0
  94. package/src/methods/getInput/index.ts +1 -0
  95. package/src/methods/handleSubmit/handleSubmit.ts +83 -0
  96. package/src/methods/handleSubmit/index.ts +1 -0
  97. package/src/methods/index.ts +23 -0
  98. package/src/methods/insert/index.ts +1 -0
  99. package/src/methods/insert/insert.ts +134 -0
  100. package/src/methods/isDirty/index.ts +1 -0
  101. package/src/methods/isDirty/isDirty.ts +72 -0
  102. package/src/methods/isEdited/index.ts +1 -0
  103. package/src/methods/isEdited/isEdited.ts +72 -0
  104. package/src/methods/isTouched/index.ts +1 -0
  105. package/src/methods/isTouched/isTouched.ts +72 -0
  106. package/src/methods/isValid/index.ts +1 -0
  107. package/src/methods/isValid/isValid.ts +74 -0
  108. package/src/methods/move/index.ts +1 -0
  109. package/src/methods/move/move.ts +124 -0
  110. package/src/methods/pickDirty/index.ts +1 -0
  111. package/src/methods/pickDirty/pickDirty.ts +87 -0
  112. package/src/methods/remove/index.ts +1 -0
  113. package/src/methods/remove/remove.ts +76 -0
  114. package/src/methods/replace/index.ts +1 -0
  115. package/src/methods/replace/replace.ts +80 -0
  116. package/src/methods/reset/index.ts +1 -0
  117. package/src/methods/reset/reset.ts +216 -0
  118. package/src/methods/setErrors/index.ts +1 -0
  119. package/src/methods/setErrors/setErrors.ts +63 -0
  120. package/src/methods/setInput/index.ts +1 -0
  121. package/src/methods/setInput/setInput.ts +87 -0
  122. package/src/methods/submit/index.ts +1 -0
  123. package/src/methods/submit/submit.ts +11 -0
  124. package/src/methods/swap/index.ts +1 -0
  125. package/src/methods/swap/swap.ts +85 -0
  126. package/src/methods/validate/index.ts +1 -0
  127. package/src/methods/validate/validate.ts +34 -0
  128. package/src/types/field.ts +48 -0
  129. package/src/types/form.ts +12 -0
  130. package/src/types/index.ts +2 -0
@@ -0,0 +1,301 @@
1
+ import type { IsAny, IsNever } from '../utils/index.ts';
2
+
3
+ /**
4
+ * Path key type.
5
+ */
6
+ export type PathKey = string | number;
7
+
8
+ /**
9
+ * Path type.
10
+ */
11
+ export type Path = readonly PathKey[];
12
+
13
+ /**
14
+ * Required path type.
15
+ */
16
+ export type RequiredPath = readonly [PathKey, ...Path];
17
+
18
+ /**
19
+ * Extracts the exact keys of a tuple, array or object. Tuples return their
20
+ * literal numeric indices, dynamic arrays return `number`, objects return
21
+ * their `keyof` keys, and any other input returns `never`.
22
+ */
23
+ export type ExactKeysOf<TValue> =
24
+ IsAny<TValue> extends true
25
+ ? never
26
+ : TValue extends readonly unknown[]
27
+ ? number extends TValue['length']
28
+ ? number
29
+ : {
30
+ [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}` ? TIndex : never;
31
+ }[number]
32
+ : TValue extends Record<PropertyKey, unknown>
33
+ ? keyof TValue & PathKey
34
+ : never;
35
+
36
+ /**
37
+ * Returns the flat object of all indexable properties of `TValue`. For object
38
+ * unions, properties from every member are merged so that any single property
39
+ * is accessible. For primitives and other non-indexable types, the result is
40
+ * `{}`.
41
+ *
42
+ * Hint: This is necessary to make properties accessible across union members.
43
+ * By default, properties that do not exist in all union options are not
44
+ * accessible and result in "any" when accessed.
45
+ */
46
+ export type PropertiesOf<TValue> = {
47
+ [TKey in ExactKeysOf<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never;
48
+ };
49
+
50
+ /**
51
+ * Lazily evaluates only the first valid path segment based on the given value.
52
+ */
53
+ type LazyPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> =
54
+ // If path to check is empty, return last valid path
55
+ TPathToCheck extends readonly []
56
+ ? TValidPath
57
+ : // If first key of path to check is valid, continue with next key
58
+ TPathToCheck extends readonly [
59
+ infer TFirstKey extends ExactKeysOf<TValue>,
60
+ ...infer TPathRest extends Path,
61
+ ]
62
+ ? LazyPath<
63
+ Required<PropertiesOf<TValue>[TFirstKey]>,
64
+ TPathRest,
65
+ readonly [...TValidPath, TFirstKey]
66
+ >
67
+ : // If current value has valid keys, return them
68
+ IsNever<ExactKeysOf<TValue>> extends false
69
+ ? readonly [...TValidPath, ExactKeysOf<TValue>]
70
+ : // Otherwise, return only last valid path
71
+ TValidPath;
72
+
73
+ /**
74
+ * Returns the path if valid, otherwise the first possible valid path based on
75
+ * the given value.
76
+ */
77
+ export type ValidPath<TValue, TPath extends RequiredPath> =
78
+ TPath extends LazyPath<Required<TValue>, TPath> ? TPath : LazyPath<Required<TValue>, TPath>;
79
+
80
+ /**
81
+ * Detects whether the consuming project is configured with
82
+ * `exactOptionalPropertyTypes: true`.
83
+ *
84
+ * Hint: If `false` the built-in `Required<T>` strips `| undefined` from
85
+ * optional properties, so `Required<{ key?: undefined }>['key']` collapses
86
+ * to `never` — under strict mode the same expression yields `undefined`.
87
+ */
88
+ type IsExactOptionalProps = Required<{ key?: undefined }>['key'] extends never ? false : true;
89
+
90
+ /**
91
+ * Like the built-in `Required<T>`, but preserves `| undefined` in two
92
+ * places where `Required<T>` strips it:
93
+ *
94
+ * 1. Optional property values under `exactOptionalPropertyTypes: false`
95
+ * — without this, input typings for `v.optional`/`v.nullish` schemas
96
+ * narrow incorrectly (issue #15).
97
+ * 2. Array/tuple element types — e.g. `(string | undefined)[]` stays
98
+ * `(string | undefined)[]` instead of becoming `string[]`. Arrays
99
+ * fall through unchanged because they only have a numeric index
100
+ * signature and don't structurally extend `Record<PropertyKey,
101
+ * unknown>` (which requires string keys).
102
+ */
103
+ export type ExactRequired<TValue> =
104
+ TValue extends Record<PropertyKey, unknown>
105
+ ? IsExactOptionalProps extends true
106
+ ? // Under `exactOptionalPropertyTypes: true`, `Required<T>` already
107
+ // preserves the exact value of optional properties — delegate to it
108
+ // so `v.exactOptional` keeps "must be `T`, never `undefined`".
109
+ Required<TValue>
110
+ : // Under loose mode, `Required<T>` strips `| undefined`. Re-derive
111
+ // it via indexed access: `TValue[TKey]` on an optional key gives
112
+ // `T | undefined` regardless of mode. Iterating `keyof Required<T>`
113
+ // remains homomorphic over `TValue`, so `readonly` is preserved.
114
+ { [TKey in keyof Required<TValue>]: TValue[TKey] }
115
+ : TValue;
116
+
117
+ /**
118
+ * Extracts the value type at the given path.
119
+ */
120
+ export type PathValue<TValue, TPath extends Path> = TPath extends readonly [
121
+ infer TKey,
122
+ ...infer TRest extends Path,
123
+ ]
124
+ ? TKey extends ExactKeysOf<ExactRequired<TValue>>
125
+ ? PathValue<PropertiesOf<ExactRequired<TValue>>[TKey], TRest>
126
+ : unknown
127
+ : TValue;
128
+
129
+ /**
130
+ * Checks whether a value is a dynamic array or contains one anywhere in its
131
+ * shape. A fixed-length tuple is not itself a dynamic array, but it counts when
132
+ * it contains one, so paths can still navigate through tuples to reach nested
133
+ * arrays.
134
+ *
135
+ * Hint: The inner conditionals (`TValue extends readonly unknown[]` and
136
+ * `TValue extends Record<PropertyKey, unknown>`) distribute over union members,
137
+ * so the inner expression returns the union of each member's result (e.g.
138
+ * `true | false` when some members contain arrays and others don't).
139
+ * Downstream code uses `IsOrHasArray<T> extends true`, but
140
+ * `boolean extends true` is `false` — so we collapse the result via
141
+ * `true extends ...`, which is `true` whenever at least one union member
142
+ * contributed `true`.
143
+ */
144
+ type IsOrHasArray<TValue> = true extends (
145
+ IsAny<TValue> extends true
146
+ ? false
147
+ : TValue extends readonly unknown[]
148
+ ? // A dynamic array counts directly; a tuple only if an element is or
149
+ // contains a dynamic array
150
+ number extends TValue['length']
151
+ ? true
152
+ : IsOrHasArray<TValue[number]>
153
+ : TValue extends Record<PropertyKey, unknown>
154
+ ? {
155
+ [TKey in keyof TValue]: IsOrHasArray<TValue[TKey]>;
156
+ }[keyof TValue]
157
+ : false
158
+ )
159
+ ? true
160
+ : false;
161
+
162
+ /**
163
+ * Extracts the exact keys of a tuple, array or object that contain arrays.
164
+ */
165
+ export type ExactKeysOfArrayPath<TValue> =
166
+ IsAny<TValue> extends true
167
+ ? never
168
+ : TValue extends readonly (infer TItem)[]
169
+ ? number extends TValue['length']
170
+ ? IsOrHasArray<TItem> extends true
171
+ ? number
172
+ : never
173
+ : {
174
+ [TKey in keyof TValue]: TKey extends `${infer TIndex extends number}`
175
+ ? IsOrHasArray<NonNullable<TValue[TKey]>> extends true
176
+ ? TIndex
177
+ : never
178
+ : never;
179
+ }[number]
180
+ : TValue extends Record<PropertyKey, unknown>
181
+ ? {
182
+ [TKey in keyof TValue]: IsOrHasArray<NonNullable<TValue[TKey]>> extends true
183
+ ? TKey
184
+ : never;
185
+ }[keyof TValue] &
186
+ PathKey
187
+ : never;
188
+
189
+ /**
190
+ * Returns the flat object of indexable properties of `TValue` whose values
191
+ * are or contain arrays. Mirrors `PropertiesOf` but keyed by
192
+ * `ExactKeysOfArrayPath` so the lookup is provably valid for array-path
193
+ * navigation in `LazyArrayPath`.
194
+ */
195
+ export type PropertiesOfArrayPath<TValue> = {
196
+ [TKey in ExactKeysOfArrayPath<TValue>]: TValue extends Record<TKey, infer TItem> ? TItem : never;
197
+ };
198
+
199
+ /**
200
+ * Lazily evaluates only the first valid array path segment based on the given value.
201
+ */
202
+ type LazyArrayPath<TValue, TPathToCheck extends Path, TValidPath extends Path = readonly []> =
203
+ // If path to check is empty, return possible array paths
204
+ TPathToCheck extends readonly []
205
+ ? TValue extends readonly unknown[]
206
+ ? // Only accept dynamic arrays; tuples have a fixed arity, so navigate
207
+ // into their indices instead of accepting the tuple as a field array
208
+ number extends TValue['length']
209
+ ? TValidPath
210
+ : IsNever<ExactKeysOfArrayPath<TValue>> extends false
211
+ ? readonly [...TValidPath, ExactKeysOfArrayPath<TValue>]
212
+ : never
213
+ : readonly [...TValidPath, ExactKeysOfArrayPath<TValue>]
214
+ : // If first key of path to check is valid, continue with next key
215
+ TPathToCheck extends readonly [
216
+ infer TFirstKey extends ExactKeysOfArrayPath<TValue>,
217
+ ...infer TPathRest extends Path,
218
+ ]
219
+ ? LazyArrayPath<
220
+ Required<PropertiesOfArrayPath<TValue>[TFirstKey]>,
221
+ TPathRest,
222
+ readonly [...TValidPath, TFirstKey]
223
+ >
224
+ : // If current value has valid array keys, return them
225
+ IsNever<ExactKeysOfArrayPath<TValue>> extends false
226
+ ? readonly [...TValidPath, ExactKeysOfArrayPath<TValue>]
227
+ : // Otherwise, no valid array keys exist
228
+ never;
229
+
230
+ /**
231
+ * Returns the path if valid, otherwise the first possible valid array path
232
+ * based on the given value.
233
+ */
234
+ export type ValidArrayPath<TValue, TPath extends RequiredPath> =
235
+ TPath extends LazyArrayPath<Required<TValue>, TPath>
236
+ ? TPath
237
+ : LazyArrayPath<Required<TValue>, TPath>;
238
+
239
+ /**
240
+ * Recursive helper for `DirtyPath` that prepends `TKey` to each deeper path,
241
+ * or falls through to `never` when the child is not an object.
242
+ */
243
+ type DeepDirtyPath<TChild, TKey extends PathKey, TDepth extends 0[]> =
244
+ TChild extends Record<PropertyKey, unknown>
245
+ ? readonly [TKey, ...DirtyPath<TChild, [...TDepth, 0]>]
246
+ : never;
247
+
248
+ /**
249
+ * Returns the union of all `RequiredPath`s that `getDirtyPaths` can emit
250
+ * for a given input type. Object fields contribute their own path and the
251
+ * paths of their descendants; arrays and tuples are atomic and contribute
252
+ * only their own path, because dirty arrays are returned as complete units.
253
+ *
254
+ * Narrowing is exact for the first 5 levels of nesting; deeper paths fall
255
+ * back to `RequiredPath` to keep the result a complete superset of any
256
+ * path the runtime can address.
257
+ *
258
+ * Hint: Arrays and tuples are atomic because they don't structurally
259
+ * extend `Record<PropertyKey, unknown>` and so fall through to `never`
260
+ * via `DeepDirtyPath` — no explicit array check is needed. `TDepth` is
261
+ * a tuple-length counter capped at 5 to bound TypeScript instantiation
262
+ * cost.
263
+ */
264
+ export type DirtyPath<TValue, TDepth extends 0[] = []> = TDepth['length'] extends 5
265
+ ? RequiredPath
266
+ : TValue extends Record<PropertyKey, unknown>
267
+ ? {
268
+ [TKey in ExactKeysOf<TValue>]:
269
+ readonly [TKey] | DeepDirtyPath<NonNullable<PropertiesOf<TValue>[TKey]>, TKey, TDepth>;
270
+ }[ExactKeysOf<TValue>]
271
+ : never;
272
+
273
+ /**
274
+ * Recursive helper for `FieldPath` that prepends `TKey` to each deeper field
275
+ * path, or falls through to `never` when the child is a leaf value.
276
+ */
277
+ type DeepFieldPath<TChild, TKey extends PathKey, TDepth extends 0[]> = TChild extends
278
+ readonly unknown[] | Record<PropertyKey, unknown>
279
+ ? readonly [TKey, ...FieldPath<TChild, [...TDepth, 0]>]
280
+ : never;
281
+
282
+ /**
283
+ * Returns the union of all `RequiredPath`s that address a field within the
284
+ * given input type. Object and array fields contribute their own path and the
285
+ * paths of their descendants; unlike `DirtyPath`, arrays are recursed into so
286
+ * that fields at any depth, including array items, can be addressed. Leaf
287
+ * values contribute only their own path (emitted by their parent).
288
+ *
289
+ * Narrowing is exact for the first 5 levels of nesting; deeper paths fall
290
+ * back to `RequiredPath` to keep the result a complete superset of any path
291
+ * the runtime can address. `TDepth` is a tuple-length counter capped at 5 to
292
+ * bound TypeScript instantiation cost.
293
+ */
294
+ export type FieldPath<TValue, TDepth extends 0[] = []> = TDepth['length'] extends 5
295
+ ? RequiredPath
296
+ : TValue extends readonly unknown[] | Record<PropertyKey, unknown>
297
+ ? {
298
+ [TKey in ExactKeysOf<TValue>]:
299
+ readonly [TKey] | DeepFieldPath<NonNullable<PropertiesOf<TValue>[TKey]>, TKey, TDepth>;
300
+ }[ExactKeysOf<TValue>]
301
+ : never;
@@ -0,0 +1 @@
1
+ export * from './schema.ts';
@@ -0,0 +1,18 @@
1
+ import type * as v from 'valibot';
2
+
3
+ /**
4
+ * Schema type.
5
+ */
6
+ export type Schema = v.GenericSchema | v.GenericSchemaAsync;
7
+
8
+ /**
9
+ * Form schema type.
10
+ *
11
+ * Forms must have an object root, so this is constrained structurally to a
12
+ * schema with an object output (sync or async) rather than to specific Valibot
13
+ * schema types. Staying decoupled from concrete Valibot types lets this later
14
+ * move to Standard Schema (e.g. to also support Zod or ArkType). Use
15
+ * {@link Schema} for nested field schemas.
16
+ */
17
+ export type FormSchema =
18
+ v.GenericSchema<Record<string, unknown>> | v.GenericSchemaAsync<Record<string, unknown>>;
@@ -0,0 +1 @@
1
+ export * from './signal.ts';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Signal interface.
3
+ */
4
+ export interface Signal<T> {
5
+ /**
6
+ * The value of the signal.
7
+ */
8
+ value: T;
9
+ }
10
+
11
+ /**
12
+ * Batch interface.
13
+ */
14
+ export interface Batch {
15
+ <T>(fn: () => T): T;
16
+ }
17
+
18
+ /**
19
+ * Untrack interface.
20
+ */
21
+ export interface Untrack {
22
+ <T>(fn: () => T): T;
23
+ }
@@ -0,0 +1 @@
1
+ export * from './utils.ts';
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Checks if a type is `any`.
3
+ */
4
+ export type IsAny<T> = 0 extends 1 & T ? true : false;
5
+
6
+ /**
7
+ * Checks if a type is `never`.
8
+ */
9
+ export type IsNever<T> = [T] extends [never] ? true : false;
10
+
11
+ /**
12
+ * Constructs a type that is maybe a promise.
13
+ */
14
+ export type MaybePromise<T> = T | Promise<T>;
15
+
16
+ /**
17
+ * Makes all properties deeply optional.
18
+ */
19
+ export type DeepPartial<TValue> = TValue extends Record<PropertyKey, unknown> | readonly unknown[]
20
+ ? { [TKey in keyof TValue]?: DeepPartial<TValue[TKey]> | undefined }
21
+ : TValue | undefined;
22
+
23
+ /**
24
+ * Makes all value properties optional.
25
+ *
26
+ * Hint: For dynamic arrays, only plain objects and nested arrays have their
27
+ * values made optional. Primitives and class instances are kept as-is to avoid
28
+ * types like `(string | undefined)[]`.
29
+ */
30
+ export type PartialValues<TValue> = TValue extends readonly (infer TItem)[]
31
+ ? number extends TValue['length']
32
+ ? // Hint: `infer TItem` is a naked type parameter that distributes the
33
+ // conditional over each union member individually. `TValue[number]`
34
+ // would not distribute, causing unions like `string | { id: number }`
35
+ // to fail the object and array check as a whole and skip recursion
36
+ // entirely, leaving object members like `{ id: number }` unchanged.
37
+ (TItem extends Record<PropertyKey, unknown> | readonly unknown[]
38
+ ? { [TKey in keyof TItem]: PartialValues<TItem[TKey]> }
39
+ : TItem)[]
40
+ : // For tuples, recurse into each position making values optional
41
+ { [TKey in keyof TValue]: PartialValues<TValue[TKey]> }
42
+ : // For objects, recurse into each property making values optional
43
+ TValue extends Record<PropertyKey, unknown>
44
+ ? { [TKey in keyof TValue]: PartialValues<TValue[TKey]> }
45
+ : // For primitives, make the value itself optional
46
+ TValue | undefined;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Internal symbol constant.
3
+ */
4
+ export const INTERNAL = '~internal' as const;
@@ -0,0 +1,3 @@
1
+ export * from './useField/index.ts';
2
+ export * from './useFieldArray/index.ts';
3
+ export * from './useForm/index.ts';
@@ -0,0 +1 @@
1
+ export * from './useField.ts';
@@ -0,0 +1,114 @@
1
+ import { useEffect, useMemo } from 'octane';
2
+ import type * as v from 'valibot';
3
+ import {
4
+ type FieldElement,
5
+ type FormSchema,
6
+ getElementInput,
7
+ getFieldBool,
8
+ getFieldInput,
9
+ getFieldStore,
10
+ INTERNAL,
11
+ type RequiredPath,
12
+ setFieldBool,
13
+ setFieldInput,
14
+ validateIfRequired,
15
+ type ValidPath,
16
+ } from '../../core/index.ts';
17
+ import { splitSlot, subSlot } from '../../internal.ts';
18
+ import type { FieldStore, FormStore } from '../../types/index.ts';
19
+ import { useSignals } from '../useSignals/useSignals.ts';
20
+
21
+ export interface UseFieldConfig<
22
+ TSchema extends FormSchema = FormSchema,
23
+ TFieldPath extends RequiredPath = RequiredPath,
24
+ > {
25
+ readonly path: ValidPath<v.InferInput<TSchema>, TFieldPath>;
26
+ }
27
+
28
+ export function useField<TSchema extends FormSchema, TFieldPath extends RequiredPath>(
29
+ form: FormStore<TSchema>,
30
+ config: UseFieldConfig<TSchema, TFieldPath>,
31
+ ): FieldStore<TSchema, TFieldPath>;
32
+
33
+ export function useField(
34
+ form: FormStore,
35
+ config: UseFieldConfig,
36
+ ...rest: [slot?: symbol]
37
+ ): FieldStore {
38
+ const [, slot] = splitSlot(rest);
39
+ useSignals(subSlot(slot, 'signals'));
40
+
41
+ const internalFormStore = form[INTERNAL];
42
+ const internalFieldStore = getFieldStore(internalFormStore, config.path);
43
+
44
+ useEffect(
45
+ () => () => {
46
+ const elements = internalFieldStore.elements.filter((element) => element.isConnected);
47
+ if (internalFieldStore.elements === internalFieldStore.initialElements) {
48
+ internalFieldStore.initialElements = elements;
49
+ }
50
+ internalFieldStore.elements = elements;
51
+ },
52
+ [internalFieldStore],
53
+ subSlot(slot, 'cleanup'),
54
+ );
55
+
56
+ const updateFromElement = (element: FieldElement) => {
57
+ setFieldInput(internalFormStore, config.path, getElementInput(element, internalFieldStore));
58
+ void validateIfRequired(internalFormStore, internalFieldStore, 'input');
59
+ void validateIfRequired(internalFormStore, internalFieldStore, 'change');
60
+ };
61
+
62
+ return useMemo(
63
+ () => ({
64
+ path: config.path,
65
+ get input() {
66
+ return getFieldInput(internalFieldStore);
67
+ },
68
+ get errors() {
69
+ return internalFieldStore.errors.value;
70
+ },
71
+ get isTouched() {
72
+ return getFieldBool(internalFieldStore, 'isTouched');
73
+ },
74
+ get isEdited() {
75
+ return getFieldBool(internalFieldStore, 'isEdited');
76
+ },
77
+ get isDirty() {
78
+ return getFieldBool(internalFieldStore, 'isDirty');
79
+ },
80
+ get isValid() {
81
+ return !getFieldBool(internalFieldStore, 'errors');
82
+ },
83
+ onChange(value: unknown) {
84
+ setFieldInput(internalFormStore, config.path, value);
85
+ void validateIfRequired(internalFormStore, internalFieldStore, 'input');
86
+ void validateIfRequired(internalFormStore, internalFieldStore, 'change');
87
+ },
88
+ props: {
89
+ name: internalFieldStore.name,
90
+ autoFocus: !!internalFieldStore.errors.value,
91
+ ref(element: FieldElement | null) {
92
+ if (element && !internalFieldStore.elements.includes(element)) {
93
+ internalFieldStore.elements.push(element);
94
+ }
95
+ },
96
+ onFocus() {
97
+ setFieldBool(internalFieldStore, 'isTouched', true);
98
+ void validateIfRequired(internalFormStore, internalFieldStore, 'touch');
99
+ },
100
+ onInput(event: InputEvent) {
101
+ updateFromElement(event.currentTarget as FieldElement);
102
+ },
103
+ onChange(event: Event) {
104
+ updateFromElement(event.currentTarget as FieldElement);
105
+ },
106
+ onBlur() {
107
+ void validateIfRequired(internalFormStore, internalFieldStore, 'blur');
108
+ },
109
+ },
110
+ }),
111
+ [internalFormStore, internalFieldStore],
112
+ subSlot(slot, 'public-store'),
113
+ );
114
+ }
@@ -0,0 +1 @@
1
+ export * from './useFieldArray.ts';
@@ -0,0 +1,63 @@
1
+ import { useMemo } from 'octane';
2
+ import type * as v from 'valibot';
3
+ import {
4
+ type FormSchema,
5
+ getFieldBool,
6
+ getFieldStore,
7
+ INTERNAL,
8
+ type InternalArrayStore,
9
+ type RequiredPath,
10
+ type ValidArrayPath,
11
+ } from '../../core/index.ts';
12
+ import { splitSlot, subSlot } from '../../internal.ts';
13
+ import type { FieldArrayStore, FormStore } from '../../types/index.ts';
14
+ import { useSignals } from '../useSignals/useSignals.ts';
15
+
16
+ export interface UseFieldArrayConfig<
17
+ TSchema extends FormSchema = FormSchema,
18
+ TFieldArrayPath extends RequiredPath = RequiredPath,
19
+ > {
20
+ readonly path: ValidArrayPath<v.InferInput<TSchema>, TFieldArrayPath>;
21
+ }
22
+
23
+ export function useFieldArray<TSchema extends FormSchema, TFieldArrayPath extends RequiredPath>(
24
+ form: FormStore<TSchema>,
25
+ config: UseFieldArrayConfig<TSchema, TFieldArrayPath>,
26
+ ): FieldArrayStore<TSchema, TFieldArrayPath>;
27
+
28
+ export function useFieldArray(
29
+ form: FormStore,
30
+ config: { readonly path: any },
31
+ ...rest: [slot?: symbol]
32
+ ): any {
33
+ const [, slot] = splitSlot(rest);
34
+ useSignals(subSlot(slot, 'signals'));
35
+ const internalFormStore = form[INTERNAL];
36
+ const internalFieldStore = getFieldStore(internalFormStore, config.path) as InternalArrayStore;
37
+
38
+ return useMemo(
39
+ () => ({
40
+ path: config.path,
41
+ get items() {
42
+ return internalFieldStore.items.value;
43
+ },
44
+ get errors() {
45
+ return internalFieldStore.errors.value;
46
+ },
47
+ get isTouched() {
48
+ return getFieldBool(internalFieldStore, 'isTouched');
49
+ },
50
+ get isEdited() {
51
+ return getFieldBool(internalFieldStore, 'isEdited');
52
+ },
53
+ get isDirty() {
54
+ return getFieldBool(internalFieldStore, 'isDirty');
55
+ },
56
+ get isValid() {
57
+ return !getFieldBool(internalFieldStore, 'errors');
58
+ },
59
+ }),
60
+ [internalFormStore, internalFieldStore],
61
+ subSlot(slot, 'public-store'),
62
+ );
63
+ }
@@ -0,0 +1 @@
1
+ export * from './useForm.ts';
@@ -0,0 +1,68 @@
1
+ import { useLayoutEffect, useMemo } from 'octane';
2
+ import * as v from 'valibot';
3
+ import {
4
+ createFormStore,
5
+ type FormConfig,
6
+ type FormSchema,
7
+ getFieldBool,
8
+ INTERNAL,
9
+ validateFormInput,
10
+ } from '../../core/index.ts';
11
+ import { splitSlot, subSlot } from '../../internal.ts';
12
+ import type { FormStore } from '../../types/index.ts';
13
+ import { useSignals } from '../useSignals/useSignals.ts';
14
+
15
+ export function useForm<TSchema extends FormSchema>(
16
+ config: FormConfig<TSchema>,
17
+ ): FormStore<TSchema>;
18
+
19
+ export function useForm(config: FormConfig, ...rest: [slot?: symbol]): FormStore {
20
+ const [, slot] = splitSlot(rest);
21
+ useSignals(subSlot(slot, 'signals'));
22
+
23
+ const internalFormStore = useMemo(
24
+ () => createFormStore(config, (input) => v.safeParseAsync(config.schema, input)),
25
+ [],
26
+ subSlot(slot, 'store'),
27
+ );
28
+
29
+ useLayoutEffect(
30
+ () => {
31
+ if (config.validate === 'initial') void validateFormInput(internalFormStore);
32
+ },
33
+ [],
34
+ subSlot(slot, 'initial-validation'),
35
+ );
36
+
37
+ return useMemo(
38
+ () => ({
39
+ [INTERNAL]: internalFormStore,
40
+ get isSubmitting() {
41
+ return internalFormStore.isSubmitting.value;
42
+ },
43
+ get isSubmitted() {
44
+ return internalFormStore.isSubmitted.value;
45
+ },
46
+ get isValidating() {
47
+ return internalFormStore.isValidating.value;
48
+ },
49
+ get isTouched() {
50
+ return getFieldBool(internalFormStore, 'isTouched');
51
+ },
52
+ get isEdited() {
53
+ return getFieldBool(internalFormStore, 'isEdited');
54
+ },
55
+ get isDirty() {
56
+ return getFieldBool(internalFormStore, 'isDirty');
57
+ },
58
+ get isValid() {
59
+ return !getFieldBool(internalFormStore, 'errors');
60
+ },
61
+ get errors() {
62
+ return internalFormStore.errors.value;
63
+ },
64
+ }),
65
+ [internalFormStore],
66
+ subSlot(slot, 'public-store'),
67
+ );
68
+ }
@@ -0,0 +1 @@
1
+ export * from './useSignals.ts';