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