@lucas-barake/effect-form 0.15.0 → 0.16.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/dist/cjs/Field.js.map +1 -1
- package/dist/cjs/FormAtoms.js +2 -0
- package/dist/cjs/FormAtoms.js.map +1 -1
- package/dist/cjs/FormBuilder.js.map +1 -1
- package/dist/cjs/Mode.js.map +1 -1
- package/dist/cjs/Path.js.map +1 -1
- package/dist/cjs/Validation.js.map +1 -1
- package/dist/cjs/internal/dirty.js.map +1 -1
- package/dist/cjs/internal/weak-registry.js.map +1 -1
- package/dist/dts/Field.d.ts.map +1 -1
- package/dist/dts/FormAtoms.d.ts +1 -0
- package/dist/dts/FormAtoms.d.ts.map +1 -1
- package/dist/dts/FormBuilder.d.ts.map +1 -1
- package/dist/dts/Mode.d.ts.map +1 -1
- package/dist/dts/Path.d.ts.map +1 -1
- package/dist/dts/Validation.d.ts.map +1 -1
- package/dist/dts/internal/dirty.d.ts.map +1 -1
- package/dist/dts/internal/weak-registry.d.ts.map +1 -1
- package/dist/esm/FormAtoms.js +2 -0
- package/dist/esm/FormAtoms.js.map +1 -1
- package/package.json +1 -1
- package/src/Field.ts +40 -40
- package/src/FormAtoms.ts +5 -0
- package/src/FormBuilder.ts +70 -70
- package/src/Mode.ts +14 -14
- package/src/Path.ts +37 -37
- package/src/Validation.ts +54 -54
- package/src/internal/dirty.ts +40 -40
- package/src/internal/weak-registry.ts +17 -17
package/src/Field.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import * as Schema from "effect/Schema"
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
2
|
|
|
3
|
-
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field")
|
|
3
|
+
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field");
|
|
4
4
|
|
|
5
|
-
export type TypeId = typeof TypeId
|
|
5
|
+
export type TypeId = typeof TypeId;
|
|
6
6
|
|
|
7
7
|
export interface FieldDef<K extends string, S extends Schema.Schema.Any> {
|
|
8
|
-
readonly _tag: "field"
|
|
9
|
-
readonly key: K
|
|
10
|
-
readonly schema: S
|
|
8
|
+
readonly _tag: "field";
|
|
9
|
+
readonly key: K;
|
|
10
|
+
readonly schema: S;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface ArrayFieldDef<K extends string, S extends Schema.Schema.Any> {
|
|
14
|
-
readonly _tag: "array"
|
|
15
|
-
readonly key: K
|
|
16
|
-
readonly itemSchema: S
|
|
14
|
+
readonly _tag: "array";
|
|
15
|
+
readonly key: K;
|
|
16
|
+
readonly itemSchema: S;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export type AnyFieldDef = FieldDef<string, Schema.Schema.Any> | ArrayFieldDef<string, Schema.Schema.Any
|
|
19
|
+
export type AnyFieldDef = FieldDef<string, Schema.Schema.Any> | ArrayFieldDef<string, Schema.Schema.Any>;
|
|
20
20
|
|
|
21
|
-
export type FieldsRecord = Record<string, AnyFieldDef
|
|
21
|
+
export type FieldsRecord = Record<string, AnyFieldDef>;
|
|
22
22
|
|
|
23
23
|
export const isArrayFieldDef = (def: AnyFieldDef): def is ArrayFieldDef<string, Schema.Schema.Any> =>
|
|
24
|
-
def._tag === "array"
|
|
24
|
+
def._tag === "array";
|
|
25
25
|
|
|
26
|
-
export const isFieldDef = (def: AnyFieldDef): def is FieldDef<string, Schema.Schema.Any> => def._tag === "field"
|
|
26
|
+
export const isFieldDef = (def: AnyFieldDef): def is FieldDef<string, Schema.Schema.Any> => def._tag === "field";
|
|
27
27
|
|
|
28
28
|
export const makeField = <K extends string, S extends Schema.Schema.Any>(
|
|
29
29
|
key: K,
|
|
@@ -32,7 +32,7 @@ export const makeField = <K extends string, S extends Schema.Schema.Any>(
|
|
|
32
32
|
_tag: "field",
|
|
33
33
|
key,
|
|
34
34
|
schema,
|
|
35
|
-
})
|
|
35
|
+
});
|
|
36
36
|
|
|
37
37
|
export const makeArrayField = <K extends string, S extends Schema.Schema.Any>(
|
|
38
38
|
key: K,
|
|
@@ -41,64 +41,64 @@ export const makeArrayField = <K extends string, S extends Schema.Schema.Any>(
|
|
|
41
41
|
_tag: "array",
|
|
42
42
|
key,
|
|
43
43
|
itemSchema,
|
|
44
|
-
})
|
|
44
|
+
});
|
|
45
45
|
|
|
46
46
|
export type EncodedFromFields<T extends FieldsRecord> = {
|
|
47
47
|
readonly [K in keyof T]: T[K] extends FieldDef<any, infer S> ? Schema.Schema.Encoded<S>
|
|
48
48
|
: T[K] extends ArrayFieldDef<any, infer S> ? ReadonlyArray<Schema.Schema.Encoded<S>>
|
|
49
|
-
: never
|
|
50
|
-
}
|
|
49
|
+
: never;
|
|
50
|
+
};
|
|
51
51
|
|
|
52
52
|
export type DecodedFromFields<T extends FieldsRecord> = {
|
|
53
53
|
readonly [K in keyof T]: T[K] extends FieldDef<any, infer S> ? Schema.Schema.Type<S>
|
|
54
54
|
: T[K] extends ArrayFieldDef<any, infer S> ? ReadonlyArray<Schema.Schema.Type<S>>
|
|
55
|
-
: never
|
|
56
|
-
}
|
|
55
|
+
: never;
|
|
56
|
+
};
|
|
57
57
|
|
|
58
58
|
export const getDefaultFromSchema = (schema: Schema.Schema.Any): unknown => {
|
|
59
|
-
const ast = schema.ast
|
|
59
|
+
const ast = schema.ast;
|
|
60
60
|
switch (ast._tag) {
|
|
61
61
|
case "StringKeyword":
|
|
62
62
|
case "TemplateLiteral":
|
|
63
|
-
return ""
|
|
63
|
+
return "";
|
|
64
64
|
case "NumberKeyword":
|
|
65
|
-
return 0
|
|
65
|
+
return 0;
|
|
66
66
|
case "BooleanKeyword":
|
|
67
|
-
return false
|
|
67
|
+
return false;
|
|
68
68
|
case "TypeLiteral": {
|
|
69
|
-
const result: Record<string, unknown> = {}
|
|
69
|
+
const result: Record<string, unknown> = {};
|
|
70
70
|
for (const prop of ast.propertySignatures) {
|
|
71
|
-
result[prop.name as string] = getDefaultFromSchema(Schema.make(prop.type))
|
|
71
|
+
result[prop.name as string] = getDefaultFromSchema(Schema.make(prop.type));
|
|
72
72
|
}
|
|
73
|
-
return result
|
|
73
|
+
return result;
|
|
74
74
|
}
|
|
75
75
|
case "Transformation":
|
|
76
|
-
return getDefaultFromSchema(Schema.make(ast.from))
|
|
76
|
+
return getDefaultFromSchema(Schema.make(ast.from));
|
|
77
77
|
case "Refinement":
|
|
78
|
-
return getDefaultFromSchema(Schema.make(ast.from))
|
|
78
|
+
return getDefaultFromSchema(Schema.make(ast.from));
|
|
79
79
|
case "Suspend":
|
|
80
|
-
return getDefaultFromSchema(Schema.make(ast.f()))
|
|
80
|
+
return getDefaultFromSchema(Schema.make(ast.f()));
|
|
81
81
|
default:
|
|
82
|
-
return ""
|
|
82
|
+
return "";
|
|
83
83
|
}
|
|
84
|
-
}
|
|
84
|
+
};
|
|
85
85
|
|
|
86
86
|
export const getDefaultEncodedValues = (fields: FieldsRecord): Record<string, unknown> => {
|
|
87
|
-
const result: Record<string, unknown> = {}
|
|
87
|
+
const result: Record<string, unknown> = {};
|
|
88
88
|
for (const [key, def] of Object.entries(fields)) {
|
|
89
89
|
if (isArrayFieldDef(def)) {
|
|
90
|
-
result[key] = []
|
|
90
|
+
result[key] = [];
|
|
91
91
|
} else {
|
|
92
|
-
result[key] = ""
|
|
92
|
+
result[key] = "";
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
return result
|
|
96
|
-
}
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
97
|
|
|
98
98
|
export const createTouchedRecord = (fields: FieldsRecord, value: boolean): Record<string, boolean> => {
|
|
99
|
-
const result: Record<string, boolean> = {}
|
|
99
|
+
const result: Record<string, boolean> = {};
|
|
100
100
|
for (const key of Object.keys(fields)) {
|
|
101
|
-
result[key] = value
|
|
101
|
+
result[key] = value;
|
|
102
102
|
}
|
|
103
|
-
return result
|
|
104
|
-
}
|
|
103
|
+
return result;
|
|
104
|
+
};
|
package/src/FormAtoms.ts
CHANGED
|
@@ -81,6 +81,7 @@ export interface FormAtoms<TFields extends Field.FieldsRecord, R, A = void, E =
|
|
|
81
81
|
readonly setValue: <S>(field: FormBuilder.FieldRef<S>) => Atom.Writable<void, S | ((prev: S) => S)>;
|
|
82
82
|
|
|
83
83
|
readonly getFieldAtom: <S>(field: FormBuilder.FieldRef<S>) => Atom.Atom<Option.Option<S>>;
|
|
84
|
+
readonly getFieldIsDirty: (field: FormBuilder.FieldRef<any>) => Atom.Atom<boolean>;
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
87
|
* Root anchor atom for the form's dependency graph.
|
|
@@ -550,6 +551,9 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
|
|
|
550
551
|
return safeAtom;
|
|
551
552
|
};
|
|
552
553
|
|
|
554
|
+
const getFieldIsDirty = (field: FormBuilder.FieldRef<any>): Atom.Atom<boolean> =>
|
|
555
|
+
getOrCreateFieldAtoms(field.key).isDirtyAtom;
|
|
556
|
+
|
|
553
557
|
const mountAtom = Atom.readable((get) => {
|
|
554
558
|
get(stateAtom);
|
|
555
559
|
get(errorsAtom);
|
|
@@ -583,6 +587,7 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
|
|
|
583
587
|
setValuesAtom,
|
|
584
588
|
setValue,
|
|
585
589
|
getFieldAtom,
|
|
590
|
+
getFieldIsDirty,
|
|
586
591
|
mountAtom,
|
|
587
592
|
keepAliveActiveAtom,
|
|
588
593
|
} as FormAtoms<TFields, R, A, E, SubmitArgs>;
|
package/src/FormBuilder.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type * as Registry from "@effect-atom/atom/Registry"
|
|
2
|
-
import type * as Effect from "effect/Effect"
|
|
3
|
-
import type * as Option from "effect/Option"
|
|
4
|
-
import * as Predicate from "effect/Predicate"
|
|
5
|
-
import * as Schema from "effect/Schema"
|
|
1
|
+
import type * as Registry from "@effect-atom/atom/Registry";
|
|
2
|
+
import type * as Effect from "effect/Effect";
|
|
3
|
+
import type * as Option from "effect/Option";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
8
|
AnyFieldDef,
|
|
@@ -11,91 +11,91 @@ import type {
|
|
|
11
11
|
EncodedFromFields,
|
|
12
12
|
FieldDef,
|
|
13
13
|
FieldsRecord,
|
|
14
|
-
} from "./Field.js"
|
|
15
|
-
import { isArrayFieldDef, isFieldDef, makeField } from "./Field.js"
|
|
14
|
+
} from "./Field.js";
|
|
15
|
+
import { isArrayFieldDef, isFieldDef, makeField } from "./Field.js";
|
|
16
16
|
|
|
17
17
|
export interface SubmittedValues<TFields extends FieldsRecord> {
|
|
18
|
-
readonly encoded: EncodedFromFields<TFields
|
|
19
|
-
readonly decoded: DecodedFromFields<TFields
|
|
18
|
+
readonly encoded: EncodedFromFields<TFields>;
|
|
19
|
+
readonly decoded: DecodedFromFields<TFields>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export const FieldTypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field")
|
|
22
|
+
export const FieldTypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field");
|
|
23
23
|
|
|
24
|
-
export type FieldTypeId = typeof FieldTypeId
|
|
24
|
+
export type FieldTypeId = typeof FieldTypeId;
|
|
25
25
|
|
|
26
26
|
export interface FieldRef<S> {
|
|
27
|
-
readonly [FieldTypeId]: FieldTypeId
|
|
28
|
-
readonly _S: S
|
|
29
|
-
readonly key: string
|
|
27
|
+
readonly [FieldTypeId]: FieldTypeId;
|
|
28
|
+
readonly _S: S;
|
|
29
|
+
readonly key: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export const makeFieldRef = <S>(key: string): FieldRef<S> => ({
|
|
33
33
|
[FieldTypeId]: FieldTypeId,
|
|
34
34
|
_S: undefined as any,
|
|
35
35
|
key,
|
|
36
|
-
})
|
|
36
|
+
});
|
|
37
37
|
|
|
38
|
-
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form")
|
|
38
|
+
export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form");
|
|
39
39
|
|
|
40
|
-
export type TypeId = typeof TypeId
|
|
40
|
+
export type TypeId = typeof TypeId;
|
|
41
41
|
|
|
42
42
|
export interface FormState<TFields extends FieldsRecord> {
|
|
43
|
-
readonly values: EncodedFromFields<TFields
|
|
44
|
-
readonly initialValues: EncodedFromFields<TFields
|
|
45
|
-
readonly lastSubmittedValues: Option.Option<SubmittedValues<TFields
|
|
46
|
-
readonly touched: { readonly [K in keyof TFields]: boolean }
|
|
47
|
-
readonly submitCount: number
|
|
48
|
-
readonly dirtyFields: ReadonlySet<string
|
|
43
|
+
readonly values: EncodedFromFields<TFields>;
|
|
44
|
+
readonly initialValues: EncodedFromFields<TFields>;
|
|
45
|
+
readonly lastSubmittedValues: Option.Option<SubmittedValues<TFields>>;
|
|
46
|
+
readonly touched: { readonly [K in keyof TFields]: boolean; };
|
|
47
|
+
readonly submitCount: number;
|
|
48
|
+
readonly dirtyFields: ReadonlySet<string>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
interface SyncRefinement {
|
|
52
|
-
readonly _tag: "sync"
|
|
53
|
-
readonly fn: (values: unknown) => Schema.FilterOutput
|
|
52
|
+
readonly _tag: "sync";
|
|
53
|
+
readonly fn: (values: unknown) => Schema.FilterOutput;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
interface AsyncRefinement {
|
|
57
|
-
readonly _tag: "async"
|
|
58
|
-
readonly fn: (values: unknown) => Effect.Effect<Schema.FilterOutput, never, unknown
|
|
57
|
+
readonly _tag: "async";
|
|
58
|
+
readonly fn: (values: unknown) => Effect.Effect<Schema.FilterOutput, never, unknown>;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
type Refinement = SyncRefinement | AsyncRefinement
|
|
61
|
+
type Refinement = SyncRefinement | AsyncRefinement;
|
|
62
62
|
|
|
63
63
|
export interface FormBuilder<TFields extends FieldsRecord, R> {
|
|
64
|
-
readonly [TypeId]: TypeId
|
|
65
|
-
readonly fields: TFields
|
|
66
|
-
readonly refinements: ReadonlyArray<Refinement
|
|
67
|
-
readonly _R?: R
|
|
64
|
+
readonly [TypeId]: TypeId;
|
|
65
|
+
readonly fields: TFields;
|
|
66
|
+
readonly refinements: ReadonlyArray<Refinement>;
|
|
67
|
+
readonly _R?: R;
|
|
68
68
|
|
|
69
69
|
addField<K extends string, S extends Schema.Schema.Any>(
|
|
70
70
|
this: FormBuilder<TFields, R>,
|
|
71
71
|
field: FieldDef<K, S>,
|
|
72
|
-
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S
|
|
72
|
+
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S>; }, R | Schema.Schema.Context<S>>;
|
|
73
73
|
|
|
74
74
|
addField<K extends string, S extends Schema.Schema.Any>(
|
|
75
75
|
this: FormBuilder<TFields, R>,
|
|
76
76
|
field: ArrayFieldDef<K, S>,
|
|
77
|
-
): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S
|
|
77
|
+
): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S>; }, R | Schema.Schema.Context<S>>;
|
|
78
78
|
|
|
79
79
|
addField<K extends string, S extends Schema.Schema.Any>(
|
|
80
80
|
this: FormBuilder<TFields, R>,
|
|
81
81
|
key: K,
|
|
82
82
|
schema: S,
|
|
83
|
-
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S
|
|
83
|
+
): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S>; }, R | Schema.Schema.Context<S>>;
|
|
84
84
|
|
|
85
85
|
merge<TFields2 extends FieldsRecord, R2>(
|
|
86
86
|
this: FormBuilder<TFields, R>,
|
|
87
87
|
other: FormBuilder<TFields2, R2>,
|
|
88
|
-
): FormBuilder<TFields & TFields2, R | R2
|
|
88
|
+
): FormBuilder<TFields & TFields2, R | R2>;
|
|
89
89
|
|
|
90
90
|
refine(
|
|
91
91
|
this: FormBuilder<TFields, R>,
|
|
92
92
|
predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput,
|
|
93
|
-
): FormBuilder<TFields, R
|
|
93
|
+
): FormBuilder<TFields, R>;
|
|
94
94
|
|
|
95
95
|
refineEffect<RD>(
|
|
96
96
|
this: FormBuilder<TFields, R>,
|
|
97
97
|
predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>,
|
|
98
|
-
): FormBuilder<TFields, R | Exclude<RD, Registry.AtomRegistry
|
|
98
|
+
): FormBuilder<TFields, R | Exclude<RD, Registry.AtomRegistry>>;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const FormBuilderProto = {
|
|
@@ -107,76 +107,76 @@ const FormBuilderProto = {
|
|
|
107
107
|
): FormBuilder<any, any> {
|
|
108
108
|
const field = typeof keyOrField === "string"
|
|
109
109
|
? makeField(keyOrField, schema!)
|
|
110
|
-
: keyOrField
|
|
111
|
-
const newSelf = Object.create(FormBuilderProto)
|
|
112
|
-
newSelf.fields = { ...this.fields, [field.key]: field }
|
|
113
|
-
newSelf.refinements = this.refinements
|
|
114
|
-
return newSelf
|
|
110
|
+
: keyOrField;
|
|
111
|
+
const newSelf = Object.create(FormBuilderProto);
|
|
112
|
+
newSelf.fields = { ...this.fields, [field.key]: field };
|
|
113
|
+
newSelf.refinements = this.refinements;
|
|
114
|
+
return newSelf;
|
|
115
115
|
},
|
|
116
116
|
merge<TFields extends FieldsRecord, R, TFields2 extends FieldsRecord, R2>(
|
|
117
117
|
this: FormBuilder<TFields, R>,
|
|
118
118
|
other: FormBuilder<TFields2, R2>,
|
|
119
119
|
): FormBuilder<TFields & TFields2, R | R2> {
|
|
120
|
-
const newSelf = Object.create(FormBuilderProto)
|
|
121
|
-
newSelf.fields = { ...this.fields, ...other.fields }
|
|
122
|
-
newSelf.refinements = [...this.refinements, ...other.refinements]
|
|
123
|
-
return newSelf
|
|
120
|
+
const newSelf = Object.create(FormBuilderProto);
|
|
121
|
+
newSelf.fields = { ...this.fields, ...other.fields };
|
|
122
|
+
newSelf.refinements = [...this.refinements, ...other.refinements];
|
|
123
|
+
return newSelf;
|
|
124
124
|
},
|
|
125
125
|
refine<TFields extends FieldsRecord, R>(
|
|
126
126
|
this: FormBuilder<TFields, R>,
|
|
127
127
|
predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput,
|
|
128
128
|
): FormBuilder<TFields, R> {
|
|
129
|
-
const newSelf = Object.create(FormBuilderProto)
|
|
130
|
-
newSelf.fields = this.fields
|
|
129
|
+
const newSelf = Object.create(FormBuilderProto);
|
|
130
|
+
newSelf.fields = this.fields;
|
|
131
131
|
newSelf.refinements = [
|
|
132
132
|
...this.refinements,
|
|
133
133
|
{ _tag: "sync" as const, fn: (values: unknown) => predicate(values as DecodedFromFields<TFields>) },
|
|
134
|
-
]
|
|
135
|
-
return newSelf
|
|
134
|
+
];
|
|
135
|
+
return newSelf;
|
|
136
136
|
},
|
|
137
137
|
refineEffect<TFields extends FieldsRecord, R, RD>(
|
|
138
138
|
this: FormBuilder<TFields, R>,
|
|
139
139
|
predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>,
|
|
140
140
|
): FormBuilder<TFields, R | Exclude<RD, Registry.AtomRegistry>> {
|
|
141
|
-
const newSelf = Object.create(FormBuilderProto)
|
|
142
|
-
newSelf.fields = this.fields
|
|
141
|
+
const newSelf = Object.create(FormBuilderProto);
|
|
142
|
+
newSelf.fields = this.fields;
|
|
143
143
|
newSelf.refinements = [
|
|
144
144
|
...this.refinements,
|
|
145
145
|
{ _tag: "async" as const, fn: (values: unknown) => predicate(values as DecodedFromFields<TFields>) },
|
|
146
|
-
]
|
|
147
|
-
return newSelf
|
|
146
|
+
];
|
|
147
|
+
return newSelf;
|
|
148
148
|
},
|
|
149
|
-
}
|
|
149
|
+
};
|
|
150
150
|
|
|
151
|
-
export const isFormBuilder = (u: unknown): u is FormBuilder<any, any> => Predicate.hasProperty(u, TypeId)
|
|
151
|
+
export const isFormBuilder = (u: unknown): u is FormBuilder<any, any> => Predicate.hasProperty(u, TypeId);
|
|
152
152
|
|
|
153
153
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
154
154
|
export const empty: FormBuilder<{}, never> = (() => {
|
|
155
|
-
const self = Object.create(FormBuilderProto)
|
|
156
|
-
self.fields = {}
|
|
157
|
-
self.refinements = []
|
|
158
|
-
return self
|
|
159
|
-
})()
|
|
155
|
+
const self = Object.create(FormBuilderProto);
|
|
156
|
+
self.fields = {};
|
|
157
|
+
self.refinements = [];
|
|
158
|
+
return self;
|
|
159
|
+
})();
|
|
160
160
|
|
|
161
161
|
export const buildSchema = <TFields extends FieldsRecord, R>(
|
|
162
162
|
self: FormBuilder<TFields, R>,
|
|
163
163
|
): Schema.Schema<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R> => {
|
|
164
|
-
const schemaFields: Record<string, Schema.Schema.Any> = {}
|
|
164
|
+
const schemaFields: Record<string, Schema.Schema.Any> = {};
|
|
165
165
|
for (const [key, def] of Object.entries(self.fields)) {
|
|
166
166
|
if (isArrayFieldDef(def)) {
|
|
167
|
-
schemaFields[key] = Schema.Array(def.itemSchema)
|
|
167
|
+
schemaFields[key] = Schema.Array(def.itemSchema);
|
|
168
168
|
} else if (isFieldDef(def)) {
|
|
169
|
-
schemaFields[key] = def.schema
|
|
169
|
+
schemaFields[key] = def.schema;
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
let schema: Schema.Schema<any, any, any> = Schema.Struct(schemaFields)
|
|
173
|
+
let schema: Schema.Schema<any, any, any> = Schema.Struct(schemaFields);
|
|
174
174
|
|
|
175
175
|
for (const refinement of self.refinements) {
|
|
176
176
|
if (refinement._tag === "sync") {
|
|
177
|
-
schema = schema.pipe(Schema.filter(refinement.fn))
|
|
177
|
+
schema = schema.pipe(Schema.filter(refinement.fn));
|
|
178
178
|
} else {
|
|
179
|
-
schema = schema.pipe(Schema.filterEffect(refinement.fn))
|
|
179
|
+
schema = schema.pipe(Schema.filterEffect(refinement.fn));
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -184,5 +184,5 @@ export const buildSchema = <TFields extends FieldsRecord, R>(
|
|
|
184
184
|
DecodedFromFields<TFields>,
|
|
185
185
|
EncodedFromFields<TFields>,
|
|
186
186
|
R
|
|
187
|
-
|
|
188
|
-
}
|
|
187
|
+
>;
|
|
188
|
+
};
|
package/src/Mode.ts
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import * as Duration from "effect/Duration"
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
2
|
|
|
3
3
|
export type FormMode =
|
|
4
4
|
| "onSubmit"
|
|
5
5
|
| "onBlur"
|
|
6
6
|
| "onChange"
|
|
7
|
-
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit?: false } }
|
|
8
|
-
| { readonly onBlur: { readonly autoSubmit: true } }
|
|
9
|
-
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit: true } }
|
|
7
|
+
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit?: false; }; }
|
|
8
|
+
| { readonly onBlur: { readonly autoSubmit: true; }; }
|
|
9
|
+
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit: true; }; };
|
|
10
10
|
|
|
11
11
|
export type FormModeWithoutAutoSubmit =
|
|
12
12
|
| "onSubmit"
|
|
13
13
|
| "onBlur"
|
|
14
14
|
| "onChange"
|
|
15
|
-
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit?: false } }
|
|
15
|
+
| { readonly onChange: { readonly debounce: Duration.DurationInput; readonly autoSubmit?: false; }; };
|
|
16
16
|
|
|
17
17
|
export interface ParsedMode {
|
|
18
|
-
readonly validation: "onSubmit" | "onBlur" | "onChange"
|
|
19
|
-
readonly debounce: number | null
|
|
20
|
-
readonly autoSubmit: boolean
|
|
18
|
+
readonly validation: "onSubmit" | "onBlur" | "onChange";
|
|
19
|
+
readonly debounce: number | null;
|
|
20
|
+
readonly autoSubmit: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export const parse = (mode: FormMode = "onSubmit"): ParsedMode => {
|
|
24
24
|
if (typeof mode === "string") {
|
|
25
|
-
return { validation: mode, debounce: null, autoSubmit: false }
|
|
25
|
+
return { validation: mode, debounce: null, autoSubmit: false };
|
|
26
26
|
}
|
|
27
27
|
if ("onBlur" in mode) {
|
|
28
|
-
return { validation: "onBlur", debounce: null, autoSubmit: true }
|
|
28
|
+
return { validation: "onBlur", debounce: null, autoSubmit: true };
|
|
29
29
|
}
|
|
30
|
-
const debounceMs = Duration.toMillis(mode.onChange.debounce)
|
|
31
|
-
const autoSubmit = mode.onChange.autoSubmit === true
|
|
32
|
-
return { validation: "onChange", debounce: debounceMs, autoSubmit }
|
|
33
|
-
}
|
|
30
|
+
const debounceMs = Duration.toMillis(mode.onChange.debounce);
|
|
31
|
+
const autoSubmit = mode.onChange.autoSubmit === true;
|
|
32
|
+
return { validation: "onChange", debounce: debounceMs, autoSubmit };
|
|
33
|
+
};
|
package/src/Path.ts
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
const BRACKET_NOTATION_REGEX = /\[(\d+)\]/g
|
|
1
|
+
const BRACKET_NOTATION_REGEX = /\[(\d+)\]/g;
|
|
2
2
|
|
|
3
3
|
export const schemaPathToFieldPath = (path: ReadonlyArray<PropertyKey>): string => {
|
|
4
|
-
if (path.length === 0) return ""
|
|
4
|
+
if (path.length === 0) return "";
|
|
5
5
|
|
|
6
|
-
let result = String(path[0])
|
|
6
|
+
let result = String(path[0]);
|
|
7
7
|
for (let i = 1; i < path.length; i++) {
|
|
8
|
-
const segment = path[i]
|
|
8
|
+
const segment = path[i];
|
|
9
9
|
if (typeof segment === "number") {
|
|
10
|
-
result += `[${segment}]
|
|
10
|
+
result += `[${segment}]`;
|
|
11
11
|
} else {
|
|
12
|
-
result += `.${String(segment)}
|
|
12
|
+
result += `.${String(segment)}`;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
return result
|
|
16
|
-
}
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
17
|
|
|
18
18
|
export const isPathUnderRoot = (path: string, rootPath: string): boolean =>
|
|
19
|
-
path === rootPath || path.startsWith(rootPath + ".") || path.startsWith(rootPath + "[")
|
|
19
|
+
path === rootPath || path.startsWith(rootPath + ".") || path.startsWith(rootPath + "[");
|
|
20
20
|
|
|
21
21
|
export const isPathOrParentDirty = (dirtyFields: ReadonlySet<string>, path: string): boolean => {
|
|
22
|
-
if (dirtyFields.has(path)) return true
|
|
22
|
+
if (dirtyFields.has(path)) return true;
|
|
23
23
|
|
|
24
|
-
let parent = path
|
|
24
|
+
let parent = path;
|
|
25
25
|
while (true) {
|
|
26
|
-
const lastDot = parent.lastIndexOf(".")
|
|
27
|
-
const lastBracket = parent.lastIndexOf("[")
|
|
28
|
-
const splitIndex = Math.max(lastDot, lastBracket)
|
|
26
|
+
const lastDot = parent.lastIndexOf(".");
|
|
27
|
+
const lastBracket = parent.lastIndexOf("[");
|
|
28
|
+
const splitIndex = Math.max(lastDot, lastBracket);
|
|
29
29
|
|
|
30
|
-
if (splitIndex === -1) break
|
|
30
|
+
if (splitIndex === -1) break;
|
|
31
31
|
|
|
32
|
-
parent = parent.substring(0, splitIndex)
|
|
33
|
-
if (dirtyFields.has(parent)) return true
|
|
32
|
+
parent = parent.substring(0, splitIndex);
|
|
33
|
+
if (dirtyFields.has(parent)) return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
38
38
|
|
|
39
39
|
export const getNestedValue = (obj: unknown, path: string): unknown => {
|
|
40
|
-
if (path === "") return obj
|
|
41
|
-
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
|
|
42
|
-
let current: unknown = obj
|
|
40
|
+
if (path === "") return obj;
|
|
41
|
+
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".");
|
|
42
|
+
let current: unknown = obj;
|
|
43
43
|
for (const part of parts) {
|
|
44
|
-
if (current == null || typeof current !== "object") return undefined
|
|
45
|
-
current = (current as Record<string, unknown>)[part]
|
|
44
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
45
|
+
current = (current as Record<string, unknown>)[part];
|
|
46
46
|
}
|
|
47
|
-
return current
|
|
48
|
-
}
|
|
47
|
+
return current;
|
|
48
|
+
};
|
|
49
49
|
|
|
50
50
|
export const setNestedValue = <T>(obj: T, path: string, value: unknown): T => {
|
|
51
|
-
if (path === "") return value as T
|
|
52
|
-
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".")
|
|
53
|
-
const result = { ...obj } as Record<string, unknown
|
|
51
|
+
if (path === "") return value as T;
|
|
52
|
+
const parts = path.replace(BRACKET_NOTATION_REGEX, ".$1").split(".");
|
|
53
|
+
const result = { ...obj } as Record<string, unknown>;
|
|
54
54
|
|
|
55
|
-
let current = result
|
|
55
|
+
let current = result;
|
|
56
56
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
57
|
-
const part = parts[i]
|
|
57
|
+
const part = parts[i];
|
|
58
58
|
if (Array.isArray(current[part])) {
|
|
59
|
-
current[part] = [...(current[part] as Array<unknown>)]
|
|
59
|
+
current[part] = [...(current[part] as Array<unknown>)];
|
|
60
60
|
} else {
|
|
61
|
-
current[part] = { ...(current[part] as Record<string, unknown>) }
|
|
61
|
+
current[part] = { ...(current[part] as Record<string, unknown>) };
|
|
62
62
|
}
|
|
63
|
-
current = current[part] as Record<string, unknown
|
|
63
|
+
current = current[part] as Record<string, unknown>;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
current[parts[parts.length - 1]] = value
|
|
67
|
-
return result as T
|
|
68
|
-
}
|
|
66
|
+
current[parts[parts.length - 1]] = value;
|
|
67
|
+
return result as T;
|
|
68
|
+
};
|