@lucas-barake/effect-form 0.1.0 → 0.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/Form.ts CHANGED
@@ -5,19 +5,15 @@ import type * as Effect from "effect/Effect"
5
5
  import * as Predicate from "effect/Predicate"
6
6
  import * as Schema from "effect/Schema"
7
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
8
+ import type {
9
+ AnyFieldDef,
10
+ ArrayFieldDef,
11
+ DecodedFromFields,
12
+ EncodedFromFields,
13
+ FieldDef,
14
+ FieldsRecord,
15
+ } from "./Field.js"
16
+ import { isArrayFieldDef, isFieldDef } from "./Field.js"
21
17
 
22
18
  /**
23
19
  * Unique identifier for Field references.
@@ -60,117 +56,23 @@ export const makeFieldRef = <S>(key: string): Field<S> => ({
60
56
  key,
61
57
  })
62
58
 
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>
59
+ // ================================
60
+ // FormBuilder
61
+ // ================================
150
62
 
151
63
  /**
152
- * Extracts the encoded (input) type from a fields record.
64
+ * Unique identifier for FormBuilder instances.
153
65
  *
154
66
  * @since 1.0.0
155
- * @category Type Helpers
67
+ * @category Symbols
156
68
  */
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
- }
69
+ export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form")
162
70
 
163
71
  /**
164
- * Extracts the decoded (output) type from a fields record.
165
- *
166
72
  * @since 1.0.0
167
- * @category Type Helpers
73
+ * @category Symbols
168
74
  */
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
- }
75
+ export type TypeId = typeof TypeId
174
76
 
175
77
  /**
176
78
  * The state of a form at runtime.
@@ -372,56 +274,6 @@ const FormBuilderProto = {
372
274
  */
373
275
  export const isFormBuilder = (u: unknown): u is FormBuilder<any, any> => Predicate.hasProperty(u, TypeId)
374
276
 
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
277
  /**
426
278
  * An empty `FormBuilder` to start building a form.
427
279
  *
@@ -488,35 +340,3 @@ export const buildSchema = <TFields extends FieldsRecord, R>(
488
340
  R
489
341
  >
490
342
  }
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
- }
package/src/FormAtoms.ts CHANGED
@@ -15,8 +15,8 @@ import * as Option from "effect/Option"
15
15
  import type * as ParseResult from "effect/ParseResult"
16
16
  import * as Schema from "effect/Schema"
17
17
  import * as Utils from "effect/Utils"
18
- import type * as Form from "./Form.js"
19
- import { buildSchema, createTouchedRecord, getDefaultFromSchema, makeFieldRef } from "./Form.js"
18
+ import * as Field from "./Field.js"
19
+ import * as Form from "./Form.js"
20
20
  import { recalculateDirtyFieldsForArray, recalculateDirtySubtree } from "./internal/dirty.js"
21
21
  import { getNestedValue, setNestedValue } from "./internal/path.js"
22
22
  import { createWeakRegistry, type WeakRegistry } from "./internal/weak-registry.js"
@@ -40,7 +40,7 @@ export interface FieldAtoms {
40
40
  * @since 1.0.0
41
41
  * @category Models
42
42
  */
43
- export interface FormAtomsConfig<TFields extends Form.FieldsRecord, R> {
43
+ export interface FormAtomsConfig<TFields extends Field.FieldsRecord, R> {
44
44
  readonly runtime: Atom.AtomRuntime<R, any>
45
45
  readonly formBuilder: Form.FormBuilder<TFields, R>
46
46
  }
@@ -51,9 +51,9 @@ export interface FormAtomsConfig<TFields extends Form.FieldsRecord, R> {
51
51
  * @since 1.0.0
52
52
  * @category Models
53
53
  */
54
- export type FieldRefs<TFields extends Form.FieldsRecord> = {
55
- readonly [K in keyof TFields]: TFields[K] extends Form.FieldDef<any, infer S> ? Form.Field<Schema.Schema.Encoded<S>>
56
- : TFields[K] extends Form.ArrayFieldDef<any, infer S> ? Form.Field<ReadonlyArray<Schema.Schema.Encoded<S>>>
54
+ export type FieldRefs<TFields extends Field.FieldsRecord> = {
55
+ readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S> ? Form.Field<Schema.Schema.Encoded<S>>
56
+ : TFields[K] extends Field.ArrayFieldDef<any, infer S> ? Form.Field<ReadonlyArray<Schema.Schema.Encoded<S>>>
57
57
  : never
58
58
  }
59
59
 
@@ -63,20 +63,20 @@ export type FieldRefs<TFields extends Form.FieldsRecord> = {
63
63
  * @since 1.0.0
64
64
  * @category Models
65
65
  */
66
- export interface FormAtoms<TFields extends Form.FieldsRecord, R> {
66
+ export interface FormAtoms<TFields extends Field.FieldsRecord, R> {
67
67
  readonly stateAtom: Atom.Writable<Option.Option<Form.FormState<TFields>>, Option.Option<Form.FormState<TFields>>>
68
68
  readonly crossFieldErrorsAtom: Atom.Writable<Map<string, string>, Map<string, string>>
69
69
  readonly dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>
70
70
  readonly isDirtyAtom: Atom.Atom<boolean>
71
71
  readonly submitCountAtom: Atom.Atom<number>
72
72
  readonly onSubmitAtom: Atom.Writable<
73
- Atom.AtomResultFn<Form.DecodedFromFields<TFields>, unknown, unknown> | null,
74
- Atom.AtomResultFn<Form.DecodedFromFields<TFields>, unknown, unknown> | null
73
+ Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null,
74
+ Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null
75
75
  >
76
76
 
77
- readonly decodeAndSubmit: Atom.AtomResultFn<Form.EncodedFromFields<TFields>, unknown, unknown>
77
+ readonly decodeAndSubmit: Atom.AtomResultFn<Field.EncodedFromFields<TFields>, unknown, unknown>
78
78
 
79
- readonly combinedSchema: Schema.Schema<Form.DecodedFromFields<TFields>, Form.EncodedFromFields<TFields>, R>
79
+ readonly combinedSchema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
80
80
 
81
81
  readonly fieldRefs: FieldRefs<TFields>
82
82
 
@@ -101,11 +101,11 @@ export interface FormAtoms<TFields extends Form.FieldsRecord, R> {
101
101
  * @since 1.0.0
102
102
  * @category Models
103
103
  */
104
- export interface FormOperations<TFields extends Form.FieldsRecord> {
104
+ export interface FormOperations<TFields extends Field.FieldsRecord> {
105
105
  /**
106
106
  * Creates the initial form state from default values.
107
107
  */
108
- readonly createInitialState: (defaultValues: Form.EncodedFromFields<TFields>) => Form.FormState<TFields>
108
+ readonly createInitialState: (defaultValues: Field.EncodedFromFields<TFields>) => Form.FormState<TFields>
109
109
 
110
110
  /**
111
111
  * Creates a reset state (back to initial values).
@@ -131,7 +131,7 @@ export interface FormOperations<TFields extends Form.FieldsRecord> {
131
131
  */
132
132
  readonly setFormValues: (
133
133
  state: Form.FormState<TFields>,
134
- values: Form.EncodedFromFields<TFields>,
134
+ values: Field.EncodedFromFields<TFields>,
135
135
  ) => Form.FormState<TFields>
136
136
 
137
137
  /**
@@ -209,13 +209,13 @@ export interface FormOperations<TFields extends Form.FieldsRecord> {
209
209
  * @since 1.0.0
210
210
  * @category Constructors
211
211
  */
212
- export const make = <TFields extends Form.FieldsRecord, R>(
212
+ export const make = <TFields extends Field.FieldsRecord, R>(
213
213
  config: FormAtomsConfig<TFields, R>,
214
214
  ): FormAtoms<TFields, R> => {
215
215
  const { formBuilder, runtime } = config
216
216
  const { fields } = formBuilder
217
217
 
218
- const combinedSchema = buildSchema(formBuilder)
218
+ const combinedSchema = Form.buildSchema(formBuilder)
219
219
 
220
220
  const stateAtom = Atom.make(Option.none<Form.FormState<TFields>>()).pipe(Atom.setIdleTTL(0))
221
221
  const crossFieldErrorsAtom = Atom.make<Map<string, string>>(new Map()).pipe(Atom.setIdleTTL(0))
@@ -232,7 +232,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
232
232
  Atom.setIdleTTL(0),
233
233
  )
234
234
 
235
- const onSubmitAtom = Atom.make<Atom.AtomResultFn<Form.DecodedFromFields<TFields>, unknown, unknown> | null>(
235
+ const onSubmitAtom = Atom.make<Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null>(
236
236
  null,
237
237
  ).pipe(Atom.setIdleTTL(0))
238
238
 
@@ -330,10 +330,10 @@ export const make = <TFields extends Form.FieldsRecord, R>(
330
330
  fieldAtomsRegistry.clear()
331
331
  }
332
332
 
333
- const decodeAndSubmit = runtime.fn<Form.EncodedFromFields<TFields>>()((values, get) =>
333
+ const decodeAndSubmit = runtime.fn<Field.EncodedFromFields<TFields>>()((values, get) =>
334
334
  Effect.gen(function*() {
335
335
  const decoded = yield* Schema.decodeUnknown(combinedSchema)(values) as Effect.Effect<
336
- Form.DecodedFromFields<TFields>,
336
+ Field.DecodedFromFields<TFields>,
337
337
  ParseResult.ParseError,
338
338
  R
339
339
  >
@@ -341,17 +341,17 @@ export const make = <TFields extends Form.FieldsRecord, R>(
341
341
  get.set(onSubmit, decoded)
342
342
  return yield* get.result(onSubmit, { suspendOnWaiting: true })
343
343
  })
344
- ).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<Form.EncodedFromFields<TFields>, unknown, unknown>
344
+ ).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<Field.EncodedFromFields<TFields>, unknown, unknown>
345
345
 
346
346
  const fieldRefs = Object.fromEntries(
347
- Object.keys(fields).map((key) => [key, makeFieldRef(key)]),
347
+ Object.keys(fields).map((key) => [key, Form.makeFieldRef(key)]),
348
348
  ) as FieldRefs<TFields>
349
349
 
350
350
  const operations: FormOperations<TFields> = {
351
351
  createInitialState: (defaultValues) => ({
352
352
  values: defaultValues,
353
353
  initialValues: defaultValues,
354
- touched: createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
354
+ touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
355
355
  submitCount: 0,
356
356
  dirtyFields: new Set(),
357
357
  }),
@@ -359,14 +359,14 @@ export const make = <TFields extends Form.FieldsRecord, R>(
359
359
  createResetState: (state) => ({
360
360
  values: state.initialValues,
361
361
  initialValues: state.initialValues,
362
- touched: createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
362
+ touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
363
363
  submitCount: 0,
364
364
  dirtyFields: new Set(),
365
365
  }),
366
366
 
367
367
  createSubmitState: (state) => ({
368
368
  ...state,
369
- touched: createTouchedRecord(fields, true) as { readonly [K in keyof TFields]: boolean },
369
+ touched: Field.createTouchedRecord(fields, true) as { readonly [K in keyof TFields]: boolean },
370
370
  submitCount: state.submitCount + 1,
371
371
  }),
372
372
 
@@ -380,7 +380,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
380
380
  )
381
381
  return {
382
382
  ...state,
383
- values: newValues as Form.EncodedFromFields<TFields>,
383
+ values: newValues as Field.EncodedFromFields<TFields>,
384
384
  dirtyFields: newDirtyFields,
385
385
  }
386
386
  },
@@ -405,12 +405,12 @@ export const make = <TFields extends Form.FieldsRecord, R>(
405
405
  }),
406
406
 
407
407
  appendArrayItem: (state, arrayPath, itemSchema, value) => {
408
- const newItem = value ?? getDefaultFromSchema(itemSchema)
408
+ const newItem = value ?? Field.getDefaultFromSchema(itemSchema)
409
409
  const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>
410
410
  const newItems = [...currentItems, newItem]
411
411
  return {
412
412
  ...state,
413
- values: setNestedValue(state.values, arrayPath, newItems) as Form.EncodedFromFields<TFields>,
413
+ values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
414
414
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
415
415
  }
416
416
  },
@@ -420,7 +420,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
420
420
  const newItems = currentItems.filter((_, i) => i !== index)
421
421
  return {
422
422
  ...state,
423
- values: setNestedValue(state.values, arrayPath, newItems) as Form.EncodedFromFields<TFields>,
423
+ values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
424
424
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
425
425
  }
426
426
  },
@@ -440,7 +440,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
440
440
  newItems[indexB] = temp
441
441
  return {
442
442
  ...state,
443
- values: setNestedValue(state.values, arrayPath, newItems) as Form.EncodedFromFields<TFields>,
443
+ values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
444
444
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
445
445
  }
446
446
  },
@@ -459,7 +459,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
459
459
  newItems.splice(toIndex, 0, item)
460
460
  return {
461
461
  ...state,
462
- values: setNestedValue(state.values, arrayPath, newItems) as Form.EncodedFromFields<TFields>,
462
+ values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
463
463
  dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
464
464
  }
465
465
  },
package/src/index.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Field definitions for type-safe forms.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ export * as Field from "./Field.js"
7
+
1
8
  /**
2
9
  * @since 1.0.0
3
10
  */