@kaliber/forms 2.1.2 → 3.0.0-beta.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/package.json +13 -1
- package/src/components.js +21 -1
- package/src/components.type.test.ts +74 -0
- package/src/fields.js +98 -16
- package/src/fields.type.test.ts +135 -0
- package/src/hooks.js +65 -7
- package/src/hooks.type.test.ts +164 -0
- package/src/normalize.js +78 -25
- package/src/normalize.type.test.ts +125 -0
- package/src/schema.js +39 -11
- package/src/schema.type.test.ts +159 -0
- package/src/snapshot.js +59 -5
- package/src/snapshot.type.test.ts +135 -0
- package/src/state.js +31 -1
- package/src/type-helpers.js +12 -0
- package/src/type.test.helpers.ts +47 -0
- package/src/types.ts +354 -0
- package/src/validation.js +96 -12
- package/validation.js +1 -1
- package/src/types.d.ts +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaliber/forms",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-beta.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -25,7 +25,19 @@
|
|
|
25
25
|
"private": false,
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"repository": "https://github.com/kaliberjs/forms.git",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"postinstall": "if test -f ./scripts/install-peer-dependencies-for-typescript.js; then node ./scripts/install-peer-dependencies-for-typescript.js; fi",
|
|
30
|
+
"test.types": "tsc --project tsconfig.type.test.json"
|
|
31
|
+
},
|
|
28
32
|
"dependencies": {
|
|
29
33
|
"react-fast-compare": "^3.0.1"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@types/react": "^18.0.0",
|
|
37
|
+
"react": "^18.3.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "22",
|
|
41
|
+
"typescript": "^5.8.2"
|
|
30
42
|
}
|
|
31
43
|
}
|
package/src/components.js
CHANGED
|
@@ -2,22 +2,42 @@ import {
|
|
|
2
2
|
useFormFieldValue, useFormFieldsValues,
|
|
3
3
|
useFormFieldSnapshot
|
|
4
4
|
} from './hooks'
|
|
5
|
+
/** @import { Field } from './types.ts' */
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @template {Field} T
|
|
9
|
+
* @template {(value: ReturnType<typeof useFormFieldValue<T>>) => any} R
|
|
10
|
+
* @arg {{ field: T, render: R }} props
|
|
11
|
+
* @returns {null | ReturnType<R>}
|
|
12
|
+
*/
|
|
6
13
|
export function FormFieldValue({ field, render }) {
|
|
7
14
|
const value = useFormFieldValue(field)
|
|
8
15
|
return valueOrNull(render(value))
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @template {[...Field[]]} const T
|
|
20
|
+
* @template {(values: ReturnType<typeof useFormFieldsValues<T>>) => any} R
|
|
21
|
+
* @arg {{ fields: T, render: R }} props
|
|
22
|
+
* @returns {null | ReturnType<R>}
|
|
23
|
+
*/
|
|
11
24
|
export function FormFieldsValues({ fields, render }) {
|
|
12
25
|
const values = useFormFieldsValues(fields)
|
|
13
26
|
return valueOrNull(render(values))
|
|
14
27
|
}
|
|
15
28
|
|
|
29
|
+
/**
|
|
30
|
+
* @template {Field} T
|
|
31
|
+
* @template {(valid: boolean) => any} R
|
|
32
|
+
* @arg {{ field: T, render: R }} props
|
|
33
|
+
* @returns {null | ReturnType<R>}
|
|
34
|
+
*/
|
|
16
35
|
export function FormFieldValid({ field, render }) {
|
|
17
36
|
const { invalid } = useFormFieldSnapshot(field)
|
|
18
37
|
return valueOrNull(render(!invalid))
|
|
19
38
|
}
|
|
20
39
|
|
|
40
|
+
/** @template T @arg {T} value @returns {T | null} */
|
|
21
41
|
function valueOrNull(value) {
|
|
22
42
|
return typeof value === 'undefined' ? null : value
|
|
23
|
-
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { FormFieldsValues, FormFieldValid, FormFieldValue } from './components'
|
|
2
|
+
import { useForm } from './hooks'
|
|
3
|
+
import { array, object } from './schema'
|
|
4
|
+
import { expectAssignable, expectNotAny, expectNotNever, Prepared } from './type.test.helpers.ts'
|
|
5
|
+
import { optionalT } from './validation'
|
|
6
|
+
|
|
7
|
+
const { form } = useForm({
|
|
8
|
+
fields: {
|
|
9
|
+
a: optionalT('string'),
|
|
10
|
+
b: optionalT('number'),
|
|
11
|
+
c: optionalT('boolean'),
|
|
12
|
+
d: object({
|
|
13
|
+
a: optionalT('string'),
|
|
14
|
+
b: optionalT('number'),
|
|
15
|
+
c: optionalT('boolean'),
|
|
16
|
+
}),
|
|
17
|
+
e: array({
|
|
18
|
+
a: optionalT('string'),
|
|
19
|
+
b: optionalT('number'),
|
|
20
|
+
c: optionalT('boolean'),
|
|
21
|
+
})
|
|
22
|
+
},
|
|
23
|
+
onSubmit() {}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
FormFieldValue({
|
|
27
|
+
field: form,
|
|
28
|
+
render(value) {
|
|
29
|
+
expectNotAny(value)
|
|
30
|
+
expectNotNever(value)
|
|
31
|
+
expectAssignable<
|
|
32
|
+
{
|
|
33
|
+
a: string
|
|
34
|
+
b: number
|
|
35
|
+
c: boolean
|
|
36
|
+
d: {
|
|
37
|
+
a: string
|
|
38
|
+
b: number
|
|
39
|
+
c: boolean
|
|
40
|
+
}
|
|
41
|
+
e: {
|
|
42
|
+
a: string
|
|
43
|
+
b: number
|
|
44
|
+
c: boolean
|
|
45
|
+
}[]
|
|
46
|
+
},
|
|
47
|
+
Prepared<typeof value>
|
|
48
|
+
>
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
FormFieldsValues({
|
|
53
|
+
fields: [form.fields.a, form.fields.b],
|
|
54
|
+
render(value) {
|
|
55
|
+
expectNotAny(value)
|
|
56
|
+
expectNotNever(value)
|
|
57
|
+
expectAssignable<
|
|
58
|
+
[string, number],
|
|
59
|
+
Prepared<typeof value>
|
|
60
|
+
>
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
FormFieldValid({
|
|
65
|
+
field: form.fields.a,
|
|
66
|
+
render(value) {
|
|
67
|
+
expectNotAny(value)
|
|
68
|
+
expectNotNever(value)
|
|
69
|
+
expectAssignable<
|
|
70
|
+
boolean,
|
|
71
|
+
Prepared<typeof value>
|
|
72
|
+
>
|
|
73
|
+
}
|
|
74
|
+
})
|
package/src/fields.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalize } from './normalize'
|
|
2
2
|
import { createState, subscribeToAll, subscribeToChildren } from './state'
|
|
3
3
|
import isEqual from 'react-fast-compare'
|
|
4
|
+
/** @import { Falsy, Field, NormalizedField, PartialWithStringKey, State, Validate, ValidationContext, ValidationError, ValidationFunction } from './types.ts' */
|
|
4
5
|
|
|
5
6
|
const constructors = {
|
|
6
7
|
basic: createBasicFormField,
|
|
@@ -8,6 +9,15 @@ const constructors = {
|
|
|
8
9
|
object: createObjectFormField,
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @template {NormalizedField.Object} const T
|
|
14
|
+
*
|
|
15
|
+
* @arg {{
|
|
16
|
+
* name?: string,
|
|
17
|
+
* initialValue?: PartialWithStringKey<NormalizedField.ToValue<T>>,
|
|
18
|
+
* field: T,
|
|
19
|
+
* }} props
|
|
20
|
+
*/
|
|
11
21
|
export function createObjectFormField({ name = '', initialValue = {}, field }) {
|
|
12
22
|
|
|
13
23
|
const fields = createFormFields(initialValue, field.fields, name && `${name}.`)
|
|
@@ -17,18 +27,19 @@ export function createObjectFormField({ name = '', initialValue = {}, field }) {
|
|
|
17
27
|
const internalState = createState(initialState)
|
|
18
28
|
const validate = bindValidate(field.validate, internalState)
|
|
19
29
|
|
|
20
|
-
const value = {
|
|
21
|
-
get() { return mapValues(fields, child => child.value.get()) },
|
|
30
|
+
const value = /** @satisfies {State.Readonly} */ ({
|
|
31
|
+
get() { return mapValues(fields, /** @arg {Field} child */ child => child.value.get()) },
|
|
22
32
|
subscribe(f) {
|
|
23
33
|
return subscribeToChildren({
|
|
24
34
|
children,
|
|
35
|
+
/** @arg {unknown} _ */
|
|
25
36
|
notify: _ => f(value.get()),
|
|
26
37
|
subscribeToChild: (x, f) => x.value.subscribe(f),
|
|
27
38
|
})
|
|
28
39
|
},
|
|
29
|
-
}
|
|
40
|
+
})
|
|
30
41
|
|
|
31
|
-
return {
|
|
42
|
+
return /** @type {Field.FromNormalizedField<T>} */ ({
|
|
32
43
|
type: 'object',
|
|
33
44
|
name,
|
|
34
45
|
validate(context) {
|
|
@@ -49,15 +60,21 @@ export function createObjectFormField({ name = '', initialValue = {}, field }) {
|
|
|
49
60
|
value,
|
|
50
61
|
state: { get: internalState.get, subscribe: internalState.subscribe },
|
|
51
62
|
fields,
|
|
52
|
-
}
|
|
63
|
+
})
|
|
53
64
|
|
|
65
|
+
/**
|
|
66
|
+
* @arg {any} initialValues
|
|
67
|
+
* @arg {T['fields']} fields
|
|
68
|
+
* @arg {string} namePrefix
|
|
69
|
+
*/
|
|
54
70
|
function createFormFields(initialValues, fields, namePrefix = '') {
|
|
55
71
|
return mapValues(fields, (field, name) => {
|
|
56
72
|
const fullName = `${namePrefix}${name}`
|
|
57
73
|
const normalizedField = normalize(field, fullName)
|
|
58
|
-
const
|
|
59
|
-
return
|
|
74
|
+
const createFormField = constructors[normalizedField.type]
|
|
75
|
+
return createFormField({
|
|
60
76
|
name: fullName,
|
|
77
|
+
// @ts-expect-error - If you know how to fix this, please let me know
|
|
61
78
|
field: normalizedField,
|
|
62
79
|
initialValue: initialValues[name]
|
|
63
80
|
})
|
|
@@ -65,7 +82,15 @@ export function createObjectFormField({ name = '', initialValue = {}, field }) {
|
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
|
|
85
|
+
/**
|
|
86
|
+
* @arg {{
|
|
87
|
+
* name?: string,
|
|
88
|
+
* initialValue?: { [name: string]: any }[],
|
|
89
|
+
* field: NormalizedField.Array,
|
|
90
|
+
* }} props
|
|
91
|
+
* @returns {Field.Array}
|
|
92
|
+
*/
|
|
93
|
+
function createArrayFormField({ name = '', initialValue = [], field }) {
|
|
69
94
|
|
|
70
95
|
let index = 0
|
|
71
96
|
|
|
@@ -76,7 +101,7 @@ function createArrayFormField({ name, initialValue = [], field }) {
|
|
|
76
101
|
const internalState = createState(initialState)
|
|
77
102
|
const validate = bindValidate(field.validate, internalState)
|
|
78
103
|
|
|
79
|
-
const value = {
|
|
104
|
+
const value = /** @satisfies {State.Readonly} */ ({
|
|
80
105
|
get() {
|
|
81
106
|
const { children } = internalState.get()
|
|
82
107
|
return children.map(child => child.value.get())
|
|
@@ -85,12 +110,12 @@ function createArrayFormField({ name, initialValue = [], field }) {
|
|
|
85
110
|
return subscribeToAll({
|
|
86
111
|
state: internalState,
|
|
87
112
|
childrenFromState: x => x.children,
|
|
88
|
-
notify:_ => f(value.get()),
|
|
113
|
+
notify: _ => f(value.get()),
|
|
89
114
|
subscribeToChild: (x, f) => x.value.subscribe(f),
|
|
90
115
|
onlyNotifyOnChildChange: true,
|
|
91
116
|
})
|
|
92
117
|
},
|
|
93
|
-
}
|
|
118
|
+
})
|
|
94
119
|
|
|
95
120
|
return {
|
|
96
121
|
type: 'array',
|
|
@@ -128,6 +153,7 @@ function createArrayFormField({ name, initialValue = [], field }) {
|
|
|
128
153
|
}
|
|
129
154
|
}
|
|
130
155
|
|
|
156
|
+
/** @arg {{ [name: string]: any }} initialValue */
|
|
131
157
|
function createFormField(initialValue) {
|
|
132
158
|
const fullName = `${name}[${index++}]`
|
|
133
159
|
const fields = typeof field.fields == 'function' ? field.fields(initialValue) : field.fields
|
|
@@ -139,13 +165,21 @@ function createArrayFormField({ name, initialValue = [], field }) {
|
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
167
|
|
|
168
|
+
/**
|
|
169
|
+
* @arg {{
|
|
170
|
+
* name: string,
|
|
171
|
+
* initialValue?: any,
|
|
172
|
+
* field: NormalizedField.Basic,
|
|
173
|
+
* }} props
|
|
174
|
+
* @returns {Field.Basic}
|
|
175
|
+
*/
|
|
142
176
|
function createBasicFormField({ name, initialValue, field }) {
|
|
143
177
|
|
|
144
178
|
const initialFormFieldState = deriveFormFieldState({ value: initialValue })
|
|
145
179
|
const internalState = createState(initialFormFieldState)
|
|
146
180
|
const validate = bindValidate(field.validate, internalState)
|
|
147
181
|
|
|
148
|
-
const value = {
|
|
182
|
+
const value = /** @satisfies {State.ReadonlyWithHistory} */ ({
|
|
149
183
|
get() { return internalState.get().value },
|
|
150
184
|
subscribe(f) {
|
|
151
185
|
return internalState.subscribe(({ value: newValue }, { value: oldValue }) => {
|
|
@@ -153,7 +187,7 @@ function createBasicFormField({ name, initialValue, field }) {
|
|
|
153
187
|
f(newValue, oldValue)
|
|
154
188
|
})
|
|
155
189
|
},
|
|
156
|
-
}
|
|
190
|
+
})
|
|
157
191
|
|
|
158
192
|
return {
|
|
159
193
|
type: 'basic',
|
|
@@ -189,17 +223,41 @@ function createBasicFormField({ name, initialValue, field }) {
|
|
|
189
223
|
}
|
|
190
224
|
}
|
|
191
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @template {Record<string, any>} T1
|
|
228
|
+
* @template {Record<string, any>} T2
|
|
229
|
+
* @arg {T1} formFieldState
|
|
230
|
+
* @arg {T2} update
|
|
231
|
+
*/
|
|
192
232
|
function updateState(formFieldState, update) {
|
|
193
233
|
return deriveFormFieldState({ ...formFieldState, ...update })
|
|
194
234
|
}
|
|
195
235
|
|
|
236
|
+
/**
|
|
237
|
+
* @template T
|
|
238
|
+
* @template {keyof T} K
|
|
239
|
+
* @arg {T} o
|
|
240
|
+
* @arg {K[]} properties
|
|
241
|
+
* @returns {Pick<T, K>}
|
|
242
|
+
*/
|
|
196
243
|
function pick(o, properties) {
|
|
244
|
+
// @ts-expect-error
|
|
197
245
|
return properties.reduce(
|
|
198
246
|
(result, property) => ({ ...result, [property]: o[property] }),
|
|
199
247
|
{}
|
|
200
248
|
)
|
|
201
249
|
}
|
|
202
250
|
|
|
251
|
+
/**
|
|
252
|
+
* @template {Record<string, any>} const T
|
|
253
|
+
* @arg {{
|
|
254
|
+
* error?: Falsy | ValidationError,
|
|
255
|
+
* isSubmitted?: boolean,
|
|
256
|
+
* isVisited?: boolean,
|
|
257
|
+
* hasFocus?: boolean,
|
|
258
|
+
* } & T} state
|
|
259
|
+
* @return {T & State.Common}
|
|
260
|
+
*/
|
|
203
261
|
function deriveFormFieldState({
|
|
204
262
|
error = false,
|
|
205
263
|
isSubmitted = false,
|
|
@@ -207,7 +265,7 @@ function deriveFormFieldState({
|
|
|
207
265
|
hasFocus = false,
|
|
208
266
|
...rest
|
|
209
267
|
}) {
|
|
210
|
-
return {
|
|
268
|
+
return /** @type {T & State.Common} */ ({
|
|
211
269
|
...rest,
|
|
212
270
|
error,
|
|
213
271
|
isSubmitted,
|
|
@@ -215,18 +273,38 @@ function deriveFormFieldState({
|
|
|
215
273
|
hasFocus,
|
|
216
274
|
invalid: !!error,
|
|
217
275
|
showError: !!error && !hasFocus && (isVisited || isSubmitted)
|
|
218
|
-
}
|
|
276
|
+
})
|
|
219
277
|
}
|
|
220
278
|
|
|
279
|
+
/**
|
|
280
|
+
* @template {{ [key: string]: any }} O
|
|
281
|
+
* @template {(v: O[keyof O], k: keyof O & string, o: O) => any} F
|
|
282
|
+
*
|
|
283
|
+
* @param {O} o
|
|
284
|
+
* @param {F} f
|
|
285
|
+
* @returns {{ [key in keyof O]: ReturnType<F> }}
|
|
286
|
+
*/
|
|
221
287
|
function mapValues(o, f) {
|
|
288
|
+
// @ts-expect-error
|
|
222
289
|
return Object.entries(o).reduce(
|
|
223
|
-
|
|
290
|
+
// @ts-expect-error
|
|
291
|
+
(result, [k, v]) => (result[k] = f(v, k, o), result),
|
|
224
292
|
{}
|
|
225
293
|
)
|
|
226
294
|
}
|
|
227
295
|
|
|
296
|
+
/**
|
|
297
|
+
* @template T
|
|
298
|
+
* @template {State.ReadWrite} S
|
|
299
|
+
* @arg {null | ValidationFunction<T>} f
|
|
300
|
+
* @arg {S} state
|
|
301
|
+
*/
|
|
228
302
|
function bindValidate(f, state) {
|
|
229
303
|
return f && (
|
|
304
|
+
/**
|
|
305
|
+
* @arg {Parameters<ValidationFunction<T>>} args
|
|
306
|
+
* @returns {S extends State.ReadWrite<infer X> ? X : never}
|
|
307
|
+
*/
|
|
230
308
|
(...args) => {
|
|
231
309
|
const error = (f && f(...args)) || false
|
|
232
310
|
return state.update(x => isEqual(error, x.error) ? x : updateState(x, { error }))
|
|
@@ -234,6 +312,10 @@ function bindValidate(f, state) {
|
|
|
234
312
|
)
|
|
235
313
|
}
|
|
236
314
|
|
|
315
|
+
/**
|
|
316
|
+
* @arg {ValidationContext} context
|
|
317
|
+
* @arg {any} parent
|
|
318
|
+
*/
|
|
237
319
|
function addParent(context, parent) {
|
|
238
320
|
return { ...context, parents: [...context.parents, parent] }
|
|
239
321
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createObjectFormField } from './fields'
|
|
2
|
+
import { normalize } from './normalize'
|
|
3
|
+
import { array, object } from './schema'
|
|
4
|
+
import { asConst } from './type-helpers'
|
|
5
|
+
import { expectAssignable, expectNotAny, expectNotNever, Prepared } from './type.test.helpers.ts'
|
|
6
|
+
import { Field, PartialWithStringKey, State, ValidationContext } from './types.ts'
|
|
7
|
+
import { email, number, optional, required } from './validation'
|
|
8
|
+
|
|
9
|
+
const simpleObjectInput = asConst({
|
|
10
|
+
noValidation: optional,
|
|
11
|
+
singleValidation: email,
|
|
12
|
+
multipleValidation: [required, number],
|
|
13
|
+
})
|
|
14
|
+
type SimpleObjectValueType = {
|
|
15
|
+
noValidation: unknown,
|
|
16
|
+
singleValidation: string,
|
|
17
|
+
multipleValidation: number,
|
|
18
|
+
}
|
|
19
|
+
type SimpleObjectFieldsType = {
|
|
20
|
+
noValidation: BasicFieldType<unknown>,
|
|
21
|
+
singleValidation: BasicFieldType<string>,
|
|
22
|
+
multipleValidation: BasicFieldType<number>,
|
|
23
|
+
}
|
|
24
|
+
const simpleObjectNormalizedField = normalize(object(simpleObjectInput))
|
|
25
|
+
const simpleObjectWithValidationNormalizedField = normalize(object(validate, simpleObjectInput))
|
|
26
|
+
|
|
27
|
+
type BasicFieldType<T> = {
|
|
28
|
+
type: 'basic',
|
|
29
|
+
name: string,
|
|
30
|
+
validate(context: ValidationContext): void,
|
|
31
|
+
setSubmitted(isSubmitted: boolean): void,
|
|
32
|
+
reset(): void,
|
|
33
|
+
value: State.Readonly<T>,
|
|
34
|
+
state: State.Readonly<State.Basic<T>>,
|
|
35
|
+
eventHandlers: {
|
|
36
|
+
onBlur(): void,
|
|
37
|
+
onFocus(): void,
|
|
38
|
+
onChange(eOrValue: Field.Event<T> | T): void,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ObjectFieldType<Fields, Value> = {
|
|
43
|
+
type: 'object',
|
|
44
|
+
name: string,
|
|
45
|
+
validate(context: ValidationContext): void,
|
|
46
|
+
setSubmitted(isSubmitted: boolean): void,
|
|
47
|
+
reset(): void,
|
|
48
|
+
value: State.Readonly<Value>,
|
|
49
|
+
state: State.Readonly<State.Object>,
|
|
50
|
+
fields: Fields
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type ArrayFieldType<Field, Value extends Array<any>> = {
|
|
54
|
+
type: 'array',
|
|
55
|
+
name: string,
|
|
56
|
+
validate(context: ValidationContext): void,
|
|
57
|
+
setSubmitted(isSubmitted: boolean): void,
|
|
58
|
+
reset(): void,
|
|
59
|
+
value: State.Readonly<Value>,
|
|
60
|
+
state: State.Readonly<State.Array<Field>>,
|
|
61
|
+
helpers: {
|
|
62
|
+
add(initialValue: PartialWithStringKey<Value extends Array<infer X> ? X : never>): void,
|
|
63
|
+
remove(entry: Field): void,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
const simpleObjectField = createObjectFormField({ field: simpleObjectNormalizedField })
|
|
69
|
+
const simpleObjectWithValidationField = createObjectFormField({ field: simpleObjectWithValidationNormalizedField })
|
|
70
|
+
type SimpleObjectFields = (typeof simpleObjectField)['fields']
|
|
71
|
+
|
|
72
|
+
type NoValidationFieldType = SimpleObjectFields['noValidation']
|
|
73
|
+
expectAssignable<
|
|
74
|
+
BasicFieldType<unknown>,
|
|
75
|
+
Prepared<NoValidationFieldType>
|
|
76
|
+
>
|
|
77
|
+
|
|
78
|
+
type SingleValidationFieldType = SimpleObjectFields['singleValidation']
|
|
79
|
+
expectAssignable<
|
|
80
|
+
BasicFieldType<string>,
|
|
81
|
+
Prepared<SingleValidationFieldType>
|
|
82
|
+
>
|
|
83
|
+
|
|
84
|
+
type MultipleValidationFieldType = SimpleObjectFields['multipleValidation']
|
|
85
|
+
expectAssignable<
|
|
86
|
+
BasicFieldType<number>,
|
|
87
|
+
Prepared<MultipleValidationFieldType>
|
|
88
|
+
>
|
|
89
|
+
|
|
90
|
+
expectNotAny(simpleObjectField)
|
|
91
|
+
expectNotNever(simpleObjectField)
|
|
92
|
+
expectAssignable<
|
|
93
|
+
ObjectFieldType<SimpleObjectFieldsType, SimpleObjectValueType>,
|
|
94
|
+
Prepared<typeof simpleObjectField>
|
|
95
|
+
>
|
|
96
|
+
expectAssignable<
|
|
97
|
+
ObjectFieldType<SimpleObjectFieldsType, SimpleObjectValueType>,
|
|
98
|
+
Prepared<typeof simpleObjectWithValidationField>
|
|
99
|
+
>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
const objectWithSubFields = createObjectFormField({
|
|
104
|
+
field: normalize(object({
|
|
105
|
+
object: simpleObjectNormalizedField,
|
|
106
|
+
array: array(simpleObjectInput)
|
|
107
|
+
}))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expectNotAny(objectWithSubFields)
|
|
111
|
+
expectNotNever(objectWithSubFields)
|
|
112
|
+
expectAssignable<
|
|
113
|
+
ObjectFieldType<
|
|
114
|
+
{
|
|
115
|
+
object: ObjectFieldType<
|
|
116
|
+
SimpleObjectFieldsType,
|
|
117
|
+
SimpleObjectValueType
|
|
118
|
+
>,
|
|
119
|
+
array: ArrayFieldType<
|
|
120
|
+
ObjectFieldType<
|
|
121
|
+
SimpleObjectFieldsType,
|
|
122
|
+
SimpleObjectValueType
|
|
123
|
+
>,
|
|
124
|
+
SimpleObjectValueType[]
|
|
125
|
+
>,
|
|
126
|
+
},
|
|
127
|
+
{ object: SimpleObjectValueType, array: SimpleObjectValueType[] }
|
|
128
|
+
>,
|
|
129
|
+
Prepared<typeof objectWithSubFields>
|
|
130
|
+
>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validate<T>(value: T) {
|
|
134
|
+
return value === 'failure' && { id: 'error' }
|
|
135
|
+
}
|
package/src/hooks.js
CHANGED
|
@@ -2,14 +2,28 @@ import isEqual from 'react-fast-compare'
|
|
|
2
2
|
import { createObjectFormField } from './fields'
|
|
3
3
|
import { normalize } from './normalize'
|
|
4
4
|
import * as snapshot from './snapshot'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { asAny } from './type-helpers'
|
|
7
|
+
/** @import { Validate, NormalizedField, Field, InitialValue, FieldInput, State, Snapshot, Expand, MapTuple } from './types.ts' */
|
|
5
8
|
|
|
6
9
|
let formCounter = 0 // This will stop working when we need a number greater than 9007199254740991
|
|
7
10
|
function useFormId() { return React.useMemo(() => `form${++formCounter}`, []) }
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @template {FieldInput.Object} const A
|
|
14
|
+
* @template {Validate<FieldInput.ObjectToValue<A>>} const C
|
|
15
|
+
*
|
|
16
|
+
* @arg {{
|
|
17
|
+
* fields: A,
|
|
18
|
+
* initialValues?: Expand<InitialValue<A>>,
|
|
19
|
+
* validate?: C,
|
|
20
|
+
* onSubmit: (snapshot: any) => void,
|
|
21
|
+
* formId?: string,
|
|
22
|
+
* }} props
|
|
23
|
+
*/
|
|
10
24
|
export function useForm({ initialValues = undefined, fields, validate = undefined, onSubmit, formId = useFormId() }) {
|
|
11
|
-
const initialValuesRef = React.useRef(null)
|
|
12
|
-
const formRef = React.useRef(null)
|
|
25
|
+
const initialValuesRef = React.useRef(/** @type {InitialValue<A> | undefined} */ (asAny(null)))
|
|
26
|
+
const formRef = React.useRef(/** @type {Field.ObjectFromObjectInput<A>} */ (asAny(null)))
|
|
13
27
|
|
|
14
28
|
if (!isEqual(initialValuesRef.current, initialValues)) {
|
|
15
29
|
initialValuesRef.current = initialValues
|
|
@@ -28,6 +42,7 @@ export function useForm({ initialValues = undefined, fields, validate = undefine
|
|
|
28
42
|
|
|
29
43
|
return { form: formRef.current, submit, reset }
|
|
30
44
|
|
|
45
|
+
/** @arg {React.FormEvent<HTMLFormElement>} e */
|
|
31
46
|
function handleSubmit(e) {
|
|
32
47
|
if (e) e.preventDefault()
|
|
33
48
|
formRef.current.setSubmitted(true)
|
|
@@ -39,6 +54,11 @@ export function useForm({ initialValues = undefined, fields, validate = undefine
|
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @template {State.Readonly} T
|
|
59
|
+
* @arg {T} state
|
|
60
|
+
* @returns {T extends State.Readonly<infer X> ? X : never}
|
|
61
|
+
*/
|
|
42
62
|
function useFormFieldState(state) {
|
|
43
63
|
const [formFieldState, setFormFieldState] = React.useState(state.get)
|
|
44
64
|
|
|
@@ -53,6 +73,10 @@ function useFormFieldState(state) {
|
|
|
53
73
|
return formFieldState
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @template {[...State.Readonly[]]} const T
|
|
78
|
+
* @arg {T} states
|
|
79
|
+
*/
|
|
56
80
|
function useFieldStates(states) {
|
|
57
81
|
const [fieldStates, setFieldStates] = React.useState(getStates)
|
|
58
82
|
|
|
@@ -70,17 +94,24 @@ function useFieldStates(states) {
|
|
|
70
94
|
() => {}
|
|
71
95
|
)
|
|
72
96
|
},
|
|
73
|
-
states // explanation below
|
|
97
|
+
states // explanation below on why we supply the array directly
|
|
74
98
|
)
|
|
75
99
|
|
|
76
100
|
return fieldStates
|
|
77
101
|
|
|
78
|
-
function getStates() {
|
|
102
|
+
function getStates() {
|
|
103
|
+
return /** @type {State.StateTupleToValueTuple<T>}*/ (states.map(x => x.get()))
|
|
104
|
+
}
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
/**
|
|
108
|
+
* @template {Field} T
|
|
109
|
+
* @arg {T} field
|
|
110
|
+
* @returns {Expand<Snapshot.FromField<T>>}
|
|
111
|
+
*/
|
|
81
112
|
export function useFormFieldSnapshot(field) {
|
|
82
113
|
const state = React.useMemo(
|
|
83
|
-
() => ({
|
|
114
|
+
() => /** @satisfies {State.Readonly} */ ({
|
|
84
115
|
get() { return snapshot.get(field) },
|
|
85
116
|
subscribe(f) { return snapshot.subscribe(field, f) }
|
|
86
117
|
}),
|
|
@@ -89,14 +120,27 @@ export function useFormFieldSnapshot(field) {
|
|
|
89
120
|
return useFormFieldState(state)
|
|
90
121
|
}
|
|
91
122
|
|
|
123
|
+
/**
|
|
124
|
+
* @template {Field} T
|
|
125
|
+
* @arg {T} field
|
|
126
|
+
* @returns {ReturnType<typeof useFormFieldState<T['value']>>}
|
|
127
|
+
*/
|
|
92
128
|
export function useFormFieldValue(field) {
|
|
93
129
|
return useFormFieldState(field.value)
|
|
94
130
|
}
|
|
95
131
|
|
|
132
|
+
/**
|
|
133
|
+
* @template {[...Field[]]} const T
|
|
134
|
+
* @arg {T} fields
|
|
135
|
+
*/
|
|
96
136
|
export function useFormFieldsValues(fields) {
|
|
97
|
-
return useFieldStates(fields.map(x => x.value))
|
|
137
|
+
return useFieldStates(/** @type {MapTuple<T, 'value'>} */ (fields.map(x => x.value)))
|
|
98
138
|
}
|
|
99
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @template T
|
|
142
|
+
* @arg {Field.Basic<T>} field
|
|
143
|
+
*/
|
|
100
144
|
export function useFormField(field) {
|
|
101
145
|
if (!field) throw new Error('No field was passed in')
|
|
102
146
|
const { name, eventHandlers } = field
|
|
@@ -105,12 +149,16 @@ export function useFormField(field) {
|
|
|
105
149
|
return { name, state, eventHandlers }
|
|
106
150
|
}
|
|
107
151
|
|
|
152
|
+
/**
|
|
153
|
+
* @arg {Field.Basic<number | string>} field
|
|
154
|
+
*/
|
|
108
155
|
export function useNumberFormField(field) {
|
|
109
156
|
const { name, state, eventHandlers: { onChange, ...originalEventHandlers } } = useFormField(field)
|
|
110
157
|
const eventHandlers = { ...originalEventHandlers, onChange: handleChange }
|
|
111
158
|
|
|
112
159
|
return { name, state, eventHandlers }
|
|
113
160
|
|
|
161
|
+
/** @arg {React.ChangeEvent<HTMLInputElement>} e */
|
|
114
162
|
function handleChange(e) {
|
|
115
163
|
const userValue = e.target.value
|
|
116
164
|
const value = Number(userValue)
|
|
@@ -118,17 +166,23 @@ export function useNumberFormField(field) {
|
|
|
118
166
|
}
|
|
119
167
|
}
|
|
120
168
|
|
|
169
|
+
/** @arg {Field.Basic<boolean>} field */
|
|
121
170
|
export function useBooleanFormField(field) {
|
|
122
171
|
const { name, state, eventHandlers: { onChange, ...originalEventHandlers } } = useFormField(field)
|
|
123
172
|
const eventHandlers = { ...originalEventHandlers, onChange: handleChange }
|
|
124
173
|
|
|
125
174
|
return { name, state, eventHandlers }
|
|
126
175
|
|
|
176
|
+
/** @arg {React.ChangeEvent<HTMLInputElement>} e */
|
|
127
177
|
function handleChange(e) {
|
|
128
178
|
onChange(e.target.checked)
|
|
129
179
|
}
|
|
130
180
|
}
|
|
131
181
|
|
|
182
|
+
/**
|
|
183
|
+
* @template {Field.ObjectFields} T
|
|
184
|
+
* @arg {Field.Array<T>} field
|
|
185
|
+
*/
|
|
132
186
|
export function useArrayFormField(field) {
|
|
133
187
|
const { name, helpers } = field
|
|
134
188
|
const state = useFormFieldState(field.state)
|
|
@@ -136,6 +190,10 @@ export function useArrayFormField(field) {
|
|
|
136
190
|
return { name, state, helpers }
|
|
137
191
|
}
|
|
138
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @template {Field.ObjectFields} T
|
|
195
|
+
* @arg {Field.Object<T>} field
|
|
196
|
+
*/
|
|
139
197
|
export function useObjectFormField(field) {
|
|
140
198
|
const { name, fields } = field
|
|
141
199
|
const state = useFormFieldState(field.state)
|