@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
|
@@ -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
|
+
});
|
|
@@ -3,9 +3,10 @@ import { produce } from "immer";
|
|
|
3
3
|
import { createNanoEvents } from "nanoevents";
|
|
4
4
|
import { FieldPath } from "../field/FieldPath";
|
|
5
5
|
import { FieldPathBuilder } from "../field/FieldPathBuilder";
|
|
6
|
-
import {
|
|
6
|
+
import { Reconcile } from "../field/Reconcile";
|
|
7
7
|
import { FormField } from "../form/FormField";
|
|
8
|
-
import { DeepPartial } from "../types/
|
|
8
|
+
import { DeepPartial, DeepReadonly } from "../types/DeepHelpers";
|
|
9
|
+
import { Suppliable, supply } from "../types/Suppliable";
|
|
9
10
|
import { ensureImmerability } from "../utils/ensureImmerability";
|
|
10
11
|
import { removeBy } from "../utils/removeBy";
|
|
11
12
|
|
|
@@ -13,7 +14,7 @@ export namespace FormController {
|
|
|
13
14
|
export type Configs<TOutput extends object> = {
|
|
14
15
|
initialData?: DeepPartial<TOutput>;
|
|
15
16
|
validationSchema?: StandardSchemaV1<unknown, TOutput>;
|
|
16
|
-
equalityComparators?:
|
|
17
|
+
equalityComparators?: Map<any, (a: any, b: any) => boolean>;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export interface PreventableEvent {
|
|
@@ -22,45 +23,62 @@ export namespace FormController {
|
|
|
22
23
|
|
|
23
24
|
export type SubmitSuccessHandler<
|
|
24
25
|
TOutput extends object,
|
|
25
|
-
TEvent extends PreventableEvent,
|
|
26
|
-
> = (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
) => void | Promise<void>;
|
|
31
|
-
|
|
32
|
-
export type SubmitErrorHandler<TEvent extends PreventableEvent> = (
|
|
33
|
-
issues: StandardSchemaV1.Issue[],
|
|
34
|
-
event: TEvent,
|
|
35
|
-
abortSignal: AbortSignal,
|
|
36
|
-
) => void | Promise<void>;
|
|
26
|
+
TEvent extends PreventableEvent | null | undefined,
|
|
27
|
+
> = (data: DeepReadonly<TOutput>, event: TEvent) => void | Promise<void>;
|
|
28
|
+
|
|
29
|
+
export type SubmitErrorHandler<
|
|
30
|
+
TEvent extends PreventableEvent | null | undefined,
|
|
31
|
+
> = (issues: StandardSchemaV1.Issue[], event: TEvent) => void | Promise<void>;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
export class FormController<TOutput extends object> {
|
|
40
|
-
_isValidating = false;
|
|
41
|
-
_isSubmitting = false;
|
|
35
|
+
private _isValidating = false;
|
|
36
|
+
private _isSubmitting = false;
|
|
37
|
+
private _triedSubmitting = false;
|
|
38
|
+
|
|
39
|
+
private pathBuilder = new FieldPathBuilder<TOutput>();
|
|
42
40
|
|
|
41
|
+
/** @internal use `this.getFields` instead */
|
|
43
42
|
_fields = new Map<string, FormField<TOutput, any>>();
|
|
43
|
+
/** @internal use `this.initialData` instead */
|
|
44
44
|
_initialData: DeepPartial<TOutput>;
|
|
45
|
+
/** @internal use `this.data` instead */
|
|
45
46
|
_data: DeepPartial<TOutput>;
|
|
47
|
+
/** @internal use `this.issues` instead */
|
|
46
48
|
_issues: StandardSchemaV1.Issue[] = [];
|
|
47
49
|
|
|
48
|
-
equalityComparators?:
|
|
50
|
+
equalityComparators?: Map<any, (a: any, b: any) => boolean>;
|
|
49
51
|
validationSchema?: StandardSchemaV1<unknown, TOutput>;
|
|
50
52
|
|
|
51
53
|
public readonly events = createNanoEvents<{
|
|
54
|
+
/** Emitted when `this.isSubmitting` changes */
|
|
52
55
|
submissionStatusChange(isSubmitting: boolean): void;
|
|
56
|
+
/** Emitted when `this.isValidating` changes */
|
|
53
57
|
validationStatusChange(isValidating: boolean): void;
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
/** Emitted when a field is registered */
|
|
60
|
+
fieldRegistered(fieldPath: FieldPath.Segments): void;
|
|
61
|
+
/** Emitted when a field is unregistered */
|
|
62
|
+
fieldUnregistered(fieldPath: FieldPath.Segments): void;
|
|
63
|
+
/** Emitted when a field's `isTouched` changes */
|
|
57
64
|
fieldTouchUpdated(path: FieldPath.Segments): void;
|
|
65
|
+
/** Emitted when a field's `isDirty` changes */
|
|
58
66
|
fieldDirtyUpdated(path: FieldPath.Segments): void;
|
|
67
|
+
/** Emitted when a field's `issues` changes */
|
|
59
68
|
fieldIssuesUpdated(fieldPath: FieldPath.Segments): void;
|
|
69
|
+
/** Emitted when a field's `reset()` is invoked */
|
|
70
|
+
fieldReset(fieldPath: FieldPath.Segments): void;
|
|
71
|
+
|
|
72
|
+
/** Emitted when a HTMLELement is bound to a field */
|
|
60
73
|
elementBound(fieldPath: FieldPath.Segments, el: HTMLElement): void;
|
|
74
|
+
/** Emitted when a HTMLELement is unbound from a field */
|
|
61
75
|
elementUnbound(fieldPath: FieldPath.Segments): void;
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
|
|
77
|
+
/** Emitted when validation is triggered on a field */
|
|
78
|
+
fieldValidationTriggered(fieldPath: FieldPath.Segments): void;
|
|
79
|
+
|
|
80
|
+
/** Emitted when a field's `value` is updated */
|
|
81
|
+
fieldValueChanged(
|
|
64
82
|
fieldPath: FieldPath.Segments,
|
|
65
83
|
newValue: {} | undefined,
|
|
66
84
|
oldValue: {} | undefined,
|
|
@@ -74,6 +92,22 @@ export class FormController<TOutput extends object> {
|
|
|
74
92
|
this._data = produce(this._initialData, () => {});
|
|
75
93
|
}
|
|
76
94
|
|
|
95
|
+
get data(): DeepReadonly<DeepPartial<TOutput>> {
|
|
96
|
+
return this._data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get initialData(): DeepReadonly<DeepPartial<TOutput>> {
|
|
100
|
+
return this._initialData;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get issues(): readonly StandardSchemaV1.Issue[] {
|
|
104
|
+
return this._issues;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get path() {
|
|
108
|
+
return this.pathBuilder;
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
get isDirty() {
|
|
78
112
|
for (const field of this._fields.values()) {
|
|
79
113
|
if (field.isDirty) return true;
|
|
@@ -82,8 +116,7 @@ export class FormController<TOutput extends object> {
|
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
get isValid() {
|
|
85
|
-
|
|
86
|
-
return this._issues.length === 0;
|
|
119
|
+
return this.isValidating || this._issues.length === 0;
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
get isValidating() {
|
|
@@ -94,6 +127,10 @@ export class FormController<TOutput extends object> {
|
|
|
94
127
|
return this._isSubmitting;
|
|
95
128
|
}
|
|
96
129
|
|
|
130
|
+
get triedSubmitting() {
|
|
131
|
+
return this._triedSubmitting;
|
|
132
|
+
}
|
|
133
|
+
|
|
97
134
|
protected setValidating(newStatus: boolean) {
|
|
98
135
|
if (this._isValidating === newStatus) return;
|
|
99
136
|
this._isValidating = newStatus;
|
|
@@ -106,85 +143,106 @@ export class FormController<TOutput extends object> {
|
|
|
106
143
|
this.events.emit("submissionStatusChange", newStatus);
|
|
107
144
|
}
|
|
108
145
|
|
|
109
|
-
|
|
110
|
-
path: TPath,
|
|
111
|
-
value: FieldPath.Resolve<TOutput, TPath>,
|
|
112
|
-
config?: { updateInitialValue?: boolean },
|
|
113
|
-
) {
|
|
114
|
-
ensureImmerability(value);
|
|
115
|
-
|
|
116
|
-
if (config?.updateInitialValue === true) {
|
|
117
|
-
this._initialData = produce(this._initialData, (draft) => {
|
|
118
|
-
FieldPath.setValue(draft as TOutput, path, value);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this._data = produce(this._data, (draft) => {
|
|
123
|
-
FieldPath.setValue(draft as TOutput, path, value);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// TODO: Rename to "register" ??
|
|
128
|
-
bindField<TPath extends FieldPath.Segments>(
|
|
146
|
+
registerField<TPath extends FieldPath.Segments>(
|
|
129
147
|
path: TPath,
|
|
130
148
|
config?: {
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Used to set value at **path** in **data**, if it's missing.
|
|
151
|
+
*/
|
|
152
|
+
defaultValue?: Suppliable<FieldPath.Resolve<TOutput, TPath>>;
|
|
153
|
+
/**
|
|
154
|
+
* Whether value in **initialData** should also be changed, if **defaultValue** is used or not
|
|
155
|
+
*
|
|
156
|
+
* If this is set to `true` and **defaultValue** is used; **initialData** will be modified
|
|
157
|
+
*/
|
|
133
158
|
overrideInitialValue?: boolean;
|
|
134
159
|
},
|
|
135
160
|
) {
|
|
136
161
|
let currentValue = FieldPath.getValue(this._data as TOutput, path);
|
|
137
162
|
|
|
138
163
|
if (currentValue == null && config?.defaultValue != null) {
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
const defaultValue = supply(config.defaultValue);
|
|
165
|
+
|
|
166
|
+
ensureImmerability(defaultValue);
|
|
167
|
+
|
|
168
|
+
if (config?.overrideInitialValue === true) {
|
|
169
|
+
this._initialData = produce(this._initialData, (draft) => {
|
|
170
|
+
FieldPath.setValue(draft as TOutput, path, defaultValue);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._data = produce(this._data, (draft) => {
|
|
175
|
+
FieldPath.setValue(draft as TOutput, path, defaultValue);
|
|
141
176
|
});
|
|
177
|
+
|
|
142
178
|
currentValue = FieldPath.getValue(this._data as TOutput, path);
|
|
143
179
|
}
|
|
144
180
|
|
|
145
181
|
const initialValue = FieldPath.getValue(this._initialData as TOutput, path);
|
|
146
182
|
|
|
147
|
-
const valueChanged = !
|
|
183
|
+
const valueChanged = !Reconcile.deepEqual(currentValue, initialValue);
|
|
148
184
|
|
|
149
|
-
const field = new FormField(this, path, {
|
|
185
|
+
const field = new FormField(this, path, config?.defaultValue, {
|
|
150
186
|
isDirty: valueChanged,
|
|
151
187
|
});
|
|
152
188
|
|
|
153
|
-
if (config?.domElement != null) {
|
|
154
|
-
field.bindElement(config.domElement);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
189
|
this._fields.set(field.stringPath, field);
|
|
158
|
-
this.events.emit("
|
|
190
|
+
this.events.emit("fieldRegistered", field.path);
|
|
159
191
|
|
|
160
192
|
if (valueChanged) {
|
|
161
|
-
this.events.emit(
|
|
193
|
+
this.events.emit(
|
|
194
|
+
"fieldValueChanged",
|
|
195
|
+
field.path,
|
|
196
|
+
currentValue,
|
|
197
|
+
initialValue,
|
|
198
|
+
);
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
return field as FormField<TOutput, FieldPath.Resolve<TOutput, TPath>>;
|
|
165
202
|
}
|
|
166
203
|
|
|
167
|
-
|
|
204
|
+
unregisterField(path: FieldPath.Segments) {
|
|
168
205
|
const stringPath = FieldPath.toStringPath(path);
|
|
206
|
+
|
|
207
|
+
if (!this._fields.has(stringPath)) return false;
|
|
208
|
+
|
|
169
209
|
this._fields.delete(stringPath);
|
|
170
|
-
this.events.emit("
|
|
210
|
+
this.events.emit("fieldUnregistered", path);
|
|
211
|
+
return true;
|
|
171
212
|
}
|
|
172
213
|
|
|
173
|
-
// TODO: Add an option to keep dirty/touched fields as they are
|
|
174
214
|
reset(newInitialData?: DeepPartial<TOutput>) {
|
|
175
|
-
|
|
176
|
-
|
|
215
|
+
const newData = newInitialData ?? this._initialData;
|
|
216
|
+
|
|
217
|
+
const fieldUpdates = [] as [FieldPath.Segments, any, any][];
|
|
177
218
|
|
|
178
219
|
for (const field of this._fields.values()) {
|
|
220
|
+
const currentValue = FieldPath.getValue(this._data, field.path);
|
|
221
|
+
const nextValue = FieldPath.getValue(newData, field.path);
|
|
222
|
+
|
|
223
|
+
if (!Reconcile.deepEqual(currentValue, nextValue)) {
|
|
224
|
+
fieldUpdates.push([field.path, nextValue, currentValue]);
|
|
225
|
+
}
|
|
226
|
+
|
|
179
227
|
field.reset();
|
|
180
228
|
}
|
|
181
229
|
|
|
182
230
|
if (newInitialData != null) {
|
|
183
|
-
this._initialData = newInitialData;
|
|
184
|
-
this._data = produce(this._initialData, () => {});
|
|
231
|
+
this._initialData = produce(newInitialData, () => {});
|
|
185
232
|
}
|
|
233
|
+
|
|
234
|
+
this._data = produce(newData, () => {});
|
|
235
|
+
this._issues = [];
|
|
236
|
+
this._triedSubmitting = false;
|
|
237
|
+
|
|
238
|
+
fieldUpdates.forEach(([path, newValue, oldValue]) => {
|
|
239
|
+
this.events.emit("fieldValueChanged", path, newValue, oldValue);
|
|
240
|
+
});
|
|
186
241
|
}
|
|
187
242
|
|
|
243
|
+
// TODO: resetGracefully(newInitialData?: DeepPartial<TOutput>)
|
|
244
|
+
// TODO: ^ Keeps dirty/touched fields as they are
|
|
245
|
+
|
|
188
246
|
getAscendantFields<TPath extends FieldPath.Segments>(path: TPath) {
|
|
189
247
|
const paths = path.map((_, i) => {
|
|
190
248
|
return path.slice(0, i + 1);
|
|
@@ -200,6 +258,10 @@ export class FormController<TOutput extends object> {
|
|
|
200
258
|
| undefined;
|
|
201
259
|
}
|
|
202
260
|
|
|
261
|
+
getFields() {
|
|
262
|
+
return this._fields.values();
|
|
263
|
+
}
|
|
264
|
+
|
|
203
265
|
clearFieldIssues<TPath extends FieldPath.Segments>(path: TPath) {
|
|
204
266
|
this._issues = this._issues.filter((issue) => {
|
|
205
267
|
return !FieldPath.equals(FieldPath.normalize(issue.path), path);
|
|
@@ -210,11 +272,9 @@ export class FormController<TOutput extends object> {
|
|
|
210
272
|
_result: StandardSchemaV1.Result<TOutput>,
|
|
211
273
|
path: TPath,
|
|
212
274
|
) {
|
|
213
|
-
const diff =
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Reconsile.deepEqual,
|
|
217
|
-
(issue) => {
|
|
275
|
+
const diff = Reconcile.arrayDiff(this._issues, _result.issues ?? [], {
|
|
276
|
+
equals: Reconcile.deepEqual,
|
|
277
|
+
include: (issue) => {
|
|
218
278
|
if (issue.path == null) return false;
|
|
219
279
|
const issuePath = FieldPath.normalize(issue.path);
|
|
220
280
|
return (
|
|
@@ -222,7 +282,7 @@ export class FormController<TOutput extends object> {
|
|
|
222
282
|
FieldPath.isDescendant(path, issuePath)
|
|
223
283
|
);
|
|
224
284
|
},
|
|
225
|
-
);
|
|
285
|
+
});
|
|
226
286
|
|
|
227
287
|
removeBy(this._issues, (issue) => diff.removed.includes(issue));
|
|
228
288
|
|
|
@@ -233,54 +293,54 @@ export class FormController<TOutput extends object> {
|
|
|
233
293
|
}
|
|
234
294
|
}
|
|
235
295
|
|
|
236
|
-
async
|
|
296
|
+
async validateForm() {
|
|
237
297
|
if (this._isValidating) return;
|
|
238
298
|
|
|
239
299
|
if (this.validationSchema == null) return;
|
|
240
300
|
|
|
241
301
|
this.setValidating(true);
|
|
242
302
|
|
|
243
|
-
if (this.getField(path) == null) this.bindField(path);
|
|
244
|
-
|
|
245
303
|
const result = await this.validationSchema["~standard"].validate(
|
|
246
304
|
this._data,
|
|
247
305
|
);
|
|
248
306
|
|
|
249
|
-
this.
|
|
250
|
-
|
|
307
|
+
for (const stringPath of this._fields.keys()) {
|
|
308
|
+
const path = FieldPath.fromStringPath(stringPath);
|
|
309
|
+
this.events.emit("fieldValidationTriggered", path);
|
|
310
|
+
this.applyValidation(result, path);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Append non-registered issues too
|
|
314
|
+
const diff = Reconcile.arrayDiff(this._issues, result.issues ?? [], {
|
|
315
|
+
equals: Reconcile.deepEqual,
|
|
316
|
+
});
|
|
317
|
+
diff.added.forEach((issue) => this._issues.push(issue));
|
|
251
318
|
|
|
252
319
|
this.setValidating(false);
|
|
253
320
|
}
|
|
254
321
|
|
|
255
|
-
async
|
|
322
|
+
async validateField<TPath extends FieldPath.Segments>(path: TPath) {
|
|
256
323
|
if (this._isValidating) return;
|
|
257
324
|
|
|
258
325
|
if (this.validationSchema == null) return;
|
|
259
326
|
|
|
260
327
|
this.setValidating(true);
|
|
261
328
|
|
|
329
|
+
if (this.getField(path) == null) this.registerField(path);
|
|
330
|
+
|
|
262
331
|
const result = await this.validationSchema["~standard"].validate(
|
|
263
332
|
this._data,
|
|
264
333
|
);
|
|
265
334
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.events.emit("validationTriggered", path);
|
|
269
|
-
this.applyValidation(result, path);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Append non-registered issues too
|
|
273
|
-
const diff = Reconsile.diff(
|
|
274
|
-
this._issues,
|
|
275
|
-
result.issues ?? [],
|
|
276
|
-
Reconsile.deepEqual,
|
|
277
|
-
);
|
|
278
|
-
diff.added.forEach((issue) => this._issues.push(issue));
|
|
335
|
+
this.events.emit("fieldValidationTriggered", path);
|
|
336
|
+
this.applyValidation(result, path);
|
|
279
337
|
|
|
280
338
|
this.setValidating(false);
|
|
281
339
|
}
|
|
282
340
|
|
|
283
|
-
createSubmitHandler<
|
|
341
|
+
createSubmitHandler<
|
|
342
|
+
TEvent extends FormController.PreventableEvent | null | undefined,
|
|
343
|
+
>(
|
|
284
344
|
onSuccess?: FormController.SubmitSuccessHandler<TOutput, TEvent>,
|
|
285
345
|
onError?: FormController.SubmitErrorHandler<TEvent>,
|
|
286
346
|
) {
|
|
@@ -292,14 +352,13 @@ export class FormController<TOutput extends object> {
|
|
|
292
352
|
if (this._isValidating) return;
|
|
293
353
|
if (this._isSubmitting) return;
|
|
294
354
|
|
|
295
|
-
|
|
296
|
-
const abortController = new AbortController();
|
|
355
|
+
this._triedSubmitting = true;
|
|
297
356
|
|
|
298
357
|
await this.validateForm();
|
|
299
358
|
|
|
300
359
|
if (this._issues.length === 0) {
|
|
301
360
|
this.setSubmitting(true);
|
|
302
|
-
await onSuccess?.(this._data as TOutput
|
|
361
|
+
await onSuccess?.(this._data as any as DeepReadonly<TOutput>, event);
|
|
303
362
|
this.setSubmitting(false);
|
|
304
363
|
return;
|
|
305
364
|
}
|
|
@@ -313,36 +372,8 @@ export class FormController<TOutput extends object> {
|
|
|
313
372
|
field.focus();
|
|
314
373
|
break;
|
|
315
374
|
}
|
|
316
|
-
await onError?.(this._issues, event
|
|
375
|
+
await onError?.(this._issues, event);
|
|
317
376
|
this.setSubmitting(false);
|
|
318
377
|
};
|
|
319
378
|
}
|
|
320
379
|
}
|
|
321
|
-
|
|
322
|
-
/* ---- TESTS ---------------- */
|
|
323
|
-
|
|
324
|
-
// interface User {
|
|
325
|
-
// name: string;
|
|
326
|
-
// address: {
|
|
327
|
-
// city: string;
|
|
328
|
-
// street: string;
|
|
329
|
-
// };
|
|
330
|
-
// friends: {
|
|
331
|
-
// name: string;
|
|
332
|
-
// tags: string[];
|
|
333
|
-
// }[];
|
|
334
|
-
// coords: [100, 200];
|
|
335
|
-
// }
|
|
336
|
-
|
|
337
|
-
// const formController = new FormController<User>({});
|
|
338
|
-
// const fieldPathBuilder = new FieldPathBuilder<User>();
|
|
339
|
-
|
|
340
|
-
// formController.events.on("valueChanged", (fieldPath, value) => {
|
|
341
|
-
// // ^?
|
|
342
|
-
// });
|
|
343
|
-
|
|
344
|
-
// const path1 = fieldPathBuilder.fromProxy((data) => data.friends[0].tags[99]);
|
|
345
|
-
// const field1 = formController.getField(path1);
|
|
346
|
-
|
|
347
|
-
// const path2 = fieldPathBuilder.fromStringPath("coords[1]");
|
|
348
|
-
// const field2 = formController.getField(path2);
|