@kaliber/forms 2.1.1 → 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/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,
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,
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,
69
+ b: number,
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,
91
+ b: number,
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 } | { type: string, b: number })[],
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, oldState)
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,12 @@
1
+ /**
2
+ * @template const T
3
+ * @arg {T} x
4
+ */
5
+ export function asConst(x) {
6
+ return x
7
+ }
8
+
9
+ /** @arg {any} x */
10
+ export function asAny(x) {
11
+ return x
12
+ }
@@ -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
+ : []