@voila.dev/effect-form 0.27.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/Field.d.ts +30 -0
  4. package/dist/Field.d.ts.map +1 -0
  5. package/dist/Field.js +86 -0
  6. package/dist/Field.js.map +1 -0
  7. package/dist/FieldState.d.ts +21 -0
  8. package/dist/FieldState.d.ts.map +1 -0
  9. package/dist/FieldState.js +2 -0
  10. package/dist/FieldState.js.map +1 -0
  11. package/dist/FormAtoms.d.ts +108 -0
  12. package/dist/FormAtoms.d.ts.map +1 -0
  13. package/dist/FormAtoms.js +620 -0
  14. package/dist/FormAtoms.js.map +1 -0
  15. package/dist/FormBuilder.d.ts +74 -0
  16. package/dist/FormBuilder.d.ts.map +1 -0
  17. package/dist/FormBuilder.js +86 -0
  18. package/dist/FormBuilder.js.map +1 -0
  19. package/dist/Mode.d.ts +34 -0
  20. package/dist/Mode.d.ts.map +1 -0
  21. package/dist/Mode.js +19 -0
  22. package/dist/Mode.js.map +1 -0
  23. package/dist/Path.d.ts +6 -0
  24. package/dist/Path.d.ts.map +1 -0
  25. package/dist/Path.js +69 -0
  26. package/dist/Path.js.map +1 -0
  27. package/dist/Validation.d.ts +11 -0
  28. package/dist/Validation.d.ts.map +1 -0
  29. package/dist/Validation.js +128 -0
  30. package/dist/Validation.js.map +1 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/internal/dirty.d.ts +3 -0
  36. package/dist/internal/dirty.d.ts.map +1 -0
  37. package/dist/internal/dirty.js +89 -0
  38. package/dist/internal/dirty.js.map +1 -0
  39. package/package.json +43 -0
  40. package/src/Field.ts +149 -0
  41. package/src/FieldState.ts +23 -0
  42. package/src/FormAtoms.ts +1202 -0
  43. package/src/FormBuilder.ts +235 -0
  44. package/src/Mode.ts +63 -0
  45. package/src/Path.ts +76 -0
  46. package/src/Validation.ts +167 -0
  47. package/src/index.ts +13 -0
  48. package/src/internal/dirty.ts +113 -0
@@ -0,0 +1,89 @@
1
+ import * as Equal from "effect/Equal";
2
+ import { getNestedValue, isPathUnderRoot } from "../Path.js";
3
+ export const recalculateDirtyFieldsForArray = (dirtyFields, initialValues, arrayPath, newItems) => {
4
+ const initialItems = (getNestedValue(initialValues, arrayPath) ??
5
+ []);
6
+ if (newItems === initialItems) {
7
+ return dirtyFields;
8
+ }
9
+ const nextDirty = new Set(Array.from(dirtyFields).filter((path) => !isPathUnderRoot(path, arrayPath)));
10
+ const loopLength = Math.max(newItems.length, initialItems.length);
11
+ for (let i = 0; i < loopLength; i++) {
12
+ const itemPath = `${arrayPath}[${i}]`;
13
+ const newItem = newItems[i];
14
+ const initialItem = initialItems[i];
15
+ if (newItem === initialItem)
16
+ continue;
17
+ if (!Equal.equals(newItem, initialItem)) {
18
+ nextDirty.add(itemPath);
19
+ }
20
+ }
21
+ if (newItems.length !== initialItems.length) {
22
+ nextDirty.add(arrayPath);
23
+ }
24
+ else {
25
+ nextDirty.delete(arrayPath);
26
+ }
27
+ return nextDirty;
28
+ };
29
+ export const recalculateDirtySubtree = (currentDirty, allInitial, allValues, rootPath = "") => {
30
+ const targetValue = rootPath
31
+ ? getNestedValue(allValues, rootPath)
32
+ : allValues;
33
+ const targetInitial = rootPath
34
+ ? getNestedValue(allInitial, rootPath)
35
+ : allInitial;
36
+ if (targetValue === targetInitial) {
37
+ if (rootPath === "") {
38
+ return new Set();
39
+ }
40
+ let changed = false;
41
+ const nextDirty = new Set(currentDirty);
42
+ for (const path of currentDirty) {
43
+ if (isPathUnderRoot(path, rootPath)) {
44
+ nextDirty.delete(path);
45
+ changed = true;
46
+ }
47
+ }
48
+ return changed ? nextDirty : currentDirty;
49
+ }
50
+ const nextDirty = new Set(currentDirty);
51
+ if (rootPath === "") {
52
+ nextDirty.clear();
53
+ }
54
+ else {
55
+ for (const path of nextDirty) {
56
+ if (isPathUnderRoot(path, rootPath)) {
57
+ nextDirty.delete(path);
58
+ }
59
+ }
60
+ }
61
+ const recurse = (current, initial, path) => {
62
+ if (current === initial)
63
+ return;
64
+ if (Array.isArray(current)) {
65
+ const initialArr = (initial ?? []);
66
+ for (let i = 0; i < Math.max(current.length, initialArr.length); i++) {
67
+ recurse(current[i], initialArr[i], path ? `${path}[${i}]` : `[${i}]`);
68
+ }
69
+ }
70
+ else if (current !== null && typeof current === "object") {
71
+ const initialObj = (initial ?? {});
72
+ for (const key in current) {
73
+ recurse(current[key], initialObj[key], path ? `${path}.${key}` : key);
74
+ }
75
+ for (const key in initialObj) {
76
+ if (!(key in current)) {
77
+ recurse(undefined, initialObj[key], path ? `${path}.${key}` : key);
78
+ }
79
+ }
80
+ }
81
+ else {
82
+ if (!Equal.equals(current, initial) && path)
83
+ nextDirty.add(path);
84
+ }
85
+ };
86
+ recurse(targetValue, targetInitial, rootPath);
87
+ return nextDirty;
88
+ };
89
+ //# sourceMappingURL=dirty.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dirty.js","sourceRoot":"","sources":["../../src/internal/dirty.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@voila.dev/effect-form",
3
+ "version": "0.27.0",
4
+ "description": "Type-safe form builder powered by Effect Schema",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "exports": {
10
+ "./package.json": "./package.json",
11
+ ".": "./dist/index.js",
12
+ "./*": "./dist/*.js",
13
+ "./internal/*": null
14
+ },
15
+ "files": [
16
+ "src/**/*.ts",
17
+ "dist/**/*.js",
18
+ "dist/**/*.js.map",
19
+ "dist/**/*.d.ts",
20
+ "dist/**/*.d.ts.map"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/voila-voila-dev/effect-form.git"
25
+ },
26
+ "homepage": "https://github.com/voila-voila-dev/effect-form",
27
+ "keywords": [
28
+ "effect",
29
+ "form",
30
+ "schema",
31
+ "validation",
32
+ "typescript"
33
+ ],
34
+ "author": "Lucas Barake",
35
+ "license": "MIT",
36
+ "sideEffects": [],
37
+ "peerDependencies": {
38
+ "effect": ">=4.0.0-rc.112 <4.0.1"
39
+ },
40
+ "devDependencies": {
41
+ "effect": "4.0.0-rc.112"
42
+ }
43
+ }
package/src/Field.ts ADDED
@@ -0,0 +1,149 @@
1
+ import * as Schema from "effect/Schema";
2
+ import * as AST from "effect/SchemaAST";
3
+
4
+ export const TypeId: unique symbol = Symbol.for("@voila.dev/effect-form/Field");
5
+
6
+ export type TypeId = typeof TypeId;
7
+
8
+ export interface FieldDef<K extends string, S extends Schema.Top> {
9
+ readonly _tag: "field";
10
+ readonly key: K;
11
+ readonly schema: S;
12
+ }
13
+
14
+ export interface ArrayFieldDef<K extends string, S extends Schema.Top> {
15
+ readonly _tag: "array";
16
+ readonly key: K;
17
+ readonly itemSchema: S;
18
+ }
19
+
20
+ export type AnyFieldDef =
21
+ | FieldDef<string, Schema.Top>
22
+ | ArrayFieldDef<string, Schema.Top>;
23
+
24
+ export type FieldsRecord = Record<string, AnyFieldDef>;
25
+
26
+ export const isArrayFieldDef = (
27
+ def: AnyFieldDef,
28
+ ): def is ArrayFieldDef<string, Schema.Top> => def._tag === "array";
29
+
30
+ export const isFieldDef = (
31
+ def: AnyFieldDef,
32
+ ): def is FieldDef<string, Schema.Top> => def._tag === "field";
33
+
34
+ export const makeField = <K extends string, S extends Schema.Top>(
35
+ key: K,
36
+ schema: S,
37
+ ): FieldDef<K, S> => ({
38
+ _tag: "field",
39
+ key,
40
+ schema,
41
+ });
42
+
43
+ export const makeArrayField = <K extends string, S extends Schema.Top>(
44
+ key: K,
45
+ itemSchema: S,
46
+ ): ArrayFieldDef<K, S> => ({
47
+ _tag: "array",
48
+ key,
49
+ itemSchema,
50
+ });
51
+
52
+ export type EncodedFromFields<T extends FieldsRecord> = {
53
+ readonly [K in keyof T]: T[K] extends FieldDef<any, infer S>
54
+ ? Schema.Codec.Encoded<S>
55
+ : T[K] extends ArrayFieldDef<any, infer S>
56
+ ? ReadonlyArray<Schema.Codec.Encoded<S>>
57
+ : never;
58
+ };
59
+
60
+ export type DecodedFromFields<T extends FieldsRecord> = {
61
+ readonly [K in keyof T]: T[K] extends FieldDef<any, infer S>
62
+ ? Schema.Schema.Type<S>
63
+ : T[K] extends ArrayFieldDef<any, infer S>
64
+ ? ReadonlyArray<Schema.Schema.Type<S>>
65
+ : never;
66
+ };
67
+
68
+ export const getDefaultFromSchema = (schema: Schema.Top): unknown => {
69
+ const ast = AST.toEncoded(schema.ast);
70
+ switch (ast._tag) {
71
+ case "String":
72
+ case "TemplateLiteral":
73
+ return "";
74
+ case "Number":
75
+ return 0;
76
+ case "Boolean":
77
+ return false;
78
+ case "Arrays":
79
+ return [];
80
+ case "Literal":
81
+ return ast.literal;
82
+ case "Enum": {
83
+ const first = ast.enums[0];
84
+ return first ? first[1] : undefined;
85
+ }
86
+ case "Objects": {
87
+ const result: Record<string, unknown> = {};
88
+ for (const prop of ast.propertySignatures) {
89
+ result[prop.name as string] = getDefaultFromSchema(
90
+ Schema.make(prop.type),
91
+ );
92
+ }
93
+ return result;
94
+ }
95
+ case "Union": {
96
+ const first = ast.types[0];
97
+ return first ? getDefaultFromSchema(Schema.make(first)) : undefined;
98
+ }
99
+ case "Never":
100
+ return undefined;
101
+ case "Suspend":
102
+ return getDefaultFromSchema(Schema.make(ast.thunk()));
103
+ default:
104
+ return "";
105
+ }
106
+ };
107
+
108
+ export const getDefaultEncodedValues = (
109
+ fields: FieldsRecord,
110
+ ): Record<string, unknown> => {
111
+ const result: Record<string, unknown> = {};
112
+ for (const [key, def] of Object.entries(fields)) {
113
+ if (isArrayFieldDef(def)) {
114
+ result[key] = [];
115
+ } else {
116
+ result[key] = "";
117
+ }
118
+ }
119
+ return result;
120
+ };
121
+
122
+ export const createTouchedRecord = (
123
+ fields: FieldsRecord,
124
+ value: boolean,
125
+ ): Record<string, boolean> => {
126
+ const result: Record<string, boolean> = {};
127
+ for (const key of Object.keys(fields)) {
128
+ result[key] = value;
129
+ }
130
+ return result;
131
+ };
132
+
133
+ export const extractStructFieldDefs = (
134
+ schema: Schema.Top,
135
+ ): ReadonlyArray<FieldDef<string, Schema.Top>> | undefined => {
136
+ const unwrapObjects = (ast: AST.AST): AST.Objects | undefined => {
137
+ const base = AST.toType(ast);
138
+ if (AST.isObjects(base)) return base;
139
+ if (AST.isSuspend(base)) return unwrapObjects(base.thunk());
140
+ return undefined;
141
+ };
142
+
143
+ const objects = unwrapObjects(schema.ast);
144
+ if (!objects) return undefined;
145
+
146
+ return objects.propertySignatures.map((prop) =>
147
+ makeField(prop.name as string, { ast: prop.type } as Schema.Top),
148
+ );
149
+ };
@@ -0,0 +1,23 @@
1
+ import type * as Option from "effect/Option";
2
+ import type * as Schema from "effect/Schema";
3
+
4
+ export type FieldValue<T> = T extends Schema.Top ? Schema.Codec.Encoded<T> : T;
5
+
6
+ export interface FieldState<E> {
7
+ readonly path: string;
8
+ readonly value: E;
9
+ readonly onChange: (value: E) => void;
10
+ readonly onBlur: () => void;
11
+ readonly error: Option.Option<string>;
12
+ readonly isTouched: boolean;
13
+ readonly isValidating: boolean;
14
+ readonly isDirty: boolean;
15
+ }
16
+
17
+ export interface ArrayFieldOperations<TItem> {
18
+ readonly items: ReadonlyArray<TItem>;
19
+ readonly append: (value?: TItem) => void;
20
+ readonly remove: (index: number) => void;
21
+ readonly swap: (indexA: number, indexB: number) => void;
22
+ readonly move: (from: number, to: number) => void;
23
+ }