@goodie-forms/core 1.2.5-alpha → 1.2.6-alpha

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 (43) hide show
  1. package/dist/field/FieldPath.d.ts +18 -9
  2. package/dist/field/FieldPath.d.ts.map +1 -1
  3. package/dist/field/FieldPathBuilder.d.ts +2 -2
  4. package/dist/field/FieldPathBuilder.d.ts.map +1 -1
  5. package/dist/field/Reconcile.d.ts +2 -2
  6. package/dist/field/Reconcile.d.ts.map +1 -1
  7. package/dist/form/FormController.d.ts +51 -20
  8. package/dist/form/FormController.d.ts.map +1 -1
  9. package/dist/form/FormField.d.ts +15 -10
  10. package/dist/form/FormField.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +523 -476
  14. package/dist/index.js.map +1 -1
  15. package/dist/types/DeepHelpers.d.ts +11 -0
  16. package/dist/types/DeepHelpers.d.ts.map +1 -0
  17. package/dist/types/FormHelpers.d.ts +5 -0
  18. package/dist/types/FormHelpers.d.ts.map +1 -0
  19. package/dist/types/Suppliable.d.ts +3 -0
  20. package/dist/types/Suppliable.d.ts.map +1 -0
  21. package/dist/validation/CustomValidation.d.ts +1 -1
  22. package/package.json +13 -3
  23. package/src/field/FieldPath.spec.ts +204 -0
  24. package/src/field/FieldPath.ts +62 -59
  25. package/src/field/FieldPathBuilder.spec.ts +47 -0
  26. package/src/field/FieldPathBuilder.ts +15 -81
  27. package/src/field/Reconcile.ts +2 -2
  28. package/src/form/FormController.spec.ts +55 -0
  29. package/src/form/FormController.ts +151 -115
  30. package/src/form/FormField.ts +63 -30
  31. package/src/index.ts +2 -2
  32. package/src/types/DeepHelpers.ts +15 -0
  33. package/src/types/FormHelpers.ts +13 -0
  34. package/src/types/Suppliable.ts +7 -0
  35. package/src/validation/CustomValidation.ts +1 -1
  36. package/dist/form/NonullFormField.d.ts +0 -9
  37. package/dist/form/NonullFormField.d.ts.map +0 -1
  38. package/dist/types/DeepPartial.d.ts +0 -6
  39. package/dist/types/DeepPartial.d.ts.map +0 -1
  40. package/src/form/NonullFormField.ts +0 -15
  41. package/src/types/DeepPartial.ts +0 -7
  42. package/tsconfig.json +0 -8
  43. package/vite.config.ts +0 -18
@@ -0,0 +1,11 @@
1
+ export type DeepPartial<T> = {
2
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
3
+ } & {
4
+ [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
5
+ };
6
+ export type DeepReadonly<T> = {
7
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
8
+ } & {
9
+ readonly [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
10
+ };
11
+ //# sourceMappingURL=DeepHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeepHelpers.d.ts","sourceRoot":"","sources":["../../src/types/DeepHelpers.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;KAC1B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GACjD,KAAK,GACL,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;KAC3B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAC1D,KAAK,GACL,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { FormField } from 'packages/core/src/form/FormField';
2
+ export type FieldValue<T extends FormField<any, any>> = NonNullable<T> extends {
3
+ value: infer V;
4
+ } ? NonNullable<V> : never;
5
+ //# sourceMappingURL=FormHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormHelpers.d.ts","sourceRoot":"","sources":["../../src/types/FormHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAG7D,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,IAClD,WAAW,CAAC,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC"}
@@ -0,0 +1,3 @@
1
+ export type Suppliable<T> = T | (() => T);
2
+ export declare function supply<T>(suppliable: T | (() => T)): T;
3
+ //# sourceMappingURL=Suppliable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Suppliable.d.ts","sourceRoot":"","sources":["../../src/types/Suppliable.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1C,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAItD"}
@@ -1,5 +1,5 @@
1
1
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
- import { DeepPartial } from '../types/DeepPartial';
2
+ import { DeepPartial } from '../types/DeepHelpers';
3
3
  import { FieldPath } from '../field/FieldPath';
4
4
  export type CustomValidationIssue<TOutput extends object> = {
5
5
  path: FieldPath.StringPaths<TOutput>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodie-forms/core",
3
- "version": "1.2.5-alpha",
3
+ "version": "1.2.6-alpha",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,13 +10,20 @@
10
10
  "default": "./dist/index.js"
11
11
  }
12
12
  },
13
+ "files": [
14
+ "src",
15
+ "dist"
16
+ ],
13
17
  "publishConfig": {
14
18
  "access": "public"
15
19
  },
16
20
  "devDependencies": {
21
+ "@vitest/coverage-v8": "^4.0.18",
22
+ "type-testing": "^0.2.0",
17
23
  "typescript": "^5.9.3",
18
24
  "vite": "^7.3.1",
19
- "vite-plugin-dts": "^4.5.4"
25
+ "vite-plugin-dts": "^4.5.4",
26
+ "vitest": "^4.0.18"
20
27
  },
21
28
  "dependencies": {
22
29
  "@standard-schema/spec": "^1.1.0",
@@ -24,6 +31,9 @@
24
31
  "nanoevents": "^9.1.0"
25
32
  },
26
33
  "scripts": {
27
- "build": "vite build"
34
+ "build": "vite build",
35
+ "coverage": "vitest run --coverage",
36
+ "test": "tsc --noEmit && vitest run",
37
+ "test:watch": "vitest"
28
38
  }
29
39
  }
@@ -0,0 +1,204 @@
1
+ import { Equal, Expect } from "type-testing";
2
+ import { expect, test } from "vitest";
3
+ import { FieldPath } from "../field/FieldPath";
4
+
5
+ interface Person {
6
+ name: string;
7
+ surname: string;
8
+ address?: {
9
+ city: string;
10
+ street: string;
11
+ };
12
+ friends?: string[];
13
+ relations?: {
14
+ name: string;
15
+ points: number;
16
+ metAddress?: {
17
+ city: string;
18
+ street: string;
19
+ };
20
+ }[];
21
+ }
22
+
23
+ const person: Person = {
24
+ name: "John",
25
+ surname: "Doe",
26
+ address: {
27
+ city: "Arkham City",
28
+ street: "Sesame Street",
29
+ },
30
+ friends: ["Alice", "Bob"],
31
+ relations: [
32
+ {
33
+ name: "Alice",
34
+ points: 99,
35
+ metAddress: {
36
+ city: "Arkham City",
37
+ street: "Sesame Street",
38
+ },
39
+ },
40
+ {
41
+ name: "Bob",
42
+ points: 0,
43
+ },
44
+ ],
45
+ };
46
+
47
+ /* ------------------------------ */
48
+
49
+ test("resolves field types by path", () => {
50
+ type Shape = {
51
+ user?: {
52
+ profile?: { name?: string };
53
+ tags?: boolean[];
54
+ };
55
+ };
56
+
57
+ type typeTests = [
58
+ Expect<
59
+ Equal<
60
+ FieldPath.Resolve<Shape, ["user"]>,
61
+ { profile?: { name?: string }; tags?: boolean[] } | undefined
62
+ >
63
+ >,
64
+
65
+ Expect<
66
+ Equal<
67
+ FieldPath.Resolve<Shape, ["user", "profile"]>,
68
+ { name?: string } | undefined
69
+ >
70
+ >,
71
+
72
+ Expect<
73
+ Equal<
74
+ FieldPath.Resolve<Shape, ["user", "profile", "name"]>,
75
+ string | undefined
76
+ >
77
+ >,
78
+
79
+ Expect<
80
+ Equal<
81
+ FieldPath.Resolve<Shape, ["user", "tags", number]>,
82
+ //
83
+ boolean
84
+ >
85
+ >,
86
+
87
+ Expect<
88
+ Equal<
89
+ FieldPath.Resolve<Shape, ["user", "tags", 0]>,
90
+ //
91
+ boolean
92
+ >
93
+ >,
94
+
95
+ Expect<
96
+ Equal<
97
+ FieldPath.Resolve<Shape, ["user", "missing"]>,
98
+ //
99
+ never
100
+ >
101
+ >,
102
+ ];
103
+ });
104
+
105
+ test("parses string paths", () => {
106
+ type PersonPaths = FieldPath.StringPaths<Person>;
107
+
108
+ const path1 = FieldPath.fromStringPath("a.b");
109
+ expect(path1).toStrictEqual(["a", "b"]);
110
+
111
+ const path2 = FieldPath.fromStringPath("a.b.c");
112
+ expect(path2).toStrictEqual(["a", "b", "c"]);
113
+
114
+ const path3 = FieldPath.fromStringPath("a[99].b.c[3]");
115
+ expect(path3).toStrictEqual(["a", 99, "b", "c", 3]);
116
+
117
+ type typeTests = [
118
+ Expect<
119
+ Equal<
120
+ PersonPaths,
121
+ | "name"
122
+ | "surname"
123
+ | "address"
124
+ | "friends"
125
+ | "relations"
126
+ | "address.city"
127
+ | "address.street"
128
+ | "friends[0]"
129
+ | FieldPath._Unfoldable<`friends[${number}]`>
130
+ | "relations[0]"
131
+ | FieldPath._Unfoldable<`relations[${number}]`>
132
+ | "relations[0].metAddress"
133
+ | FieldPath._Unfoldable<`relations[${number}].metAddress`>
134
+ | "relations[0].name"
135
+ | FieldPath._Unfoldable<`relations[${number}].name`>
136
+ | "relations[0].points"
137
+ | FieldPath._Unfoldable<`relations[${number}].points`>
138
+ | "relations[0].metAddress.city"
139
+ | FieldPath._Unfoldable<`relations[${number}].metAddress.city`>
140
+ | "relations[0].metAddress.street"
141
+ | FieldPath._Unfoldable<`relations[${number}].metAddress.street`>
142
+ >
143
+ >,
144
+ ];
145
+ });
146
+
147
+ test("gets fields properly", () => {
148
+ const getFieldValue = (stringPath: FieldPath.StringPaths<Person>) =>
149
+ FieldPath.getValue(person, FieldPath.fromStringPath(stringPath));
150
+
151
+ expect(getFieldValue("name")).toBe(person.name);
152
+ expect(getFieldValue("surname")).toBe(person.surname);
153
+ expect(getFieldValue("address")).toBe(person.address);
154
+ expect(getFieldValue("address.city")).toBe(person.address?.city);
155
+ expect(getFieldValue("address.street")).toBe(person.address?.street);
156
+ expect(getFieldValue("friends")).toBe(person.friends);
157
+ expect(getFieldValue("friends[0]")).toBe(person.friends?.[0]);
158
+ expect(getFieldValue("friends[1]")).toBe(person.friends?.[1]);
159
+ expect(getFieldValue("relations")).toBe(person.relations);
160
+ expect(getFieldValue("relations[0]")).toBe(person.relations?.[0]);
161
+ expect(getFieldValue("relations[1]")).toBe(person.relations?.[1]);
162
+ expect(getFieldValue("relations[1].metAddress.city")).toBe(
163
+ person.relations?.[1].metAddress?.city,
164
+ );
165
+ });
166
+
167
+ test("sets fields properly", () => {
168
+ const obj = {
169
+ a: 1,
170
+ b: 2,
171
+ c: {
172
+ d: 3,
173
+ e: [4, 5],
174
+ },
175
+ };
176
+
177
+ type ObjPaths = FieldPath.StringPaths<typeof obj>;
178
+
179
+ const getPath = <TPath extends ObjPaths>(stringPath: TPath) => {
180
+ return FieldPath.fromStringPath(stringPath);
181
+ };
182
+
183
+ const setFieldValue = <TPath extends FieldPath.Segments>(
184
+ path: TPath,
185
+ value: FieldPath.Resolve<typeof obj, TPath>,
186
+ ) => {
187
+ FieldPath.setValue(obj, path, value);
188
+ };
189
+
190
+ setFieldValue(getPath("a"), 99);
191
+ setFieldValue(getPath("b"), 99);
192
+ setFieldValue(getPath("c.d"), 99);
193
+ setFieldValue(getPath("c.e"), [99, 99]);
194
+ setFieldValue(getPath("c.e[2]"), 99);
195
+
196
+ expect(obj).toMatchObject({
197
+ a: 99,
198
+ b: 99,
199
+ c: {
200
+ d: 99,
201
+ e: [99, 99, 99],
202
+ },
203
+ });
204
+ });
@@ -1,12 +1,7 @@
1
1
  export namespace FieldPath {
2
2
  export type Segments = readonly PropertyKey[];
3
- export type CanonicalPath = string;
4
3
  export type StringPath = string;
5
4
 
6
- export function toCanonicalPath(path: Segments) {
7
- return path.join(".") as CanonicalPath;
8
- }
9
-
10
5
  export function toStringPath(path: Segments) {
11
6
  const normalizedPath = normalize(path);
12
7
  let result = "";
@@ -112,15 +107,13 @@ export namespace FieldPath {
112
107
  return true;
113
108
  }
114
109
 
115
- type Unfoldable<T> = T & { _____foldMark?: never } & {};
110
+ /** @internal used to mark and prevent array types, so IntellSense can suggest and accept array indices */
111
+ export type _Unfoldable<T> = T & { _____foldMark?: never } & {};
116
112
 
117
- export type Resolve<
118
- TObject,
119
- TPath extends readonly PropertyKey[],
120
- > = TPath extends []
113
+ export type Resolve<TObject, TPath extends Segments> = TPath extends []
121
114
  ? TObject
122
115
  : TPath extends readonly [infer Prop, ...infer Rest]
123
- ? Rest extends readonly PropertyKey[]
116
+ ? Rest extends Segments
124
117
  ? Resolve<ResolveStep<TObject, Prop>, Rest>
125
118
  : never
126
119
  : never;
@@ -150,7 +143,7 @@ export namespace FieldPath {
150
143
  TCanonicalStringPaths extends `${string}[*]${string}`
151
144
  ?
152
145
  | ReplaceAll<TCanonicalStringPaths, "[*]", "[0]">
153
- | Unfoldable<ReplaceAll<TCanonicalStringPaths, "[*]", `[${number}]`>>
146
+ | _Unfoldable<ReplaceAll<TCanonicalStringPaths, "[*]", `[${number}]`>>
154
147
  : TCanonicalStringPaths;
155
148
 
156
149
  type CanonicalStringPaths<TObject extends object> = {
@@ -210,9 +203,47 @@ export namespace FieldPath {
210
203
  ? K
211
204
  : never;
212
205
 
206
+ export function walkPath<
207
+ TObject extends object,
208
+ const TPath extends FieldPath.Segments,
209
+ >(object: TObject, path: TPath): { target: any; key: PropertyKey };
210
+ export function walkPath<
211
+ TObject extends object,
212
+ const TPath extends FieldPath.Segments,
213
+ >(
214
+ object: TObject,
215
+ path: TPath,
216
+ opts: { returnOnEmptyBranch: true },
217
+ ): { target: any; key: PropertyKey | null };
218
+ export function walkPath<
219
+ TObject extends object,
220
+ const TPath extends FieldPath.Segments,
221
+ >(object: TObject, path: TPath, opts?: { returnOnEmptyBranch?: boolean }) {
222
+ let current: any = object;
223
+
224
+ for (let i = 0; i < path.length - 1; i++) {
225
+ const pathFragment = path[i];
226
+ const nextFragment = path[i + 1];
227
+
228
+ if (current[pathFragment] == null) {
229
+ if (opts?.returnOnEmptyBranch) return { target: null, key: null };
230
+ current[pathFragment] = typeof nextFragment === "number" ? [] : {};
231
+ }
232
+
233
+ current = current[pathFragment];
234
+ }
235
+
236
+ const lastFragment = path[path.length - 1];
237
+
238
+ return {
239
+ target: current,
240
+ key: lastFragment,
241
+ };
242
+ }
243
+
213
244
  export function getValue<
214
245
  TObject extends object,
215
- const TPath extends readonly PropertyKey[],
246
+ const TPath extends Segments,
216
247
  >(
217
248
  object: TObject,
218
249
  path: TPath,
@@ -229,64 +260,36 @@ export namespace FieldPath {
229
260
 
230
261
  export function setValue<
231
262
  TObject extends object,
232
- const TPath extends readonly PropertyKey[],
233
- >(object: TObject, key: TPath, value: FieldPath.Resolve<TObject, TPath>) {
234
- return FieldPath.modifyValue(object, key, () => value);
263
+ const TPath extends Segments,
264
+ >(object: TObject, path: TPath, value: FieldPath.Resolve<TObject, TPath>) {
265
+ const { target, key } = walkPath(object, path);
266
+ target[key] = value;
235
267
  }
236
268
 
237
269
  export function modifyValue<
238
270
  TObject extends object,
239
- const TPath extends readonly PropertyKey[],
271
+ const TPath extends Segments,
240
272
  >(
241
273
  object: TObject,
242
274
  path: TPath,
243
275
  modifier: (
244
- currentValue: FieldPath.Resolve<TObject, TPath>,
245
- ) => FieldPath.Resolve<TObject, TPath> | void,
276
+ currentValue: FieldPath.Resolve<TObject, TPath> | undefined,
277
+ ) => void,
246
278
  ) {
247
- let current: any = object;
248
-
249
- for (let i = 0; i < path.length - 1; i++) {
250
- const pathFragment = path[i];
251
- const nextFragment = path[i + 1];
252
-
253
- if (current[pathFragment] == null) {
254
- current[pathFragment] = typeof nextFragment === "number" ? [] : {};
255
- }
256
-
257
- current = current[pathFragment];
258
- }
259
-
260
- const lastFragment = path[path.length - 1];
261
-
262
- const oldValue = current[lastFragment];
263
- const newValue = modifier(oldValue);
264
-
265
- if (newValue !== undefined) {
266
- current[lastFragment] = newValue;
267
- }
279
+ const { target, key } = walkPath(object, path);
280
+ modifier(target[key]);
268
281
  }
269
282
 
270
- export function deleteValue<
271
- TObject extends object,
272
- TPath extends readonly PropertyKey[],
273
- >(object: TObject, path: TPath) {
274
- let current: any = object;
275
-
276
- for (let i = 0; i < path.length - 1; i++) {
277
- const pathFragment = path[i];
278
-
279
- if (current[pathFragment] == null) return;
280
-
281
- current = current[pathFragment];
282
- }
283
+ export function deleteValue<TObject extends object, TPath extends Segments>(
284
+ object: TObject,
285
+ path: TPath,
286
+ ) {
287
+ const { target, key } = walkPath(object, path, {
288
+ returnOnEmptyBranch: true,
289
+ });
283
290
 
284
- const lastFragment = path[path.length - 1];
291
+ if (key == null) return;
285
292
 
286
- delete current[lastFragment];
293
+ delete target[key];
287
294
  }
288
295
  }
289
-
290
- // const x = {} as { foo: { bar: string[] } };
291
- // FieldPath.setValue(x, FieldPath.fromStringPath("foo.bar[9]"), "C");
292
- // console.log(x); // <-- { foo: { bar: [<9xempty>, "C"] } }
@@ -0,0 +1,47 @@
1
+ import { expect, test } from "vitest";
2
+ import { FieldPath } from "../field/FieldPath";
3
+ import { FieldPathBuilder } from "../field/FieldPathBuilder";
4
+
5
+ interface User {
6
+ name: string;
7
+ address: {
8
+ city: string;
9
+ street: string;
10
+ };
11
+ friends: {
12
+ name: string;
13
+ tags: string[];
14
+ }[];
15
+ coords: [100, 200];
16
+ }
17
+
18
+ const builder = new FieldPathBuilder<User>();
19
+
20
+ const data: User = {
21
+ name: "",
22
+ address: { city: "", street: "" },
23
+ friends: [{ name: "", tags: ["A", "B"] }],
24
+ coords: [100, 200] as const,
25
+ };
26
+
27
+ test("builds path segments from proxy and string paths", () => {
28
+ const path1 = builder.of((data) => data.friends[0].tags[1]);
29
+ const value1 = FieldPath.getValue(data, path1);
30
+ expect(path1).toStrictEqual(["friends", 0, "tags", 1]);
31
+ expect(value1).toStrictEqual(data.friends[0].tags[1]);
32
+
33
+ const path2 = builder.of("friends[0].tags[0]");
34
+ const value2 = FieldPath.getValue(data, path2);
35
+ expect(path2).toStrictEqual(["friends", 0, "tags", 0]);
36
+ expect(value2).toStrictEqual(data.friends[0].tags[0]);
37
+
38
+ const path3 = builder.of("friends[0].tags");
39
+ const value3 = FieldPath.getValue(data, path3);
40
+ expect(path3).toStrictEqual(["friends", 0, "tags"]);
41
+ expect(value3).toStrictEqual(data.friends[0].tags);
42
+
43
+ const path4 = builder.of("coords[1]");
44
+ const value4 = FieldPath.getValue(data, path4);
45
+ expect(path4).toStrictEqual(["coords", 1]);
46
+ expect(value4).toStrictEqual(data.coords[1]);
47
+ });
@@ -44,89 +44,23 @@ export class FieldPathBuilder<TOutput extends object> {
44
44
  }) as any;
45
45
  }
46
46
 
47
- public fromStringPath<TStrPath extends FieldPath.StringPaths<TOutput>>(
47
+ public of<TStrPath extends FieldPath.StringPaths<TOutput>>(
48
48
  stringPath: TStrPath,
49
- ) {
50
- return FieldPath.fromStringPath(
51
- stringPath,
52
- ) as unknown as FieldPath.ParseStringPath<TStrPath>;
53
- }
49
+ ): FieldPath.ParseStringPath<TStrPath>;
54
50
 
55
- public fromProxy<TProxy extends FieldPathBuilder.Proxy<any, any>>(
51
+ public of<TProxy extends FieldPathBuilder.Proxy<any, any>>(
56
52
  consumer: (data: FieldPathBuilder.Proxy<TOutput, []>) => TProxy,
57
- ): TProxy[typeof resolverCall] {
58
- return consumer(FieldPathBuilder.wrap<TOutput, []>([]))[resolverCall];
59
- }
60
- }
61
-
62
- /* ---- TESTS ---------------- */
63
-
64
- // interface User {
65
- // name: string;
66
- // address: {
67
- // city: string;
68
- // street: string;
69
- // };
70
- // friends: {
71
- // name: string;
72
- // tags: string[];
73
- // }[];
74
- // coords: [100, 200];
75
- // }
76
-
77
- // const builder = new FieldPathBuilder<User>();
78
-
79
- // const data: User = {
80
- // name: "",
81
- // address: { city: "", street: "" },
82
- // friends: [{ name: "", tags: ["A", "B"] }],
83
- // coords: [100, 200] as const,
84
- // };
85
-
86
- // const path = builder.fromProxy((data) => data.friends[0].tags[1]);
87
- // // ^?
88
- // const value = FieldPath.getValue(data, path);
89
- // // ^?
90
- // console.log(path, "=", value);
91
-
92
- // const path2 = builder.fromStringPath("friends[0].tags[0]");
93
- // // ^?
94
- // const value2 = FieldPath.getValue(data, path2);
95
- // // ^?
96
- // console.log(path2, "=", value2);
53
+ ): TProxy[typeof resolverCall];
97
54
 
98
- // const path3 = builder.fromStringPath("coords[0]");
99
- // // ^?
100
- // const value3 = FieldPath.getValue(data, path3);
101
- // console.log(path3, "=", value3);
102
-
103
- // const path4 = builder.fromStringPath("coords[1]");
104
- // // ^?
105
- // const value4 = FieldPath.getValue(data, path4);
106
- // // ^?
107
- // console.log(path4, "=", value4);
108
-
109
- // type Shape = {
110
- // user?: {
111
- // profile?: { name?: string };
112
- // tags?: boolean[];
113
- // };
114
- // };
115
-
116
- // type A = FieldPath.Resolve<Shape, ["user"]>;
117
- // // ^?
118
-
119
- // type A2 = FieldPath.Resolve<Shape, ["user", "profile"]>;
120
- // // ^?
121
-
122
- // type B = FieldPath.Resolve<Shape, ["user", "profile", "name"]>;
123
- // // ^?
124
-
125
- // type C = FieldPath.Resolve<Shape, ["user", "tags", number]>;
126
- // // ^?
127
-
128
- // type C2 = FieldPath.Resolve<Shape, ["user", "tags", 0]>;
129
- // // ^?
55
+ public of(
56
+ arg:
57
+ | FieldPath.StringPaths<TOutput>
58
+ | ((data: FieldPathBuilder.Proxy<TOutput, []>) => any),
59
+ ) {
60
+ if (typeof arg === "function") {
61
+ return arg(FieldPathBuilder.wrap<TOutput, []>([]))[resolverCall];
62
+ }
130
63
 
131
- // type D = FieldPath.Resolve<Shape, ["user", "missing"]>;
132
- // // ^?
64
+ return FieldPath.fromStringPath(arg as any);
65
+ }
66
+ }
@@ -1,4 +1,4 @@
1
- export namespace Reconsile {
1
+ export namespace Reconcile {
2
2
  export function deepEqual(
3
3
  a: any,
4
4
  b: any,
@@ -70,7 +70,7 @@ export namespace Reconsile {
70
70
  return true;
71
71
  }
72
72
 
73
- export function diff<T>(
73
+ export function arrayDiff<T>(
74
74
  prev: readonly T[],
75
75
  next: readonly T[],
76
76
  equals: (a: T, b: T) => boolean,
@@ -0,0 +1,55 @@
1
+ import { expect, test } from "vitest";
2
+ import { FormController } from "../form/FormController";
3
+
4
+ interface User {
5
+ name: string;
6
+ address: {
7
+ city: string;
8
+ street: string;
9
+ };
10
+ friends: {
11
+ name: string;
12
+ tags: string[];
13
+ }[];
14
+ coords: [100, 200 | 201];
15
+ }
16
+
17
+ test("registers fields", () => {
18
+ const formController = new FormController<User>({});
19
+
20
+ let changeInvoked = 0;
21
+
22
+ formController.events.on("fieldValueChanged", () => changeInvoked++);
23
+
24
+ const path1 = formController.path.of((data) => data.friends[0].tags[99]);
25
+ expect(formController.getField(path1)).toBeUndefined();
26
+ const field1 = formController.registerField(path1, { defaultValue: "Tag99" });
27
+ expect(field1.value).toBe("Tag99");
28
+ expect(field1.initialValue).toBeUndefined();
29
+
30
+ const path2 = formController.path.of("coords[1]");
31
+ expect(formController.getField(path2)).toBeUndefined();
32
+ const field2 = formController.registerField(path2);
33
+ expect(field2.value).toBeUndefined();
34
+ expect(field2.initialValue).toBeUndefined();
35
+
36
+ field1.setValue("Tag100");
37
+ field2.setValue(200);
38
+
39
+ const path3 = formController.path.of("coords");
40
+ expect(formController.getField(path3)).toBeUndefined();
41
+ const field3 = formController.registerField(path3, {
42
+ defaultValue: [100, 200],
43
+ overrideInitialValue: true,
44
+ });
45
+ expect(field3.value).toEqual([undefined, 200]);
46
+ expect(field3.initialValue).toBeUndefined();
47
+
48
+ field3.modifyValue((val) => {
49
+ val[0] = 100;
50
+ val[1]++;
51
+ });
52
+
53
+ expect(field3.value).toStrictEqual([100, 201]);
54
+ expect(changeInvoked).toBe(5);
55
+ });