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