@playfast/reform-forms 0.1.0 → 1.1.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/src/form.ts CHANGED
@@ -1,415 +1,74 @@
1
1
  import {
2
2
  Context,
3
3
  Effect,
4
- Either,
5
4
  Function as Fn,
6
5
  Layer,
7
6
  Option,
8
- Record,
9
- Runtime,
10
7
  Schema as S,
11
8
  } from 'effect'
12
9
  import {
13
10
  Bus,
14
- CurrentTracker,
15
11
  Event,
16
12
  Reducer,
17
13
  Reducers,
18
14
  State,
19
15
  type Store,
20
- type Trigger,
21
16
  } from '@playfast/reform'
22
- import type { AnySource, Source } 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'
23
32
  import {
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'
33
+ appendAtPath,
34
+ initialState,
35
+ markTouched,
36
+ moveAtPath,
37
+ removeAtPath,
38
+ setAtPath,
39
+ swapAtPath,
40
+ } from './formState'
37
41
  import {
38
42
  error,
39
- errorsToRecord,
40
- firstError,
41
43
  normalizeErrors,
42
- routeParseError,
43
- type FormError,
44
44
  type FormErrors,
45
45
  } from './validation'
46
46
 
47
47
  export { error }
48
48
 
49
- // Plain-JSON UI-contract shape (flows into FieldBinding.limitations, serialized
50
- // over the wire): optional knobs are part of the external contract, so the
51
- // `ExternalApi` postfix opts this out of the Option-only field rule.
52
- export interface FieldLimitationsExternalApi<A = unknown> {
53
- readonly required?: boolean
54
- readonly disabled?: boolean
55
- readonly readonly?: boolean
56
- readonly visible?: boolean
57
- readonly min?: number
58
- readonly max?: number
59
- readonly minLength?: number
60
- readonly maxLength?: number
61
- readonly minItems?: number
62
- readonly maxItems?: number
63
- readonly options?: ReadonlyArray<A>
64
- readonly meta?: Readonly<Record<string, unknown>>
65
- }
66
-
67
- export type FieldLimitations<A = unknown> = FieldLimitationsExternalApi<A>
68
-
69
- export type Limitations = Readonly<Record<string, FieldLimitations>>
70
-
71
- export interface FormState<Values> {
72
- readonly values: Values
73
- readonly initialValues: Values
74
- readonly touched: Readonly<Record<string, boolean>>
75
- readonly errors: FormErrors
76
- readonly dirtyPaths: ReadonlyArray<string>
77
- readonly submitCount: number
78
- readonly validationCount: number
79
- readonly lastSubmittedValues: Option.Option<Values>
80
- readonly arrayKeys: Readonly<Record<string, ReadonlyArray<string>>>
81
- }
82
-
83
- export interface FieldBinding<A> {
84
- readonly path: string
85
- readonly value: A
86
- readonly set: (value: A | ((prev: A) => A)) => void
87
- readonly blur: () => void
88
- readonly error: Option.Option<string>
89
- readonly dirty: boolean
90
- readonly touched: boolean
91
- readonly validating: boolean
92
- readonly limitations: FieldLimitations<A>
93
- }
94
-
95
- export interface ArrayItemView<Item> {
96
- readonly key: string
97
- readonly index: number
98
- readonly value: Item
99
- readonly remove: () => void
100
- readonly move: (to: number) => void
101
- }
102
-
103
- export interface ArrayBinding<Item> {
104
- readonly path: string
105
- readonly items: ReadonlyArray<ArrayItemView<Item>>
106
- readonly append: (value?: Item) => void
107
- readonly remove: (index: number) => void
108
- readonly move: (from: number, to: number) => void
109
- readonly swap: (a: number, b: number) => void
110
- readonly limitations: FieldLimitations<ReadonlyArray<Item>>
111
- }
112
-
113
- export interface FormView<Values, Inputs = {}> {
114
- readonly values: Values
115
- readonly inputs: Inputs
116
- readonly errors: FormErrors
117
- readonly dirty: boolean
118
- readonly canSubmit: boolean
119
- readonly submitCount: number
120
- readonly validationCount: number
121
- readonly lastSubmittedValues: Option.Option<Values>
122
- readonly submit: () => void
123
- readonly reset: () => void
124
- readonly validate: () => void
125
- readonly field: <P extends FieldPath<Values>>(path: P) => FieldBinding<PathValue<Values, P>>
126
- readonly array: <P extends ArrayPath<Values>>(path: P) => ArrayBinding<ArrayItem<Values, P>>
127
- readonly variantValue: <P extends VariantPath<Values>>(path: P) => VariantValue<Values, P>
128
- }
129
-
130
- type InputRecord = Readonly<Record<string, AnySource>>
131
-
132
- // Extracted structurally (`S['name']`) rather than through a `Source<infer N,
133
- // unknown>` conditional: `Source`'s value parameter is invariant (`in out A`),
134
- // so a concrete `StateToken<'x', V>` never matches `Source<_, unknown>` and the
135
- // conditional would erase every input's key to `never`.
136
- type SourceName<S extends AnySource> = S['name']
137
- type SourceValue<S> = S extends Source<string, infer A> ? A : never
138
-
139
- type InputsObject<Inputs extends InputRecord> = {
140
- readonly [K in keyof Inputs as SourceName<Inputs[K]>]: SourceValue<Inputs[K]>
141
- }
142
-
143
- type InputStores<Inputs extends InputRecord> = {
144
- [K in keyof Inputs]: Inputs[K] extends { readonly store: Context.Tag<infer Service, any> }
145
- ? Service
146
- : never
147
- }[keyof Inputs]
148
-
149
- type ValuesOfSchema<S extends S.Schema.Any> = S.Schema.Encoded<S>
150
- type DecodedOfSchema<S extends S.Schema.Any> = S.Schema.Type<S>
151
-
152
- export interface RuntimeConfig<Values, Decoded, Inputs> {
153
- readonly initial: Values
154
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob (not a serializable shape); Option would break external form definitions
155
- readonly limit?: (ctx: {
156
- readonly values: Values
157
- readonly inputs: Inputs
158
- }) => Limitations
159
- readonly validate: (ctx: {
160
- readonly values: Values
161
- readonly decoded: Decoded
162
- readonly inputs: Inputs
163
- }) => Effect.Effect<ReadonlyArray<FormError>, never, never>
164
- readonly submit: (ctx: {
165
- readonly values: Values
166
- readonly decoded: Decoded
167
- readonly inputs: Inputs
168
- }) => Effect.Effect<void, unknown, never>
169
- }
170
-
171
- export interface FormLiveConfig<Values, Decoded, Inputs, R = never> {
172
- readonly initial: Values
173
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
174
- readonly limit?: (ctx: {
175
- readonly values: Values
176
- readonly inputs: Inputs
177
- }) => Limitations
178
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
179
- readonly validate?: (ctx: {
180
- readonly values: Values
181
- readonly decoded: Decoded
182
- readonly inputs: Inputs
183
- }) =>
184
- | void
185
- | FormError
186
- | ReadonlyArray<FormError>
187
- | Effect.Effect<void | FormError | ReadonlyArray<FormError>, never, R>
188
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
189
- readonly submit?: (ctx: {
190
- readonly values: Values
191
- readonly decoded: Decoded
192
- readonly inputs: Inputs
193
- }) => void | Effect.Effect<unknown, unknown, R>
194
- }
195
-
196
- export interface FormManifest<N extends string, Schema extends S.Schema.Any, Inputs extends InputRecord> {
197
- readonly kind: 'Form'
198
- readonly name: N
199
- readonly schema: Schema
200
- readonly inputs: Inputs
201
- }
202
-
203
- interface FormEvents {
204
- readonly set: Event.EventClass<string, { readonly path: string; readonly value: unknown }>
205
- readonly blur: Event.EventClass<string, { readonly path: string }>
206
- readonly reset: Event.EventClass<string, {}>
207
- readonly validationFinished: Event.EventClass<string, { readonly errors: FormErrors }>
208
- readonly submitAttempted: Event.EventClass<string, {}>
209
- readonly submitSucceeded: Event.EventClass<string, { readonly values: unknown }>
210
- readonly append: Event.EventClass<string, { readonly path: string; readonly value: unknown }>
211
- readonly remove: Event.EventClass<string, { readonly path: string; readonly index: number }>
212
- readonly move: Event.EventClass<string, { readonly path: string; readonly from: number; readonly to: number }>
213
- readonly swap: Event.EventClass<string, { readonly path: string; readonly a: number; readonly b: number }>
214
- }
215
-
216
- export interface FormClass<
217
- N extends string,
218
- Schema extends S.Schema.Any,
219
- Inputs extends InputRecord,
220
- > {
221
- new (): {}
222
- readonly manifest: FormManifest<N, Schema, Inputs>
223
- readonly state: State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>>
224
- readonly config: Context.Tag<
225
- RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>,
226
- RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>
227
- >
228
- readonly events: FormEvents
229
- readonly reducer: Reducer.StateReducerClass<
230
- State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>>,
231
- ReadonlyArray<Event.AnyEvent>
232
- >
233
- }
234
-
235
- // `any` (not `InputRecord`) in the inputs slot: the inputs type flows into the
236
- // invariant `config` tag (via `InputsObject`), so a wildcard there must be `any`
237
- // for a concrete form — whose inputs object has real keys — to remain assignable
238
- // to `AnyForm`. Same Schema-variance escape the schema slot already uses.
239
- export type AnyForm = FormClass<string, S.Schema.Any, any>
240
-
241
- export type Values<F extends AnyForm> = F extends FormClass<string, infer Schema, any>
242
- ? ValuesOfSchema<Schema>
243
- : never
244
-
245
- export type Decoded<F extends AnyForm> = F extends FormClass<string, infer Schema, any>
246
- ? DecodedOfSchema<Schema>
247
- : never
248
-
249
- export type Inputs<F extends AnyForm> = F extends FormClass<string, S.Schema.Any, infer Inputs>
250
- ? InputsObject<Inputs>
251
- : never
252
-
253
- export type View<F extends AnyForm> = FormView<Values<F>, Inputs<F>>
254
-
255
- const unknownSchema: S.Schema<any, any> = Fn.unsafeCoerce(S.Unknown)
256
-
257
- const arrayKeyCounter = { current: 0 }
258
- const makeArrayKey = (): string => `form-item-${arrayKeyCounter.current++}`
259
-
260
- export const arrayKeysFor = (source: unknown, path = ''): Record<string, ReadonlyArray<string>> => {
261
- const out: Record<string, ReadonlyArray<string>> = {}
262
- const visit = (node: KeyVisitNode): void => {
263
- if (Array.isArray(node.current)) {
264
- out[node.path] = node.current.map(() => makeArrayKey())
265
- node.current.forEach((element, index) => visit({ current: element, path: `${node.path}[${index}]` }))
266
- return
267
- }
268
- if (node.current !== null && typeof node.current === 'object') {
269
- Record.toEntries(Fn.unsafeCoerce<unknown, Record<string, unknown>>(node.current)).forEach(([key, child]) =>
270
- visit({ current: child, path: node.path.length === 0 ? key : `${node.path}.${key}` }),
271
- )
272
- }
273
- }
274
- visit({ current: source, path })
275
- return out
276
- }
277
-
278
- const initialState = <Values>(initial: Values): FormState<Values> => ({
279
- values: initial,
280
- initialValues: initial,
281
- touched: {},
282
- errors: {},
283
- dirtyPaths: [],
284
- submitCount: 0,
285
- validationCount: 0,
286
- lastSubmittedValues: Option.none(),
287
- arrayKeys: arrayKeysFor(initial),
288
- })
289
-
290
- const markTouched = (touched: Readonly<Record<string, boolean>>, path: string): Readonly<Record<string, boolean>> =>
291
- ({ ...touched, [path]: true })
292
-
293
- interface ArrayLookup {
294
- readonly source: unknown
295
- readonly path: string
296
- }
297
-
298
- interface KeyVisitNode {
299
- readonly current: unknown
300
- readonly path: string
301
- }
302
-
303
- interface PathInput<Values> {
304
- readonly state: FormState<Values>
305
- readonly path: string
306
- }
307
-
308
- interface SetPathInput<Values> extends PathInput<Values> {
309
- readonly value: unknown
310
- }
311
-
312
- interface RemovePathInput<Values> extends PathInput<Values> {
313
- readonly index: number
314
- }
315
-
316
- interface MovePathInput<Values> extends PathInput<Values> {
317
- readonly from: number
318
- readonly to: number
319
- }
320
-
321
- interface SwapPathInput<Values> extends PathInput<Values> {
322
- readonly first: number
323
- readonly second: number
324
- }
325
-
326
- const withValues = <Values>(state: FormState<Values>, nextValues: Values): FormState<Values> => ({
327
- ...state,
328
- values: nextValues,
329
- dirtyPaths: recalculateDirtyPaths(state.initialValues, nextValues),
330
- })
331
-
332
- const currentArray = (input: ArrayLookup): ReadonlyArray<unknown> => {
333
- const current = getNestedValue(input.source, input.path)
334
- return Array.isArray(current) ? current : []
335
- }
336
-
337
- const updateKeys = (
338
- state: FormState<unknown>,
339
- path: string,
340
- transform: (keys: ReadonlyArray<string>) => ReadonlyArray<string>,
341
- ): Readonly<Record<string, ReadonlyArray<string>>> => {
342
- const existing = state.arrayKeys[path] ?? currentArray({ source: state.values, path }).map(() => makeArrayKey())
343
- return { ...state.arrayKeys, [path]: transform(existing) }
344
- }
345
-
346
- const setAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> =>
347
- withValues(input.state, setNestedValue(input.state.values, input.path, input.value))
348
-
349
- const appendAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> => {
350
- const elements = currentArray({ source: input.state.values, path: input.path })
351
- const nextValues = setNestedValue(input.state.values, input.path, [...elements, input.value])
352
- return {
353
- ...withValues(input.state, nextValues),
354
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => [...keys, makeArrayKey()]),
355
- }
356
- }
357
-
358
- const removeAtPath = <Values>(input: RemovePathInput<Values>): FormState<Values> => {
359
- const elements = currentArray({ source: input.state.values, path: input.path })
360
- if (input.index < 0 || input.index >= elements.length) {
361
- return input.state
362
- }
363
- const nextValues = setNestedValue(
364
- input.state.values,
365
- input.path,
366
- elements.filter((_, position) => position !== input.index),
367
- )
368
- return {
369
- ...withValues(input.state, nextValues),
370
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
371
- keys.filter((_, position) => position !== input.index),
372
- ),
373
- }
374
- }
375
-
376
- const moveAtPath = <Values>(input: MovePathInput<Values>): FormState<Values> => {
377
- const elements = currentArray({ source: input.state.values, path: input.path })
378
- const next = moveAt(elements, input.from, input.to)
379
- if (next === elements) {
380
- return input.state
381
- }
382
- const nextValues = setNestedValue(input.state.values, input.path, next)
383
- return {
384
- ...withValues(input.state, nextValues),
385
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => moveAt(keys, input.from, input.to)),
386
- }
387
- }
388
-
389
- const swapAtPath = <Values>(input: SwapPathInput<Values>): FormState<Values> => {
390
- const elements = currentArray({ source: input.state.values, path: input.path })
391
- if (
392
- input.first < 0 ||
393
- input.second < 0 ||
394
- input.first >= elements.length ||
395
- input.second >= elements.length ||
396
- input.first === input.second
397
- ) {
398
- return input.state
399
- }
400
- const swapped = replaceAt(replaceAt(elements, input.first, elements[input.second]), input.second, elements[input.first])
401
- const nextValues = setNestedValue(input.state.values, input.path, swapped)
402
- return {
403
- ...withValues(input.state, nextValues),
404
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
405
- replaceAt(
406
- replaceAt(keys, input.first, keys[input.second] ?? makeArrayKey()),
407
- input.second,
408
- keys[input.first] ?? makeArrayKey(),
409
- ),
410
- ),
411
- }
412
- }
49
+ export type {
50
+ AnyForm,
51
+ ArrayBinding,
52
+ ArrayItemView,
53
+ Decoded,
54
+ FieldBinding,
55
+ FieldLimitations,
56
+ FieldLimitationsExternalApi,
57
+ FormClass,
58
+ FormLiveConfig,
59
+ FormManifest,
60
+ FormState,
61
+ FormView,
62
+ Inputs,
63
+ Limitations,
64
+ RuntimeConfig,
65
+ Values,
66
+ View,
67
+ } from './formTypes'
68
+
69
+ const unknownSchema: S.Schema.AnyNoContext = Fn.unsafeCoerce(S.Unknown)
70
+
71
+ export { arrayKeysFor } from './formState'
413
72
 
414
73
  interface MakeConfig<Schema extends S.Schema.Any, Inputs extends InputRecord> {
415
74
  readonly schema: Schema
@@ -571,169 +230,4 @@ export const live = <
571
230
  )
572
231
  }
573
232
 
574
- const readInput = <Source extends AnySource>(
575
- source: Source,
576
- ): Effect.Effect<SourceValue<Source>, never, Store<SourceValue<Source>>> =>
577
- Fn.unsafeCoerce(
578
- Effect.gen(function* () {
579
- const store = yield* source.store
580
- const tracker = yield* Effect.serviceOption(CurrentTracker)
581
- if (Option.isSome(tracker)) {
582
- tracker.value.add(store)
583
- }
584
- return store.getSnapshot()
585
- }),
586
- )
587
-
588
- const readInputs = <Inputs extends InputRecord>(
589
- inputs: Inputs,
590
- ): Effect.Effect<InputsObject<Inputs>, never, InputStores<Inputs>> =>
591
- Fn.unsafeCoerce(
592
- Effect.gen(function* () {
593
- const pairs = yield* Effect.forEach(Object.values(inputs), (source) =>
594
- Effect.map(readInput(source), (snapshot) => [source.name, snapshot] as const),
595
- )
596
- return Record.fromEntries(pairs)
597
- }),
598
- )
599
-
600
- const limitationsFor = <A>(
601
- limitations: Limitations,
602
- path: string,
603
- ): FieldLimitations<A> =>
604
- Fn.unsafeCoerce(Option.getOrElse(Option.fromNullable(limitations[path]), () => ({})))
605
-
606
- const trigger = <P>(eventTrigger: Effect.Effect<Trigger<P>, never, Bus>): Effect.Effect<Trigger<P>, never, Bus> =>
607
- eventTrigger
608
-
609
- // oxlint-disable-next-line reform-rules/prefer-effect-fn -- public generic view: the explicit R (Store | RuntimeConfig | Bus | InputStores) must be preserved verbatim; Effect.fn would re-infer requirements off the erased AnyForm wildcard
610
- export const view = <F extends AnyForm>(
611
- form: F,
612
- ): Effect.Effect<View<F>, never, Store<FormState<Values<F>>> | RuntimeConfig<Values<F>, Decoded<F>, Inputs<F>> | Bus | InputStores<F['manifest']['inputs']>> =>
613
- Effect.gen(function* () {
614
- const state = yield* form.state
615
- const runtimeConfig = yield* form.config
616
- // Erased boundary (same shape as the coercions closing `live`/`readInputs`):
617
- // under the `AnyForm` constraint `form.manifest.inputs` is the wildcard
618
- // `any`, which would dissolve the generator's R to `unknown`; restate the
619
- // call at the concrete `F`'s types so R keeps the declared input stores.
620
- const inputs: Inputs<F> = Fn.unsafeCoerce(
621
- yield* readInputs(Fn.unsafeCoerce<typeof form.manifest.inputs, InputRecord>(form.manifest.inputs)),
622
- )
623
- const limitations = Option.getOrElse(
624
- Option.fromNullable(runtimeConfig.limit?.({ values: state.values, inputs })),
625
- () => ({}),
626
- )
627
- const setField = yield* trigger(form.events.set.trigger)
628
- const blurField = yield* trigger(form.events.blur.trigger)
629
- const reset = yield* trigger(form.events.reset.trigger)
630
- const append = yield* trigger(form.events.append.trigger)
631
- const remove = yield* trigger(form.events.remove.trigger)
632
- const move = yield* trigger(form.events.move.trigger)
633
- const swap = yield* trigger(form.events.swap.trigger)
634
- const runtime = yield* Effect.runtime<Bus>()
635
-
636
- const schema: S.Schema<Decoded<F>, Values<F>, never> = Fn.unsafeCoerce(form.manifest.schema)
637
- const decodeEither = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
638
- const canSubmit = Either.isRight(decodeEither) && Record.keys(state.errors).length === 0
639
-
640
- // 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
641
- const runValidation = (publishSubmitAttempt: boolean) =>
642
- Effect.gen(function* () {
643
- if (publishSubmitAttempt) {
644
- yield* Event.dispatch(form.events.submitAttempted, {})
645
- }
646
- const decoded = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
647
- if (Either.isLeft(decoded)) {
648
- yield* Event.dispatch(form.events.validationFinished, { errors: routeParseError(decoded.left) })
649
- return Option.none<Decoded<F>>()
650
- }
651
- const custom = yield* runtimeConfig.validate({ values: state.values, decoded: decoded.right, inputs })
652
- const errors = errorsToRecord(custom)
653
- yield* Event.dispatch(form.events.validationFinished, { errors })
654
- return Record.keys(errors).length === 0
655
- ? Option.some(Fn.unsafeCoerce<unknown, Decoded<F>>(decoded.right))
656
- : Option.none<Decoded<F>>()
657
- })
658
-
659
- const submit = () => {
660
- Runtime.runFork(runtime)(
661
- Effect.gen(function* () {
662
- const decoded = yield* runValidation(true)
663
- if (Option.isNone(decoded)) {
664
- return
665
- }
666
- yield* runtimeConfig.submit({ values: state.values, decoded: decoded.value, inputs })
667
- yield* Event.dispatch(form.events.submitSucceeded, { values: state.values })
668
- }).pipe(Effect.catchAllCause((cause) => Effect.logError('reform form submit failed', cause))),
669
- )
670
- }
671
-
672
- const validate = () => {
673
- Runtime.runFork(runtime)(runValidation(false))
674
- }
675
-
676
- const field = <P extends FieldPath<Values<F>>>(path: P): FieldBinding<PathValue<Values<F>, P>> => {
677
- const fieldValue: PathValue<Values<F>, P> = Fn.unsafeCoerce(getNestedValue(state.values, path))
678
- return {
679
- path,
680
- value: fieldValue,
681
- set: (next) => {
682
- if (typeof next === 'function') {
683
- const updater = Fn.unsafeCoerce<typeof next, (prev: PathValue<Values<F>, P>) => PathValue<Values<F>, P>>(
684
- next,
685
- )
686
- setField({ path, value: updater(fieldValue) })
687
- return
688
- }
689
- setField({ path, value: next })
690
- },
691
- blur: () => blurField({ path }),
692
- error: firstError(state.errors, path),
693
- dirty: isPathOrParentDirty(state.dirtyPaths, path),
694
- touched: state.touched[path] === true,
695
- validating: false,
696
- limitations: limitationsFor<PathValue<Values<F>, P>>(limitations, path),
697
- }
698
- }
699
-
700
- const array = <P extends ArrayPath<Values<F>>>(path: P): ArrayBinding<ArrayItem<Values<F>, P>> => {
701
- const arrayValues: ReadonlyArray<ArrayItem<Values<F>, P>> = Fn.unsafeCoerce(
702
- currentArray({ source: state.values, path }),
703
- )
704
- const keys = state.arrayKeys[path] ?? arrayValues.map((_, index) => `${path}-${index}`)
705
- return {
706
- path,
707
- items: arrayValues.map((element, index) => ({
708
- value: element,
709
- index,
710
- key: keys[index] ?? `${path}-${index}`,
711
- remove: () => remove({ path, index }),
712
- move: (to) => move({ path, from: index, to }),
713
- })),
714
- append: (element) => append({ path, value: element }),
715
- remove: (index) => remove({ path, index }),
716
- move: (from, to) => move({ path, from, to }),
717
- swap: (first, second) => swap({ path, a: first, b: second }),
718
- limitations: limitationsFor<ReadonlyArray<ArrayItem<Values<F>, P>>>(limitations, path),
719
- }
720
- }
721
-
722
- const formView: View<F> = Fn.unsafeCoerce({
723
- values: state.values,
724
- inputs,
725
- errors: state.errors,
726
- dirty: state.dirtyPaths.length > 0,
727
- canSubmit,
728
- submitCount: state.submitCount,
729
- validationCount: state.validationCount,
730
- lastSubmittedValues: state.lastSubmittedValues,
731
- submit,
732
- reset: () => reset({}),
733
- validate,
734
- field,
735
- array,
736
- variantValue: (path: string) => Fn.unsafeCoerce(getNestedValue(state.values, path)),
737
- })
738
- return formView
739
- })
233
+ export { view } from './formView'