@lucas-barake/effect-form 0.1.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/Form/package.json +6 -0
- package/FormAtoms/package.json +6 -0
- package/LICENSE +21 -0
- package/Mode/package.json +6 -0
- package/README.md +5 -0
- package/Validation/package.json +6 -0
- package/dist/cjs/Form.js +299 -0
- package/dist/cjs/Form.js.map +1 -0
- package/dist/cjs/FormAtoms.js +266 -0
- package/dist/cjs/FormAtoms.js.map +1 -0
- package/dist/cjs/Mode.js +64 -0
- package/dist/cjs/Mode.js.map +1 -0
- package/dist/cjs/Validation.js +69 -0
- package/dist/cjs/Validation.js.map +1 -0
- package/dist/cjs/index.js +35 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/dirty.js +101 -0
- package/dist/cjs/internal/dirty.js.map +1 -0
- package/dist/cjs/internal/path.js +96 -0
- package/dist/cjs/internal/path.js.map +1 -0
- package/dist/cjs/internal/weak-registry.js +52 -0
- package/dist/cjs/internal/weak-registry.js.map +1 -0
- package/dist/dts/Form.d.ts +317 -0
- package/dist/dts/Form.d.ts.map +1 -0
- package/dist/dts/FormAtoms.d.ts +145 -0
- package/dist/dts/FormAtoms.d.ts.map +1 -0
- package/dist/dts/Mode.d.ts +55 -0
- package/dist/dts/Mode.d.ts.map +1 -0
- package/dist/dts/Validation.d.ts +23 -0
- package/dist/dts/Validation.d.ts.map +1 -0
- package/dist/dts/index.d.ts +26 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/dirty.d.ts +13 -0
- package/dist/dts/internal/dirty.d.ts.map +1 -0
- package/dist/dts/internal/path.d.ts +32 -0
- package/dist/dts/internal/path.d.ts.map +1 -0
- package/dist/dts/internal/weak-registry.d.ts +7 -0
- package/dist/dts/internal/weak-registry.d.ts.map +1 -0
- package/dist/esm/Form.js +263 -0
- package/dist/esm/Form.js.map +1 -0
- package/dist/esm/FormAtoms.js +238 -0
- package/dist/esm/FormAtoms.js.map +1 -0
- package/dist/esm/Mode.js +36 -0
- package/dist/esm/Mode.js.map +1 -0
- package/dist/esm/Validation.js +40 -0
- package/dist/esm/Validation.js.map +1 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/dirty.js +72 -0
- package/dist/esm/internal/dirty.js.map +1 -0
- package/dist/esm/internal/path.js +86 -0
- package/dist/esm/internal/path.js.map +1 -0
- package/dist/esm/internal/weak-registry.js +45 -0
- package/dist/esm/internal/weak-registry.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/package.json +64 -0
- package/src/Form.ts +522 -0
- package/src/FormAtoms.ts +485 -0
- package/src/Mode.ts +59 -0
- package/src/Validation.ts +43 -0
- package/src/index.ts +28 -0
- package/src/internal/dirty.ts +96 -0
- package/src/internal/path.ts +93 -0
- package/src/internal/weak-registry.ts +60 -0
package/src/Form.ts
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0
|
|
3
|
+
*/
|
|
4
|
+
import type * as Effect from "effect/Effect"
|
|
5
|
+
import * as Predicate from "effect/Predicate"
|
|
6
|
+
import * as Schema from "effect/Schema"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unique identifier for FormBuilder instances.
|
|
10
|
+
*
|
|
11
|
+
* @since 1.0.0
|
|
12
|
+
* @category Symbols
|
|
13
|
+
*/
|
|
14
|
+
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form")
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @since 1.0.0
|
|
18
|
+
* @category Symbols
|
|
19
|
+
*/
|
|
20
|
+
export type TypeId = typeof TypeId
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Unique identifier for Field references.
|
|
24
|
+
*
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
* @category Symbols
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export const FieldTypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field")
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @since 1.0.0
|
|
33
|
+
* @category Symbols
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
export type FieldTypeId = typeof FieldTypeId
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A field reference carrying type and path info for type-safe setValue operations.
|
|
40
|
+
*
|
|
41
|
+
* @since 1.0.0
|
|
42
|
+
* @category Models
|
|
43
|
+
*/
|
|
44
|
+
export interface Field<S> {
|
|
45
|
+
readonly [FieldTypeId]: FieldTypeId
|
|
46
|
+
readonly _S: S
|
|
47
|
+
readonly key: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a field reference for type-safe setValue operations.
|
|
52
|
+
*
|
|
53
|
+
* @since 1.0.0
|
|
54
|
+
* @category Constructors
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export const makeFieldRef = <S>(key: string): Field<S> => ({
|
|
58
|
+
[FieldTypeId]: FieldTypeId,
|
|
59
|
+
_S: undefined as any,
|
|
60
|
+
key,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A scalar field definition containing the key and schema.
|
|
65
|
+
*
|
|
66
|
+
* @since 1.0.0
|
|
67
|
+
* @category Models
|
|
68
|
+
*/
|
|
69
|
+
export interface FieldDef<K extends string, S extends Schema.Schema.Any> {
|
|
70
|
+
readonly _tag: "field"
|
|
71
|
+
readonly key: K
|
|
72
|
+
readonly schema: S
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An array field definition containing a schema for items.
|
|
77
|
+
*
|
|
78
|
+
* @since 1.0.0
|
|
79
|
+
* @category Models
|
|
80
|
+
*/
|
|
81
|
+
export interface ArrayFieldDef<K extends string, S extends Schema.Schema.Any> {
|
|
82
|
+
readonly _tag: "array"
|
|
83
|
+
readonly key: K
|
|
84
|
+
readonly itemSchema: S
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Union of all field definition types.
|
|
89
|
+
*
|
|
90
|
+
* @since 1.0.0
|
|
91
|
+
* @category Models
|
|
92
|
+
*/
|
|
93
|
+
export type AnyFieldDef = FieldDef<string, Schema.Schema.Any> | ArrayFieldDef<string, Schema.Schema.Any>
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a scalar field definition.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const NameField = Form.makeField("name", Schema.String)
|
|
101
|
+
* const form = Form.empty.addField(NameField)
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @since 1.0.0
|
|
105
|
+
* @category Constructors
|
|
106
|
+
*/
|
|
107
|
+
export const makeField = <K extends string, S extends Schema.Schema.Any>(
|
|
108
|
+
key: K,
|
|
109
|
+
schema: S,
|
|
110
|
+
): FieldDef<K, S> => ({
|
|
111
|
+
_tag: "field",
|
|
112
|
+
key,
|
|
113
|
+
schema,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates an array field definition.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* // Array of primitives
|
|
122
|
+
* const TagsField = Form.makeArrayField("tags", Schema.String)
|
|
123
|
+
*
|
|
124
|
+
* // Array of objects
|
|
125
|
+
* const ItemsField = Form.makeArrayField("items", Schema.Struct({
|
|
126
|
+
* name: Schema.String,
|
|
127
|
+
* quantity: Schema.Number
|
|
128
|
+
* }))
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @since 1.0.0
|
|
132
|
+
* @category Constructors
|
|
133
|
+
*/
|
|
134
|
+
export const makeArrayField = <K extends string, S extends Schema.Schema.Any>(
|
|
135
|
+
key: K,
|
|
136
|
+
itemSchema: S,
|
|
137
|
+
): ArrayFieldDef<K, S> => ({
|
|
138
|
+
_tag: "array",
|
|
139
|
+
key,
|
|
140
|
+
itemSchema,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A record of field definitions.
|
|
145
|
+
*
|
|
146
|
+
* @since 1.0.0
|
|
147
|
+
* @category Models
|
|
148
|
+
*/
|
|
149
|
+
export type FieldsRecord = Record<string, AnyFieldDef>
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extracts the encoded (input) type from a fields record.
|
|
153
|
+
*
|
|
154
|
+
* @since 1.0.0
|
|
155
|
+
* @category Type Helpers
|
|
156
|
+
*/
|
|
157
|
+
export type EncodedFromFields<T extends FieldsRecord> = {
|
|
158
|
+
readonly [K in keyof T]: T[K] extends FieldDef<any, infer S> ? Schema.Schema.Encoded<S>
|
|
159
|
+
: T[K] extends ArrayFieldDef<any, infer S> ? ReadonlyArray<Schema.Schema.Encoded<S>>
|
|
160
|
+
: never
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extracts the decoded (output) type from a fields record.
|
|
165
|
+
*
|
|
166
|
+
* @since 1.0.0
|
|
167
|
+
* @category Type Helpers
|
|
168
|
+
*/
|
|
169
|
+
export type DecodedFromFields<T extends FieldsRecord> = {
|
|
170
|
+
readonly [K in keyof T]: T[K] extends FieldDef<any, infer S> ? Schema.Schema.Type<S>
|
|
171
|
+
: T[K] extends ArrayFieldDef<any, infer S> ? ReadonlyArray<Schema.Schema.Type<S>>
|
|
172
|
+
: never
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The state of a form at runtime.
|
|
177
|
+
*
|
|
178
|
+
* @since 1.0.0
|
|
179
|
+
* @category Models
|
|
180
|
+
*/
|
|
181
|
+
export interface FormState<TFields extends FieldsRecord> {
|
|
182
|
+
readonly values: EncodedFromFields<TFields>
|
|
183
|
+
readonly initialValues: EncodedFromFields<TFields>
|
|
184
|
+
readonly touched: { readonly [K in keyof TFields]: boolean }
|
|
185
|
+
readonly submitCount: number
|
|
186
|
+
readonly dirtyFields: ReadonlySet<string>
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface SyncRefinement {
|
|
190
|
+
readonly _tag: "sync"
|
|
191
|
+
readonly fn: (values: unknown) => Schema.FilterOutput
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface AsyncRefinement {
|
|
195
|
+
readonly _tag: "async"
|
|
196
|
+
readonly fn: (values: unknown) => Effect.Effect<Schema.FilterOutput, never, unknown>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type Refinement = SyncRefinement | AsyncRefinement
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A builder for constructing type-safe forms with Effect Schema validation.
|
|
203
|
+
*
|
|
204
|
+
* **Details**
|
|
205
|
+
*
|
|
206
|
+
* FormBuilder uses a fluent API pattern to define form fields. Each field
|
|
207
|
+
* includes a Schema for validation. The builder accumulates field definitions
|
|
208
|
+
* and context requirements (`R`) from schemas that use Effect services.
|
|
209
|
+
*
|
|
210
|
+
* @since 1.0.0
|
|
211
|
+
* @category Models
|
|
212
|
+
*/
|
|
213
|
+
export interface FormBuilder<TFields extends FieldsRecord, R> {
|
|
214
|
+
readonly [TypeId]: TypeId
|
|
215
|
+
readonly fields: TFields
|
|
216
|
+
readonly refinements: ReadonlyArray<Refinement>
|
|
217
|
+
readonly _R?: R
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Adds a scalar field to the form builder.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* const NameField = Form.makeField("name", Schema.String)
|
|
225
|
+
* const form = Form.empty.addField(NameField)
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
addField<K extends string, S extends Schema.Schema.Any>(
|
|
229
|
+
this: FormBuilder<TFields, R>,
|
|
230
|
+
field: FieldDef<K, S>,
|
|
231
|
+
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Schema.Context<S>>
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Adds an array field to the form builder.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```ts
|
|
238
|
+
* const ItemsField = Form.makeArrayField("items", Schema.Struct({ name: Schema.String }))
|
|
239
|
+
* const form = Form.empty.addField(ItemsField)
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
addField<K extends string, S extends Schema.Schema.Any>(
|
|
243
|
+
this: FormBuilder<TFields, R>,
|
|
244
|
+
field: ArrayFieldDef<K, S>,
|
|
245
|
+
): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S> }, R | Schema.Schema.Context<S>>
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Merges another FormBuilder's fields into this one.
|
|
249
|
+
* Useful for composing reusable field groups.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* const addressFields = Form.empty
|
|
254
|
+
* .addField("street", Schema.String)
|
|
255
|
+
* .addField("city", Schema.String)
|
|
256
|
+
*
|
|
257
|
+
* const userForm = Form.empty
|
|
258
|
+
* .addField("name", Schema.String)
|
|
259
|
+
* .merge(addressFields)
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
merge<TFields2 extends FieldsRecord, R2>(
|
|
263
|
+
this: FormBuilder<TFields, R>,
|
|
264
|
+
other: FormBuilder<TFields2, R2>,
|
|
265
|
+
): FormBuilder<TFields & TFields2, R | R2>
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Adds a synchronous cross-field validation refinement to the form.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* const form = Form.empty
|
|
273
|
+
* .addField("password", Schema.String)
|
|
274
|
+
* .addField("confirmPassword", Schema.String)
|
|
275
|
+
* .refine((values) => {
|
|
276
|
+
* if (values.password !== values.confirmPassword) {
|
|
277
|
+
* return { path: ["confirmPassword"], message: "Passwords must match" }
|
|
278
|
+
* }
|
|
279
|
+
* })
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
refine(
|
|
283
|
+
this: FormBuilder<TFields, R>,
|
|
284
|
+
predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput,
|
|
285
|
+
): FormBuilder<TFields, R>
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Adds an asynchronous cross-field validation refinement to the form.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```ts
|
|
292
|
+
* const form = Form.empty
|
|
293
|
+
* .addField("username", Schema.String)
|
|
294
|
+
* .refineEffect((values) =>
|
|
295
|
+
* Effect.gen(function* () {
|
|
296
|
+
* const taken = yield* checkUsername(values.username)
|
|
297
|
+
* if (taken) return { path: ["username"], message: "Already taken" }
|
|
298
|
+
* })
|
|
299
|
+
* )
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
refineEffect<RD>(
|
|
303
|
+
this: FormBuilder<TFields, R>,
|
|
304
|
+
predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>,
|
|
305
|
+
): FormBuilder<TFields, R | RD>
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const FormBuilderProto = {
|
|
309
|
+
[TypeId]: TypeId,
|
|
310
|
+
addField<TFields extends FieldsRecord, R>(
|
|
311
|
+
this: FormBuilder<TFields, R>,
|
|
312
|
+
field: AnyFieldDef,
|
|
313
|
+
): FormBuilder<any, any> {
|
|
314
|
+
const newSelf = Object.create(FormBuilderProto)
|
|
315
|
+
newSelf.fields = { ...this.fields, [field.key]: field }
|
|
316
|
+
newSelf.refinements = this.refinements
|
|
317
|
+
return newSelf
|
|
318
|
+
},
|
|
319
|
+
merge<TFields extends FieldsRecord, R, TFields2 extends FieldsRecord, R2>(
|
|
320
|
+
this: FormBuilder<TFields, R>,
|
|
321
|
+
other: FormBuilder<TFields2, R2>,
|
|
322
|
+
): FormBuilder<TFields & TFields2, R | R2> {
|
|
323
|
+
const newSelf = Object.create(FormBuilderProto)
|
|
324
|
+
newSelf.fields = { ...this.fields, ...other.fields }
|
|
325
|
+
newSelf.refinements = [...this.refinements, ...other.refinements]
|
|
326
|
+
return newSelf
|
|
327
|
+
},
|
|
328
|
+
refine<TFields extends FieldsRecord, R>(
|
|
329
|
+
this: FormBuilder<TFields, R>,
|
|
330
|
+
predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput,
|
|
331
|
+
): FormBuilder<TFields, R> {
|
|
332
|
+
const newSelf = Object.create(FormBuilderProto)
|
|
333
|
+
newSelf.fields = this.fields
|
|
334
|
+
newSelf.refinements = [
|
|
335
|
+
...this.refinements,
|
|
336
|
+
{ _tag: "sync" as const, fn: (values: unknown) => predicate(values as DecodedFromFields<TFields>) },
|
|
337
|
+
]
|
|
338
|
+
return newSelf
|
|
339
|
+
},
|
|
340
|
+
refineEffect<TFields extends FieldsRecord, R, RD>(
|
|
341
|
+
this: FormBuilder<TFields, R>,
|
|
342
|
+
predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>,
|
|
343
|
+
): FormBuilder<TFields, R | RD> {
|
|
344
|
+
const newSelf = Object.create(FormBuilderProto)
|
|
345
|
+
newSelf.fields = this.fields
|
|
346
|
+
newSelf.refinements = [
|
|
347
|
+
...this.refinements,
|
|
348
|
+
{ _tag: "async" as const, fn: (values: unknown) => predicate(values as DecodedFromFields<TFields>) },
|
|
349
|
+
]
|
|
350
|
+
return newSelf
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Checks if a value is a `FormBuilder`.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```ts
|
|
359
|
+
* import * as Form from "@lucas-barake/effect-form"
|
|
360
|
+
*
|
|
361
|
+
* const builder = Form.empty
|
|
362
|
+
*
|
|
363
|
+
* console.log(Form.isFormBuilder(builder))
|
|
364
|
+
* // Output: true
|
|
365
|
+
*
|
|
366
|
+
* console.log(Form.isFormBuilder({}))
|
|
367
|
+
* // Output: false
|
|
368
|
+
* ```
|
|
369
|
+
*
|
|
370
|
+
* @since 1.0.0
|
|
371
|
+
* @category Guards
|
|
372
|
+
*/
|
|
373
|
+
export const isFormBuilder = (u: unknown): u is FormBuilder<any, any> => Predicate.hasProperty(u, TypeId)
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Checks if a field definition is an array field.
|
|
377
|
+
*
|
|
378
|
+
* @since 1.0.0
|
|
379
|
+
* @category Guards
|
|
380
|
+
*/
|
|
381
|
+
export const isArrayFieldDef = (def: AnyFieldDef): def is ArrayFieldDef<string, any> => def._tag === "array"
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Checks if a field definition is a simple field.
|
|
385
|
+
*
|
|
386
|
+
* @since 1.0.0
|
|
387
|
+
* @category Guards
|
|
388
|
+
*/
|
|
389
|
+
export const isFieldDef = (def: AnyFieldDef): def is FieldDef<string, Schema.Schema.Any> => def._tag === "field"
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Gets a default encoded value from a schema.
|
|
393
|
+
*
|
|
394
|
+
* @since 1.0.0
|
|
395
|
+
* @category Helpers
|
|
396
|
+
*/
|
|
397
|
+
export const getDefaultFromSchema = (schema: Schema.Schema.Any): unknown => {
|
|
398
|
+
const ast = schema.ast
|
|
399
|
+
switch (ast._tag) {
|
|
400
|
+
case "StringKeyword":
|
|
401
|
+
case "TemplateLiteral":
|
|
402
|
+
return ""
|
|
403
|
+
case "NumberKeyword":
|
|
404
|
+
return 0
|
|
405
|
+
case "BooleanKeyword":
|
|
406
|
+
return false
|
|
407
|
+
case "TypeLiteral": {
|
|
408
|
+
const result: Record<string, unknown> = {}
|
|
409
|
+
for (const prop of ast.propertySignatures) {
|
|
410
|
+
result[prop.name as string] = getDefaultFromSchema(Schema.make(prop.type))
|
|
411
|
+
}
|
|
412
|
+
return result
|
|
413
|
+
}
|
|
414
|
+
case "Transformation":
|
|
415
|
+
return getDefaultFromSchema(Schema.make(ast.from))
|
|
416
|
+
case "Refinement":
|
|
417
|
+
return getDefaultFromSchema(Schema.make(ast.from))
|
|
418
|
+
case "Suspend":
|
|
419
|
+
return getDefaultFromSchema(Schema.make(ast.f()))
|
|
420
|
+
default:
|
|
421
|
+
return ""
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* An empty `FormBuilder` to start building a form.
|
|
427
|
+
*
|
|
428
|
+
* **Details**
|
|
429
|
+
*
|
|
430
|
+
* This is the entry point for building a form. Use method chaining to add
|
|
431
|
+
* fields and then build the form with a React adapter.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* import * as Form from "@lucas-barake/effect-form"
|
|
436
|
+
* import * as Schema from "effect/Schema"
|
|
437
|
+
*
|
|
438
|
+
* const EmailField = Form.makeField("email", Schema.String)
|
|
439
|
+
* const PasswordField = Form.makeField("password", Schema.String)
|
|
440
|
+
*
|
|
441
|
+
* const loginForm = Form.empty
|
|
442
|
+
* .addField(EmailField)
|
|
443
|
+
* .addField(PasswordField)
|
|
444
|
+
* ```
|
|
445
|
+
*
|
|
446
|
+
* @since 1.0.0
|
|
447
|
+
* @category Constructors
|
|
448
|
+
*/
|
|
449
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
450
|
+
export const empty: FormBuilder<{}, never> = (() => {
|
|
451
|
+
const self = Object.create(FormBuilderProto)
|
|
452
|
+
self.fields = {}
|
|
453
|
+
self.refinements = []
|
|
454
|
+
return self
|
|
455
|
+
})()
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Builds a combined Schema from a FormBuilder's field definitions.
|
|
459
|
+
*
|
|
460
|
+
* @since 1.0.0
|
|
461
|
+
* @category Schema
|
|
462
|
+
*/
|
|
463
|
+
export const buildSchema = <TFields extends FieldsRecord, R>(
|
|
464
|
+
self: FormBuilder<TFields, R>,
|
|
465
|
+
): Schema.Schema<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R> => {
|
|
466
|
+
const schemaFields: Record<string, Schema.Schema.Any> = {}
|
|
467
|
+
for (const [key, def] of Object.entries(self.fields)) {
|
|
468
|
+
if (isArrayFieldDef(def)) {
|
|
469
|
+
schemaFields[key] = Schema.Array(def.itemSchema)
|
|
470
|
+
} else if (isFieldDef(def)) {
|
|
471
|
+
schemaFields[key] = def.schema
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let schema: Schema.Schema<any, any, any> = Schema.Struct(schemaFields)
|
|
476
|
+
|
|
477
|
+
for (const refinement of self.refinements) {
|
|
478
|
+
if (refinement._tag === "sync") {
|
|
479
|
+
schema = schema.pipe(Schema.filter(refinement.fn))
|
|
480
|
+
} else {
|
|
481
|
+
schema = schema.pipe(Schema.filterEffect(refinement.fn))
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return schema as Schema.Schema<
|
|
486
|
+
DecodedFromFields<TFields>,
|
|
487
|
+
EncodedFromFields<TFields>,
|
|
488
|
+
R
|
|
489
|
+
>
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Gets default encoded values for a fields record.
|
|
494
|
+
*
|
|
495
|
+
* @since 1.0.0
|
|
496
|
+
* @category Helpers
|
|
497
|
+
*/
|
|
498
|
+
export const getDefaultEncodedValues = (fields: FieldsRecord): Record<string, unknown> => {
|
|
499
|
+
const result: Record<string, unknown> = {}
|
|
500
|
+
for (const [key, def] of Object.entries(fields)) {
|
|
501
|
+
if (isArrayFieldDef(def)) {
|
|
502
|
+
result[key] = []
|
|
503
|
+
} else {
|
|
504
|
+
result[key] = ""
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Creates a touched record with all fields set to the given value.
|
|
512
|
+
*
|
|
513
|
+
* @since 1.0.0
|
|
514
|
+
* @category Helpers
|
|
515
|
+
*/
|
|
516
|
+
export const createTouchedRecord = (fields: FieldsRecord, value: boolean): Record<string, boolean> => {
|
|
517
|
+
const result: Record<string, boolean> = {}
|
|
518
|
+
for (const key of Object.keys(fields)) {
|
|
519
|
+
result[key] = value
|
|
520
|
+
}
|
|
521
|
+
return result
|
|
522
|
+
}
|