@goodie-forms/core 1.2.5-alpha → 1.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/README.md +61 -0
- 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 +5 -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 +550 -505
- 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 +23 -3
- package/src/field/FieldPath.spec.ts +204 -0
- package/src/field/FieldPath.ts +63 -59
- package/src/field/FieldPathBuilder.spec.ts +47 -0
- package/src/field/FieldPathBuilder.ts +15 -81
- package/src/field/Reconcile.ts +12 -7
- package/src/form/FormController.spec.ts +55 -0
- package/src/form/FormController.ts +152 -121
- package/src/form/FormField.ts +66 -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
package/src/form/FormField.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { produce } from "immer";
|
|
1
|
+
import { Draft, produce } from "immer";
|
|
2
2
|
import { FieldPath } from "../field/FieldPath";
|
|
3
|
-
import {
|
|
3
|
+
import { Reconcile } from "../field/Reconcile";
|
|
4
4
|
import { FormController } from "../form/FormController";
|
|
5
|
+
import { DeepReadonly } from "../types/DeepHelpers";
|
|
6
|
+
import { Suppliable, supply } from "../types/Suppliable";
|
|
5
7
|
import { ensureImmerability } from "../utils/ensureImmerability";
|
|
6
8
|
import { getId } from "../utils/getId";
|
|
7
9
|
|
|
@@ -13,9 +15,11 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
13
15
|
protected _isTouched = false;
|
|
14
16
|
protected _isDirty = false;
|
|
15
17
|
|
|
18
|
+
/** @internal register via `FormController::registerField` instead */
|
|
16
19
|
constructor(
|
|
17
20
|
public readonly controller: FormController<TOutput>,
|
|
18
21
|
public readonly path: FieldPath.Segments,
|
|
22
|
+
public readonly defaultValue?: Suppliable<TValue>,
|
|
19
23
|
initialState?: {
|
|
20
24
|
isTouched?: boolean;
|
|
21
25
|
isDirty?: boolean;
|
|
@@ -25,20 +29,16 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
25
29
|
if (initialState?.isDirty) this._setDirty(true);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
get canonicalPath() {
|
|
29
|
-
return FieldPath.toCanonicalPath(this.path);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
32
|
get stringPath() {
|
|
33
33
|
return FieldPath.toStringPath(this.path);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
get value(): TValue | undefined {
|
|
37
|
-
return FieldPath.getValue(this.controller.
|
|
36
|
+
get value(): DeepReadonly<TValue> | undefined {
|
|
37
|
+
return FieldPath.getValue(this.controller.data, this.path);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
get initialValue(): TValue | undefined {
|
|
41
|
-
return FieldPath.getValue(this.controller.
|
|
40
|
+
get initialValue(): DeepReadonly<TValue> | undefined {
|
|
41
|
+
return FieldPath.getValue(this.controller.initialData, this.path);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
get boundElement() {
|
|
@@ -63,6 +63,16 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
63
63
|
return this.issues.length === 0;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
ensureDefault(opts?: Parameters<typeof this.modifyData>[1]) {
|
|
67
|
+
if (this.value == null && this.defaultValue != null) {
|
|
68
|
+
const defaultValue = supply(this.defaultValue);
|
|
69
|
+
|
|
70
|
+
if (defaultValue != null) {
|
|
71
|
+
this.setValue(defaultValue, opts);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
protected _setTouched(isTouched: boolean) {
|
|
67
77
|
const changed = this._isTouched !== isTouched;
|
|
68
78
|
this._isTouched = isTouched;
|
|
@@ -92,26 +102,33 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
bindElement(el: HTMLElement | undefined) {
|
|
95
|
-
this.target = el;
|
|
96
105
|
if (el != null) {
|
|
106
|
+
this.target = el;
|
|
97
107
|
this.controller.events.emit("elementBound", this.path, el);
|
|
98
108
|
} else {
|
|
109
|
+
this.unbindElement();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
unbindElement() {
|
|
114
|
+
if (this.target != null) {
|
|
115
|
+
this.target = undefined;
|
|
99
116
|
this.controller.events.emit("elementUnbound", this.path);
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
return this.
|
|
120
|
+
clearIssues() {
|
|
121
|
+
return this.controller.clearFieldIssues(this.path);
|
|
105
122
|
}
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
private modifyData(
|
|
125
|
+
draftConsumer: (draft: Draft<typeof this.controller.data>) => void,
|
|
109
126
|
opts?: {
|
|
110
127
|
shouldTouch?: boolean;
|
|
111
128
|
shouldMarkDirty?: boolean;
|
|
112
129
|
},
|
|
113
|
-
)
|
|
114
|
-
if (opts?.shouldTouch == null || opts?.shouldTouch) {
|
|
130
|
+
) {
|
|
131
|
+
if (opts?.shouldTouch == null || opts?.shouldTouch === true) {
|
|
115
132
|
this.touch();
|
|
116
133
|
}
|
|
117
134
|
|
|
@@ -123,11 +140,7 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
123
140
|
const oldValues = ascendantFields.map((field) => field?.value);
|
|
124
141
|
oldValues.forEach((v) => ensureImmerability(v));
|
|
125
142
|
|
|
126
|
-
this.controller._data = produce(this.controller._data,
|
|
127
|
-
FieldPath.modifyValue(draft as TOutput, this.path, (oldValue) => {
|
|
128
|
-
return modifier(oldValue);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
143
|
+
this.controller._data = produce(this.controller._data, draftConsumer);
|
|
131
144
|
|
|
132
145
|
const newValues = ascendantFields.map((field) => field?.value);
|
|
133
146
|
newValues.forEach((v) => ensureImmerability(v));
|
|
@@ -138,10 +151,10 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
138
151
|
const ctorA = a.constructor;
|
|
139
152
|
const ctorB = b.constructor;
|
|
140
153
|
if (ctorA !== ctorB) return;
|
|
141
|
-
return this.controller.equalityComparators?.
|
|
154
|
+
return this.controller.equalityComparators?.get(ctorA)?.(a, b);
|
|
142
155
|
};
|
|
143
156
|
|
|
144
|
-
const valueChanged = !
|
|
157
|
+
const valueChanged = !Reconcile.deepEqual(
|
|
145
158
|
oldValues[oldValues.length - 1],
|
|
146
159
|
newValues[newValues.length - 1],
|
|
147
160
|
compareCustom,
|
|
@@ -151,7 +164,7 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
151
164
|
for (let i = ascendantFields.length - 1; i >= 0; i--) {
|
|
152
165
|
const field = ascendantFields[i];
|
|
153
166
|
this.controller.events.emit(
|
|
154
|
-
"
|
|
167
|
+
"fieldValueChanged",
|
|
155
168
|
field.path,
|
|
156
169
|
newValues[i],
|
|
157
170
|
oldValues[i],
|
|
@@ -160,7 +173,7 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
if (opts?.shouldMarkDirty == null || opts?.shouldMarkDirty) {
|
|
163
|
-
const gotDirty = !
|
|
176
|
+
const gotDirty = !Reconcile.deepEqual(
|
|
164
177
|
initialValues[initialValues.length - 1],
|
|
165
178
|
newValues[newValues.length - 1],
|
|
166
179
|
compareCustom,
|
|
@@ -169,9 +182,30 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
169
182
|
}
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
// TODO: impl
|
|
186
|
+
// private modifyInitialData() {}
|
|
187
|
+
|
|
188
|
+
setValue(value: TValue, opts?: Parameters<typeof this.modifyData>[1]) {
|
|
189
|
+
return this.modifyData((data) => {
|
|
190
|
+
FieldPath.setValue(data as TOutput, this.path, value as never);
|
|
191
|
+
}, opts);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
modifyValue(
|
|
195
|
+
modifier: (currentValue: TValue) => undefined,
|
|
196
|
+
opts?: Parameters<typeof this.modifyData>[1],
|
|
197
|
+
): void {
|
|
198
|
+
return this.modifyData((data) => {
|
|
199
|
+
FieldPath.modifyValue(data as TOutput, this.path, (oldValue) => {
|
|
200
|
+
modifier(oldValue as TValue);
|
|
201
|
+
});
|
|
202
|
+
}, opts);
|
|
203
|
+
}
|
|
204
|
+
|
|
172
205
|
reset() {
|
|
173
206
|
this._setTouched(false);
|
|
174
207
|
this._setDirty(false);
|
|
208
|
+
this.controller.events.emit("fieldReset", this.path);
|
|
175
209
|
}
|
|
176
210
|
|
|
177
211
|
touch() {
|
|
@@ -179,22 +213,24 @@ export class FormField<TOutput extends object, TValue> {
|
|
|
179
213
|
}
|
|
180
214
|
|
|
181
215
|
markDirty() {
|
|
182
|
-
this.touch();
|
|
183
216
|
this._setDirty(true);
|
|
184
217
|
}
|
|
185
218
|
|
|
186
|
-
|
|
219
|
+
validate() {
|
|
187
220
|
this.controller.validateField(this.path);
|
|
188
221
|
}
|
|
189
222
|
|
|
190
|
-
focus(opts?: {
|
|
223
|
+
focus(opts?: {
|
|
224
|
+
shouldTouch?: boolean;
|
|
225
|
+
scrollOptions?: ScrollIntoViewOptions;
|
|
226
|
+
}) {
|
|
191
227
|
if (opts?.shouldTouch == null || opts.shouldTouch) {
|
|
192
228
|
this.target?.addEventListener("focus", () => this.touch(), {
|
|
193
229
|
once: true,
|
|
194
230
|
});
|
|
195
231
|
}
|
|
196
232
|
|
|
197
|
-
this.target?.scrollIntoView();
|
|
233
|
+
this.target?.scrollIntoView(opts?.scrollOptions);
|
|
198
234
|
this.target?.focus();
|
|
199
235
|
}
|
|
200
236
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,9 +4,9 @@ export * from "./field/Reconcile";
|
|
|
4
4
|
|
|
5
5
|
export * from "./form/FormController";
|
|
6
6
|
export * from "./form/FormField";
|
|
7
|
-
export * from "./form/NonullFormField";
|
|
8
7
|
|
|
9
8
|
export * from "./validation/CustomValidation";
|
|
10
9
|
|
|
11
|
-
export * from "./types/
|
|
10
|
+
export * from "./types/DeepHelpers";
|
|
11
|
+
export * from "./types/Suppliable";
|
|
12
12
|
export * from "./types/Mixin";
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
|
5
|
+
? never
|
|
6
|
+
: K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type DeepReadonly<T> = {
|
|
10
|
+
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
|
|
11
|
+
} & {
|
|
12
|
+
readonly [K in keyof T as T[K] extends (...args: any[]) => any
|
|
13
|
+
? never
|
|
14
|
+
: K]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
|
15
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FormField } from "packages/core/src/form/FormField";
|
|
2
|
+
|
|
3
|
+
// TODO: Consider
|
|
4
|
+
export type FieldValue<T extends FormField<any, any>> =
|
|
5
|
+
NonNullable<T> extends { value: infer V } ? NonNullable<V> : never;
|
|
6
|
+
|
|
7
|
+
// TODO: Move to a proper test file
|
|
8
|
+
// declare const x: FormField<{ a: 1; b: 2 }, number>;
|
|
9
|
+
|
|
10
|
+
// type X0 = FieldValue<typeof x>;
|
|
11
|
+
// // ^?
|
|
12
|
+
// type X1 = (typeof x)["value"];
|
|
13
|
+
// // ^?
|
|
@@ -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
|
|
|
5
5
|
export type CustomValidationIssue<TOutput extends object> = {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { FormField } from './FormField';
|
|
2
|
-
import { Mixin } from '../types/Mixin';
|
|
3
|
-
export type NonnullFormField<TOutput extends object, TValue> = Mixin<FormField<TOutput, TValue>, {
|
|
4
|
-
modifyValue: (modifier: (currentValue: TValue) => TValue | void, opts?: {
|
|
5
|
-
shouldTouch?: boolean;
|
|
6
|
-
shouldMarkDirty?: boolean;
|
|
7
|
-
}) => void;
|
|
8
|
-
}>;
|
|
9
|
-
//# sourceMappingURL=NonullFormField.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"NonullFormField.d.ts","sourceRoot":"","sources":["../../src/form/NonullFormField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,MAAM,MAAM,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,IAAI,KAAK,CAClE,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,EAC1B;IACE,WAAW,EAAE,CACX,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,EACjD,IAAI,CAAC,EAAE;QACL,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,KACE,IAAI,CAAC;CACX,CACF,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
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
|
-
//# sourceMappingURL=DeepPartial.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"DeepPartial.d.ts","sourceRoot":"","sources":["../../src/types/DeepPartial.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"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { FormField } from "./FormField";
|
|
2
|
-
import { Mixin } from "../types/Mixin";
|
|
3
|
-
|
|
4
|
-
export type NonnullFormField<TOutput extends object, TValue> = Mixin<
|
|
5
|
-
FormField<TOutput, TValue>,
|
|
6
|
-
{
|
|
7
|
-
modifyValue: (
|
|
8
|
-
modifier: (currentValue: TValue) => TValue | void,
|
|
9
|
-
opts?: {
|
|
10
|
-
shouldTouch?: boolean;
|
|
11
|
-
shouldMarkDirty?: boolean;
|
|
12
|
-
},
|
|
13
|
-
) => void;
|
|
14
|
-
}
|
|
15
|
-
>;
|
package/src/types/DeepPartial.ts
DELETED
package/tsconfig.json
DELETED
package/vite.config.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vite";
|
|
2
|
-
|
|
3
|
-
import dts from "vite-plugin-dts";
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
plugins: [dts({ entryRoot: "src" })],
|
|
7
|
-
build: {
|
|
8
|
-
lib: {
|
|
9
|
-
entry: "src/index.ts",
|
|
10
|
-
formats: ["es"],
|
|
11
|
-
fileName: "index",
|
|
12
|
-
},
|
|
13
|
-
rollupOptions: {
|
|
14
|
-
external: [],
|
|
15
|
-
},
|
|
16
|
-
sourcemap: true,
|
|
17
|
-
},
|
|
18
|
-
});
|