@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/src/index.ts CHANGED
@@ -16,13 +16,7 @@ export type {
16
16
  Values,
17
17
  View,
18
18
  } from './form'
19
- export {
20
- arrayKeysFor as unsafeArrayKeysFor,
21
- error,
22
- live,
23
- make,
24
- view,
25
- } from './form'
19
+ export { arrayKeysFor as unsafeArrayKeysFor, error, live, make, view } from './form'
26
20
  export type {
27
21
  ArrayItem,
28
22
  ArrayPath,
package/src/path.ts CHANGED
@@ -24,52 +24,48 @@ type IsTagged<T> = T extends Tagged ? true : false
24
24
  export type FieldPath<T> = T extends Primitive
25
25
  ? never
26
26
  : {
27
- readonly [K in StringKey<T>]:
28
- T[K] extends ReadonlyArray<unknown>
27
+ readonly [K in StringKey<T>]: T[K] extends ReadonlyArray<unknown>
28
+ ? never
29
+ : IsTagged<T[K]> extends true
29
30
  ? never
30
- : IsTagged<T[K]> extends true
31
- ? never
32
- : T[K] extends Primitive
33
- ? K
34
- : T[K] extends object
35
- ? K | `${K}.${FieldPath<T[K]>}`
36
- : K
31
+ : T[K] extends Primitive
32
+ ? K
33
+ : T[K] extends object
34
+ ? K | `${K}.${FieldPath<T[K]>}`
35
+ : K
37
36
  }[StringKey<T>]
38
37
 
39
38
  export type ArrayPath<T> = T extends Primitive
40
39
  ? never
41
40
  : {
42
- readonly [K in StringKey<T>]:
43
- T[K] extends ReadonlyArray<unknown>
44
- ? K
45
- : IsTagged<T[K]> extends true
46
- ? never
47
- : T[K] extends object
48
- ? `${K}.${ArrayPath<T[K]>}`
49
- : never
41
+ readonly [K in StringKey<T>]: T[K] extends ReadonlyArray<unknown>
42
+ ? K
43
+ : IsTagged<T[K]> extends true
44
+ ? never
45
+ : T[K] extends object
46
+ ? `${K}.${ArrayPath<T[K]>}`
47
+ : never
50
48
  }[StringKey<T>]
51
49
 
52
50
  export type VariantPath<T> = T extends Primitive
53
51
  ? never
54
52
  : {
55
- readonly [K in StringKey<T>]:
56
- IsTagged<T[K]> extends true
57
- ? K
58
- : T[K] extends ReadonlyArray<unknown>
59
- ? never
60
- : T[K] extends object
61
- ? `${K}.${VariantPath<T[K]>}`
62
- : never
53
+ readonly [K in StringKey<T>]: IsTagged<T[K]> extends true
54
+ ? K
55
+ : T[K] extends ReadonlyArray<unknown>
56
+ ? never
57
+ : T[K] extends object
58
+ ? `${K}.${VariantPath<T[K]>}`
59
+ : never
63
60
  }[StringKey<T>]
64
61
 
65
- export type PathValue<T, P extends string> =
66
- P extends `${infer Head}.${infer Tail}`
67
- ? Head extends keyof T
68
- ? PathValue<T[Head], Tail>
69
- : never
70
- : P extends keyof T
71
- ? T[P]
72
- : never
62
+ export type PathValue<T, P extends string> = P extends `${infer Head}.${infer Tail}`
63
+ ? Head extends keyof T
64
+ ? PathValue<T[Head], Tail>
65
+ : never
66
+ : P extends keyof T
67
+ ? T[P]
68
+ : never
73
69
 
74
70
  export type ArrayItem<T, P extends string> =
75
71
  PathValue<T, P> extends ReadonlyArray<infer Item> ? Item : never
@@ -196,7 +192,9 @@ export const recalculateDirtyPaths = (
196
192
  ) {
197
193
  const currentRecord = asRecord(current)
198
194
  const originalRecord = asRecord(original)
199
- const keys = Array.from(new Set([...Record.keys(currentRecord), ...Record.keys(originalRecord)]))
195
+ const keys = Array.from(
196
+ new Set([...Record.keys(currentRecord), ...Record.keys(originalRecord)]),
197
+ )
200
198
  return keys.flatMap((key) =>
201
199
  visit({
202
200
  current: currentRecord[key],
package/src/formState.ts DELETED
@@ -1,166 +0,0 @@
1
- import { Function as Fn, Option, Record } from 'effect'
2
- import {
3
- getNestedValue,
4
- moveAt,
5
- recalculateDirtyPaths,
6
- replaceAt,
7
- setNestedValue,
8
- } from './path'
9
- import type { FormState } from './formTypes'
10
-
11
- const arrayKeyCounter = { current: 0 }
12
- const makeArrayKey = (): string => `form-item-${arrayKeyCounter.current++}`
13
-
14
- export const arrayKeysFor = (source: unknown, path = ''): Record<string, ReadonlyArray<string>> => {
15
- const out: Record<string, ReadonlyArray<string>> = {}
16
- const visit = (node: KeyVisitNode): void => {
17
- if (Array.isArray(node.current)) {
18
- out[node.path] = node.current.map(() => makeArrayKey())
19
- node.current.forEach((element, index) => visit({ current: element, path: `${node.path}[${index}]` }))
20
- return
21
- }
22
- if (node.current !== null && typeof node.current === 'object') {
23
- Record.toEntries(Fn.unsafeCoerce<unknown, Record<string, unknown>>(node.current)).forEach(([key, child]) =>
24
- visit({ current: child, path: node.path.length === 0 ? key : `${node.path}.${key}` }),
25
- )
26
- }
27
- }
28
- visit({ current: source, path })
29
- return out
30
- }
31
-
32
- export const initialState = <Values>(initial: Values): FormState<Values> => ({
33
- values: initial,
34
- initialValues: initial,
35
- touched: {},
36
- errors: {},
37
- dirtyPaths: [],
38
- submitCount: 0,
39
- validationCount: 0,
40
- lastSubmittedValues: Option.none(),
41
- arrayKeys: arrayKeysFor(initial),
42
- })
43
-
44
- export const markTouched = (touched: Readonly<Record<string, boolean>>, path: string): Readonly<Record<string, boolean>> =>
45
- ({ ...touched, [path]: true })
46
-
47
- interface ArrayLookup {
48
- readonly source: unknown
49
- readonly path: string
50
- }
51
-
52
- interface KeyVisitNode {
53
- readonly current: unknown
54
- readonly path: string
55
- }
56
-
57
- interface PathInput<Values> {
58
- readonly state: FormState<Values>
59
- readonly path: string
60
- }
61
-
62
- interface SetPathInput<Values> extends PathInput<Values> {
63
- readonly value: unknown
64
- }
65
-
66
- interface RemovePathInput<Values> extends PathInput<Values> {
67
- readonly index: number
68
- }
69
-
70
- interface MovePathInput<Values> extends PathInput<Values> {
71
- readonly from: number
72
- readonly to: number
73
- }
74
-
75
- interface SwapPathInput<Values> extends PathInput<Values> {
76
- readonly first: number
77
- readonly second: number
78
- }
79
-
80
- const withValues = <Values>(state: FormState<Values>, nextValues: Values): FormState<Values> => ({
81
- ...state,
82
- values: nextValues,
83
- dirtyPaths: recalculateDirtyPaths(state.initialValues, nextValues),
84
- })
85
-
86
- export const currentArray = (input: ArrayLookup): ReadonlyArray<unknown> => {
87
- const current = getNestedValue(input.source, input.path)
88
- return Array.isArray(current) ? current : []
89
- }
90
-
91
- const updateKeys = (
92
- state: FormState<unknown>,
93
- path: string,
94
- transform: (keys: ReadonlyArray<string>) => ReadonlyArray<string>,
95
- ): Readonly<Record<string, ReadonlyArray<string>>> => {
96
- const existing = state.arrayKeys[path] ?? currentArray({ source: state.values, path }).map(() => makeArrayKey())
97
- return { ...state.arrayKeys, [path]: transform(existing) }
98
- }
99
-
100
- export const setAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> =>
101
- withValues(input.state, setNestedValue(input.state.values, input.path, input.value))
102
-
103
- export const appendAtPath = <Values>(input: SetPathInput<Values>): FormState<Values> => {
104
- const elements = currentArray({ source: input.state.values, path: input.path })
105
- const nextValues = setNestedValue(input.state.values, input.path, [...elements, input.value])
106
- return {
107
- ...withValues(input.state, nextValues),
108
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => [...keys, makeArrayKey()]),
109
- }
110
- }
111
-
112
- export const removeAtPath = <Values>(input: RemovePathInput<Values>): FormState<Values> => {
113
- const elements = currentArray({ source: input.state.values, path: input.path })
114
- if (input.index < 0 || input.index >= elements.length) {
115
- return input.state
116
- }
117
- const nextValues = setNestedValue(
118
- input.state.values,
119
- input.path,
120
- elements.filter((_, position) => position !== input.index),
121
- )
122
- return {
123
- ...withValues(input.state, nextValues),
124
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
125
- keys.filter((_, position) => position !== input.index),
126
- ),
127
- }
128
- }
129
-
130
- export const moveAtPath = <Values>(input: MovePathInput<Values>): FormState<Values> => {
131
- const elements = currentArray({ source: input.state.values, path: input.path })
132
- const next = moveAt(elements, input.from, input.to)
133
- if (next === elements) {
134
- return input.state
135
- }
136
- const nextValues = setNestedValue(input.state.values, input.path, next)
137
- return {
138
- ...withValues(input.state, nextValues),
139
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) => moveAt(keys, input.from, input.to)),
140
- }
141
- }
142
-
143
- export const swapAtPath = <Values>(input: SwapPathInput<Values>): FormState<Values> => {
144
- const elements = currentArray({ source: input.state.values, path: input.path })
145
- if (
146
- input.first < 0 ||
147
- input.second < 0 ||
148
- input.first >= elements.length ||
149
- input.second >= elements.length ||
150
- input.first === input.second
151
- ) {
152
- return input.state
153
- }
154
- const swapped = replaceAt(replaceAt(elements, input.first, elements[input.second]), input.second, elements[input.first])
155
- const nextValues = setNestedValue(input.state.values, input.path, swapped)
156
- return {
157
- ...withValues(input.state, nextValues),
158
- arrayKeys: updateKeys(Fn.unsafeCoerce(input.state), input.path, (keys) =>
159
- replaceAt(
160
- replaceAt(keys, input.first, keys[input.second] ?? makeArrayKey()),
161
- input.second,
162
- keys[input.first] ?? makeArrayKey(),
163
- ),
164
- ),
165
- }
166
- }
package/src/formTypes.ts DELETED
@@ -1,211 +0,0 @@
1
- import { Context, Effect, Option, Schema as S } from 'effect'
2
- import type { AnySource, Event, Reducer, Source, State } from '@playfast/reform'
3
- import type {
4
- ArrayItem,
5
- ArrayPath,
6
- FieldPath,
7
- PathValue,
8
- VariantPath,
9
- VariantValue,
10
- } from './path'
11
- import type { FormError, FormErrors } from './validation'
12
-
13
- export type AnyValue = S.Schema.Type<S.Schema.Any>
14
-
15
- // Wire-serialized UI knobs: ExternalApi postfix allows optional fields (Option would break the contract).
16
- export interface FieldLimitationsExternalApi<A = unknown> {
17
- readonly required?: boolean
18
- readonly disabled?: boolean
19
- readonly readonly?: boolean
20
- readonly visible?: boolean
21
- readonly min?: number
22
- readonly max?: number
23
- readonly minLength?: number
24
- readonly maxLength?: number
25
- readonly minItems?: number
26
- readonly maxItems?: number
27
- readonly options?: ReadonlyArray<A>
28
- readonly meta?: Readonly<Record<string, unknown>>
29
- }
30
-
31
- export type FieldLimitations<A = unknown> = FieldLimitationsExternalApi<A>
32
-
33
- export type Limitations = Readonly<Record<string, FieldLimitations>>
34
-
35
- export interface FormState<Values> {
36
- readonly values: Values
37
- readonly initialValues: Values
38
- readonly touched: Readonly<Record<string, boolean>>
39
- readonly errors: FormErrors
40
- readonly dirtyPaths: ReadonlyArray<string>
41
- readonly submitCount: number
42
- readonly validationCount: number
43
- readonly lastSubmittedValues: Option.Option<Values>
44
- readonly arrayKeys: Readonly<Record<string, ReadonlyArray<string>>>
45
- }
46
-
47
- export interface FieldBinding<A> {
48
- readonly path: string
49
- readonly value: A
50
- readonly set: (value: A | ((prev: A) => A)) => void
51
- readonly blur: () => void
52
- readonly error: Option.Option<string>
53
- readonly dirty: boolean
54
- readonly touched: boolean
55
- readonly validating: boolean
56
- readonly limitations: FieldLimitations<A>
57
- }
58
-
59
- export interface ArrayItemView<Item> {
60
- readonly key: string
61
- readonly index: number
62
- readonly value: Item
63
- readonly remove: () => void
64
- readonly move: (to: number) => void
65
- }
66
-
67
- export interface ArrayBinding<Item> {
68
- readonly path: string
69
- readonly items: ReadonlyArray<ArrayItemView<Item>>
70
- readonly append: (value?: Item) => void
71
- readonly remove: (index: number) => void
72
- readonly move: (from: number, to: number) => void
73
- readonly swap: (a: number, b: number) => void
74
- readonly limitations: FieldLimitations<ReadonlyArray<Item>>
75
- }
76
-
77
- export interface FormView<Values, Inputs = {}> {
78
- readonly values: Values
79
- readonly inputs: Inputs
80
- readonly errors: FormErrors
81
- readonly dirty: boolean
82
- readonly canSubmit: boolean
83
- readonly submitCount: number
84
- readonly validationCount: number
85
- readonly lastSubmittedValues: Option.Option<Values>
86
- readonly submit: () => void
87
- readonly reset: () => void
88
- readonly validate: () => void
89
- readonly field: <P extends FieldPath<Values>>(path: P) => FieldBinding<PathValue<Values, P>>
90
- readonly array: <P extends ArrayPath<Values>>(path: P) => ArrayBinding<ArrayItem<Values, P>>
91
- readonly variantValue: <P extends VariantPath<Values>>(path: P) => VariantValue<Values, P>
92
- }
93
-
94
- export type InputRecord = Readonly<Record<string, AnySource>>
95
-
96
- // Structural `S['name']`: Source value param is invariant, so `Source<_, unknown>` erases keys to never.
97
- export type SourceName<S extends AnySource> = S['name']
98
- export type SourceValue<S> = S extends Source<string, infer A> ? A : never
99
-
100
- export type InputsObject<Inputs extends InputRecord> = {
101
- readonly [K in keyof Inputs as SourceName<Inputs[K]>]: SourceValue<Inputs[K]>
102
- }
103
-
104
- export type InputStores<Inputs extends InputRecord> = {
105
- [K in keyof Inputs]: Inputs[K] extends { readonly store: Context.Tag<infer Service, AnyValue> }
106
- ? Service
107
- : never
108
- }[keyof Inputs]
109
-
110
- export type ValuesOfSchema<S extends S.Schema.Any> = S.Schema.Encoded<S>
111
- export type DecodedOfSchema<S extends S.Schema.Any> = S.Schema.Type<S>
112
-
113
- export interface RuntimeConfig<Values, Decoded, Inputs> {
114
- readonly initial: Values
115
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob (not a serializable shape); Option would break external form definitions
116
- readonly limit?: (ctx: {
117
- readonly values: Values
118
- readonly inputs: Inputs
119
- }) => Limitations
120
- readonly validate: (ctx: {
121
- readonly values: Values
122
- readonly decoded: Decoded
123
- readonly inputs: Inputs
124
- }) => Effect.Effect<ReadonlyArray<FormError>, never, never>
125
- readonly submit: (ctx: {
126
- readonly values: Values
127
- readonly decoded: Decoded
128
- readonly inputs: Inputs
129
- }) => Effect.Effect<void, unknown, never>
130
- }
131
-
132
- export interface FormLiveConfig<Values, Decoded, Inputs, R = never> {
133
- readonly initial: Values
134
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
135
- readonly limit?: (ctx: {
136
- readonly values: Values
137
- readonly inputs: Inputs
138
- }) => Limitations
139
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
140
- readonly validate?: (ctx: {
141
- readonly values: Values
142
- readonly decoded: Decoded
143
- readonly inputs: Inputs
144
- }) =>
145
- | void
146
- | FormError
147
- | ReadonlyArray<FormError>
148
- | Effect.Effect<void | FormError | ReadonlyArray<FormError>, never, R>
149
- // oxlint-disable-next-line reform-rules/no-optional-fields -- optional Effect-returning config knob; Option would break external form definitions
150
- readonly submit?: (ctx: {
151
- readonly values: Values
152
- readonly decoded: Decoded
153
- readonly inputs: Inputs
154
- }) => void | Effect.Effect<unknown, unknown, R>
155
- }
156
-
157
- export interface FormManifest<N extends string, Schema extends S.Schema.Any, Inputs extends InputRecord> {
158
- readonly kind: 'Form'
159
- readonly name: N
160
- readonly schema: Schema
161
- readonly inputs: Inputs
162
- }
163
-
164
- export interface FormEvents {
165
- readonly set: Event.EventClass<string, { readonly path: string; readonly value: unknown }>
166
- readonly blur: Event.EventClass<string, { readonly path: string }>
167
- readonly reset: Event.EventClass<string, {}>
168
- readonly validationFinished: Event.EventClass<string, { readonly errors: FormErrors }>
169
- readonly submitAttempted: Event.EventClass<string, {}>
170
- readonly submitSucceeded: Event.EventClass<string, { readonly values: unknown }>
171
- readonly append: Event.EventClass<string, { readonly path: string; readonly value: unknown }>
172
- readonly remove: Event.EventClass<string, { readonly path: string; readonly index: number }>
173
- readonly move: Event.EventClass<string, { readonly path: string; readonly from: number; readonly to: number }>
174
- readonly swap: Event.EventClass<string, { readonly path: string; readonly a: number; readonly b: number }>
175
- }
176
-
177
- export interface FormClass<
178
- N extends string,
179
- Schema extends S.Schema.Any,
180
- Inputs extends InputRecord,
181
- > {
182
- new (): {}
183
- readonly manifest: FormManifest<N, Schema, Inputs>
184
- readonly state: State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>>
185
- readonly config: Context.Tag<
186
- RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>,
187
- RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<Inputs>>
188
- >
189
- readonly events: FormEvents
190
- readonly reducer: Reducer.StateReducerClass<
191
- State.StateClass<`${N}/state`, FormState<ValuesOfSchema<Schema>>>,
192
- ReadonlyArray<Event.AnyEvent>
193
- >
194
- }
195
-
196
- // `any` inputs wildcard: config tag is invariant via InputsObject; InputRecord would reject concrete forms.
197
- export type AnyForm = FormClass<string, S.Schema.Any, AnyValue>
198
-
199
- export type Values<F extends AnyForm> = F extends FormClass<string, infer Schema, AnyValue>
200
- ? ValuesOfSchema<Schema>
201
- : never
202
-
203
- export type Decoded<F extends AnyForm> = F extends FormClass<string, infer Schema, AnyValue>
204
- ? DecodedOfSchema<Schema>
205
- : never
206
-
207
- export type Inputs<F extends AnyForm> = F extends FormClass<string, S.Schema.Any, infer Inputs>
208
- ? InputsObject<Inputs>
209
- : never
210
-
211
- export type View<F extends AnyForm> = FormView<Values<F>, Inputs<F>>
package/src/formView.ts DELETED
@@ -1,202 +0,0 @@
1
- import {
2
- Effect,
3
- Either,
4
- Function as Fn,
5
- Option,
6
- Record,
7
- Runtime,
8
- Schema as S,
9
- } from 'effect'
10
- import {
11
- Bus,
12
- CurrentTracker,
13
- Event,
14
- type Store,
15
- type Trigger,
16
- } from '@playfast/reform'
17
- import type { AnySource } from '@playfast/reform'
18
- import { currentArray } from './formState'
19
- import { getNestedValue, isPathOrParentDirty } from './path'
20
- import type { ArrayItem, ArrayPath, FieldPath, PathValue } from './path'
21
- import { errorsToRecord, firstError, routeParseError } from './validation'
22
- import type {
23
- AnyForm,
24
- ArrayBinding,
25
- Decoded,
26
- FieldBinding,
27
- FieldLimitations,
28
- FormState,
29
- InputRecord,
30
- Inputs,
31
- InputsObject,
32
- InputStores,
33
- Limitations,
34
- RuntimeConfig,
35
- SourceValue,
36
- Values,
37
- View,
38
- } from './formTypes'
39
-
40
- const readInput = <Source extends AnySource>(
41
- source: Source,
42
- ): Effect.Effect<SourceValue<Source>, never, Store<SourceValue<Source>>> =>
43
- Fn.unsafeCoerce(
44
- Effect.gen(function* () {
45
- const store = yield* source.store
46
- const tracker = yield* Effect.serviceOption(CurrentTracker)
47
- if (Option.isSome(tracker)) {
48
- tracker.value.add(store)
49
- }
50
- return store.getSnapshot()
51
- }),
52
- )
53
-
54
- const readInputs = <Inputs extends InputRecord>(
55
- inputs: Inputs,
56
- ): Effect.Effect<InputsObject<Inputs>, never, InputStores<Inputs>> =>
57
- Fn.unsafeCoerce(
58
- Effect.gen(function* () {
59
- const pairs = yield* Effect.forEach(Object.values(inputs), (source) =>
60
- Effect.map(readInput(source), (snapshot) => [source.name, snapshot] as const),
61
- )
62
- return Record.fromEntries(pairs)
63
- }),
64
- )
65
-
66
- const limitationsFor = <A>(
67
- limitations: Limitations,
68
- path: string,
69
- ): FieldLimitations<A> =>
70
- Fn.unsafeCoerce(Option.getOrElse(Option.fromNullable(limitations[path]), () => ({})))
71
-
72
- const trigger = <P>(eventTrigger: Effect.Effect<Trigger<P>, never, Bus>): Effect.Effect<Trigger<P>, never, Bus> =>
73
- eventTrigger
74
-
75
- // 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
76
- export const view = <F extends AnyForm>(
77
- form: F,
78
- ): Effect.Effect<View<F>, never, Store<FormState<Values<F>>> | RuntimeConfig<Values<F>, Decoded<F>, Inputs<F>> | Bus | InputStores<F['manifest']['inputs']>> =>
79
- Effect.gen(function* () {
80
- const state = yield* form.state
81
- const runtimeConfig = yield* form.config
82
- // AnyForm erases inputs to `any`; restate at concrete F so R keeps declared input stores.
83
- const inputs: Inputs<F> = Fn.unsafeCoerce(
84
- yield* readInputs(Fn.unsafeCoerce<typeof form.manifest.inputs, InputRecord>(form.manifest.inputs)),
85
- )
86
- const limitations = Option.getOrElse(
87
- Option.fromNullable(runtimeConfig.limit?.({ values: state.values, inputs })),
88
- () => ({}),
89
- )
90
- const setField = yield* trigger(form.events.set.trigger)
91
- const blurField = yield* trigger(form.events.blur.trigger)
92
- const reset = yield* trigger(form.events.reset.trigger)
93
- const append = yield* trigger(form.events.append.trigger)
94
- const remove = yield* trigger(form.events.remove.trigger)
95
- const move = yield* trigger(form.events.move.trigger)
96
- const swap = yield* trigger(form.events.swap.trigger)
97
- const runtime = yield* Effect.runtime<Bus>()
98
-
99
- const schema: S.Schema<Decoded<F>, Values<F>, never> = Fn.unsafeCoerce(form.manifest.schema)
100
- const decodeEither = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
101
- const canSubmit = Either.isRight(decodeEither) && Record.keys(state.errors).length === 0
102
-
103
- // 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
104
- const runValidation = (publishSubmitAttempt: boolean) =>
105
- Effect.gen(function* () {
106
- if (publishSubmitAttempt) {
107
- yield* Event.dispatch(form.events.submitAttempted, {})
108
- }
109
- const decoded = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
110
- if (Either.isLeft(decoded)) {
111
- yield* Event.dispatch(form.events.validationFinished, { errors: routeParseError(decoded.left) })
112
- return Option.none<Decoded<F>>()
113
- }
114
- const custom = yield* runtimeConfig.validate({ values: state.values, decoded: decoded.right, inputs })
115
- const errors = errorsToRecord(custom)
116
- yield* Event.dispatch(form.events.validationFinished, { errors })
117
- return Record.keys(errors).length === 0
118
- ? Option.some(Fn.unsafeCoerce<unknown, Decoded<F>>(decoded.right))
119
- : Option.none<Decoded<F>>()
120
- })
121
-
122
- const submit = () => {
123
- Runtime.runFork(runtime)(
124
- Effect.gen(function* () {
125
- const decoded = yield* runValidation(true)
126
- if (Option.isNone(decoded)) {
127
- return
128
- }
129
- yield* runtimeConfig.submit({ values: state.values, decoded: decoded.value, inputs })
130
- yield* Event.dispatch(form.events.submitSucceeded, { values: state.values })
131
- }).pipe(Effect.catchAllCause((cause) => Effect.logError('reform form submit failed', cause))),
132
- )
133
- }
134
-
135
- const validate = () => {
136
- Runtime.runFork(runtime)(runValidation(false))
137
- }
138
-
139
- const field = <P extends FieldPath<Values<F>>>(path: P): FieldBinding<PathValue<Values<F>, P>> => {
140
- const fieldValue: PathValue<Values<F>, P> = Fn.unsafeCoerce(getNestedValue(state.values, path))
141
- return {
142
- path,
143
- value: fieldValue,
144
- set: (next) => {
145
- if (typeof next === 'function') {
146
- const updater = Fn.unsafeCoerce<typeof next, (prev: PathValue<Values<F>, P>) => PathValue<Values<F>, P>>(
147
- next,
148
- )
149
- setField({ path, value: updater(fieldValue) })
150
- return
151
- }
152
- setField({ path, value: next })
153
- },
154
- blur: () => blurField({ path }),
155
- error: firstError(state.errors, path),
156
- dirty: isPathOrParentDirty(state.dirtyPaths, path),
157
- touched: state.touched[path] === true,
158
- validating: false,
159
- limitations: limitationsFor<PathValue<Values<F>, P>>(limitations, path),
160
- }
161
- }
162
-
163
- const array = <P extends ArrayPath<Values<F>>>(path: P): ArrayBinding<ArrayItem<Values<F>, P>> => {
164
- const arrayValues: ReadonlyArray<ArrayItem<Values<F>, P>> = Fn.unsafeCoerce(
165
- currentArray({ source: state.values, path }),
166
- )
167
- const keys = state.arrayKeys[path] ?? arrayValues.map((_, index) => `${path}-${index}`)
168
- return {
169
- path,
170
- items: arrayValues.map((element, index) => ({
171
- value: element,
172
- index,
173
- key: keys[index] ?? `${path}-${index}`,
174
- remove: () => remove({ path, index }),
175
- move: (to) => move({ path, from: index, to }),
176
- })),
177
- append: (element) => append({ path, value: element }),
178
- remove: (index) => remove({ path, index }),
179
- move: (from, to) => move({ path, from, to }),
180
- swap: (first, second) => swap({ path, a: first, b: second }),
181
- limitations: limitationsFor<ReadonlyArray<ArrayItem<Values<F>, P>>>(limitations, path),
182
- }
183
- }
184
-
185
- const formView: View<F> = Fn.unsafeCoerce({
186
- values: state.values,
187
- inputs,
188
- errors: state.errors,
189
- dirty: state.dirtyPaths.length > 0,
190
- canSubmit,
191
- submitCount: state.submitCount,
192
- validationCount: state.validationCount,
193
- lastSubmittedValues: state.lastSubmittedValues,
194
- submit,
195
- reset: () => reset({}),
196
- validate,
197
- field,
198
- array,
199
- variantValue: (path: string) => Fn.unsafeCoerce(getNestedValue(state.values, path)),
200
- })
201
- return formView
202
- })