@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/UPSTREAM.md +95 -0
- package/package.json +65 -0
- package/src/components/Field/Field.tsrx +22 -0
- package/src/components/Field/Field.tsrx.d.ts +17 -0
- package/src/components/Field/index.ts +1 -0
- package/src/components/FieldArray/FieldArray.tsrx +22 -0
- package/src/components/FieldArray/FieldArray.tsrx.d.ts +18 -0
- package/src/components/FieldArray/index.ts +1 -0
- package/src/components/Form/Form.tsrx +28 -0
- package/src/components/Form/Form.tsrx.d.ts +15 -0
- package/src/components/Form/index.ts +1 -0
- package/src/components/index.ts +3 -0
- package/src/core/array/copyItemState/copyItemState.ts +101 -0
- package/src/core/array/copyItemState/index.ts +1 -0
- package/src/core/array/index.ts +3 -0
- package/src/core/array/resetItemState/index.ts +1 -0
- package/src/core/array/resetItemState/resetItemState.ts +172 -0
- package/src/core/array/swapItemState/index.ts +1 -0
- package/src/core/array/swapItemState/swapItemState.ts +138 -0
- package/src/core/field/focusFieldElement/focusFieldElement.ts +32 -0
- package/src/core/field/focusFieldElement/index.ts +1 -0
- package/src/core/field/getDirtyFieldInput/getDirtyFieldInput.ts +66 -0
- package/src/core/field/getDirtyFieldInput/index.ts +1 -0
- package/src/core/field/getElementInput/getElementInput.ts +78 -0
- package/src/core/field/getElementInput/index.ts +1 -0
- package/src/core/field/getFieldBool/getFieldBool.ts +22 -0
- package/src/core/field/getFieldBool/index.ts +1 -0
- package/src/core/field/getFieldInput/getFieldInput.ts +52 -0
- package/src/core/field/getFieldInput/index.ts +1 -0
- package/src/core/field/getFieldStore/getFieldStore.ts +34 -0
- package/src/core/field/getFieldStore/index.ts +1 -0
- package/src/core/field/index.ts +11 -0
- package/src/core/field/initializeFieldStore/index.ts +1 -0
- package/src/core/field/initializeFieldStore/initializeFieldStore.ts +325 -0
- package/src/core/field/setFieldBool/index.ts +1 -0
- package/src/core/field/setFieldBool/setFieldBool.ts +29 -0
- package/src/core/field/setFieldInput/index.ts +1 -0
- package/src/core/field/setFieldInput/setFieldInput.ts +180 -0
- package/src/core/field/setInitialFieldInput/index.ts +1 -0
- package/src/core/field/setInitialFieldInput/setInitialFieldInput.ts +99 -0
- package/src/core/field/walkFieldStore/index.ts +1 -0
- package/src/core/field/walkFieldStore/walkFieldStore.ts +49 -0
- package/src/core/form/createFormStore/createFormStore.ts +56 -0
- package/src/core/form/createFormStore/index.ts +1 -0
- package/src/core/form/decodeFormData/decodeFormData.ts +436 -0
- package/src/core/form/decodeFormData/index.ts +1 -0
- package/src/core/form/index.ts +4 -0
- package/src/core/form/validateFormInput/index.ts +1 -0
- package/src/core/form/validateFormInput/validateFormInput.ts +138 -0
- package/src/core/form/validateIfRequired/index.ts +1 -0
- package/src/core/form/validateIfRequired/validateIfRequired.ts +31 -0
- package/src/core/framework/index.ts +80 -0
- package/src/core/index.ts +6 -0
- package/src/core/types/field/field.ts +201 -0
- package/src/core/types/field/index.ts +1 -0
- package/src/core/types/form/form.ts +140 -0
- package/src/core/types/form/index.ts +1 -0
- package/src/core/types/index.ts +6 -0
- package/src/core/types/path/index.ts +10 -0
- package/src/core/types/path/path.ts +301 -0
- package/src/core/types/schema/index.ts +1 -0
- package/src/core/types/schema/schema.ts +18 -0
- package/src/core/types/signal/index.ts +1 -0
- package/src/core/types/signal/signal.ts +23 -0
- package/src/core/types/utils/index.ts +1 -0
- package/src/core/types/utils/utils.ts +46 -0
- package/src/core/values.ts +4 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useField/index.ts +1 -0
- package/src/hooks/useField/useField.ts +114 -0
- package/src/hooks/useFieldArray/index.ts +1 -0
- package/src/hooks/useFieldArray/useFieldArray.ts +63 -0
- package/src/hooks/useForm/index.ts +1 -0
- package/src/hooks/useForm/useForm.ts +68 -0
- package/src/hooks/useSignals/index.ts +1 -0
- package/src/hooks/useSignals/useSignals.ts +31 -0
- package/src/index.ts +19 -0
- package/src/internal.ts +32 -0
- package/src/methods/focus/focus.ts +35 -0
- package/src/methods/focus/index.ts +1 -0
- package/src/methods/getDeepErrorEntries/getDeepErrorEntries.ts +108 -0
- package/src/methods/getDeepErrorEntries/index.ts +1 -0
- package/src/methods/getDeepErrors/getDeepErrors.ts +90 -0
- package/src/methods/getDeepErrors/index.ts +1 -0
- package/src/methods/getDirtyInput/getDirtyInput.ts +87 -0
- package/src/methods/getDirtyInput/index.ts +1 -0
- package/src/methods/getDirtyPaths/getDirtyPaths.ts +123 -0
- package/src/methods/getDirtyPaths/index.ts +1 -0
- package/src/methods/getErrors/getErrors.ts +70 -0
- package/src/methods/getErrors/index.ts +1 -0
- package/src/methods/getInput/getInput.ts +75 -0
- package/src/methods/getInput/index.ts +1 -0
- package/src/methods/handleSubmit/handleSubmit.ts +83 -0
- package/src/methods/handleSubmit/index.ts +1 -0
- package/src/methods/index.ts +23 -0
- package/src/methods/insert/index.ts +1 -0
- package/src/methods/insert/insert.ts +134 -0
- package/src/methods/isDirty/index.ts +1 -0
- package/src/methods/isDirty/isDirty.ts +72 -0
- package/src/methods/isEdited/index.ts +1 -0
- package/src/methods/isEdited/isEdited.ts +72 -0
- package/src/methods/isTouched/index.ts +1 -0
- package/src/methods/isTouched/isTouched.ts +72 -0
- package/src/methods/isValid/index.ts +1 -0
- package/src/methods/isValid/isValid.ts +74 -0
- package/src/methods/move/index.ts +1 -0
- package/src/methods/move/move.ts +124 -0
- package/src/methods/pickDirty/index.ts +1 -0
- package/src/methods/pickDirty/pickDirty.ts +87 -0
- package/src/methods/remove/index.ts +1 -0
- package/src/methods/remove/remove.ts +76 -0
- package/src/methods/replace/index.ts +1 -0
- package/src/methods/replace/replace.ts +80 -0
- package/src/methods/reset/index.ts +1 -0
- package/src/methods/reset/reset.ts +216 -0
- package/src/methods/setErrors/index.ts +1 -0
- package/src/methods/setErrors/setErrors.ts +63 -0
- package/src/methods/setInput/index.ts +1 -0
- package/src/methods/setInput/setInput.ts +87 -0
- package/src/methods/submit/index.ts +1 -0
- package/src/methods/submit/submit.ts +11 -0
- package/src/methods/swap/index.ts +1 -0
- package/src/methods/swap/swap.ts +85 -0
- package/src/methods/validate/index.ts +1 -0
- package/src/methods/validate/validate.ts +34 -0
- package/src/types/field.ts +48 -0
- package/src/types/form.ts +12 -0
- 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 @@
|
|
|
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';
|