@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.
- package/dist/field/FieldPath.d.ts +18 -9
- package/dist/field/FieldPath.d.ts.map +1 -1
- package/dist/field/FieldPathBuilder.d.ts +2 -2
- package/dist/field/FieldPathBuilder.d.ts.map +1 -1
- package/dist/field/Reconcile.d.ts +2 -2
- package/dist/field/Reconcile.d.ts.map +1 -1
- package/dist/form/FormController.d.ts +51 -20
- package/dist/form/FormController.d.ts.map +1 -1
- package/dist/form/FormField.d.ts +15 -10
- package/dist/form/FormField.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +523 -476
- package/dist/index.js.map +1 -1
- package/dist/types/DeepHelpers.d.ts +11 -0
- package/dist/types/DeepHelpers.d.ts.map +1 -0
- package/dist/types/FormHelpers.d.ts +5 -0
- package/dist/types/FormHelpers.d.ts.map +1 -0
- package/dist/types/Suppliable.d.ts +3 -0
- package/dist/types/Suppliable.d.ts.map +1 -0
- package/dist/validation/CustomValidation.d.ts +1 -1
- package/package.json +13 -3
- package/src/field/FieldPath.spec.ts +204 -0
- package/src/field/FieldPath.ts +62 -59
- package/src/field/FieldPathBuilder.spec.ts +47 -0
- package/src/field/FieldPathBuilder.ts +15 -81
- package/src/field/Reconcile.ts +2 -2
- package/src/form/FormController.spec.ts +55 -0
- package/src/form/FormController.ts +151 -115
- package/src/form/FormField.ts +63 -30
- package/src/index.ts +2 -2
- package/src/types/DeepHelpers.ts +15 -0
- package/src/types/FormHelpers.ts +13 -0
- package/src/types/Suppliable.ts +7 -0
- package/src/validation/CustomValidation.ts +1 -1
- package/dist/form/NonullFormField.d.ts +0 -9
- package/dist/form/NonullFormField.d.ts.map +0 -1
- package/dist/types/DeepPartial.d.ts +0 -6
- package/dist/types/DeepPartial.d.ts.map +0 -1
- package/src/form/NonullFormField.ts +0 -15
- package/src/types/DeepPartial.ts +0 -7
- package/tsconfig.json +0 -8
- 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 @@
|
|
|
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 @@
|
|
|
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/
|
|
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.
|
|
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
|
+
});
|
package/src/field/FieldPath.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
|
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
|
|
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
|
|
233
|
-
>(object: TObject,
|
|
234
|
-
|
|
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
|
|
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
|
-
) =>
|
|
276
|
+
currentValue: FieldPath.Resolve<TObject, TPath> | undefined,
|
|
277
|
+
) => void,
|
|
246
278
|
) {
|
|
247
|
-
|
|
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
|
|
272
|
-
TPath
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
291
|
+
if (key == null) return;
|
|
285
292
|
|
|
286
|
-
delete
|
|
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
|
|
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
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
132
|
-
|
|
64
|
+
return FieldPath.fromStringPath(arg as any);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/field/Reconcile.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export namespace
|
|
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
|
|
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
|
+
});
|