@playfast/reform-forms 1.1.1 → 1.2.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/README.md +3 -5
- package/package.json +19 -19
- package/src/form.test.ts +37 -52
- package/src/form.ts +691 -87
- package/src/index.ts +1 -7
- package/src/path.ts +32 -34
- package/src/formState.ts +0 -166
- package/src/formTypes.ts +0 -211
- package/src/formView.ts +0 -202
package/src/form.ts
CHANGED
|
@@ -1,76 +1,443 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Context,
|
|
3
3
|
Effect,
|
|
4
|
+
Either,
|
|
4
5
|
Function as Fn,
|
|
5
6
|
Layer,
|
|
6
7
|
Option,
|
|
8
|
+
Record,
|
|
9
|
+
Runtime,
|
|
7
10
|
Schema as S,
|
|
8
11
|
} from 'effect'
|
|
12
|
+
import { Event, Reducer, State, type Trigger } from '@playfast/reform'
|
|
9
13
|
import {
|
|
14
|
+
type AnySource,
|
|
10
15
|
Bus,
|
|
11
|
-
|
|
12
|
-
Reducer,
|
|
16
|
+
CurrentTracker,
|
|
13
17
|
Reducers,
|
|
14
|
-
|
|
18
|
+
type SourceIdentifier,
|
|
19
|
+
type SourceName,
|
|
20
|
+
type SourceValue as ReformSourceValue,
|
|
15
21
|
type Store,
|
|
16
|
-
} from '@playfast/reform'
|
|
17
|
-
import type {
|
|
18
|
-
AnyForm,
|
|
19
|
-
Decoded,
|
|
20
|
-
DecodedOfSchema,
|
|
21
|
-
FormClass,
|
|
22
|
-
FormEvents,
|
|
23
|
-
FormLiveConfig,
|
|
24
|
-
FormState,
|
|
25
|
-
InputRecord,
|
|
26
|
-
Inputs,
|
|
27
|
-
InputsObject,
|
|
28
|
-
RuntimeConfig,
|
|
29
|
-
Values,
|
|
30
|
-
ValuesOfSchema,
|
|
31
|
-
} from './formTypes'
|
|
22
|
+
} from '@playfast/reform/internal'
|
|
32
23
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
getNestedValue,
|
|
25
|
+
isPathOrParentDirty,
|
|
26
|
+
moveAt,
|
|
27
|
+
recalculateDirtyPaths,
|
|
28
|
+
replaceAt,
|
|
29
|
+
setNestedValue,
|
|
30
|
+
type ArrayItem,
|
|
31
|
+
type ArrayPath,
|
|
32
|
+
type FieldPath,
|
|
33
|
+
type PathValue,
|
|
34
|
+
type VariantPath,
|
|
35
|
+
type VariantValue,
|
|
36
|
+
} from './path'
|
|
41
37
|
import {
|
|
42
38
|
error,
|
|
39
|
+
errorsToRecord,
|
|
40
|
+
firstError,
|
|
43
41
|
normalizeErrors,
|
|
42
|
+
routeParseError,
|
|
43
|
+
type FormError,
|
|
44
44
|
type FormErrors,
|
|
45
45
|
} from './validation'
|
|
46
46
|
|
|
47
47
|
export { error }
|
|
48
48
|
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
49
|
+
export interface FieldLimitationsExternalApi<A = unknown> {
|
|
50
|
+
readonly required?: boolean
|
|
51
|
+
readonly disabled?: boolean
|
|
52
|
+
readonly readonly?: boolean
|
|
53
|
+
readonly visible?: boolean
|
|
54
|
+
readonly min?: number
|
|
55
|
+
readonly max?: number
|
|
56
|
+
readonly minLength?: number
|
|
57
|
+
readonly maxLength?: number
|
|
58
|
+
readonly minItems?: number
|
|
59
|
+
readonly maxItems?: number
|
|
60
|
+
readonly options?: ReadonlyArray<A>
|
|
61
|
+
readonly meta?: Readonly<Record<string, unknown>>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type FieldLimitations<A = unknown> = FieldLimitationsExternalApi<A>
|
|
65
|
+
|
|
66
|
+
export type Limitations = Readonly<Record<string, FieldLimitations>>
|
|
67
|
+
|
|
68
|
+
export interface FormState<Values> {
|
|
69
|
+
readonly values: Values
|
|
70
|
+
readonly initialValues: Values
|
|
71
|
+
readonly touched: Readonly<Record<string, boolean>>
|
|
72
|
+
readonly errors: FormErrors
|
|
73
|
+
readonly dirtyPaths: ReadonlyArray<string>
|
|
74
|
+
readonly submitCount: number
|
|
75
|
+
readonly validationCount: number
|
|
76
|
+
readonly lastSubmittedValues: Option.Option<Values>
|
|
77
|
+
readonly arrayKeys: Readonly<Record<string, ReadonlyArray<string>>>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FieldBinding<A> {
|
|
81
|
+
readonly path: string
|
|
82
|
+
readonly value: A
|
|
83
|
+
readonly set: (value: A | ((prev: A) => A)) => void
|
|
84
|
+
readonly blur: () => void
|
|
85
|
+
readonly error: Option.Option<string>
|
|
86
|
+
readonly dirty: boolean
|
|
87
|
+
readonly touched: boolean
|
|
88
|
+
readonly validating: boolean
|
|
89
|
+
readonly limitations: FieldLimitations<A>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ArrayItemView<Item> {
|
|
93
|
+
readonly key: string
|
|
94
|
+
readonly index: number
|
|
95
|
+
readonly value: Item
|
|
96
|
+
readonly remove: () => void
|
|
97
|
+
readonly move: (to: number) => void
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ArrayBinding<Item> {
|
|
101
|
+
readonly path: string
|
|
102
|
+
readonly items: ReadonlyArray<ArrayItemView<Item>>
|
|
103
|
+
readonly append: (value?: Item) => void
|
|
104
|
+
readonly remove: (index: number) => void
|
|
105
|
+
readonly move: (from: number, to: number) => void
|
|
106
|
+
readonly swap: (a: number, b: number) => void
|
|
107
|
+
readonly limitations: FieldLimitations<ReadonlyArray<Item>>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface FormView<Values, Inputs = {}> {
|
|
111
|
+
readonly values: Values
|
|
112
|
+
readonly inputs: Inputs
|
|
113
|
+
readonly errors: FormErrors
|
|
114
|
+
readonly dirty: boolean
|
|
115
|
+
readonly canSubmit: boolean
|
|
116
|
+
readonly submitCount: number
|
|
117
|
+
readonly validationCount: number
|
|
118
|
+
readonly lastSubmittedValues: Option.Option<Values>
|
|
119
|
+
readonly submit: () => void
|
|
120
|
+
readonly reset: () => void
|
|
121
|
+
readonly validate: () => void
|
|
122
|
+
readonly field: <P extends FieldPath<Values>>(path: P) => FieldBinding<PathValue<Values, P>>
|
|
123
|
+
readonly array: <P extends ArrayPath<Values>>(path: P) => ArrayBinding<ArrayItem<Values, P>>
|
|
124
|
+
readonly variantValue: <P extends VariantPath<Values>>(path: P) => VariantValue<Values, P>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type InputRecord = Readonly<Record<string, AnySource>>
|
|
128
|
+
|
|
129
|
+
type InputsObject<Inputs extends InputRecord> = {
|
|
130
|
+
readonly [K in keyof Inputs as SourceName<Inputs[K]>]: ReformSourceValue<Inputs[K]>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type InputStores<Inputs extends InputRecord> = {
|
|
134
|
+
[K in keyof Inputs]: Inputs[K] extends {
|
|
135
|
+
readonly store: Context.Tag<infer Service, infer _Store>
|
|
136
|
+
}
|
|
137
|
+
? Service
|
|
138
|
+
: never
|
|
139
|
+
}[keyof Inputs]
|
|
140
|
+
|
|
141
|
+
type ValuesOfSchema<Schema extends S.Schema.AnyNoContext> = S.Schema.Encoded<Schema>
|
|
142
|
+
type DecodedOfSchema<Schema extends S.Schema.AnyNoContext> = S.Schema.Type<Schema>
|
|
143
|
+
|
|
144
|
+
export interface RuntimeConfig<Values, Decoded, Inputs> {
|
|
145
|
+
readonly initial: Values
|
|
146
|
+
// oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob (not a serializable shape); Option would break external form definitions
|
|
147
|
+
readonly limit?: (ctx: { readonly values: Values; readonly inputs: Inputs }) => Limitations
|
|
148
|
+
readonly validate: (ctx: {
|
|
149
|
+
readonly values: Values
|
|
150
|
+
readonly decoded: Decoded
|
|
151
|
+
readonly inputs: Inputs
|
|
152
|
+
}) => Effect.Effect<ReadonlyArray<FormError>, never, never>
|
|
153
|
+
readonly submit: (ctx: {
|
|
154
|
+
readonly values: Values
|
|
155
|
+
readonly decoded: Decoded
|
|
156
|
+
readonly inputs: Inputs
|
|
157
|
+
}) => Effect.Effect<void, unknown, never>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface FormLiveConfig<Values, Decoded, Inputs, R = never> {
|
|
161
|
+
readonly initial: Values
|
|
162
|
+
// oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
|
|
163
|
+
readonly limit?: (ctx: { readonly values: Values; readonly inputs: Inputs }) => Limitations
|
|
164
|
+
// oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
|
|
165
|
+
readonly validate?: (ctx: {
|
|
166
|
+
readonly values: Values
|
|
167
|
+
readonly decoded: Decoded
|
|
168
|
+
readonly inputs: Inputs
|
|
169
|
+
}) =>
|
|
170
|
+
| void
|
|
171
|
+
| FormError
|
|
172
|
+
| ReadonlyArray<FormError>
|
|
173
|
+
| Effect.Effect<void | FormError | ReadonlyArray<FormError>, never, R>
|
|
174
|
+
// oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
|
|
175
|
+
readonly submit?: (ctx: {
|
|
176
|
+
readonly values: Values
|
|
177
|
+
readonly decoded: Decoded
|
|
178
|
+
readonly inputs: Inputs
|
|
179
|
+
}) => void | Effect.Effect<unknown, unknown, R>
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface FormSchemaReflection {
|
|
183
|
+
readonly ast: S.Schema<unknown, unknown, unknown>['ast']
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface FormManifestReflection<N extends string> {
|
|
187
|
+
readonly kind: 'Form'
|
|
188
|
+
readonly name: N
|
|
189
|
+
readonly schema: FormSchemaReflection
|
|
190
|
+
readonly inputs: InputRecord
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface FormManifest<
|
|
194
|
+
N extends string,
|
|
195
|
+
Schema extends S.Schema.AnyNoContext,
|
|
196
|
+
Inputs extends InputRecord,
|
|
197
|
+
> extends FormManifestReflection<N> {
|
|
198
|
+
readonly schema: Schema
|
|
199
|
+
readonly inputs: Inputs
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface FormEvents {
|
|
203
|
+
readonly set: Event.EventClass<string, { readonly path: string; readonly value: unknown }>
|
|
204
|
+
readonly blur: Event.EventClass<string, { readonly path: string }>
|
|
205
|
+
readonly reset: Event.EventClass<string, {}>
|
|
206
|
+
readonly validationFinished: Event.EventClass<string, { readonly errors: FormErrors }>
|
|
207
|
+
readonly submitAttempted: Event.EventClass<string, {}>
|
|
208
|
+
readonly submitSucceeded: Event.EventClass<string, { readonly values: unknown }>
|
|
209
|
+
readonly append: Event.EventClass<string, { readonly path: string; readonly value: unknown }>
|
|
210
|
+
readonly remove: Event.EventClass<string, { readonly path: string; readonly index: number }>
|
|
211
|
+
readonly move: Event.EventClass<
|
|
212
|
+
string,
|
|
213
|
+
{ readonly path: string; readonly from: number; readonly to: number }
|
|
214
|
+
>
|
|
215
|
+
readonly swap: Event.EventClass<
|
|
216
|
+
string,
|
|
217
|
+
{ readonly path: string; readonly a: number; readonly b: number }
|
|
218
|
+
>
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface FormVisitor<Result> {
|
|
222
|
+
readonly visit: <
|
|
223
|
+
N extends string,
|
|
224
|
+
Schema extends S.Schema.AnyNoContext,
|
|
225
|
+
Inputs extends InputRecord,
|
|
226
|
+
>(
|
|
227
|
+
form: FormClass<N, Schema, Inputs>,
|
|
228
|
+
) => Result
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface AnyForm {
|
|
232
|
+
new (): {}
|
|
233
|
+
readonly manifest: FormManifestReflection<string>
|
|
234
|
+
readonly capture: <Result>(visitor: FormVisitor<Result>) => Result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface FormClass<
|
|
238
|
+
N extends string,
|
|
239
|
+
Schema extends S.Schema.AnyNoContext,
|
|
240
|
+
Inputs extends InputRecord,
|
|
241
|
+
> {
|
|
242
|
+
new (): {}
|
|
243
|
+
readonly manifest: FormManifest<N, Schema, Inputs>
|
|
244
|
+
readonly capture: <Result>(visitor: FormVisitor<Result>) => Result
|
|
245
|
+
readonly state: State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>>
|
|
246
|
+
readonly config: Context.Tag<
|
|
247
|
+
RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>,
|
|
248
|
+
RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>
|
|
249
|
+
>
|
|
250
|
+
readonly events: FormEvents
|
|
251
|
+
readonly reducer: Reducer.StateReducerClass<
|
|
252
|
+
State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>>,
|
|
253
|
+
ReadonlyArray<Event.AnyEvent>
|
|
254
|
+
>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type Values<F> =
|
|
258
|
+
F extends FormClass<infer _Name, infer Schema, infer _Inputs> ? ValuesOfSchema<Schema> : never
|
|
259
|
+
|
|
260
|
+
export type Decoded<F> =
|
|
261
|
+
F extends FormClass<infer _Name, infer Schema, infer _Inputs> ? DecodedOfSchema<Schema> : never
|
|
262
|
+
|
|
263
|
+
export type Inputs<F> =
|
|
264
|
+
F extends FormClass<infer _Name, infer _Schema, infer Inputs> ? InputsObject<Inputs> : never
|
|
265
|
+
|
|
266
|
+
export type View<F> = FormView<Values<F>, Inputs<F>>
|
|
267
|
+
|
|
268
|
+
const arrayKeyCounter = { current: 0 }
|
|
269
|
+
const makeArrayKey = (): string => `form-item-${arrayKeyCounter.current++}`
|
|
270
|
+
|
|
271
|
+
export const arrayKeysFor = (source: unknown, path = ''): Record<string, ReadonlyArray<string>> => {
|
|
272
|
+
const out: Record<string, ReadonlyArray<string>> = {}
|
|
273
|
+
const visit = (node: KeyVisitNode): void => {
|
|
274
|
+
if (Array.isArray(node.current)) {
|
|
275
|
+
out[node.path] = node.current.map(() => makeArrayKey())
|
|
276
|
+
node.current.forEach((element, index) =>
|
|
277
|
+
visit({ current: element, path: `${node.path}[${index}]` }),
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
if (node.current !== null && typeof node.current === 'object') {
|
|
282
|
+
Record.toEntries(Fn.unsafeCoerce<unknown, Record<string, unknown>>(node.current)).forEach(
|
|
283
|
+
([key, child]) =>
|
|
284
|
+
visit({ current: child, path: node.path.length === 0 ? key : `${node.path}.${key}` }),
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
visit({ current: source, path })
|
|
289
|
+
return out
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const initialState = <Values>(initial: Values): FormState<Values> => ({
|
|
293
|
+
values: initial,
|
|
294
|
+
initialValues: initial,
|
|
295
|
+
touched: {},
|
|
296
|
+
errors: {},
|
|
297
|
+
dirtyPaths: [],
|
|
298
|
+
submitCount: 0,
|
|
299
|
+
validationCount: 0,
|
|
300
|
+
lastSubmittedValues: Option.none(),
|
|
301
|
+
arrayKeys: arrayKeysFor(initial),
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const markTouched = (
|
|
305
|
+
touched: Readonly<Record<string, boolean>>,
|
|
306
|
+
path: string,
|
|
307
|
+
): Readonly<Record<string, boolean>> => ({ ...touched, [path]: true })
|
|
308
|
+
|
|
309
|
+
interface ArrayLookup {
|
|
310
|
+
readonly source: unknown
|
|
311
|
+
readonly path: string
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
interface KeyVisitNode {
|
|
315
|
+
readonly current: unknown
|
|
316
|
+
readonly path: string
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
interface PathInput<Values> {
|
|
320
|
+
readonly state: FormState<Values>
|
|
321
|
+
readonly path: string
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
interface SetPathInput<Values> extends PathInput<Values> {
|
|
325
|
+
readonly value: unknown
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface RemovePathInput<Values> extends PathInput<Values> {
|
|
329
|
+
readonly index: number
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
interface MovePathInput<Values> extends PathInput<Values> {
|
|
333
|
+
readonly from: number
|
|
334
|
+
readonly to: number
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface SwapPathInput<Values> extends PathInput<Values> {
|
|
338
|
+
readonly first: number
|
|
339
|
+
readonly second: number
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const withValues = <Values>(state: FormState<Values>, nextValues: Values): FormState<Values> => ({
|
|
343
|
+
...state,
|
|
344
|
+
values: nextValues,
|
|
345
|
+
dirtyPaths: recalculateDirtyPaths(state.initialValues, nextValues),
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const currentArray = (input: ArrayLookup): ReadonlyArray<unknown> => {
|
|
349
|
+
const current = getNestedValue(input.source, input.path)
|
|
350
|
+
return Array.isArray(current) ? current : []
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const updateKeys = (
|
|
354
|
+
state: FormState<unknown>,
|
|
355
|
+
path: string,
|
|
356
|
+
transform: (keys: ReadonlyArray<string>) => ReadonlyArray<string>,
|
|
357
|
+
): Readonly<Record<string, ReadonlyArray<string>>> => {
|
|
358
|
+
const existing =
|
|
359
|
+
state.arrayKeys[path] ?? currentArray({ source: state.values, path }).map(() => makeArrayKey())
|
|
360
|
+
return { ...state.arrayKeys, [path]: transform(existing) }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const setAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> =>
|
|
364
|
+
withValues(input.state, setNestedValue(input.state.values, input.path, input.value))
|
|
365
|
+
|
|
366
|
+
const appendAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> => {
|
|
367
|
+
const elements = currentArray({ source: input.state.values, path: input.path })
|
|
368
|
+
const nextValues = setNestedValue(input.state.values, input.path, [...elements, input.value])
|
|
369
|
+
return {
|
|
370
|
+
...withValues(input.state, nextValues),
|
|
371
|
+
arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => [
|
|
372
|
+
...keys,
|
|
373
|
+
makeArrayKey(),
|
|
374
|
+
]),
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const removeAtPath = <Values>(input: RemovePathInput<Values>): FormState<Values> => {
|
|
379
|
+
const elements = currentArray({ source: input.state.values, path: input.path })
|
|
380
|
+
if (input.index < 0 || input.index >= elements.length) {
|
|
381
|
+
return input.state
|
|
382
|
+
}
|
|
383
|
+
const nextValues = setNestedValue(
|
|
384
|
+
input.state.values,
|
|
385
|
+
input.path,
|
|
386
|
+
elements.filter((_, position) => position !== input.index),
|
|
387
|
+
)
|
|
388
|
+
return {
|
|
389
|
+
...withValues(input.state, nextValues),
|
|
390
|
+
arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
|
|
391
|
+
keys.filter((_, position) => position !== input.index),
|
|
392
|
+
),
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const moveAtPath = <Values>(input: MovePathInput<Values>): FormState<Values> => {
|
|
397
|
+
const elements = currentArray({ source: input.state.values, path: input.path })
|
|
398
|
+
const next = moveAt(elements, input.from, input.to)
|
|
399
|
+
if (next === elements) {
|
|
400
|
+
return input.state
|
|
401
|
+
}
|
|
402
|
+
const nextValues = setNestedValue(input.state.values, input.path, next)
|
|
403
|
+
return {
|
|
404
|
+
...withValues(input.state, nextValues),
|
|
405
|
+
arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
|
|
406
|
+
moveAt(keys, input.from, input.to),
|
|
407
|
+
),
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const swapAtPath = <Values>(input: SwapPathInput<Values>): FormState<Values> => {
|
|
412
|
+
const elements = currentArray({ source: input.state.values, path: input.path })
|
|
413
|
+
if (
|
|
414
|
+
input.first < 0 ||
|
|
415
|
+
input.second < 0 ||
|
|
416
|
+
input.first >= elements.length ||
|
|
417
|
+
input.second >= elements.length ||
|
|
418
|
+
input.first === input.second
|
|
419
|
+
) {
|
|
420
|
+
return input.state
|
|
421
|
+
}
|
|
422
|
+
const swapped = replaceAt(
|
|
423
|
+
replaceAt(elements, input.first, elements[input.second]),
|
|
424
|
+
input.second,
|
|
425
|
+
elements[input.first],
|
|
426
|
+
)
|
|
427
|
+
const nextValues = setNestedValue(input.state.values, input.path, swapped)
|
|
428
|
+
return {
|
|
429
|
+
...withValues(input.state, nextValues),
|
|
430
|
+
arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
|
|
431
|
+
replaceAt(
|
|
432
|
+
replaceAt(keys, input.first, keys[input.second] ?? makeArrayKey()),
|
|
433
|
+
input.second,
|
|
434
|
+
keys[input.first] ?? makeArrayKey(),
|
|
435
|
+
),
|
|
436
|
+
),
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
interface MakeConfig<Schema extends S.Schema.AnyNoContext, Inputs extends InputRecord> {
|
|
74
441
|
readonly schema: Schema
|
|
75
442
|
// oxlint-disable-next-line reform-rules/no-optional-fields -- optional source map on the public make() config; Option would break callers passing only a schema
|
|
76
443
|
readonly inputs?: Inputs
|
|
@@ -78,30 +445,51 @@ interface MakeConfig<Schema extends S.Schema.Any, Inputs extends InputRecord> {
|
|
|
78
445
|
|
|
79
446
|
export const make = <
|
|
80
447
|
const N extends string,
|
|
81
|
-
Schema extends S.Schema.
|
|
448
|
+
Schema extends S.Schema.AnyNoContext,
|
|
82
449
|
const Inputs extends InputRecord = {},
|
|
83
450
|
>(
|
|
84
451
|
name: N,
|
|
85
452
|
config: MakeConfig<Schema, Inputs>,
|
|
86
453
|
): FormClass<N, Schema, Inputs> => {
|
|
87
|
-
const
|
|
88
|
-
|
|
454
|
+
const valuesSchema = S.encodedSchema(config.schema)
|
|
455
|
+
const stringRecord = S.Record({ key: S.String, value: S.String })
|
|
456
|
+
const stateSchema = S.Struct({
|
|
457
|
+
values: valuesSchema,
|
|
458
|
+
initialValues: valuesSchema,
|
|
459
|
+
touched: S.Record({ key: S.String, value: S.Boolean }),
|
|
460
|
+
errors: stringRecord,
|
|
461
|
+
dirtyPaths: S.Array(S.String),
|
|
462
|
+
submitCount: S.Number,
|
|
463
|
+
validationCount: S.Number,
|
|
464
|
+
lastSubmittedValues: S.OptionFromSelf(valuesSchema),
|
|
465
|
+
arrayKeys: S.Record({ key: S.String, value: S.Array(S.String) }),
|
|
466
|
+
})
|
|
467
|
+
const state: State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>> = State.make(
|
|
468
|
+
`${name}/state`,
|
|
469
|
+
stateSchema,
|
|
470
|
+
{ title: `${name} form state` },
|
|
89
471
|
)
|
|
90
472
|
const events: FormEvents = {
|
|
91
473
|
set: Event.make(`${name}/SetField`, S.Struct({ path: S.String, value: S.Unknown })),
|
|
92
474
|
blur: Event.make(`${name}/BlurField`, S.Struct({ path: S.String })),
|
|
93
475
|
reset: Event.make(`${name}/Reset`, S.Struct({})),
|
|
94
|
-
validationFinished: Event.make(
|
|
95
|
-
|
|
96
|
-
|
|
476
|
+
validationFinished: Event.make(
|
|
477
|
+
`${name}/ValidationFinished`,
|
|
478
|
+
S.Struct({
|
|
479
|
+
errors: S.Record({ key: S.String, value: S.String }),
|
|
480
|
+
}),
|
|
481
|
+
),
|
|
97
482
|
submitAttempted: Event.make(`${name}/SubmitAttempted`, S.Struct({})),
|
|
98
483
|
submitSucceeded: Event.make(`${name}/SubmitSucceeded`, S.Struct({ values: S.Unknown })),
|
|
99
484
|
append: Event.make(`${name}/AppendItem`, S.Struct({ path: S.String, value: S.Unknown })),
|
|
100
485
|
remove: Event.make(`${name}/RemoveItem`, S.Struct({ path: S.String, index: S.Number })),
|
|
101
|
-
move: Event.make(
|
|
486
|
+
move: Event.make(
|
|
487
|
+
`${name}/MoveItem`,
|
|
488
|
+
S.Struct({ path: S.String, from: S.Number, to: S.Number }),
|
|
489
|
+
),
|
|
102
490
|
swap: Event.make(`${name}/SwapItems`, S.Struct({ path: S.String, a: S.Number, b: S.Number })),
|
|
103
491
|
}
|
|
104
|
-
const reducer = Reducer.make(`${name}/FormReducer`, {
|
|
492
|
+
const reducer: FormClass<N, Schema, Inputs>['reducer'] = Reducer.make(`${name}/FormReducer`, {
|
|
105
493
|
states: [state],
|
|
106
494
|
events: Object.values(events),
|
|
107
495
|
})
|
|
@@ -109,10 +497,12 @@ export const make = <
|
|
|
109
497
|
RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>,
|
|
110
498
|
RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>
|
|
111
499
|
>(`reform/form/${name}/config`)
|
|
112
|
-
const inputs: Inputs = Fn.unsafeCoerce(
|
|
500
|
+
const inputs: Inputs = Fn.unsafeCoerce(
|
|
501
|
+
Option.getOrElse(Option.fromNullable(config.inputs), () => ({})),
|
|
502
|
+
)
|
|
113
503
|
class FormImpl {
|
|
114
|
-
static readonly manifest = {
|
|
115
|
-
kind: 'Form'
|
|
504
|
+
static readonly manifest: FormManifest<N, Schema, Inputs> = {
|
|
505
|
+
kind: 'Form',
|
|
116
506
|
name,
|
|
117
507
|
schema: config.schema,
|
|
118
508
|
inputs,
|
|
@@ -121,33 +511,50 @@ export const make = <
|
|
|
121
511
|
static readonly config = runtimeConfig
|
|
122
512
|
static readonly events = events
|
|
123
513
|
static readonly reducer = reducer
|
|
514
|
+
static capture<Result>(visitor: FormVisitor<Result>): Result {
|
|
515
|
+
return visitor.visit(FormImpl)
|
|
516
|
+
}
|
|
124
517
|
}
|
|
125
|
-
return
|
|
518
|
+
return FormImpl
|
|
126
519
|
}
|
|
127
520
|
|
|
128
521
|
export const live = <
|
|
129
|
-
|
|
522
|
+
N extends string,
|
|
523
|
+
Schema extends S.Schema.AnyNoContext,
|
|
524
|
+
FormInputs extends InputRecord,
|
|
130
525
|
R = never,
|
|
131
526
|
>(
|
|
132
|
-
form:
|
|
133
|
-
config: FormLiveConfig<
|
|
527
|
+
form: FormClass<N, Schema, FormInputs>,
|
|
528
|
+
config: FormLiveConfig<
|
|
529
|
+
ValuesOfSchema<Schema>,
|
|
530
|
+
DecodedOfSchema<Schema>,
|
|
531
|
+
InputsObject<FormInputs>,
|
|
532
|
+
R
|
|
533
|
+
>,
|
|
134
534
|
): Layer.Layer<
|
|
135
|
-
|
|
535
|
+
| State.StateStore<`${N}/state`, FormState<ValuesOfSchema<Schema>>>
|
|
536
|
+
| RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<FormInputs>>,
|
|
136
537
|
never,
|
|
137
538
|
Bus | Reducers | R
|
|
138
539
|
> => {
|
|
139
540
|
const configTag: Context.Tag<
|
|
140
|
-
RuntimeConfig<
|
|
141
|
-
RuntimeConfig<
|
|
142
|
-
> =
|
|
541
|
+
RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<FormInputs>>,
|
|
542
|
+
RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<FormInputs>>
|
|
543
|
+
> = form.config
|
|
143
544
|
const configLayer = Layer.effect(
|
|
144
545
|
configTag,
|
|
145
546
|
Effect.map(Effect.context<R | Bus>(), (context) => {
|
|
146
|
-
const runtimeConfig: RuntimeConfig<
|
|
547
|
+
const runtimeConfig: RuntimeConfig<
|
|
548
|
+
ValuesOfSchema<Schema>,
|
|
549
|
+
DecodedOfSchema<Schema>,
|
|
550
|
+
InputsObject<FormInputs>
|
|
551
|
+
> = {
|
|
147
552
|
initial: config.initial,
|
|
148
553
|
validate: (ctx) => {
|
|
149
554
|
const validationOutput = config.validate?.(ctx)
|
|
150
|
-
const effect = Effect.isEffect(validationOutput)
|
|
555
|
+
const effect = Effect.isEffect(validationOutput)
|
|
556
|
+
? validationOutput
|
|
557
|
+
: Effect.succeed(validationOutput)
|
|
151
558
|
return Fn.unsafeCoerce(
|
|
152
559
|
Effect.map(Effect.provide(effect, context), (rawErrors) => normalizeErrors(rawErrors)),
|
|
153
560
|
)
|
|
@@ -158,9 +565,7 @@ export const live = <
|
|
|
158
565
|
return Fn.unsafeCoerce(Effect.asVoid(Effect.provide(effect, context)))
|
|
159
566
|
},
|
|
160
567
|
}
|
|
161
|
-
return config.limit === undefined
|
|
162
|
-
? runtimeConfig
|
|
163
|
-
: { ...runtimeConfig, limit: config.limit }
|
|
568
|
+
return config.limit === undefined ? runtimeConfig : { ...runtimeConfig, limit: config.limit }
|
|
164
569
|
}),
|
|
165
570
|
)
|
|
166
571
|
|
|
@@ -176,12 +581,19 @@ export const live = <
|
|
|
176
581
|
|
|
177
582
|
const reducerLayer = Reducer.live(
|
|
178
583
|
form.reducer,
|
|
179
|
-
(
|
|
584
|
+
(
|
|
585
|
+
state: FormState<ValuesOfSchema<Schema>>,
|
|
586
|
+
event: InternalEvent,
|
|
587
|
+
): FormState<ValuesOfSchema<Schema>> => {
|
|
180
588
|
if (event._tag === form.events.set.tag) {
|
|
181
|
-
return 'path' in event && 'value' in event
|
|
589
|
+
return 'path' in event && 'value' in event
|
|
590
|
+
? setAtPath({ state, path: event.path, value: event.value })
|
|
591
|
+
: state
|
|
182
592
|
}
|
|
183
593
|
if (event._tag === form.events.blur.tag) {
|
|
184
|
-
return 'path' in event
|
|
594
|
+
return 'path' in event
|
|
595
|
+
? { ...state, touched: markTouched(state.touched, event.path) }
|
|
596
|
+
: state
|
|
185
597
|
}
|
|
186
598
|
if (event._tag === form.events.reset.tag) {
|
|
187
599
|
return initialState(state.initialValues)
|
|
@@ -195,9 +607,15 @@ export const live = <
|
|
|
195
607
|
return { ...state, submitCount: state.submitCount + 1 }
|
|
196
608
|
}
|
|
197
609
|
if (event._tag === form.events.submitSucceeded.tag) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
610
|
+
if (!('values' in event)) {
|
|
611
|
+
return state
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
...state,
|
|
615
|
+
lastSubmittedValues: Option.some(
|
|
616
|
+
Fn.unsafeCoerce<unknown, ValuesOfSchema<Schema>>(event.values),
|
|
617
|
+
),
|
|
618
|
+
}
|
|
201
619
|
}
|
|
202
620
|
if (event._tag === form.events.append.tag) {
|
|
203
621
|
return 'path' in event && 'value' in event
|
|
@@ -223,11 +641,197 @@ export const live = <
|
|
|
223
641
|
},
|
|
224
642
|
)
|
|
225
643
|
|
|
226
|
-
return
|
|
227
|
-
Layer.
|
|
228
|
-
Layer.provideMerge(State.live(form.state, initialState(config.initial))),
|
|
229
|
-
),
|
|
644
|
+
return Layer.mergeAll(configLayer, reducerLayer).pipe(
|
|
645
|
+
Layer.provideMerge(State.live(form.state, initialState(config.initial))),
|
|
230
646
|
)
|
|
231
647
|
}
|
|
232
648
|
|
|
233
|
-
|
|
649
|
+
const readInput = Effect.fn('forms.readInput')(function* <Input extends AnySource>(
|
|
650
|
+
source: Input,
|
|
651
|
+
): Effect.fn.Return<ReformSourceValue<Input>, never, SourceIdentifier<Input>> {
|
|
652
|
+
const store = yield* Context.GenericTag<SourceIdentifier<Input>, Store<ReformSourceValue<Input>>>(
|
|
653
|
+
source.store.key,
|
|
654
|
+
)
|
|
655
|
+
const tracker = yield* Effect.serviceOption(CurrentTracker)
|
|
656
|
+
if (Option.isSome(tracker)) {
|
|
657
|
+
tracker.value.add(store)
|
|
658
|
+
}
|
|
659
|
+
return store.getSnapshot()
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
const readInputs = <Inputs extends InputRecord>(
|
|
663
|
+
inputs: Inputs,
|
|
664
|
+
): Effect.Effect<InputsObject<Inputs>, never, InputStores<Inputs>> =>
|
|
665
|
+
Fn.unsafeCoerce(
|
|
666
|
+
Effect.gen(function* () {
|
|
667
|
+
const pairs = yield* Effect.forEach(Object.values(inputs), (source) =>
|
|
668
|
+
Effect.map(readInput(source), (snapshot): readonly [string, unknown] => [
|
|
669
|
+
source.name,
|
|
670
|
+
snapshot,
|
|
671
|
+
]),
|
|
672
|
+
)
|
|
673
|
+
return Record.fromEntries(pairs)
|
|
674
|
+
}),
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
const limitationsFor = <A>(limitations: Limitations, path: string): FieldLimitations<A> =>
|
|
678
|
+
Fn.unsafeCoerce(Option.getOrElse(Option.fromNullable(limitations[path]), () => ({})))
|
|
679
|
+
|
|
680
|
+
const trigger = <P>(
|
|
681
|
+
eventTrigger: Effect.Effect<Trigger<P>, never, Bus>,
|
|
682
|
+
): Effect.Effect<Trigger<P>, never, Bus> => eventTrigger
|
|
683
|
+
|
|
684
|
+
// oxlint-disable-next-line reform-rules/prefer-effect-fn -- public generic view preserves the exact form state/config/input requirements verbatim
|
|
685
|
+
export const view = <
|
|
686
|
+
N extends string,
|
|
687
|
+
Schema extends S.Schema.AnyNoContext,
|
|
688
|
+
FormInputs extends InputRecord,
|
|
689
|
+
>(
|
|
690
|
+
form: FormClass<N, Schema, FormInputs>,
|
|
691
|
+
): Effect.Effect<
|
|
692
|
+
FormView<ValuesOfSchema<Schema>, InputsObject<FormInputs>>,
|
|
693
|
+
never,
|
|
694
|
+
| State.StateStore<`${N}/state`, FormState<ValuesOfSchema<Schema>>>
|
|
695
|
+
| RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<FormInputs>>
|
|
696
|
+
| Bus
|
|
697
|
+
| InputStores<FormInputs>
|
|
698
|
+
> =>
|
|
699
|
+
Effect.gen(function* () {
|
|
700
|
+
const state = yield* form.state
|
|
701
|
+
const runtimeConfig = yield* form.config
|
|
702
|
+
const inputs = yield* readInputs(form.manifest.inputs)
|
|
703
|
+
const limitations = Option.getOrElse(
|
|
704
|
+
Option.fromNullable(runtimeConfig.limit?.({ values: state.values, inputs })),
|
|
705
|
+
() => ({}),
|
|
706
|
+
)
|
|
707
|
+
const setField = yield* trigger(form.events.set.trigger)
|
|
708
|
+
const blurField = yield* trigger(form.events.blur.trigger)
|
|
709
|
+
const reset = yield* trigger(form.events.reset.trigger)
|
|
710
|
+
const append = yield* trigger(form.events.append.trigger)
|
|
711
|
+
const remove = yield* trigger(form.events.remove.trigger)
|
|
712
|
+
const move = yield* trigger(form.events.move.trigger)
|
|
713
|
+
const swap = yield* trigger(form.events.swap.trigger)
|
|
714
|
+
const runtime = yield* Effect.runtime<Bus>()
|
|
715
|
+
|
|
716
|
+
const schema = form.manifest.schema
|
|
717
|
+
const decodeEither = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
|
|
718
|
+
const canSubmit = Either.isRight(decodeEither) && Record.keys(state.errors).length === 0
|
|
719
|
+
|
|
720
|
+
// oxlint-disable-next-line reform-rules/prefer-effect-fn -- local closure whose inferred R must flow into the parent view gen; Effect.fn would force an explicit Return annotation over the erased AnyForm requirements
|
|
721
|
+
const runValidation = (publishSubmitAttempt: boolean) =>
|
|
722
|
+
Effect.gen(function* () {
|
|
723
|
+
if (publishSubmitAttempt) {
|
|
724
|
+
yield* Event.dispatch(form.events.submitAttempted, {})
|
|
725
|
+
}
|
|
726
|
+
const decoded = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
|
|
727
|
+
if (Either.isLeft(decoded)) {
|
|
728
|
+
yield* Event.dispatch(form.events.validationFinished, {
|
|
729
|
+
errors: routeParseError(decoded.left),
|
|
730
|
+
})
|
|
731
|
+
return Option.none<DecodedOfSchema<Schema>>()
|
|
732
|
+
}
|
|
733
|
+
const custom = yield* runtimeConfig.validate({
|
|
734
|
+
values: state.values,
|
|
735
|
+
decoded: decoded.right,
|
|
736
|
+
inputs,
|
|
737
|
+
})
|
|
738
|
+
const errors = errorsToRecord(custom)
|
|
739
|
+
yield* Event.dispatch(form.events.validationFinished, { errors })
|
|
740
|
+
return Record.keys(errors).length === 0
|
|
741
|
+
? Option.some(decoded.right)
|
|
742
|
+
: Option.none<DecodedOfSchema<Schema>>()
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
const submit = () => {
|
|
746
|
+
Runtime.runFork(runtime)(
|
|
747
|
+
Effect.gen(function* () {
|
|
748
|
+
const decoded = yield* runValidation(true)
|
|
749
|
+
if (Option.isNone(decoded)) {
|
|
750
|
+
return
|
|
751
|
+
}
|
|
752
|
+
yield* runtimeConfig.submit({ values: state.values, decoded: decoded.value, inputs })
|
|
753
|
+
yield* Event.dispatch(form.events.submitSucceeded, { values: state.values })
|
|
754
|
+
}).pipe(
|
|
755
|
+
Effect.catchAllCause((cause) => Effect.logError('reform form submit failed', cause)),
|
|
756
|
+
),
|
|
757
|
+
)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const validate = () => {
|
|
761
|
+
Runtime.runFork(runtime)(runValidation(false))
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const field = <P extends FieldPath<ValuesOfSchema<Schema>>>(
|
|
765
|
+
path: P,
|
|
766
|
+
): FieldBinding<PathValue<ValuesOfSchema<Schema>, P>> => {
|
|
767
|
+
const fieldValue: PathValue<ValuesOfSchema<Schema>, P> = Fn.unsafeCoerce(
|
|
768
|
+
getNestedValue(state.values, path),
|
|
769
|
+
)
|
|
770
|
+
return {
|
|
771
|
+
path,
|
|
772
|
+
value: fieldValue,
|
|
773
|
+
set: (next) => {
|
|
774
|
+
if (typeof next === 'function') {
|
|
775
|
+
const updater = Fn.unsafeCoerce<
|
|
776
|
+
typeof next,
|
|
777
|
+
(prev: PathValue<ValuesOfSchema<Schema>, P>) => PathValue<ValuesOfSchema<Schema>, P>
|
|
778
|
+
>(next)
|
|
779
|
+
setField({ path, value: updater(fieldValue) })
|
|
780
|
+
return
|
|
781
|
+
}
|
|
782
|
+
setField({ path, value: next })
|
|
783
|
+
},
|
|
784
|
+
blur: () => blurField({ path }),
|
|
785
|
+
error: firstError(state.errors, path),
|
|
786
|
+
dirty: isPathOrParentDirty(state.dirtyPaths, path),
|
|
787
|
+
touched: state.touched[path] === true,
|
|
788
|
+
validating: false,
|
|
789
|
+
limitations: limitationsFor<PathValue<ValuesOfSchema<Schema>, P>>(limitations, path),
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const array = <P extends ArrayPath<ValuesOfSchema<Schema>>>(
|
|
794
|
+
path: P,
|
|
795
|
+
): ArrayBinding<ArrayItem<ValuesOfSchema<Schema>, P>> => {
|
|
796
|
+
const arrayValues: ReadonlyArray<ArrayItem<ValuesOfSchema<Schema>, P>> = Fn.unsafeCoerce(
|
|
797
|
+
currentArray({ source: state.values, path }),
|
|
798
|
+
)
|
|
799
|
+
const keys = state.arrayKeys[path] ?? arrayValues.map((_, index) => `${path}-${index}`)
|
|
800
|
+
return {
|
|
801
|
+
path,
|
|
802
|
+
items: arrayValues.map((element, index) => ({
|
|
803
|
+
value: element,
|
|
804
|
+
index,
|
|
805
|
+
key: keys[index] ?? `${path}-${index}`,
|
|
806
|
+
remove: () => remove({ path, index }),
|
|
807
|
+
move: (to) => move({ path, from: index, to }),
|
|
808
|
+
})),
|
|
809
|
+
append: (element) => append({ path, value: element }),
|
|
810
|
+
remove: (index) => remove({ path, index }),
|
|
811
|
+
move: (from, to) => move({ path, from, to }),
|
|
812
|
+
swap: (first, second) => swap({ path, a: first, b: second }),
|
|
813
|
+
limitations: limitationsFor<ReadonlyArray<ArrayItem<ValuesOfSchema<Schema>, P>>>(
|
|
814
|
+
limitations,
|
|
815
|
+
path,
|
|
816
|
+
),
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const formView: FormView<ValuesOfSchema<Schema>, InputsObject<FormInputs>> = Fn.unsafeCoerce({
|
|
821
|
+
values: state.values,
|
|
822
|
+
inputs,
|
|
823
|
+
errors: state.errors,
|
|
824
|
+
dirty: state.dirtyPaths.length > 0,
|
|
825
|
+
canSubmit,
|
|
826
|
+
submitCount: state.submitCount,
|
|
827
|
+
validationCount: state.validationCount,
|
|
828
|
+
lastSubmittedValues: state.lastSubmittedValues,
|
|
829
|
+
submit,
|
|
830
|
+
reset: () => reset({}),
|
|
831
|
+
validate,
|
|
832
|
+
field,
|
|
833
|
+
array,
|
|
834
|
+
variantValue: (path: string) => Fn.unsafeCoerce(getNestedValue(state.values, path)),
|
|
835
|
+
})
|
|
836
|
+
return formView
|
|
837
|
+
})
|