@lucas-barake/effect-form 0.1.0 → 0.3.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/Field/package.json +6 -0
- package/dist/cjs/Field.js +171 -0
- package/dist/cjs/Field.js.map +1 -0
- package/dist/cjs/Form.js +11 -132
- package/dist/cjs/Form.js.map +1 -1
- package/dist/cjs/FormAtoms.js +45 -22
- package/dist/cjs/FormAtoms.js.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/dts/Field.d.ts +145 -0
- package/dist/dts/Field.d.ts.map +1 -0
- package/dist/dts/Form.d.ts +8 -126
- package/dist/dts/Form.d.ts.map +1 -1
- package/dist/dts/FormAtoms.d.ts +20 -41
- package/dist/dts/FormAtoms.d.ts.map +1 -1
- package/dist/dts/index.d.ts +6 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/Field.js +137 -0
- package/dist/esm/Field.js.map +1 -0
- package/dist/esm/Form.js +7 -121
- package/dist/esm/Form.js.map +1 -1
- package/dist/esm/FormAtoms.js +45 -22
- package/dist/esm/FormAtoms.js.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +9 -1
- package/src/Field.ts +220 -0
- package/src/Form.ts +19 -197
- package/src/FormAtoms.ts +95 -81
- package/src/index.ts +7 -0
package/src/Form.ts
CHANGED
|
@@ -2,22 +2,19 @@
|
|
|
2
2
|
* @since 1.0.0
|
|
3
3
|
*/
|
|
4
4
|
import type * as Effect from "effect/Effect"
|
|
5
|
+
import type * as Option from "effect/Option"
|
|
5
6
|
import * as Predicate from "effect/Predicate"
|
|
6
7
|
import * as Schema from "effect/Schema"
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @since 1.0.0
|
|
18
|
-
* @category Symbols
|
|
19
|
-
*/
|
|
20
|
-
export type TypeId = typeof TypeId
|
|
9
|
+
import type {
|
|
10
|
+
AnyFieldDef,
|
|
11
|
+
ArrayFieldDef,
|
|
12
|
+
DecodedFromFields,
|
|
13
|
+
EncodedFromFields,
|
|
14
|
+
FieldDef,
|
|
15
|
+
FieldsRecord,
|
|
16
|
+
} from "./Field.js"
|
|
17
|
+
import { isArrayFieldDef, isFieldDef } from "./Field.js"
|
|
21
18
|
|
|
22
19
|
/**
|
|
23
20
|
* Unique identifier for Field references.
|
|
@@ -60,117 +57,23 @@ export const makeFieldRef = <S>(key: string): Field<S> => ({
|
|
|
60
57
|
key,
|
|
61
58
|
})
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
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>
|
|
60
|
+
// ================================
|
|
61
|
+
// FormBuilder
|
|
62
|
+
// ================================
|
|
150
63
|
|
|
151
64
|
/**
|
|
152
|
-
*
|
|
65
|
+
* Unique identifier for FormBuilder instances.
|
|
153
66
|
*
|
|
154
67
|
* @since 1.0.0
|
|
155
|
-
* @category
|
|
68
|
+
* @category Symbols
|
|
156
69
|
*/
|
|
157
|
-
export
|
|
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
|
-
}
|
|
70
|
+
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form")
|
|
162
71
|
|
|
163
72
|
/**
|
|
164
|
-
* Extracts the decoded (output) type from a fields record.
|
|
165
|
-
*
|
|
166
73
|
* @since 1.0.0
|
|
167
|
-
* @category
|
|
74
|
+
* @category Symbols
|
|
168
75
|
*/
|
|
169
|
-
export type
|
|
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
|
-
}
|
|
76
|
+
export type TypeId = typeof TypeId
|
|
174
77
|
|
|
175
78
|
/**
|
|
176
79
|
* The state of a form at runtime.
|
|
@@ -181,6 +84,7 @@ export type DecodedFromFields<T extends FieldsRecord> = {
|
|
|
181
84
|
export interface FormState<TFields extends FieldsRecord> {
|
|
182
85
|
readonly values: EncodedFromFields<TFields>
|
|
183
86
|
readonly initialValues: EncodedFromFields<TFields>
|
|
87
|
+
readonly lastSubmittedValues: Option.Option<EncodedFromFields<TFields>>
|
|
184
88
|
readonly touched: { readonly [K in keyof TFields]: boolean }
|
|
185
89
|
readonly submitCount: number
|
|
186
90
|
readonly dirtyFields: ReadonlySet<string>
|
|
@@ -372,56 +276,6 @@ const FormBuilderProto = {
|
|
|
372
276
|
*/
|
|
373
277
|
export const isFormBuilder = (u: unknown): u is FormBuilder<any, any> => Predicate.hasProperty(u, TypeId)
|
|
374
278
|
|
|
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
279
|
/**
|
|
426
280
|
* An empty `FormBuilder` to start building a form.
|
|
427
281
|
*
|
|
@@ -488,35 +342,3 @@ export const buildSchema = <TFields extends FieldsRecord, R>(
|
|
|
488
342
|
R
|
|
489
343
|
>
|
|
490
344
|
}
|
|
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
|
@@ -9,14 +9,12 @@
|
|
|
9
9
|
import * as Atom from "@effect-atom/atom/Atom"
|
|
10
10
|
import type * as Registry from "@effect-atom/atom/Registry"
|
|
11
11
|
import * as Effect from "effect/Effect"
|
|
12
|
-
import * as Equal from "effect/Equal"
|
|
13
12
|
import { pipe } from "effect/Function"
|
|
14
13
|
import * as Option from "effect/Option"
|
|
15
14
|
import type * as ParseResult from "effect/ParseResult"
|
|
16
15
|
import * as Schema from "effect/Schema"
|
|
17
|
-
import * as
|
|
18
|
-
import
|
|
19
|
-
import { buildSchema, createTouchedRecord, getDefaultFromSchema, makeFieldRef } from "./Form.js"
|
|
16
|
+
import * as Field from "./Field.js"
|
|
17
|
+
import * as Form from "./Form.js"
|
|
20
18
|
import { recalculateDirtyFieldsForArray, recalculateDirtySubtree } from "./internal/dirty.js"
|
|
21
19
|
import { getNestedValue, setNestedValue } from "./internal/path.js"
|
|
22
20
|
import { createWeakRegistry, type WeakRegistry } from "./internal/weak-registry.js"
|
|
@@ -40,7 +38,7 @@ export interface FieldAtoms {
|
|
|
40
38
|
* @since 1.0.0
|
|
41
39
|
* @category Models
|
|
42
40
|
*/
|
|
43
|
-
export interface FormAtomsConfig<TFields extends
|
|
41
|
+
export interface FormAtomsConfig<TFields extends Field.FieldsRecord, R> {
|
|
44
42
|
readonly runtime: Atom.AtomRuntime<R, any>
|
|
45
43
|
readonly formBuilder: Form.FormBuilder<TFields, R>
|
|
46
44
|
}
|
|
@@ -51,9 +49,9 @@ export interface FormAtomsConfig<TFields extends Form.FieldsRecord, R> {
|
|
|
51
49
|
* @since 1.0.0
|
|
52
50
|
* @category Models
|
|
53
51
|
*/
|
|
54
|
-
export type FieldRefs<TFields extends
|
|
55
|
-
readonly [K in keyof TFields]: TFields[K] extends
|
|
56
|
-
: TFields[K] extends
|
|
52
|
+
export type FieldRefs<TFields extends Field.FieldsRecord> = {
|
|
53
|
+
readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S> ? Form.Field<Schema.Schema.Encoded<S>>
|
|
54
|
+
: TFields[K] extends Field.ArrayFieldDef<any, infer S> ? Form.Field<ReadonlyArray<Schema.Schema.Encoded<S>>>
|
|
57
55
|
: never
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -63,20 +61,23 @@ export type FieldRefs<TFields extends Form.FieldsRecord> = {
|
|
|
63
61
|
* @since 1.0.0
|
|
64
62
|
* @category Models
|
|
65
63
|
*/
|
|
66
|
-
export interface FormAtoms<TFields extends
|
|
64
|
+
export interface FormAtoms<TFields extends Field.FieldsRecord, R> {
|
|
67
65
|
readonly stateAtom: Atom.Writable<Option.Option<Form.FormState<TFields>>, Option.Option<Form.FormState<TFields>>>
|
|
68
66
|
readonly crossFieldErrorsAtom: Atom.Writable<Map<string, string>, Map<string, string>>
|
|
69
67
|
readonly dirtyFieldsAtom: Atom.Atom<ReadonlySet<string>>
|
|
70
68
|
readonly isDirtyAtom: Atom.Atom<boolean>
|
|
71
69
|
readonly submitCountAtom: Atom.Atom<number>
|
|
70
|
+
readonly lastSubmittedValuesAtom: Atom.Atom<Option.Option<Field.EncodedFromFields<TFields>>>
|
|
71
|
+
readonly changedSinceSubmitFieldsAtom: Atom.Atom<ReadonlySet<string>>
|
|
72
|
+
readonly hasChangedSinceSubmitAtom: Atom.Atom<boolean>
|
|
72
73
|
readonly onSubmitAtom: Atom.Writable<
|
|
73
|
-
Atom.AtomResultFn<
|
|
74
|
-
Atom.AtomResultFn<
|
|
74
|
+
Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null,
|
|
75
|
+
Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null
|
|
75
76
|
>
|
|
76
77
|
|
|
77
|
-
readonly decodeAndSubmit: Atom.AtomResultFn<
|
|
78
|
+
readonly decodeAndSubmit: Atom.AtomResultFn<Field.EncodedFromFields<TFields>, unknown, unknown>
|
|
78
79
|
|
|
79
|
-
readonly combinedSchema: Schema.Schema<
|
|
80
|
+
readonly combinedSchema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
|
|
80
81
|
|
|
81
82
|
readonly fieldRefs: FieldRefs<TFields>
|
|
82
83
|
|
|
@@ -101,51 +102,30 @@ export interface FormAtoms<TFields extends Form.FieldsRecord, R> {
|
|
|
101
102
|
* @since 1.0.0
|
|
102
103
|
* @category Models
|
|
103
104
|
*/
|
|
104
|
-
export interface FormOperations<TFields extends
|
|
105
|
-
|
|
106
|
-
* Creates the initial form state from default values.
|
|
107
|
-
*/
|
|
108
|
-
readonly createInitialState: (defaultValues: Form.EncodedFromFields<TFields>) => Form.FormState<TFields>
|
|
105
|
+
export interface FormOperations<TFields extends Field.FieldsRecord> {
|
|
106
|
+
readonly createInitialState: (defaultValues: Field.EncodedFromFields<TFields>) => Form.FormState<TFields>
|
|
109
107
|
|
|
110
|
-
/**
|
|
111
|
-
* Creates a reset state (back to initial values).
|
|
112
|
-
*/
|
|
113
108
|
readonly createResetState: (state: Form.FormState<TFields>) => Form.FormState<TFields>
|
|
114
109
|
|
|
115
|
-
/**
|
|
116
|
-
* Creates state with all fields marked as touched and submit count incremented.
|
|
117
|
-
*/
|
|
118
110
|
readonly createSubmitState: (state: Form.FormState<TFields>) => Form.FormState<TFields>
|
|
119
111
|
|
|
120
|
-
/**
|
|
121
|
-
* Updates a single field value in the state.
|
|
122
|
-
*/
|
|
123
112
|
readonly setFieldValue: (
|
|
124
113
|
state: Form.FormState<TFields>,
|
|
125
114
|
fieldPath: string,
|
|
126
115
|
value: unknown,
|
|
127
116
|
) => Form.FormState<TFields>
|
|
128
117
|
|
|
129
|
-
/**
|
|
130
|
-
* Sets all form values, recalculating dirty fields.
|
|
131
|
-
*/
|
|
132
118
|
readonly setFormValues: (
|
|
133
119
|
state: Form.FormState<TFields>,
|
|
134
|
-
values:
|
|
120
|
+
values: Field.EncodedFromFields<TFields>,
|
|
135
121
|
) => Form.FormState<TFields>
|
|
136
122
|
|
|
137
|
-
/**
|
|
138
|
-
* Sets a field as touched.
|
|
139
|
-
*/
|
|
140
123
|
readonly setFieldTouched: (
|
|
141
124
|
state: Form.FormState<TFields>,
|
|
142
125
|
fieldPath: string,
|
|
143
126
|
touched: boolean,
|
|
144
127
|
) => Form.FormState<TFields>
|
|
145
128
|
|
|
146
|
-
/**
|
|
147
|
-
* Appends an item to an array field.
|
|
148
|
-
*/
|
|
149
129
|
readonly appendArrayItem: (
|
|
150
130
|
state: Form.FormState<TFields>,
|
|
151
131
|
arrayPath: string,
|
|
@@ -153,18 +133,12 @@ export interface FormOperations<TFields extends Form.FieldsRecord> {
|
|
|
153
133
|
value?: unknown,
|
|
154
134
|
) => Form.FormState<TFields>
|
|
155
135
|
|
|
156
|
-
/**
|
|
157
|
-
* Removes an item from an array field.
|
|
158
|
-
*/
|
|
159
136
|
readonly removeArrayItem: (
|
|
160
137
|
state: Form.FormState<TFields>,
|
|
161
138
|
arrayPath: string,
|
|
162
139
|
index: number,
|
|
163
140
|
) => Form.FormState<TFields>
|
|
164
141
|
|
|
165
|
-
/**
|
|
166
|
-
* Swaps two items in an array field.
|
|
167
|
-
*/
|
|
168
142
|
readonly swapArrayItems: (
|
|
169
143
|
state: Form.FormState<TFields>,
|
|
170
144
|
arrayPath: string,
|
|
@@ -172,15 +146,18 @@ export interface FormOperations<TFields extends Form.FieldsRecord> {
|
|
|
172
146
|
indexB: number,
|
|
173
147
|
) => Form.FormState<TFields>
|
|
174
148
|
|
|
175
|
-
/**
|
|
176
|
-
* Moves an item in an array field.
|
|
177
|
-
*/
|
|
178
149
|
readonly moveArrayItem: (
|
|
179
150
|
state: Form.FormState<TFields>,
|
|
180
151
|
arrayPath: string,
|
|
181
152
|
fromIndex: number,
|
|
182
153
|
toIndex: number,
|
|
183
154
|
) => Form.FormState<TFields>
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Reverts values to the last submitted state.
|
|
158
|
+
* No-op if form has never been submitted or is already in sync.
|
|
159
|
+
*/
|
|
160
|
+
readonly revertToLastSubmit: (state: Form.FormState<TFields>) => Form.FormState<TFields>
|
|
184
161
|
}
|
|
185
162
|
|
|
186
163
|
/**
|
|
@@ -209,13 +186,13 @@ export interface FormOperations<TFields extends Form.FieldsRecord> {
|
|
|
209
186
|
* @since 1.0.0
|
|
210
187
|
* @category Constructors
|
|
211
188
|
*/
|
|
212
|
-
export const make = <TFields extends
|
|
189
|
+
export const make = <TFields extends Field.FieldsRecord, R>(
|
|
213
190
|
config: FormAtomsConfig<TFields, R>,
|
|
214
191
|
): FormAtoms<TFields, R> => {
|
|
215
192
|
const { formBuilder, runtime } = config
|
|
216
193
|
const { fields } = formBuilder
|
|
217
194
|
|
|
218
|
-
const combinedSchema = buildSchema(formBuilder)
|
|
195
|
+
const combinedSchema = Form.buildSchema(formBuilder)
|
|
219
196
|
|
|
220
197
|
const stateAtom = Atom.make(Option.none<Form.FormState<TFields>>()).pipe(Atom.setIdleTTL(0))
|
|
221
198
|
const crossFieldErrorsAtom = Atom.make<Map<string, string>>(new Map()).pipe(Atom.setIdleTTL(0))
|
|
@@ -232,26 +209,28 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
232
209
|
Atom.setIdleTTL(0),
|
|
233
210
|
)
|
|
234
211
|
|
|
235
|
-
const
|
|
236
|
-
|
|
212
|
+
const lastSubmittedValuesAtom = Atom.readable(
|
|
213
|
+
(get) => Option.getOrThrow(get(stateAtom)).lastSubmittedValues,
|
|
237
214
|
).pipe(Atom.setIdleTTL(0))
|
|
238
215
|
|
|
239
|
-
const
|
|
240
|
-
state
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
216
|
+
const changedSinceSubmitFieldsAtom = Atom.readable((get) => {
|
|
217
|
+
const state = Option.getOrThrow(get(stateAtom))
|
|
218
|
+
return Option.match(state.lastSubmittedValues, {
|
|
219
|
+
onNone: () => new Set<string>(),
|
|
220
|
+
onSome: (lastSubmitted) => recalculateDirtySubtree(new Set(), lastSubmitted, state.values, ""),
|
|
221
|
+
})
|
|
222
|
+
}).pipe(Atom.setIdleTTL(0))
|
|
223
|
+
|
|
224
|
+
const hasChangedSinceSubmitAtom = Atom.readable((get) => {
|
|
225
|
+
const state = Option.getOrThrow(get(stateAtom))
|
|
226
|
+
if (Option.isNone(state.lastSubmittedValues)) return false
|
|
227
|
+
if (state.values === state.lastSubmittedValues.value) return false
|
|
228
|
+
return get(changedSinceSubmitFieldsAtom).size > 0
|
|
229
|
+
}).pipe(Atom.setIdleTTL(0))
|
|
230
|
+
|
|
231
|
+
const onSubmitAtom = Atom.make<Atom.AtomResultFn<Field.DecodedFromFields<TFields>, unknown, unknown> | null>(
|
|
232
|
+
null,
|
|
233
|
+
).pipe(Atom.setIdleTTL(0))
|
|
255
234
|
|
|
256
235
|
const validationAtomsRegistry = createWeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>()
|
|
257
236
|
const fieldAtomsRegistry = createWeakRegistry<FieldAtoms>()
|
|
@@ -282,12 +261,18 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
282
261
|
(get) => getNestedValue(Option.getOrThrow(get(stateAtom)).values, fieldPath),
|
|
283
262
|
(ctx, value) => {
|
|
284
263
|
const currentState = Option.getOrThrow(ctx.get(stateAtom))
|
|
264
|
+
const newValues = setNestedValue(currentState.values, fieldPath, value)
|
|
285
265
|
ctx.set(
|
|
286
266
|
stateAtom,
|
|
287
267
|
Option.some({
|
|
288
268
|
...currentState,
|
|
289
|
-
values:
|
|
290
|
-
dirtyFields:
|
|
269
|
+
values: newValues,
|
|
270
|
+
dirtyFields: recalculateDirtySubtree(
|
|
271
|
+
currentState.dirtyFields,
|
|
272
|
+
currentState.initialValues,
|
|
273
|
+
newValues,
|
|
274
|
+
fieldPath,
|
|
275
|
+
),
|
|
291
276
|
}),
|
|
292
277
|
)
|
|
293
278
|
},
|
|
@@ -330,10 +315,10 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
330
315
|
fieldAtomsRegistry.clear()
|
|
331
316
|
}
|
|
332
317
|
|
|
333
|
-
const decodeAndSubmit = runtime.fn<
|
|
318
|
+
const decodeAndSubmit = runtime.fn<Field.EncodedFromFields<TFields>>()((values, get) =>
|
|
334
319
|
Effect.gen(function*() {
|
|
335
320
|
const decoded = yield* Schema.decodeUnknown(combinedSchema)(values) as Effect.Effect<
|
|
336
|
-
|
|
321
|
+
Field.DecodedFromFields<TFields>,
|
|
337
322
|
ParseResult.ParseError,
|
|
338
323
|
R
|
|
339
324
|
>
|
|
@@ -341,17 +326,18 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
341
326
|
get.set(onSubmit, decoded)
|
|
342
327
|
return yield* get.result(onSubmit, { suspendOnWaiting: true })
|
|
343
328
|
})
|
|
344
|
-
).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<
|
|
329
|
+
).pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<Field.EncodedFromFields<TFields>, unknown, unknown>
|
|
345
330
|
|
|
346
331
|
const fieldRefs = Object.fromEntries(
|
|
347
|
-
Object.keys(fields).map((key) => [key, makeFieldRef(key)]),
|
|
332
|
+
Object.keys(fields).map((key) => [key, Form.makeFieldRef(key)]),
|
|
348
333
|
) as FieldRefs<TFields>
|
|
349
334
|
|
|
350
335
|
const operations: FormOperations<TFields> = {
|
|
351
336
|
createInitialState: (defaultValues) => ({
|
|
352
337
|
values: defaultValues,
|
|
353
338
|
initialValues: defaultValues,
|
|
354
|
-
|
|
339
|
+
lastSubmittedValues: Option.none(),
|
|
340
|
+
touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
|
|
355
341
|
submitCount: 0,
|
|
356
342
|
dirtyFields: new Set(),
|
|
357
343
|
}),
|
|
@@ -359,14 +345,16 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
359
345
|
createResetState: (state) => ({
|
|
360
346
|
values: state.initialValues,
|
|
361
347
|
initialValues: state.initialValues,
|
|
362
|
-
|
|
348
|
+
lastSubmittedValues: Option.none(),
|
|
349
|
+
touched: Field.createTouchedRecord(fields, false) as { readonly [K in keyof TFields]: boolean },
|
|
363
350
|
submitCount: 0,
|
|
364
351
|
dirtyFields: new Set(),
|
|
365
352
|
}),
|
|
366
353
|
|
|
367
354
|
createSubmitState: (state) => ({
|
|
368
355
|
...state,
|
|
369
|
-
|
|
356
|
+
lastSubmittedValues: Option.some(state.values),
|
|
357
|
+
touched: Field.createTouchedRecord(fields, true) as { readonly [K in keyof TFields]: boolean },
|
|
370
358
|
submitCount: state.submitCount + 1,
|
|
371
359
|
}),
|
|
372
360
|
|
|
@@ -380,7 +368,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
380
368
|
)
|
|
381
369
|
return {
|
|
382
370
|
...state,
|
|
383
|
-
values: newValues as
|
|
371
|
+
values: newValues as Field.EncodedFromFields<TFields>,
|
|
384
372
|
dirtyFields: newDirtyFields,
|
|
385
373
|
}
|
|
386
374
|
},
|
|
@@ -405,12 +393,12 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
405
393
|
}),
|
|
406
394
|
|
|
407
395
|
appendArrayItem: (state, arrayPath, itemSchema, value) => {
|
|
408
|
-
const newItem = value ?? getDefaultFromSchema(itemSchema)
|
|
396
|
+
const newItem = value ?? Field.getDefaultFromSchema(itemSchema)
|
|
409
397
|
const currentItems = (getNestedValue(state.values, arrayPath) ?? []) as ReadonlyArray<unknown>
|
|
410
398
|
const newItems = [...currentItems, newItem]
|
|
411
399
|
return {
|
|
412
400
|
...state,
|
|
413
|
-
values: setNestedValue(state.values, arrayPath, newItems) as
|
|
401
|
+
values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
|
|
414
402
|
dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
|
|
415
403
|
}
|
|
416
404
|
},
|
|
@@ -420,7 +408,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
420
408
|
const newItems = currentItems.filter((_, i) => i !== index)
|
|
421
409
|
return {
|
|
422
410
|
...state,
|
|
423
|
-
values: setNestedValue(state.values, arrayPath, newItems) as
|
|
411
|
+
values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
|
|
424
412
|
dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
|
|
425
413
|
}
|
|
426
414
|
},
|
|
@@ -440,7 +428,7 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
440
428
|
newItems[indexB] = temp
|
|
441
429
|
return {
|
|
442
430
|
...state,
|
|
443
|
-
values: setNestedValue(state.values, arrayPath, newItems) as
|
|
431
|
+
values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
|
|
444
432
|
dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
|
|
445
433
|
}
|
|
446
434
|
},
|
|
@@ -459,10 +447,33 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
459
447
|
newItems.splice(toIndex, 0, item)
|
|
460
448
|
return {
|
|
461
449
|
...state,
|
|
462
|
-
values: setNestedValue(state.values, arrayPath, newItems) as
|
|
450
|
+
values: setNestedValue(state.values, arrayPath, newItems) as Field.EncodedFromFields<TFields>,
|
|
463
451
|
dirtyFields: recalculateDirtyFieldsForArray(state.dirtyFields, state.initialValues, arrayPath, newItems),
|
|
464
452
|
}
|
|
465
453
|
},
|
|
454
|
+
|
|
455
|
+
revertToLastSubmit: (state) => {
|
|
456
|
+
if (Option.isNone(state.lastSubmittedValues)) {
|
|
457
|
+
return state
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (state.values === state.lastSubmittedValues.value) {
|
|
461
|
+
return state
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const newDirtyFields = recalculateDirtySubtree(
|
|
465
|
+
state.dirtyFields,
|
|
466
|
+
state.initialValues,
|
|
467
|
+
state.lastSubmittedValues.value,
|
|
468
|
+
"",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
...state,
|
|
473
|
+
values: state.lastSubmittedValues.value,
|
|
474
|
+
dirtyFields: newDirtyFields,
|
|
475
|
+
}
|
|
476
|
+
},
|
|
466
477
|
}
|
|
467
478
|
|
|
468
479
|
return {
|
|
@@ -471,6 +482,9 @@ export const make = <TFields extends Form.FieldsRecord, R>(
|
|
|
471
482
|
dirtyFieldsAtom,
|
|
472
483
|
isDirtyAtom,
|
|
473
484
|
submitCountAtom,
|
|
485
|
+
lastSubmittedValuesAtom,
|
|
486
|
+
changedSinceSubmitFieldsAtom,
|
|
487
|
+
hasChangedSinceSubmitAtom,
|
|
474
488
|
onSubmitAtom,
|
|
475
489
|
decodeAndSubmit,
|
|
476
490
|
combinedSchema,
|