@kaliber/forms 2.1.2 → 3.0.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/src/snapshot.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { subscribeToAll, subscribeToChildren } from './state'
|
|
2
|
+
/** @import { State, Expand, Falsy, Field, Snapshot, ValidationError } from './types.ts' */
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @template {Field} T
|
|
6
|
+
* @param {T} field
|
|
7
|
+
* @returns {Expand<Snapshot.FromField<T>>}
|
|
8
|
+
*/
|
|
3
9
|
export function get(field) {
|
|
10
|
+
// @ts-expect-error
|
|
4
11
|
return {
|
|
5
12
|
'object': getForObject,
|
|
6
13
|
'array': getForArray,
|
|
@@ -8,11 +15,24 @@ export function get(field) {
|
|
|
8
15
|
}[field.type](field)
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @template {Field} T
|
|
20
|
+
* @arg {T} field
|
|
21
|
+
* @arg {(snapshot: ReturnType<typeof get<T>>) => void} f
|
|
22
|
+
* @returns {State.Unsubscribe}
|
|
23
|
+
*/
|
|
11
24
|
export function subscribe(field, f) {
|
|
12
25
|
return subscribeToFieldState(field, x => f(get(x)))
|
|
13
26
|
}
|
|
14
27
|
|
|
28
|
+
/**
|
|
29
|
+
* @template {Field} T
|
|
30
|
+
* @arg {T} field
|
|
31
|
+
* @arg {(field: T) => void} f
|
|
32
|
+
* @returns {State.Unsubscribe}
|
|
33
|
+
*/
|
|
15
34
|
export function subscribeToFieldState(field, f) {
|
|
35
|
+
// @ts-expect-error
|
|
16
36
|
return {
|
|
17
37
|
'object': subscribeForObject,
|
|
18
38
|
'array': subscribeForArray,
|
|
@@ -20,6 +40,11 @@ export function subscribeToFieldState(field, f) {
|
|
|
20
40
|
}[field.type](field, f)
|
|
21
41
|
}
|
|
22
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @template {Field.Object} T
|
|
45
|
+
* @arg {T} field
|
|
46
|
+
* @returns {Snapshot.FromField<T>}
|
|
47
|
+
*/
|
|
23
48
|
function getForObject(field) {
|
|
24
49
|
const { error, invalid } = field.state.get()
|
|
25
50
|
const { childrenInvalid, childErrors, childValues } = Object.entries(field.fields).reduce(
|
|
@@ -34,13 +59,18 @@ function getForObject(field) {
|
|
|
34
59
|
{ childrenInvalid: false, childErrors: {}, childValues: {} }
|
|
35
60
|
)
|
|
36
61
|
|
|
37
|
-
return {
|
|
62
|
+
return /** @type {Snapshot.FromField<T>} */ ({
|
|
38
63
|
invalid: invalid || childrenInvalid,
|
|
39
64
|
value: childValues,
|
|
40
65
|
error: { self: error, children: childErrors },
|
|
41
|
-
}
|
|
66
|
+
})
|
|
42
67
|
}
|
|
43
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @template {Field.Array} T
|
|
71
|
+
* @arg {T} field
|
|
72
|
+
* @returns {Snapshot.FromField<T>}
|
|
73
|
+
*/
|
|
44
74
|
function getForArray(field) {
|
|
45
75
|
const { children, error, invalid } = field.state.get()
|
|
46
76
|
const { childrenInvalid, childValues, childErrors } = children.reduce(
|
|
@@ -54,26 +84,44 @@ function getForArray(field) {
|
|
|
54
84
|
},
|
|
55
85
|
{ childrenInvalid: false, childValues: [], childErrors: [] }
|
|
56
86
|
)
|
|
57
|
-
return {
|
|
87
|
+
return /** @type {Snapshot.FromField<T>} */ ({
|
|
58
88
|
invalid: invalid || childrenInvalid,
|
|
59
89
|
value: childValues,
|
|
60
90
|
error: { self: error, children: childErrors },
|
|
61
|
-
}
|
|
91
|
+
})
|
|
62
92
|
}
|
|
63
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @template {Field.Basic} T
|
|
96
|
+
* @arg {T} field
|
|
97
|
+
* @returns {Snapshot.FromField<T>}
|
|
98
|
+
*/
|
|
64
99
|
function getForBasic(field) {
|
|
65
100
|
const { value, error, invalid } = field.state.get()
|
|
66
|
-
return { value, error, invalid }
|
|
101
|
+
return /** @type {Snapshot.FromField<T>} */ ({ value, error, invalid })
|
|
67
102
|
}
|
|
68
103
|
|
|
104
|
+
/**
|
|
105
|
+
* @template {Field.Object} T
|
|
106
|
+
* @arg {T} field
|
|
107
|
+
* @arg {(field: T) => void} f
|
|
108
|
+
* @returns {State.Unsubscribe}
|
|
109
|
+
*/
|
|
69
110
|
function subscribeForObject(field, f) {
|
|
70
111
|
return subscribeToChildren({
|
|
71
112
|
children: Object.values(field.fields),
|
|
113
|
+
/** @arg {ReturnType<typeof get<T>>} _ */
|
|
72
114
|
notify: _ => f(field),
|
|
73
115
|
subscribeToChild: subscribe,
|
|
74
116
|
})
|
|
75
117
|
}
|
|
76
118
|
|
|
119
|
+
/**
|
|
120
|
+
* @template {Field.Array} T
|
|
121
|
+
* @arg {T} field
|
|
122
|
+
* @arg {(field: T) => void} f
|
|
123
|
+
* @returns {State.Unsubscribe}
|
|
124
|
+
*/
|
|
77
125
|
function subscribeForArray(field, f) {
|
|
78
126
|
return subscribeToAll({
|
|
79
127
|
state: field.state,
|
|
@@ -83,6 +131,12 @@ function subscribeForArray(field, f) {
|
|
|
83
131
|
})
|
|
84
132
|
}
|
|
85
133
|
|
|
134
|
+
/**
|
|
135
|
+
* @template {Field.Basic} T
|
|
136
|
+
* @arg {T} field
|
|
137
|
+
* @arg {(field: T) => void} f
|
|
138
|
+
* @returns {State.Unsubscribe}
|
|
139
|
+
*/
|
|
86
140
|
function subscribeForBasic(field, f) {
|
|
87
141
|
return field.state.subscribe(_ => f(field))
|
|
88
142
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useForm } from './hooks'
|
|
2
|
+
import { array, object } from './schema'
|
|
3
|
+
import * as snapshot from './snapshot'
|
|
4
|
+
import { asConst } from './type-helpers.js'
|
|
5
|
+
import { expectAssignable, expectNotAny, expectNotNever, Prepared } from './type.test.helpers.ts'
|
|
6
|
+
import { Falsy, ValidationError } from './types.ts'
|
|
7
|
+
import { optionalT, requiredT } from './validation'
|
|
8
|
+
|
|
9
|
+
const { form } = useForm({
|
|
10
|
+
fields: {
|
|
11
|
+
a: optionalT('string'),
|
|
12
|
+
b: optionalT('number'),
|
|
13
|
+
c: object({
|
|
14
|
+
a: optionalT('string'),
|
|
15
|
+
b: optionalT('number'),
|
|
16
|
+
}),
|
|
17
|
+
d: array({
|
|
18
|
+
a: optionalT('string'),
|
|
19
|
+
b: optionalT('number'),
|
|
20
|
+
}),
|
|
21
|
+
e: array(
|
|
22
|
+
(initialValue: { type: string }) => {
|
|
23
|
+
return (
|
|
24
|
+
initialValue.type === 'a' ? asConst({ type: requiredT('string'), a: optionalT('string') }) :
|
|
25
|
+
initialValue.type === 'b' ? asConst({ type: requiredT('string'), b: optionalT('number') }) :
|
|
26
|
+
throwError(`Unknown type: '${initialValue.type}'`)
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
},
|
|
31
|
+
onSubmit() {}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
const result = snapshot.get(form.fields.a)
|
|
36
|
+
expectNotAny(result)
|
|
37
|
+
expectNotNever(result)
|
|
38
|
+
expectAssignable<
|
|
39
|
+
{
|
|
40
|
+
invalid: boolean,
|
|
41
|
+
value: string | undefined,
|
|
42
|
+
error: Falsy | ValidationError
|
|
43
|
+
},
|
|
44
|
+
Prepared<typeof result>
|
|
45
|
+
>
|
|
46
|
+
}
|
|
47
|
+
{
|
|
48
|
+
const result = snapshot.get(form.fields.b)
|
|
49
|
+
expectNotAny(result)
|
|
50
|
+
expectNotNever(result)
|
|
51
|
+
expectAssignable<
|
|
52
|
+
{
|
|
53
|
+
invalid: boolean,
|
|
54
|
+
value: number | undefined,
|
|
55
|
+
error: Falsy | ValidationError
|
|
56
|
+
},
|
|
57
|
+
Prepared<typeof result>
|
|
58
|
+
>
|
|
59
|
+
}
|
|
60
|
+
{
|
|
61
|
+
const result = snapshot.get(form.fields.c)
|
|
62
|
+
expectNotAny(result)
|
|
63
|
+
expectNotNever(result)
|
|
64
|
+
expectAssignable<
|
|
65
|
+
{
|
|
66
|
+
invalid: boolean,
|
|
67
|
+
value: {
|
|
68
|
+
a: string | undefined,
|
|
69
|
+
b: number | undefined,
|
|
70
|
+
},
|
|
71
|
+
error: {
|
|
72
|
+
self: Falsy | ValidationError,
|
|
73
|
+
children: {
|
|
74
|
+
a: Falsy | ValidationError,
|
|
75
|
+
b: Falsy | ValidationError,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
Prepared<typeof result>
|
|
80
|
+
>
|
|
81
|
+
}
|
|
82
|
+
{
|
|
83
|
+
const result = snapshot.get(form.fields.d)
|
|
84
|
+
expectNotAny(result)
|
|
85
|
+
expectNotNever(result)
|
|
86
|
+
expectAssignable<
|
|
87
|
+
{
|
|
88
|
+
invalid: boolean,
|
|
89
|
+
value: {
|
|
90
|
+
a: string | undefined,
|
|
91
|
+
b: number | undefined,
|
|
92
|
+
}[],
|
|
93
|
+
error: {
|
|
94
|
+
self: Falsy | ValidationError,
|
|
95
|
+
children: {
|
|
96
|
+
self: Falsy | ValidationError,
|
|
97
|
+
children: {
|
|
98
|
+
a: Falsy | ValidationError,
|
|
99
|
+
b: Falsy | ValidationError,
|
|
100
|
+
}
|
|
101
|
+
}[]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
Prepared<typeof result>
|
|
105
|
+
>
|
|
106
|
+
}
|
|
107
|
+
{
|
|
108
|
+
const result = snapshot.get(form.fields.e)
|
|
109
|
+
expectNotAny(result)
|
|
110
|
+
expectNotNever(result)
|
|
111
|
+
expectAssignable<
|
|
112
|
+
{
|
|
113
|
+
invalid: boolean,
|
|
114
|
+
value: ({ type: string, a: string | undefined } | { type: string, b: number | undefined })[],
|
|
115
|
+
error: {
|
|
116
|
+
self: Falsy | ValidationError,
|
|
117
|
+
children: (
|
|
118
|
+
{
|
|
119
|
+
self: Falsy | ValidationError,
|
|
120
|
+
children:
|
|
121
|
+
{ type: Falsy | ValidationError, a: Falsy | ValidationError } |
|
|
122
|
+
{ type: Falsy | ValidationError, b: Falsy | ValidationError }
|
|
123
|
+
}
|
|
124
|
+
)[]
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
Prepared<typeof result>
|
|
128
|
+
>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
{
|
|
132
|
+
const result = snapshot.get(form)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function throwError(m: string): never { throw new Error(m) }
|
package/src/state.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/** @import { Field, State } from './types.ts' */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @template T
|
|
5
|
+
* @arg {T} initialState
|
|
6
|
+
* @returns {State.ReadWrite<T>}
|
|
7
|
+
*/
|
|
1
8
|
export function createState(initialState) {
|
|
2
9
|
let state = initialState
|
|
3
10
|
let listeners = new Set()
|
|
@@ -20,6 +27,19 @@ export function createState(initialState) {
|
|
|
20
27
|
}
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @template T
|
|
32
|
+
* @template {Field} R
|
|
33
|
+
* @template S
|
|
34
|
+
* @arg {{
|
|
35
|
+
* state: State.ReadonlyWithHistory<T>,
|
|
36
|
+
* childrenFromState: (value: T) => R[],
|
|
37
|
+
* notify: State.Subscription<T | S>,
|
|
38
|
+
* subscribeToChild: (child: R, notify: State.Subscription<S>) => State.Unsubscribe,
|
|
39
|
+
* onlyNotifyOnChildChange?: boolean,
|
|
40
|
+
* }} props
|
|
41
|
+
* @returns {State.Unsubscribe}
|
|
42
|
+
*/
|
|
23
43
|
export function subscribeToAll({
|
|
24
44
|
state,
|
|
25
45
|
childrenFromState,
|
|
@@ -37,7 +57,7 @@ export function subscribeToAll({
|
|
|
37
57
|
unsubscribeChildren = subscribeToChildren({ children: newChildren, notify, subscribeToChild })
|
|
38
58
|
}
|
|
39
59
|
if (onlyNotifyOnChildChange && !childrenChanged) return
|
|
40
|
-
notify(newState
|
|
60
|
+
notify(newState)
|
|
41
61
|
})
|
|
42
62
|
|
|
43
63
|
return () => {
|
|
@@ -46,6 +66,16 @@ export function subscribeToAll({
|
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
69
|
+
/**
|
|
70
|
+
* @template {Field} R
|
|
71
|
+
* @template {State.SubscriptionWithHistory<any> | State.Subscription<any>} N
|
|
72
|
+
* @arg {{
|
|
73
|
+
* children: R[],
|
|
74
|
+
* notify: N,
|
|
75
|
+
* subscribeToChild: (child: R, notify: N) => State.Unsubscribe,
|
|
76
|
+
* }} props
|
|
77
|
+
* @returns {State.Unsubscribe}
|
|
78
|
+
*/
|
|
49
79
|
export function subscribeToChildren({ children, notify, subscribeToChild }) {
|
|
50
80
|
return children.reduce(
|
|
51
81
|
(unsubscribePrevious, x) => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** We need this because `never` matches all types (if we mistakenly infer an any or an infer type, we have problem) */
|
|
2
|
+
export function expectNotNever<T>(...expectNotNever: [T] & NotNever<T>) {}
|
|
3
|
+
type NotNever<T> = CheckNever<T, [T]>
|
|
4
|
+
|
|
5
|
+
type CheckNever<T, IfNotNever> =
|
|
6
|
+
T extends Array<infer X> ? CheckNever<X, IfNotNever> :
|
|
7
|
+
T extends Record<any, infer X> ? CheckNever<X, IfNotNever> :
|
|
8
|
+
T extends never ? [] :
|
|
9
|
+
IfNotNever
|
|
10
|
+
|
|
11
|
+
/** We need this because `any` matches all types (if we mistakenly infer an any or an infer type, we have problem) */
|
|
12
|
+
export function expectNotAny<Expected>(actual: NotAny<Expected>) {}
|
|
13
|
+
type NotAny<T> = 0 extends (1 & T) ? never : T
|
|
14
|
+
|
|
15
|
+
export function expectAssignable<const Expected, const Actual extends Expected & NoExtraKeys<Actual, Expected> & MarkedPrepared>() {}
|
|
16
|
+
|
|
17
|
+
// We want our `Expected` types to be precise and not miss any additional keys
|
|
18
|
+
type NoExtraKeys<T, U> = {
|
|
19
|
+
[K in keyof T]: K extends keyof U ? T[K] : `Key '${K extends string ? K : '[unknown key]'}' was not expected`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Needed to force `Prepared<typeof value>` instead of `typeof value` in `Actual` of `expectAssignable`
|
|
23
|
+
type MarkedPrepared = {
|
|
24
|
+
__isPrepared: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Converting `any` into `unknown` helps to make sure `{ a: any }` does not match `{ a: { b: any } }`
|
|
28
|
+
export type Prepared<T> =
|
|
29
|
+
MarkedPrepared & ReplaceAnyWithUnknown<T>
|
|
30
|
+
|
|
31
|
+
type ReplaceAnyWithUnknown<T> =
|
|
32
|
+
0 extends (1 & T) ? unknown & Any :
|
|
33
|
+
T extends ((...args: infer A) => infer R) ? ((...args: DeepPrepareTuple<A>) => ReplaceAnyWithUnknown<R>) :
|
|
34
|
+
[T] extends [readonly (infer U)[]]
|
|
35
|
+
? { [K in keyof T]: ReplaceAnyWithUnknown<T[K]> }
|
|
36
|
+
:
|
|
37
|
+
T extends object
|
|
38
|
+
? { [K in keyof T]: ReplaceAnyWithUnknown<T[K]> }
|
|
39
|
+
:
|
|
40
|
+
T
|
|
41
|
+
|
|
42
|
+
// `any` matches with anything, so for better error reporting we replace it
|
|
43
|
+
type Any = { __any__: any }
|
|
44
|
+
|
|
45
|
+
type DeepPrepareTuple<T> = T extends [infer A, ...infer X]
|
|
46
|
+
? [ReplaceAnyWithUnknown<A>, ...DeepPrepareTuple<X>]
|
|
47
|
+
: []
|