@playfast/reform-forms 1.2.0 → 1.2.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/package.json +1 -1
- package/src/form.ts +47 -706
- package/src/formMake.ts +95 -0
- package/src/formState.ts +181 -0
- package/src/formTypes.ts +235 -0
- package/src/formView.ts +236 -0
package/src/formView.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Effect,
|
|
4
|
+
Either,
|
|
5
|
+
Function as Fn,
|
|
6
|
+
Option,
|
|
7
|
+
Record,
|
|
8
|
+
Runtime,
|
|
9
|
+
Schema as S,
|
|
10
|
+
} from 'effect'
|
|
11
|
+
import { Event, type Trigger } from '@playfast/reform'
|
|
12
|
+
import {
|
|
13
|
+
type AnySource,
|
|
14
|
+
Bus,
|
|
15
|
+
CurrentTracker,
|
|
16
|
+
type SourceIdentifier,
|
|
17
|
+
type SourceValue as ReformSourceValue,
|
|
18
|
+
type Store,
|
|
19
|
+
} from '@playfast/reform/internal'
|
|
20
|
+
import { State } from '@playfast/reform'
|
|
21
|
+
import {
|
|
22
|
+
getNestedValue,
|
|
23
|
+
isPathOrParentDirty,
|
|
24
|
+
type ArrayItem,
|
|
25
|
+
type ArrayPath,
|
|
26
|
+
type FieldPath,
|
|
27
|
+
type PathValue,
|
|
28
|
+
} from './path'
|
|
29
|
+
import { errorsToRecord, firstError, routeParseError } from './validation'
|
|
30
|
+
import { currentArray } from './formState'
|
|
31
|
+
import type {
|
|
32
|
+
ArrayBinding,
|
|
33
|
+
DecodedOfSchema,
|
|
34
|
+
FieldLimitations,
|
|
35
|
+
FieldBinding,
|
|
36
|
+
FormClass,
|
|
37
|
+
FormState,
|
|
38
|
+
FormView,
|
|
39
|
+
InputRecord,
|
|
40
|
+
InputsObject,
|
|
41
|
+
InputStores,
|
|
42
|
+
Limitations,
|
|
43
|
+
RuntimeConfig,
|
|
44
|
+
ValuesOfSchema,
|
|
45
|
+
} from './formTypes'
|
|
46
|
+
|
|
47
|
+
const readInput = Effect.fn('forms.readInput')(function* <Input extends AnySource>(
|
|
48
|
+
source: Input,
|
|
49
|
+
): Effect.fn.Return<ReformSourceValue<Input>, never, SourceIdentifier<Input>> {
|
|
50
|
+
const store = yield* Context.GenericTag<SourceIdentifier<Input>, Store<ReformSourceValue<Input>>>(
|
|
51
|
+
source.store.key,
|
|
52
|
+
)
|
|
53
|
+
const tracker = yield* Effect.serviceOption(CurrentTracker)
|
|
54
|
+
if (Option.isSome(tracker)) {
|
|
55
|
+
tracker.value.add(store)
|
|
56
|
+
}
|
|
57
|
+
return store.getSnapshot()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const readInputs = <Inputs extends InputRecord>(
|
|
61
|
+
inputs: Inputs,
|
|
62
|
+
): Effect.Effect<InputsObject<Inputs>, never, InputStores<Inputs>> =>
|
|
63
|
+
Fn.unsafeCoerce(
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const pairs = yield* Effect.forEach(Object.values(inputs), (source) =>
|
|
66
|
+
Effect.map(readInput(source), (snapshot): readonly [string, unknown] => [
|
|
67
|
+
source.name,
|
|
68
|
+
snapshot,
|
|
69
|
+
]),
|
|
70
|
+
)
|
|
71
|
+
return Record.fromEntries(pairs)
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const limitationsFor = <A>(limitations: Limitations, path: string): FieldLimitations<A> =>
|
|
76
|
+
Fn.unsafeCoerce(Option.getOrElse(Option.fromNullable(limitations[path]), () => ({})))
|
|
77
|
+
|
|
78
|
+
const trigger = <P>(
|
|
79
|
+
eventTrigger: Effect.Effect<Trigger<P>, never, Bus>,
|
|
80
|
+
): Effect.Effect<Trigger<P>, never, Bus> => eventTrigger
|
|
81
|
+
|
|
82
|
+
// oxlint-disable-next-line reform-rules/prefer-effect-fn -- public generic view preserves the exact form state/config/input requirements verbatim
|
|
83
|
+
export const view = <
|
|
84
|
+
N extends string,
|
|
85
|
+
Schema extends S.Schema.AnyNoContext,
|
|
86
|
+
FormInputs extends InputRecord,
|
|
87
|
+
>(
|
|
88
|
+
form: FormClass<N, Schema, FormInputs>,
|
|
89
|
+
): Effect.Effect<
|
|
90
|
+
FormView<ValuesOfSchema<Schema>, InputsObject<FormInputs>>,
|
|
91
|
+
never,
|
|
92
|
+
| State.StateStore<`${N}/state`, FormState<ValuesOfSchema<Schema>>>
|
|
93
|
+
| RuntimeConfig<ValuesOfSchema<Schema>, DecodedOfSchema<Schema>, InputsObject<FormInputs>>
|
|
94
|
+
| Bus
|
|
95
|
+
| InputStores<FormInputs>
|
|
96
|
+
> =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const state = yield* form.state
|
|
99
|
+
const runtimeConfig = yield* form.config
|
|
100
|
+
const inputs = yield* readInputs(form.manifest.inputs)
|
|
101
|
+
const limitations = Option.getOrElse(
|
|
102
|
+
Option.fromNullable(runtimeConfig.limit?.({ values: state.values, inputs })),
|
|
103
|
+
() => ({}),
|
|
104
|
+
)
|
|
105
|
+
const setField = yield* trigger(form.events.set.trigger)
|
|
106
|
+
const blurField = yield* trigger(form.events.blur.trigger)
|
|
107
|
+
const reset = yield* trigger(form.events.reset.trigger)
|
|
108
|
+
const append = yield* trigger(form.events.append.trigger)
|
|
109
|
+
const remove = yield* trigger(form.events.remove.trigger)
|
|
110
|
+
const move = yield* trigger(form.events.move.trigger)
|
|
111
|
+
const swap = yield* trigger(form.events.swap.trigger)
|
|
112
|
+
const runtime = yield* Effect.runtime<Bus>()
|
|
113
|
+
|
|
114
|
+
const schema = form.manifest.schema
|
|
115
|
+
const decodeEither = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
|
|
116
|
+
const canSubmit = Either.isRight(decodeEither) && Record.keys(state.errors).length === 0
|
|
117
|
+
|
|
118
|
+
// 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
|
|
119
|
+
const runValidation = (publishSubmitAttempt: boolean) =>
|
|
120
|
+
Effect.gen(function* () {
|
|
121
|
+
if (publishSubmitAttempt) {
|
|
122
|
+
yield* Event.dispatch(form.events.submitAttempted, {})
|
|
123
|
+
}
|
|
124
|
+
const decoded = S.decodeUnknownEither(schema, { errors: 'all' })(state.values)
|
|
125
|
+
if (Either.isLeft(decoded)) {
|
|
126
|
+
yield* Event.dispatch(form.events.validationFinished, {
|
|
127
|
+
errors: routeParseError(decoded.left),
|
|
128
|
+
})
|
|
129
|
+
return Option.none<DecodedOfSchema<Schema>>()
|
|
130
|
+
}
|
|
131
|
+
const custom = yield* runtimeConfig.validate({
|
|
132
|
+
values: state.values,
|
|
133
|
+
decoded: decoded.right,
|
|
134
|
+
inputs,
|
|
135
|
+
})
|
|
136
|
+
const errors = errorsToRecord(custom)
|
|
137
|
+
yield* Event.dispatch(form.events.validationFinished, { errors })
|
|
138
|
+
return Record.keys(errors).length === 0
|
|
139
|
+
? Option.some(decoded.right)
|
|
140
|
+
: Option.none<DecodedOfSchema<Schema>>()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const submit = () => {
|
|
144
|
+
Runtime.runFork(runtime)(
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const decoded = yield* runValidation(true)
|
|
147
|
+
if (Option.isNone(decoded)) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
yield* runtimeConfig.submit({ values: state.values, decoded: decoded.value, inputs })
|
|
151
|
+
yield* Event.dispatch(form.events.submitSucceeded, { values: state.values })
|
|
152
|
+
}).pipe(
|
|
153
|
+
Effect.catchAllCause((cause) => Effect.logError('reform form submit failed', cause)),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const validate = () => {
|
|
159
|
+
Runtime.runFork(runtime)(runValidation(false))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const field = <P extends FieldPath<ValuesOfSchema<Schema>>>(
|
|
163
|
+
path: P,
|
|
164
|
+
): FieldBinding<PathValue<ValuesOfSchema<Schema>, P>> => {
|
|
165
|
+
const fieldValue: PathValue<ValuesOfSchema<Schema>, P> = Fn.unsafeCoerce(
|
|
166
|
+
getNestedValue(state.values, path),
|
|
167
|
+
)
|
|
168
|
+
return {
|
|
169
|
+
path,
|
|
170
|
+
value: fieldValue,
|
|
171
|
+
set: (next) => {
|
|
172
|
+
if (typeof next === 'function') {
|
|
173
|
+
const updater = Fn.unsafeCoerce<
|
|
174
|
+
typeof next,
|
|
175
|
+
(prev: PathValue<ValuesOfSchema<Schema>, P>) => PathValue<ValuesOfSchema<Schema>, P>
|
|
176
|
+
>(next)
|
|
177
|
+
setField({ path, value: updater(fieldValue) })
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
setField({ path, value: next })
|
|
181
|
+
},
|
|
182
|
+
blur: () => blurField({ path }),
|
|
183
|
+
error: firstError(state.errors, path),
|
|
184
|
+
dirty: isPathOrParentDirty(state.dirtyPaths, path),
|
|
185
|
+
touched: state.touched[path] === true,
|
|
186
|
+
validating: false,
|
|
187
|
+
limitations: limitationsFor<PathValue<ValuesOfSchema<Schema>, P>>(limitations, path),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const array = <P extends ArrayPath<ValuesOfSchema<Schema>>>(
|
|
192
|
+
path: P,
|
|
193
|
+
): ArrayBinding<ArrayItem<ValuesOfSchema<Schema>, P>> => {
|
|
194
|
+
const arrayValues: ReadonlyArray<ArrayItem<ValuesOfSchema<Schema>, P>> = Fn.unsafeCoerce(
|
|
195
|
+
currentArray({ source: state.values, path }),
|
|
196
|
+
)
|
|
197
|
+
const keys = state.arrayKeys[path] ?? arrayValues.map((_, index) => `${path}-${index}`)
|
|
198
|
+
return {
|
|
199
|
+
path,
|
|
200
|
+
items: arrayValues.map((element, index) => ({
|
|
201
|
+
value: element,
|
|
202
|
+
index,
|
|
203
|
+
key: keys[index] ?? `${path}-${index}`,
|
|
204
|
+
remove: () => remove({ path, index }),
|
|
205
|
+
move: (to) => move({ path, from: index, to }),
|
|
206
|
+
})),
|
|
207
|
+
append: (element) => append({ path, value: element }),
|
|
208
|
+
remove: (index) => remove({ path, index }),
|
|
209
|
+
move: (from, to) => move({ path, from, to }),
|
|
210
|
+
swap: (first, second) => swap({ path, a: first, b: second }),
|
|
211
|
+
limitations: limitationsFor<ReadonlyArray<ArrayItem<ValuesOfSchema<Schema>, P>>>(
|
|
212
|
+
limitations,
|
|
213
|
+
path,
|
|
214
|
+
),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const formView: FormView<ValuesOfSchema<Schema>, InputsObject<FormInputs>> = Fn.unsafeCoerce({
|
|
219
|
+
values: state.values,
|
|
220
|
+
inputs,
|
|
221
|
+
errors: state.errors,
|
|
222
|
+
dirty: state.dirtyPaths.length > 0,
|
|
223
|
+
canSubmit,
|
|
224
|
+
submitCount: state.submitCount,
|
|
225
|
+
validationCount: state.validationCount,
|
|
226
|
+
lastSubmittedValues: state.lastSubmittedValues,
|
|
227
|
+
submit,
|
|
228
|
+
reset: () => reset({}),
|
|
229
|
+
validate,
|
|
230
|
+
field,
|
|
231
|
+
array,
|
|
232
|
+
variantValue: (path: string) => Fn.unsafeCoerce(getNestedValue(state.values, path)),
|
|
233
|
+
})
|
|
234
|
+
return formView
|
|
235
|
+
})
|
|
236
|
+
|