@goodie-forms/core 1.0.0-alpha → 1.1.1-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/index.js +757 -646
- package/dist/index.js.map +1 -1
- package/dist/src/form/CustomValidation.d.ts +10 -0
- package/dist/src/form/CustomValidation.d.ts.map +1 -0
- package/dist/src/form/Field.d.ts +31 -0
- package/dist/src/form/Field.d.ts.map +1 -0
- package/dist/src/form/FormController.d.ts +68 -0
- package/dist/src/form/FormController.d.ts.map +1 -0
- package/dist/{form → src/form}/FormField.d.ts +6 -3
- package/dist/src/form/FormField.d.ts.map +1 -0
- package/dist/src/form/NonullFormField.d.ts +10 -0
- package/dist/src/form/NonullFormField.d.ts.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/types/DeepPartial.d.ts +6 -0
- package/dist/src/types/DeepPartial.d.ts.map +1 -0
- package/dist/src/types/Mixin.d.ts +2 -0
- package/dist/src/types/Mixin.d.ts.map +1 -0
- package/dist/src/utils/ensureImmerability.d.ts +2 -0
- package/dist/src/utils/ensureImmerability.d.ts.map +1 -0
- package/dist/src/utils/getId.d.ts.map +1 -0
- package/dist/src/utils/removeBy.d.ts.map +1 -0
- package/dist/test/test1.d.ts +2 -0
- package/dist/test/test1.d.ts.map +1 -0
- package/package.json +27 -27
- package/src/form/CustomValidation.ts +52 -0
- package/src/form/Field.ts +334 -194
- package/src/form/FormController.ts +310 -290
- package/src/form/FormField.ts +202 -199
- package/src/form/NonullFormField.ts +21 -0
- package/src/index.ts +5 -3
- package/src/types/DeepPartial.ts +7 -3
- package/src/types/Mixin.ts +2 -0
- package/src/utils/ensureImmerability.ts +30 -0
- package/src/utils/getId.ts +5 -5
- package/src/utils/removeBy.ts +11 -11
- package/test/test1.ts +52 -0
- package/tsconfig.json +7 -7
- package/vite.config.ts +18 -18
- package/dist/form/Field.d.ts +0 -17
- package/dist/form/Field.d.ts.map +0 -1
- package/dist/form/FormController.d.ts +0 -62
- package/dist/form/FormController.d.ts.map +0 -1
- package/dist/form/FormField.d.ts.map +0 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/types/DeepPartial.d.ts +0 -4
- package/dist/types/DeepPartial.d.ts.map +0 -1
- package/dist/utils/getId.d.ts.map +0 -1
- package/dist/utils/removeBy.d.ts.map +0 -1
- /package/dist/{utils → src/utils}/getId.d.ts +0 -0
- /package/dist/{utils → src/utils}/removeBy.d.ts +0 -0
|
@@ -1,290 +1,310 @@
|
|
|
1
|
-
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
-
import { enableArrayMethods, enableMapSet, produce } from "immer";
|
|
3
|
-
import { createNanoEvents } from "nanoevents";
|
|
4
|
-
import { Field } from "../form/Field";
|
|
5
|
-
import { DeepPartial } from "../types/DeepPartial";
|
|
6
|
-
import { removeBy } from "../utils/removeBy";
|
|
7
|
-
import { FormField } from "./FormField";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export class FormController<TShape extends object
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
get
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
config?: {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
path
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
field
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async
|
|
233
|
-
if (this.
|
|
234
|
-
|
|
235
|
-
if (this.validationSchema == null) return;
|
|
236
|
-
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
1
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import { enableArrayMethods, enableMapSet, produce } from "immer";
|
|
3
|
+
import { createNanoEvents } from "nanoevents";
|
|
4
|
+
import { Field } from "../form/Field";
|
|
5
|
+
import { DeepPartial } from "../types/DeepPartial";
|
|
6
|
+
import { removeBy } from "../utils/removeBy";
|
|
7
|
+
import { FormField } from "./FormField";
|
|
8
|
+
import { ensureImmerability } from "../utils/ensureImmerability";
|
|
9
|
+
|
|
10
|
+
enableMapSet();
|
|
11
|
+
enableArrayMethods();
|
|
12
|
+
|
|
13
|
+
export namespace Form {
|
|
14
|
+
export type FormConfigs<TShape extends object> = ConstructorParameters<
|
|
15
|
+
typeof FormController<TShape>
|
|
16
|
+
>[0];
|
|
17
|
+
|
|
18
|
+
export interface PreventableEvent {
|
|
19
|
+
preventDefault(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SubmitSuccessHandler<
|
|
23
|
+
TShape extends object,
|
|
24
|
+
TEvent extends PreventableEvent
|
|
25
|
+
> = (
|
|
26
|
+
data: TShape,
|
|
27
|
+
event: TEvent,
|
|
28
|
+
abortSignal: AbortSignal
|
|
29
|
+
) => void | Promise<void>;
|
|
30
|
+
|
|
31
|
+
export type SubmitErrorHandler<TEvent extends PreventableEvent> = (
|
|
32
|
+
issues: StandardSchemaV1.Issue[],
|
|
33
|
+
event: TEvent,
|
|
34
|
+
abortSignal: AbortSignal
|
|
35
|
+
) => void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// TODO: Rename TShape to TOutput, as it represents the targetted data shape on successful submission cb
|
|
39
|
+
export class FormController<TShape extends object> {
|
|
40
|
+
_isValidating = false;
|
|
41
|
+
_isSubmitting = false;
|
|
42
|
+
|
|
43
|
+
_fields = new Map<Field.Paths<TShape>, FormField<TShape, any>>();
|
|
44
|
+
_initialData: DeepPartial<TShape>;
|
|
45
|
+
_data: DeepPartial<TShape>;
|
|
46
|
+
_issues: StandardSchemaV1.Issue[] = [];
|
|
47
|
+
|
|
48
|
+
equalityComparators?: Record<any, (a: any, b: any) => boolean>;
|
|
49
|
+
validationSchema?: StandardSchemaV1<unknown, TShape>;
|
|
50
|
+
|
|
51
|
+
public readonly events = createNanoEvents<{
|
|
52
|
+
submissionStatusChange(isSubmitting: boolean): void;
|
|
53
|
+
validationStatusChange(isValidating: boolean): void;
|
|
54
|
+
|
|
55
|
+
fieldBound(fieldPath: Field.Paths<TShape>): void;
|
|
56
|
+
fieldUnbound(fieldPath: Field.Paths<TShape>): void;
|
|
57
|
+
fieldTouchUpdated(path: Field.Paths<TShape>): void;
|
|
58
|
+
fieldDirtyUpdated(path: Field.Paths<TShape>): void;
|
|
59
|
+
fieldIssuesUpdated(fieldPath: Field.Paths<TShape>): void;
|
|
60
|
+
elementBound(fieldPath: Field.Paths<TShape>, el: HTMLElement): void;
|
|
61
|
+
elementUnbound(fieldPath: Field.Paths<TShape>): void;
|
|
62
|
+
validationTriggered(fieldPath: Field.Paths<TShape>): void;
|
|
63
|
+
valueChanged(
|
|
64
|
+
fieldPath: Field.Paths<TShape>,
|
|
65
|
+
newValue: Field.GetValue<TShape, Field.Paths<TShape>> | undefined,
|
|
66
|
+
oldValue: Field.GetValue<TShape, Field.Paths<TShape>> | undefined
|
|
67
|
+
): void;
|
|
68
|
+
}>();
|
|
69
|
+
|
|
70
|
+
constructor(config: {
|
|
71
|
+
initialData?: DeepPartial<TShape>;
|
|
72
|
+
validationSchema?: StandardSchemaV1<unknown, TShape>;
|
|
73
|
+
equalityComparators?: Record<any, (a: any, b: any) => boolean>;
|
|
74
|
+
}) {
|
|
75
|
+
this.validationSchema = config.validationSchema;
|
|
76
|
+
this.equalityComparators = config.equalityComparators;
|
|
77
|
+
this._initialData = config.initialData ?? ({} as DeepPartial<TShape>);
|
|
78
|
+
this._data = produce(this._initialData, () => {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get isDirty() {
|
|
82
|
+
for (const field of this._fields.values()) {
|
|
83
|
+
if (field.isDirty) return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get isValid() {
|
|
89
|
+
// TODO: Does it still count valid while validating?
|
|
90
|
+
return this._issues.length === 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get isValidating() {
|
|
94
|
+
return this._isValidating;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get isSubmitting() {
|
|
98
|
+
return this._isSubmitting;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected setValidating(newStatus: boolean) {
|
|
102
|
+
if (this._isValidating === newStatus) return;
|
|
103
|
+
this._isValidating = newStatus;
|
|
104
|
+
this.events.emit("validationStatusChange", newStatus);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected setSubmitting(newStatus: boolean) {
|
|
108
|
+
if (this._isSubmitting === newStatus) return;
|
|
109
|
+
this._isSubmitting = newStatus;
|
|
110
|
+
this.events.emit("submissionStatusChange", newStatus);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_unsafeSetFieldValue<TPath extends Field.Paths<TShape>>(
|
|
114
|
+
path: TPath,
|
|
115
|
+
value: Field.GetValue<TShape, TPath>,
|
|
116
|
+
config?: { updateInitialValue?: boolean }
|
|
117
|
+
) {
|
|
118
|
+
ensureImmerability(value);
|
|
119
|
+
|
|
120
|
+
if (config?.updateInitialValue === true) {
|
|
121
|
+
this._initialData = produce(this._initialData, (draft) => {
|
|
122
|
+
Field.setValue(draft as TShape, path, value);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
this._data = produce(this._data, (draft) => {
|
|
126
|
+
Field.setValue(draft as TShape, path, value);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// TODO: Rename to "register" ??
|
|
131
|
+
bindField<TPath extends Field.Paths<TShape>>(
|
|
132
|
+
path: TPath,
|
|
133
|
+
config?: {
|
|
134
|
+
defaultValue?: Field.GetValue<TShape, TPath>;
|
|
135
|
+
domElement?: HTMLElement;
|
|
136
|
+
overrideInitialValue?: boolean;
|
|
137
|
+
}
|
|
138
|
+
) {
|
|
139
|
+
let currentValue = Field.getValue(this._data as TShape, path);
|
|
140
|
+
|
|
141
|
+
if (currentValue == null && config?.defaultValue != null) {
|
|
142
|
+
this._unsafeSetFieldValue(path, config.defaultValue, {
|
|
143
|
+
updateInitialValue: config.overrideInitialValue,
|
|
144
|
+
});
|
|
145
|
+
currentValue = Field.getValue(this._data as TShape, path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const initialValue = Field.getValue(this._initialData as TShape, path);
|
|
149
|
+
|
|
150
|
+
const field = new FormField(this, path, {
|
|
151
|
+
isDirty: !Field.deepEqual(currentValue, initialValue),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (config?.domElement != null) {
|
|
155
|
+
field.bindElement(config.domElement);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this._fields.set(path, field);
|
|
159
|
+
this.events.emit("fieldBound", path);
|
|
160
|
+
|
|
161
|
+
return field;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
unbindField(path: Field.Paths<TShape>) {
|
|
165
|
+
this._fields.delete(path);
|
|
166
|
+
this.events.emit("fieldUnbound", path);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// TODO: Add an option to keep dirty/touched fields as they are
|
|
170
|
+
reset(newInitialData?: DeepPartial<TShape>) {
|
|
171
|
+
this._data = this._initialData;
|
|
172
|
+
this._issues = [];
|
|
173
|
+
|
|
174
|
+
for (const field of this._fields.values()) {
|
|
175
|
+
field.reset();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (newInitialData != null) {
|
|
179
|
+
this._initialData = newInitialData;
|
|
180
|
+
this._data = produce(this._initialData, () => {});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getAscendantFields<TPath extends Field.Paths<TShape>>(path: TPath) {
|
|
185
|
+
const pathFragments = Field.parsePathFragments(path);
|
|
186
|
+
|
|
187
|
+
const paths = pathFragments.map((_, i) => {
|
|
188
|
+
return Field.parsePath(
|
|
189
|
+
pathFragments.slice(0, i + 1)
|
|
190
|
+
) as Field.Paths<TShape>;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return paths.map((path) => this.getField(path)).filter((field) => !!field);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
getField<TPath extends Field.Paths<TShape>>(path: TPath) {
|
|
197
|
+
return this._fields.get(path) as FormField<TShape, TPath> | undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
clearFieldIssues<TPath extends Field.Paths<TShape>>(path: TPath) {
|
|
201
|
+
this._issues = this._issues.filter((issue) => {
|
|
202
|
+
if (issue.path == null) return true;
|
|
203
|
+
const issuePath = Field.parsePath(issue.path);
|
|
204
|
+
return issuePath !== path;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async applyValidation<TPath extends Field.Paths<TShape>>(
|
|
209
|
+
_result: StandardSchemaV1.Result<TShape>,
|
|
210
|
+
path: TPath
|
|
211
|
+
) {
|
|
212
|
+
const diff = Field.diff(
|
|
213
|
+
this._issues,
|
|
214
|
+
_result.issues ?? [],
|
|
215
|
+
Field.deepEqual,
|
|
216
|
+
(issue) => {
|
|
217
|
+
if (issue.path == null) return false;
|
|
218
|
+
const issuePath = Field.parsePath(issue.path);
|
|
219
|
+
return issuePath === path || Field.isDescendant(path, issuePath);
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
removeBy(this._issues, (issue) => diff.removed.includes(issue));
|
|
224
|
+
|
|
225
|
+
diff.added.forEach((issue) => this._issues.push(issue));
|
|
226
|
+
|
|
227
|
+
if (diff.added.length !== 0 || diff.removed.length !== 0) {
|
|
228
|
+
this.events.emit("fieldIssuesUpdated", path);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async validateField<TPath extends Field.Paths<TShape>>(path: TPath) {
|
|
233
|
+
if (this._isValidating) return;
|
|
234
|
+
|
|
235
|
+
if (this.validationSchema == null) return;
|
|
236
|
+
|
|
237
|
+
this.setValidating(true);
|
|
238
|
+
|
|
239
|
+
if (this.getField(path) == null) this.bindField(path);
|
|
240
|
+
|
|
241
|
+
const result = await this.validationSchema["~standard"].validate(
|
|
242
|
+
this._data
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
this.events.emit("validationTriggered", path);
|
|
246
|
+
this.applyValidation(result, path);
|
|
247
|
+
|
|
248
|
+
this.setValidating(false);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async validateForm() {
|
|
252
|
+
if (this._isValidating) return;
|
|
253
|
+
|
|
254
|
+
if (this.validationSchema == null) return;
|
|
255
|
+
|
|
256
|
+
this.setValidating(true);
|
|
257
|
+
|
|
258
|
+
const result = await this.validationSchema["~standard"].validate(
|
|
259
|
+
this._data
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
for (const path of this._fields.keys()) {
|
|
263
|
+
this.events.emit("validationTriggered", path);
|
|
264
|
+
this.applyValidation(result, path);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Append non-registered issues too
|
|
268
|
+
const diff = Field.diff(this._issues, result.issues ?? [], Field.deepEqual);
|
|
269
|
+
diff.added.forEach((issue) => this._issues.push(issue));
|
|
270
|
+
|
|
271
|
+
this.setValidating(false);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
createSubmitHandler<TEvent extends Form.PreventableEvent>(
|
|
275
|
+
onSuccess?: Form.SubmitSuccessHandler<TShape, TEvent>,
|
|
276
|
+
onError?: Form.SubmitErrorHandler<TEvent>
|
|
277
|
+
) {
|
|
278
|
+
return async (event: TEvent) => {
|
|
279
|
+
if (event != null) {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (this._isValidating) return;
|
|
284
|
+
if (this._isSubmitting) return;
|
|
285
|
+
|
|
286
|
+
const abortController = new AbortController();
|
|
287
|
+
|
|
288
|
+
await this.validateForm();
|
|
289
|
+
|
|
290
|
+
if (this._issues.length === 0) {
|
|
291
|
+
this.setSubmitting(true);
|
|
292
|
+
await onSuccess?.(this._data as TShape, event, abortController.signal);
|
|
293
|
+
this.setSubmitting(false);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const issue of this._issues) {
|
|
298
|
+
if (issue.path == null) continue;
|
|
299
|
+
const fieldPath = Field.parsePath(issue.path) as Field.Paths<TShape>;
|
|
300
|
+
const field = this.getField(fieldPath);
|
|
301
|
+
if (field == null) continue;
|
|
302
|
+
if (field.boundElement == null) continue;
|
|
303
|
+
field.focus();
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
await onError?.(this._issues, event, abortController.signal);
|
|
307
|
+
this.setSubmitting(false);
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|