@orkestrel/form 0.0.3 → 0.0.5
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 +13 -13
- package/dist/src/core/index.cjs +284 -338
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +255 -168
- package/dist/src/core/index.d.ts +255 -168
- package/dist/src/core/index.js +281 -340
- package/dist/src/core/index.js.map +1 -1
- package/package.json +17 -13
package/dist/src/core/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { arrayOf, attempt, cloneJSONRecord, isArray, isBoolean, isBoundedJSONRecord, isContractError, isFiniteNumber, isFunction, isInteger, isRecord, isString,
|
|
1
|
+
import { arrayOf, attempt, cloneJSONRecord, isArray, isBoolean, isBoundedJSONRecord, isContractError, isFiniteNumber, isFunction, isInteger, isRecord, isString, keyOf, parseNumber, readArrayEntries, recordOf, unionOf } from "@orkestrel/contract";
|
|
2
2
|
import { Emitter } from "@orkestrel/emitter";
|
|
3
3
|
//#region src/core/constants.ts
|
|
4
|
-
/**
|
|
4
|
+
/** Lists every field control, in the order declared by the public contract. */
|
|
5
5
|
var FIELD_CONTROLS = Object.freeze([
|
|
6
6
|
"text",
|
|
7
7
|
"editor",
|
|
@@ -16,13 +16,69 @@ var FIELD_CONTROLS = Object.freeze([
|
|
|
16
16
|
"checkbox",
|
|
17
17
|
"file"
|
|
18
18
|
]);
|
|
19
|
-
/**
|
|
19
|
+
/** Lists the members every field declares, whatever its control. */
|
|
20
|
+
var FIELD_BASE_KEYS = Object.freeze([
|
|
21
|
+
"control",
|
|
22
|
+
"name",
|
|
23
|
+
"label",
|
|
24
|
+
"help",
|
|
25
|
+
"group",
|
|
26
|
+
"hidden",
|
|
27
|
+
"disabled",
|
|
28
|
+
"locked",
|
|
29
|
+
"rule",
|
|
30
|
+
"meta"
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Lists every member one field control permits, composed from {@link FIELD_BASE_KEYS} and the
|
|
34
|
+
* members the control's own interface adds.
|
|
35
|
+
*/
|
|
36
|
+
var FIELD_KEYS = Object.freeze({
|
|
37
|
+
text: Object.freeze([
|
|
38
|
+
...FIELD_BASE_KEYS,
|
|
39
|
+
"default",
|
|
40
|
+
"placeholder"
|
|
41
|
+
]),
|
|
42
|
+
editor: Object.freeze([
|
|
43
|
+
...FIELD_BASE_KEYS,
|
|
44
|
+
"default",
|
|
45
|
+
"placeholder"
|
|
46
|
+
]),
|
|
47
|
+
password: Object.freeze([...FIELD_BASE_KEYS, "mask"]),
|
|
48
|
+
number: Object.freeze([
|
|
49
|
+
...FIELD_BASE_KEYS,
|
|
50
|
+
"default",
|
|
51
|
+
"placeholder"
|
|
52
|
+
]),
|
|
53
|
+
date: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
54
|
+
time: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
55
|
+
datetime: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
56
|
+
color: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
57
|
+
confirm: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
58
|
+
select: Object.freeze([
|
|
59
|
+
...FIELD_BASE_KEYS,
|
|
60
|
+
"choices",
|
|
61
|
+
"default",
|
|
62
|
+
"open"
|
|
63
|
+
]),
|
|
64
|
+
checkbox: Object.freeze([
|
|
65
|
+
...FIELD_BASE_KEYS,
|
|
66
|
+
"choices",
|
|
67
|
+
"default"
|
|
68
|
+
]),
|
|
69
|
+
file: Object.freeze([
|
|
70
|
+
...FIELD_BASE_KEYS,
|
|
71
|
+
"accept",
|
|
72
|
+
"multiple"
|
|
73
|
+
])
|
|
74
|
+
});
|
|
75
|
+
/** Lists every form lifecycle status. */
|
|
20
76
|
var FORM_STATUSES = Object.freeze([
|
|
21
77
|
"editing",
|
|
22
78
|
"settled",
|
|
23
79
|
"abandoned"
|
|
24
80
|
]);
|
|
25
|
-
/**
|
|
81
|
+
/** Holds the default failure copy for every named field rule. */
|
|
26
82
|
var RULE_MESSAGES = Object.freeze({
|
|
27
83
|
required: "This field is required",
|
|
28
84
|
minimum: "Must be at least {limit}",
|
|
@@ -34,50 +90,62 @@ var RULE_MESSAGES = Object.freeze({
|
|
|
34
90
|
integer: "Must be an integer",
|
|
35
91
|
alphanumeric: "Must contain only letters and numbers"
|
|
36
92
|
});
|
|
37
|
-
/**
|
|
93
|
+
/** Matches a practical whole-address email shape. */
|
|
38
94
|
var EMAIL_PATTERN = Object.freeze(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
|
39
|
-
/**
|
|
95
|
+
/** Matches an absolute HTTP or HTTPS URL shape. */
|
|
40
96
|
var URL_PATTERN = Object.freeze(/^https?:\/\/[^\s]+$/);
|
|
41
|
-
/**
|
|
97
|
+
/** Matches one or more ASCII letters or digits. */
|
|
42
98
|
var ALPHANUMERIC_PATTERN = Object.freeze(/^[A-Za-z0-9]+$/);
|
|
43
|
-
/**
|
|
99
|
+
/** Matches a signed or unsigned base-ten integer string. */
|
|
44
100
|
var INTEGER_PATTERN = Object.freeze(/^[+-]?\d+$/);
|
|
45
|
-
/**
|
|
101
|
+
/** Matches a six-digit hexadecimal color string. */
|
|
46
102
|
var COLOR_PATTERN = Object.freeze(/^#[0-9A-Fa-f]{6}$/);
|
|
47
|
-
/**
|
|
103
|
+
/** Matches an ISO calendar date string in `YYYY-MM-DD` form. */
|
|
48
104
|
var DATE_PATTERN = Object.freeze(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/);
|
|
49
|
-
/**
|
|
105
|
+
/** Matches a 24-hour time string with optional seconds. */
|
|
50
106
|
var TIME_PATTERN = Object.freeze(/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/);
|
|
51
|
-
/**
|
|
107
|
+
/** Matches an ISO local date and time string with optional seconds. */
|
|
52
108
|
var DATETIME_PATTERN = Object.freeze(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/);
|
|
53
|
-
/**
|
|
109
|
+
/** Caps the accepted source length for an authored regular expression. */
|
|
54
110
|
var PATTERN_LIMIT = 256;
|
|
55
|
-
/**
|
|
111
|
+
/** Caps the number of fields one schema may declare. */
|
|
56
112
|
var FIELD_LIMIT = 512;
|
|
57
|
-
/**
|
|
113
|
+
/** Caps the number of groups one schema may declare. */
|
|
58
114
|
var GROUP_LIMIT = 64;
|
|
59
|
-
/**
|
|
115
|
+
/** Caps the number of choices one `select` or `checkbox` field may offer. */
|
|
60
116
|
var CHOICE_LIMIT = 1024;
|
|
61
|
-
/**
|
|
117
|
+
/** Caps the number of entries one list-valued answer may hold. */
|
|
62
118
|
var LIST_LIMIT = 1024;
|
|
63
|
-
/**
|
|
119
|
+
/** Caps the length, in UTF-16 code units, of a schema, group, or field name. */
|
|
64
120
|
var NAME_LIMIT = 128;
|
|
65
|
-
/**
|
|
121
|
+
/** Caps the length, in UTF-16 code units, of any single retained string. */
|
|
66
122
|
var STRING_LIMIT = 65536;
|
|
67
|
-
/**
|
|
123
|
+
/** Caps the total length, in UTF-16 code units, of every string one schema retains. */
|
|
68
124
|
var TEXT_LIMIT = 1048576;
|
|
69
|
-
/**
|
|
125
|
+
/** Caps the total number of records, arrays, and leaves one schema retains. */
|
|
70
126
|
var NODE_LIMIT = 16384;
|
|
71
127
|
//#endregion
|
|
72
128
|
//#region src/core/errors.ts
|
|
73
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Represents an error raised by the form domain.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const error = new FormError('FIELD', 'The schema declares no field named "nickname"', {
|
|
135
|
+
* field: 'nickname',
|
|
136
|
+
* })
|
|
137
|
+
*
|
|
138
|
+
* error.code // 'FIELD'
|
|
139
|
+
* error.context // { field: 'nickname' }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
74
142
|
var FormError = class extends Error {
|
|
75
|
-
/**
|
|
143
|
+
/** Holds the machine-readable reason for this failure. */
|
|
76
144
|
code;
|
|
77
|
-
/**
|
|
145
|
+
/** Holds structured values that locate or explain this failure. */
|
|
78
146
|
context;
|
|
79
147
|
/**
|
|
80
|
-
*
|
|
148
|
+
* Creates a form error.
|
|
81
149
|
*
|
|
82
150
|
* @param code - The machine-readable reason.
|
|
83
151
|
* @param message - The human-readable failure text.
|
|
@@ -91,10 +159,10 @@ var FormError = class extends Error {
|
|
|
91
159
|
}
|
|
92
160
|
};
|
|
93
161
|
/**
|
|
94
|
-
*
|
|
162
|
+
* Determines whether an unknown value is a form error.
|
|
95
163
|
*
|
|
96
164
|
* @param input - The value to inspect.
|
|
97
|
-
* @returns
|
|
165
|
+
* @returns True if the value is a {@link FormError} instance; false otherwise.
|
|
98
166
|
*/
|
|
99
167
|
function isFormError(input) {
|
|
100
168
|
return input instanceof FormError;
|
|
@@ -102,37 +170,37 @@ function isFormError(input) {
|
|
|
102
170
|
//#endregion
|
|
103
171
|
//#region src/core/validators.ts
|
|
104
172
|
/**
|
|
105
|
-
*
|
|
173
|
+
* Determines whether an unknown value is a declared field control.
|
|
106
174
|
*
|
|
107
175
|
* @param input - The value to inspect.
|
|
108
|
-
* @returns
|
|
176
|
+
* @returns True if the value is a field control; false otherwise.
|
|
109
177
|
*/
|
|
110
178
|
function isFieldControl(input) {
|
|
111
179
|
return FIELD_CONTROLS.some((control) => control === input);
|
|
112
180
|
}
|
|
113
181
|
/**
|
|
114
|
-
*
|
|
182
|
+
* Determines whether an unknown value is a form lifecycle status.
|
|
115
183
|
*
|
|
116
184
|
* @param input - The value to inspect.
|
|
117
|
-
* @returns
|
|
185
|
+
* @returns True if the value is a form status; false otherwise.
|
|
118
186
|
*/
|
|
119
187
|
function isFormStatus(input) {
|
|
120
188
|
return FORM_STATUSES.some((status) => status === input);
|
|
121
189
|
}
|
|
122
190
|
/**
|
|
123
|
-
*
|
|
191
|
+
* Determines whether an unknown value has a form field value shape.
|
|
124
192
|
*
|
|
125
193
|
* @param input - The value to inspect.
|
|
126
|
-
* @returns
|
|
194
|
+
* @returns True if the value is a field value; false otherwise.
|
|
127
195
|
*/
|
|
128
196
|
function isFieldValue(input) {
|
|
129
197
|
return unionOf(isString, isFiniteNumber, isBoolean, arrayOf(isString))(input);
|
|
130
198
|
}
|
|
131
199
|
/**
|
|
132
|
-
*
|
|
200
|
+
* Determines whether an unknown value is one exact field choice record.
|
|
133
201
|
*
|
|
134
202
|
* @param input - The value to inspect.
|
|
135
|
-
* @returns
|
|
203
|
+
* @returns True if the value is a field choice; false otherwise.
|
|
136
204
|
*/
|
|
137
205
|
function isFieldChoice(input) {
|
|
138
206
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -145,10 +213,10 @@ function isFieldChoice(input) {
|
|
|
145
213
|
}, ["help", "disabled"])(input);
|
|
146
214
|
}
|
|
147
215
|
/**
|
|
148
|
-
*
|
|
216
|
+
* Determines whether an unknown value is one exact field rule record.
|
|
149
217
|
*
|
|
150
218
|
* @param input - The value to inspect.
|
|
151
|
-
* @returns
|
|
219
|
+
* @returns True if the value is a structurally valid field rule; false otherwise.
|
|
152
220
|
*/
|
|
153
221
|
function isFieldRule(input) {
|
|
154
222
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -167,7 +235,7 @@ function isFieldRule(input) {
|
|
|
167
235
|
}, true)(input);
|
|
168
236
|
}
|
|
169
237
|
/**
|
|
170
|
-
*
|
|
238
|
+
* Determines whether an unknown value is one exact discriminated form field.
|
|
171
239
|
*
|
|
172
240
|
* @remarks
|
|
173
241
|
* Metadata is admitted structurally as bounded JSON. An accessor-bearing metadata record is
|
|
@@ -175,120 +243,15 @@ function isFieldRule(input) {
|
|
|
175
243
|
* data properties only.
|
|
176
244
|
*
|
|
177
245
|
* @param input - The value to inspect.
|
|
178
|
-
* @returns
|
|
246
|
+
* @returns True if the value is a structurally valid form field; false otherwise.
|
|
179
247
|
*/
|
|
180
248
|
function isFormField(input) {
|
|
181
249
|
const outcome = attempt(() => {
|
|
182
250
|
if (!isRecord(input) || !Object.hasOwn(input, "control") || !Object.hasOwn(input, "name")) return false;
|
|
183
251
|
const control = input.control;
|
|
184
252
|
if (!isFieldControl(control)) return false;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
switch (control) {
|
|
188
|
-
case "text":
|
|
189
|
-
case "editor": return [
|
|
190
|
-
"control",
|
|
191
|
-
"name",
|
|
192
|
-
"label",
|
|
193
|
-
"help",
|
|
194
|
-
"group",
|
|
195
|
-
"hidden",
|
|
196
|
-
"disabled",
|
|
197
|
-
"locked",
|
|
198
|
-
"rule",
|
|
199
|
-
"meta",
|
|
200
|
-
"default",
|
|
201
|
-
"placeholder"
|
|
202
|
-
].includes(key);
|
|
203
|
-
case "password": return [
|
|
204
|
-
"control",
|
|
205
|
-
"name",
|
|
206
|
-
"label",
|
|
207
|
-
"help",
|
|
208
|
-
"group",
|
|
209
|
-
"hidden",
|
|
210
|
-
"disabled",
|
|
211
|
-
"locked",
|
|
212
|
-
"rule",
|
|
213
|
-
"meta",
|
|
214
|
-
"mask"
|
|
215
|
-
].includes(key);
|
|
216
|
-
case "number": return [
|
|
217
|
-
"control",
|
|
218
|
-
"name",
|
|
219
|
-
"label",
|
|
220
|
-
"help",
|
|
221
|
-
"group",
|
|
222
|
-
"hidden",
|
|
223
|
-
"disabled",
|
|
224
|
-
"locked",
|
|
225
|
-
"rule",
|
|
226
|
-
"meta",
|
|
227
|
-
"default",
|
|
228
|
-
"placeholder"
|
|
229
|
-
].includes(key);
|
|
230
|
-
case "date":
|
|
231
|
-
case "time":
|
|
232
|
-
case "datetime":
|
|
233
|
-
case "color":
|
|
234
|
-
case "confirm": return [
|
|
235
|
-
"control",
|
|
236
|
-
"name",
|
|
237
|
-
"label",
|
|
238
|
-
"help",
|
|
239
|
-
"group",
|
|
240
|
-
"hidden",
|
|
241
|
-
"disabled",
|
|
242
|
-
"locked",
|
|
243
|
-
"rule",
|
|
244
|
-
"meta",
|
|
245
|
-
"default"
|
|
246
|
-
].includes(key);
|
|
247
|
-
case "select": return [
|
|
248
|
-
"control",
|
|
249
|
-
"name",
|
|
250
|
-
"label",
|
|
251
|
-
"help",
|
|
252
|
-
"group",
|
|
253
|
-
"hidden",
|
|
254
|
-
"disabled",
|
|
255
|
-
"locked",
|
|
256
|
-
"rule",
|
|
257
|
-
"meta",
|
|
258
|
-
"choices",
|
|
259
|
-
"default",
|
|
260
|
-
"open"
|
|
261
|
-
].includes(key);
|
|
262
|
-
case "checkbox": return [
|
|
263
|
-
"control",
|
|
264
|
-
"name",
|
|
265
|
-
"label",
|
|
266
|
-
"help",
|
|
267
|
-
"group",
|
|
268
|
-
"hidden",
|
|
269
|
-
"disabled",
|
|
270
|
-
"locked",
|
|
271
|
-
"rule",
|
|
272
|
-
"meta",
|
|
273
|
-
"choices",
|
|
274
|
-
"default"
|
|
275
|
-
].includes(key);
|
|
276
|
-
case "file": return [
|
|
277
|
-
"control",
|
|
278
|
-
"name",
|
|
279
|
-
"label",
|
|
280
|
-
"help",
|
|
281
|
-
"group",
|
|
282
|
-
"hidden",
|
|
283
|
-
"disabled",
|
|
284
|
-
"locked",
|
|
285
|
-
"rule",
|
|
286
|
-
"meta",
|
|
287
|
-
"accept",
|
|
288
|
-
"multiple"
|
|
289
|
-
].includes(key);
|
|
290
|
-
}
|
|
291
|
-
})) return false;
|
|
253
|
+
const permitted = FIELD_KEYS[control];
|
|
254
|
+
if (!Reflect.ownKeys(input).every((key) => isString(key) && permitted.includes(key))) return false;
|
|
292
255
|
const name = input.name;
|
|
293
256
|
const hasLabel = Object.hasOwn(input, "label");
|
|
294
257
|
const label = hasLabel ? input.label : void 0;
|
|
@@ -369,10 +332,10 @@ function isFormField(input) {
|
|
|
369
332
|
return outcome.success && outcome.value;
|
|
370
333
|
}
|
|
371
334
|
/**
|
|
372
|
-
*
|
|
335
|
+
* Determines whether an unknown value is one exact form group record.
|
|
373
336
|
*
|
|
374
337
|
* @param input - The value to inspect.
|
|
375
|
-
* @returns
|
|
338
|
+
* @returns True if the value is a form group; false otherwise.
|
|
376
339
|
*/
|
|
377
340
|
function isFormGroup(input) {
|
|
378
341
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -384,10 +347,10 @@ function isFormGroup(input) {
|
|
|
384
347
|
}, ["help"])(input);
|
|
385
348
|
}
|
|
386
349
|
/**
|
|
387
|
-
*
|
|
350
|
+
* Determines whether an unknown value is one exact structural form schema.
|
|
388
351
|
*
|
|
389
352
|
* @param input - The value to inspect.
|
|
390
|
-
* @returns
|
|
353
|
+
* @returns True if the value is a structurally valid form schema; false otherwise.
|
|
391
354
|
*/
|
|
392
355
|
function isFormSchema(input) {
|
|
393
356
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -406,10 +369,10 @@ function isFormSchema(input) {
|
|
|
406
369
|
])(input);
|
|
407
370
|
}
|
|
408
371
|
/**
|
|
409
|
-
*
|
|
372
|
+
* Determines whether an unknown value is a record of field values.
|
|
410
373
|
*
|
|
411
374
|
* @param input - The value to inspect.
|
|
412
|
-
* @returns
|
|
375
|
+
* @returns True if the value is a form values record; false otherwise.
|
|
413
376
|
*/
|
|
414
377
|
function isFormValues(input) {
|
|
415
378
|
const outcome = attempt(() => {
|
|
@@ -419,10 +382,10 @@ function isFormValues(input) {
|
|
|
419
382
|
return outcome.success && outcome.value;
|
|
420
383
|
}
|
|
421
384
|
/**
|
|
422
|
-
*
|
|
385
|
+
* Determines whether an unknown value is one exact field error record.
|
|
423
386
|
*
|
|
424
387
|
* @param input - The value to inspect.
|
|
425
|
-
* @returns
|
|
388
|
+
* @returns True if the value is a field error; false otherwise.
|
|
426
389
|
*/
|
|
427
390
|
function isFieldError(input) {
|
|
428
391
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -430,7 +393,7 @@ function isFieldError(input) {
|
|
|
430
393
|
return recordOf({
|
|
431
394
|
field: isString,
|
|
432
395
|
message: isString,
|
|
433
|
-
rule:
|
|
396
|
+
rule: keyOf(RULE_MESSAGES)
|
|
434
397
|
}, ["rule"])(input);
|
|
435
398
|
}
|
|
436
399
|
//#endregion
|
|
@@ -439,7 +402,7 @@ function cloneValue(value) {
|
|
|
439
402
|
return isArray(value) ? Object.freeze(value.slice()) : value;
|
|
440
403
|
}
|
|
441
404
|
/**
|
|
442
|
-
*
|
|
405
|
+
* Clones a field's choices into an owned frozen snapshot.
|
|
443
406
|
*
|
|
444
407
|
* @param choices - The choices to own.
|
|
445
408
|
* @returns A frozen list of frozen choice records.
|
|
@@ -448,7 +411,7 @@ function cloneChoices(choices) {
|
|
|
448
411
|
return Object.freeze(choices.map((choice) => Object.freeze({ ...choice })));
|
|
449
412
|
}
|
|
450
413
|
/**
|
|
451
|
-
*
|
|
414
|
+
* Clones one form field into an owned frozen snapshot.
|
|
452
415
|
*
|
|
453
416
|
* @param field - The field to own.
|
|
454
417
|
* @returns A frozen field with every nested collection owned.
|
|
@@ -499,7 +462,7 @@ function cloneFormField(field) {
|
|
|
499
462
|
}
|
|
500
463
|
}
|
|
501
464
|
/**
|
|
502
|
-
*
|
|
465
|
+
* Clones a form schema into an owned frozen snapshot.
|
|
503
466
|
*
|
|
504
467
|
* @param schema - The schema to own.
|
|
505
468
|
* @returns A frozen schema with every nested record and list owned.
|
|
@@ -515,11 +478,64 @@ function cloneFormSchema(schema) {
|
|
|
515
478
|
//#endregion
|
|
516
479
|
//#region src/core/helpers.ts
|
|
517
480
|
/**
|
|
518
|
-
*
|
|
481
|
+
* Writes one own enumerable data property onto a record.
|
|
482
|
+
*
|
|
483
|
+
* @param target - The record to write into.
|
|
484
|
+
* @param name - The property name to write.
|
|
485
|
+
* @param value - The value to store.
|
|
486
|
+
*
|
|
487
|
+
* @remarks
|
|
488
|
+
* Plain assignment runs an inherited setter, so writing a `__proto__` key that way reaches
|
|
489
|
+
* `Object.prototype` and leaves the record without the entry. Defining the property writes the
|
|
490
|
+
* record itself, whatever the prototype chain declares. The entry stays writable and configurable.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* const values: Record<string, number> = {}
|
|
495
|
+
* defineEntry(values, '__proto__', 1)
|
|
496
|
+
* Object.hasOwn(values, '__proto__') // true
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
function defineEntry(target, name, value) {
|
|
500
|
+
Object.defineProperty(target, name, {
|
|
501
|
+
value,
|
|
502
|
+
enumerable: true,
|
|
503
|
+
configurable: true,
|
|
504
|
+
writable: true
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Writes one own enumerable data property that cannot be rewritten or removed.
|
|
509
|
+
*
|
|
510
|
+
* @param target - The record to write into.
|
|
511
|
+
* @param name - The property name to write.
|
|
512
|
+
* @param value - The value to store.
|
|
513
|
+
*
|
|
514
|
+
* @remarks
|
|
515
|
+
* The prototype-safe write of {@link defineEntry}, frozen: the entry is neither writable nor
|
|
516
|
+
* configurable, so the record a parser hands back cannot be edited through the key it just filled.
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* const values: Record<string, number> = {}
|
|
521
|
+
* freezeEntry(values, '__proto__', 1)
|
|
522
|
+
* Object.getOwnPropertyDescriptor(values, '__proto__')?.writable // false
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
function freezeEntry(target, name, value) {
|
|
526
|
+
Object.defineProperty(target, name, {
|
|
527
|
+
value,
|
|
528
|
+
enumerable: true,
|
|
529
|
+
configurable: false,
|
|
530
|
+
writable: false
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Checks whether a value has the shape required by one field control.
|
|
519
535
|
*
|
|
520
536
|
* @param field - The field that owns the value.
|
|
521
537
|
* @param value - The unknown value to inspect.
|
|
522
|
-
* @returns
|
|
538
|
+
* @returns True if the control can hold the value; false otherwise.
|
|
523
539
|
*/
|
|
524
540
|
function matchesField(field, value) {
|
|
525
541
|
if (isString(value) && value.length > 65536) return false;
|
|
@@ -548,7 +564,7 @@ function matchesField(field, value) {
|
|
|
548
564
|
}
|
|
549
565
|
}
|
|
550
566
|
/**
|
|
551
|
-
*
|
|
567
|
+
* Decides whether a raw binding value projects to an answered field.
|
|
552
568
|
*
|
|
553
569
|
* @remarks
|
|
554
570
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
@@ -557,13 +573,13 @@ function matchesField(field, value) {
|
|
|
557
573
|
* its `required` rule remains presence-only.
|
|
558
574
|
*
|
|
559
575
|
* @param value - The raw field value, or absence.
|
|
560
|
-
* @returns
|
|
576
|
+
* @returns True if the binding preserves the value as an answer; false otherwise.
|
|
561
577
|
*/
|
|
562
578
|
function matchesAnswer(value) {
|
|
563
579
|
return value !== void 0 && (!isString(value) || value.trim().length > 0);
|
|
564
580
|
}
|
|
565
581
|
/**
|
|
566
|
-
*
|
|
582
|
+
* Checks whether a named rule applies to one field control.
|
|
567
583
|
*
|
|
568
584
|
* @remarks
|
|
569
585
|
* The runtime control-membership check keeps this boundary total for JavaScript callers that
|
|
@@ -571,7 +587,7 @@ function matchesAnswer(value) {
|
|
|
571
587
|
*
|
|
572
588
|
* @param control - The field control to inspect.
|
|
573
589
|
* @param rule - The named rule to inspect.
|
|
574
|
-
* @returns
|
|
590
|
+
* @returns True if the control evaluates that rule; false otherwise.
|
|
575
591
|
*/
|
|
576
592
|
function appliesRule(control, rule) {
|
|
577
593
|
if (!FIELD_CONTROLS.some((candidate) => candidate === control)) return false;
|
|
@@ -589,23 +605,21 @@ function appliesRule(control, rule) {
|
|
|
589
605
|
return false;
|
|
590
606
|
}
|
|
591
607
|
/**
|
|
592
|
-
*
|
|
608
|
+
* Evaluates one field rule against its current value.
|
|
593
609
|
*
|
|
594
610
|
* @param field - The field and rule to evaluate.
|
|
595
611
|
* @param value - The current value, or absence.
|
|
596
612
|
* @param values - Every value available to a custom rule.
|
|
597
613
|
* @param messages - Optional rule-specific message replacements.
|
|
598
614
|
* @returns Every failure in rule order.
|
|
615
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
616
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
599
617
|
*/
|
|
600
618
|
function evaluateField(field, value, values, messages) {
|
|
601
619
|
const errors = [];
|
|
602
620
|
const rule = field.rule;
|
|
603
621
|
if (value === void 0) {
|
|
604
|
-
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(
|
|
605
|
-
field: field.name,
|
|
606
|
-
message: formatMessage("required", void 0, messages),
|
|
607
|
-
rule: "required"
|
|
608
|
-
}));
|
|
622
|
+
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
|
|
609
623
|
}
|
|
610
624
|
if (rule === void 0) return Object.freeze(errors);
|
|
611
625
|
if (rule.minimum !== void 0 && appliesRule(field.control, "minimum")) {
|
|
@@ -627,11 +641,7 @@ function evaluateField(field, value, values, messages) {
|
|
|
627
641
|
case "checkbox":
|
|
628
642
|
case "file": failed = isArray(value) && isFiniteNumber(rule.minimum) && value.length < rule.minimum;
|
|
629
643
|
}
|
|
630
|
-
if (failed) errors.push(
|
|
631
|
-
field: field.name,
|
|
632
|
-
message: formatMessage("minimum", rule.minimum, messages),
|
|
633
|
-
rule: "minimum"
|
|
634
|
-
}));
|
|
644
|
+
if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
|
|
635
645
|
}
|
|
636
646
|
if (rule.maximum !== void 0 && appliesRule(field.control, "maximum")) {
|
|
637
647
|
let failed = false;
|
|
@@ -652,63 +662,27 @@ function evaluateField(field, value, values, messages) {
|
|
|
652
662
|
case "checkbox":
|
|
653
663
|
case "file": failed = isArray(value) && isFiniteNumber(rule.maximum) && value.length > rule.maximum;
|
|
654
664
|
}
|
|
655
|
-
if (failed) errors.push(
|
|
656
|
-
field: field.name,
|
|
657
|
-
message: formatMessage("maximum", rule.maximum, messages),
|
|
658
|
-
rule: "maximum"
|
|
659
|
-
}));
|
|
665
|
+
if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
|
|
660
666
|
}
|
|
661
667
|
if (rule.step !== void 0 && appliesRule(field.control, "step") && isFiniteNumber(value)) {
|
|
662
668
|
const multiple = (value - (isFiniteNumber(rule.minimum) ? rule.minimum : 0)) / rule.step;
|
|
663
|
-
if (!isFiniteNumber(rule.step) || rule.step === 0 || !isFiniteNumber(multiple) || Math.abs(multiple - Math.round(multiple)) > 1e-9) errors.push(
|
|
664
|
-
field: field.name,
|
|
665
|
-
message: formatMessage("step", rule.step, messages),
|
|
666
|
-
rule: "step"
|
|
667
|
-
}));
|
|
669
|
+
if (!isFiniteNumber(rule.step) || rule.step === 0 || !isFiniteNumber(multiple) || Math.abs(multiple - Math.round(multiple)) > 1e-9) errors.push(createFieldError(field, "step", rule.step, messages));
|
|
668
670
|
}
|
|
669
671
|
if (isString(value)) {
|
|
670
672
|
if (rule.pattern !== void 0 && appliesRule(field.control, "pattern")) {
|
|
671
673
|
const pattern = rule.pattern;
|
|
672
|
-
if (pattern.length > 256) errors.push(
|
|
673
|
-
field: field.name,
|
|
674
|
-
message: formatMessage("pattern", void 0, messages),
|
|
675
|
-
rule: "pattern"
|
|
676
|
-
}));
|
|
674
|
+
if (pattern.length > 256) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
677
675
|
else {
|
|
678
676
|
const outcome = attempt(() => new RegExp(pattern).test(value));
|
|
679
|
-
if (!outcome.success || !outcome.value) errors.push(
|
|
680
|
-
field: field.name,
|
|
681
|
-
message: formatMessage("pattern", void 0, messages),
|
|
682
|
-
rule: "pattern"
|
|
683
|
-
}));
|
|
677
|
+
if (!outcome.success || !outcome.value) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
684
678
|
}
|
|
685
679
|
}
|
|
686
|
-
if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}));
|
|
691
|
-
if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(Object.freeze({
|
|
692
|
-
field: field.name,
|
|
693
|
-
message: formatMessage("url", void 0, messages),
|
|
694
|
-
rule: "url"
|
|
695
|
-
}));
|
|
696
|
-
if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(Object.freeze({
|
|
697
|
-
field: field.name,
|
|
698
|
-
message: formatMessage("alphanumeric", void 0, messages),
|
|
699
|
-
rule: "alphanumeric"
|
|
700
|
-
}));
|
|
701
|
-
if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(Object.freeze({
|
|
702
|
-
field: field.name,
|
|
703
|
-
message: formatMessage("integer", void 0, messages),
|
|
704
|
-
rule: "integer"
|
|
705
|
-
}));
|
|
680
|
+
if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(createFieldError(field, "email", void 0, messages));
|
|
681
|
+
if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(createFieldError(field, "url", void 0, messages));
|
|
682
|
+
if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(createFieldError(field, "alphanumeric", void 0, messages));
|
|
683
|
+
if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
706
684
|
}
|
|
707
|
-
if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !isInteger(value)) errors.push(
|
|
708
|
-
field: field.name,
|
|
709
|
-
message: formatMessage("integer", void 0, messages),
|
|
710
|
-
rule: "integer"
|
|
711
|
-
}));
|
|
685
|
+
if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !isInteger(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
712
686
|
if (rule.custom !== void 0) {
|
|
713
687
|
const result = rule.custom(value, values);
|
|
714
688
|
if (isString(result)) errors.push(Object.freeze({
|
|
@@ -719,12 +693,14 @@ function evaluateField(field, value, values, messages) {
|
|
|
719
693
|
return Object.freeze(errors);
|
|
720
694
|
}
|
|
721
695
|
/**
|
|
722
|
-
*
|
|
696
|
+
* Evaluates every active field in schema order.
|
|
723
697
|
*
|
|
724
698
|
* @param schema - The form schema to evaluate.
|
|
725
699
|
* @param values - The values keyed by field name.
|
|
726
700
|
* @param options - Optional message replacements and the effective disabled field set.
|
|
727
701
|
* @returns Every field failure in schema and rule order.
|
|
702
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
703
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
728
704
|
*/
|
|
729
705
|
function evaluateForm(schema, values, options) {
|
|
730
706
|
const errors = [];
|
|
@@ -735,7 +711,7 @@ function evaluateForm(schema, values, options) {
|
|
|
735
711
|
return Object.freeze(errors);
|
|
736
712
|
}
|
|
737
713
|
/**
|
|
738
|
-
*
|
|
714
|
+
* Computes the values explicitly seeded by a schema.
|
|
739
715
|
*
|
|
740
716
|
* @param schema - The schema whose defaults to collect.
|
|
741
717
|
* @returns A value record containing only fields with defaults.
|
|
@@ -744,12 +720,7 @@ function computeDefaults(schema) {
|
|
|
744
720
|
const defaults = {};
|
|
745
721
|
for (const field of schema.fields) switch (field.control) {
|
|
746
722
|
case "checkbox":
|
|
747
|
-
if (field.default !== void 0)
|
|
748
|
-
value: cloneValue(field.default),
|
|
749
|
-
enumerable: true,
|
|
750
|
-
configurable: true,
|
|
751
|
-
writable: true
|
|
752
|
-
});
|
|
723
|
+
if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
|
|
753
724
|
break;
|
|
754
725
|
case "password":
|
|
755
726
|
case "file": break;
|
|
@@ -761,35 +732,30 @@ function computeDefaults(schema) {
|
|
|
761
732
|
case "datetime":
|
|
762
733
|
case "color":
|
|
763
734
|
case "confirm":
|
|
764
|
-
case "select": if (field.default !== void 0)
|
|
765
|
-
value: field.default,
|
|
766
|
-
enumerable: true,
|
|
767
|
-
configurable: true,
|
|
768
|
-
writable: true
|
|
769
|
-
});
|
|
735
|
+
case "select": if (field.default !== void 0) defineEntry(defaults, field.name, field.default);
|
|
770
736
|
}
|
|
771
737
|
return Object.freeze(defaults);
|
|
772
738
|
}
|
|
773
739
|
/**
|
|
774
|
-
*
|
|
740
|
+
* Compares two field values by scalar identity or ordered list content.
|
|
775
741
|
*
|
|
776
742
|
* @param a - The first field value.
|
|
777
743
|
* @param b - The second field value.
|
|
778
|
-
* @returns
|
|
744
|
+
* @returns True if both values contain the same answer; false otherwise.
|
|
779
745
|
*/
|
|
780
746
|
function matchesValue(a, b) {
|
|
781
747
|
if (isArray(a) || isArray(b)) return isArray(a) && isArray(b) && a.length === b.length && a.every((entry, index) => entry === b[index]);
|
|
782
748
|
return a === b;
|
|
783
749
|
}
|
|
784
750
|
/**
|
|
785
|
-
*
|
|
751
|
+
* Extracts the names whose answers differ between two form value records.
|
|
786
752
|
*
|
|
787
753
|
* @remarks
|
|
788
754
|
* Presence is compared in both directions before present values are compared through
|
|
789
755
|
* {@link matchesValue}. The returned set is a new snapshot, exposed as readonly because later
|
|
790
756
|
* changes to either input never alter its membership.
|
|
791
757
|
*
|
|
792
|
-
* @param current - The values
|
|
758
|
+
* @param current - The values the form holds.
|
|
793
759
|
* @param opened - The values held when the form opened.
|
|
794
760
|
* @returns A readonly snapshot of changed field names.
|
|
795
761
|
*/
|
|
@@ -808,17 +774,17 @@ function extractChanges(current, opened) {
|
|
|
808
774
|
return changed;
|
|
809
775
|
}
|
|
810
776
|
/**
|
|
811
|
-
*
|
|
777
|
+
* Compares two form value records by keys and value content.
|
|
812
778
|
*
|
|
813
779
|
* @param a - The first value record.
|
|
814
780
|
* @param b - The second value record.
|
|
815
|
-
* @returns
|
|
781
|
+
* @returns True if both records contain the same answers; false otherwise.
|
|
816
782
|
*/
|
|
817
783
|
function matchesValues(a, b) {
|
|
818
784
|
return extractChanges(a, b).size === 0;
|
|
819
785
|
}
|
|
820
786
|
/**
|
|
821
|
-
*
|
|
787
|
+
* Resolves and interpolates one rule message.
|
|
822
788
|
*
|
|
823
789
|
* @param rule - The rule whose message to resolve.
|
|
824
790
|
* @param limit - The optional operand substituted for `{limit}`.
|
|
@@ -830,7 +796,34 @@ function formatMessage(rule, limit, messages) {
|
|
|
830
796
|
return limit === void 0 ? message : message.replaceAll("{limit}", String(limit));
|
|
831
797
|
}
|
|
832
798
|
/**
|
|
833
|
-
*
|
|
799
|
+
* Creates one named-rule failure against a field.
|
|
800
|
+
*
|
|
801
|
+
* @param field - The field the rule failed on.
|
|
802
|
+
* @param rule - The named rule that failed.
|
|
803
|
+
* @param limit - The rule's operand, substituted for `{limit}`, or absence when it carries none.
|
|
804
|
+
* @param messages - Optional rule-specific message replacements.
|
|
805
|
+
* @returns A frozen {@link FieldError} carrying the field's name, the resolved text, and the rule.
|
|
806
|
+
*
|
|
807
|
+
* @remarks
|
|
808
|
+
* A `custom` validator and the form's `invalidate` method both report a message of their own
|
|
809
|
+
* under no rule name, so neither builds its failure here.
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```ts
|
|
813
|
+
* const field: FormField = { control: 'text', name: 'nickname', rule: { minimum: 3 } }
|
|
814
|
+
* const error = createFieldError(field, 'minimum', 3)
|
|
815
|
+
* error.message // 'Must be at least 3'
|
|
816
|
+
* ```
|
|
817
|
+
*/
|
|
818
|
+
function createFieldError(field, rule, limit, messages) {
|
|
819
|
+
return Object.freeze({
|
|
820
|
+
field: field.name,
|
|
821
|
+
message: formatMessage(rule, limit, messages),
|
|
822
|
+
rule
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Projects a schema into JSON while removing custom validators and absent values.
|
|
834
827
|
*
|
|
835
828
|
* @param schema - The schema to project.
|
|
836
829
|
* @returns A deep JSON copy of the serializable schema.
|
|
@@ -932,7 +925,7 @@ function serializeForm(schema) {
|
|
|
932
925
|
return cloneJSONRecord(output);
|
|
933
926
|
}
|
|
934
927
|
/**
|
|
935
|
-
*
|
|
928
|
+
* Selects referenced groups in first-reference field order.
|
|
936
929
|
*
|
|
937
930
|
* @param schema - The schema whose group references to resolve.
|
|
938
931
|
* @returns The referenced schema groups without duplicates.
|
|
@@ -947,7 +940,7 @@ function extractGroups(schema) {
|
|
|
947
940
|
return Object.freeze(groups);
|
|
948
941
|
}
|
|
949
942
|
/**
|
|
950
|
-
*
|
|
943
|
+
* Audits a structurally valid schema for domain invariants.
|
|
951
944
|
*
|
|
952
945
|
* @param schema - The form schema to audit.
|
|
953
946
|
* @returns Human-readable invariant violations, or an empty list when the schema is sound.
|
|
@@ -1122,7 +1115,7 @@ function auditSchema(schema) {
|
|
|
1122
1115
|
//#endregion
|
|
1123
1116
|
//#region src/core/parsers.ts
|
|
1124
1117
|
/**
|
|
1125
|
-
*
|
|
1118
|
+
* Parses unknown wire data into an owned, semantically sound form schema.
|
|
1126
1119
|
*
|
|
1127
1120
|
* @param input - The unknown schema value to parse.
|
|
1128
1121
|
* @returns An owned schema with custom rules removed, or `undefined` on refusal.
|
|
@@ -1133,12 +1126,7 @@ function parseForm(input) {
|
|
|
1133
1126
|
const schema = {};
|
|
1134
1127
|
for (const key of Reflect.ownKeys(input)) {
|
|
1135
1128
|
if (!isString(key)) return void 0;
|
|
1136
|
-
|
|
1137
|
-
value: input[key],
|
|
1138
|
-
enumerable: true,
|
|
1139
|
-
configurable: true,
|
|
1140
|
-
writable: true
|
|
1141
|
-
});
|
|
1129
|
+
defineEntry(schema, key, input[key]);
|
|
1142
1130
|
}
|
|
1143
1131
|
const fields = schema.fields;
|
|
1144
1132
|
if (!isArray(fields)) return void 0;
|
|
@@ -1154,24 +1142,14 @@ function parseForm(input) {
|
|
|
1154
1142
|
const copy = {};
|
|
1155
1143
|
for (const key of Reflect.ownKeys(field)) {
|
|
1156
1144
|
if (!isString(key)) return void 0;
|
|
1157
|
-
|
|
1158
|
-
value: field[key],
|
|
1159
|
-
enumerable: true,
|
|
1160
|
-
configurable: true,
|
|
1161
|
-
writable: true
|
|
1162
|
-
});
|
|
1145
|
+
defineEntry(copy, key, field[key]);
|
|
1163
1146
|
}
|
|
1164
1147
|
const rule = copy.rule;
|
|
1165
1148
|
if (isRecord(rule)) {
|
|
1166
1149
|
const projected = {};
|
|
1167
1150
|
for (const key of Reflect.ownKeys(rule)) {
|
|
1168
1151
|
if (!isString(key)) return void 0;
|
|
1169
|
-
if (key !== "custom")
|
|
1170
|
-
value: rule[key],
|
|
1171
|
-
enumerable: true,
|
|
1172
|
-
configurable: true,
|
|
1173
|
-
writable: true
|
|
1174
|
-
});
|
|
1152
|
+
if (key !== "custom") defineEntry(projected, key, rule[key]);
|
|
1175
1153
|
}
|
|
1176
1154
|
copy.rule = projected;
|
|
1177
1155
|
}
|
|
@@ -1185,7 +1163,7 @@ function parseForm(input) {
|
|
|
1185
1163
|
return outcome.success ? outcome.value : void 0;
|
|
1186
1164
|
}
|
|
1187
1165
|
/**
|
|
1188
|
-
*
|
|
1166
|
+
* Parses one answer against its field control.
|
|
1189
1167
|
*
|
|
1190
1168
|
* @param field - The field that defines the accepted value.
|
|
1191
1169
|
* @param input - The unknown value to parse.
|
|
@@ -1207,7 +1185,7 @@ function parseValue(field, input) {
|
|
|
1207
1185
|
return outcome.success ? outcome.value : void 0;
|
|
1208
1186
|
}
|
|
1209
1187
|
/**
|
|
1210
|
-
*
|
|
1188
|
+
* Parses a strict answer record against the fields declared by a schema.
|
|
1211
1189
|
*
|
|
1212
1190
|
* @param schema - The schema that owns the accepted field names and controls.
|
|
1213
1191
|
* @param input - The unknown answer record to parse.
|
|
@@ -1223,12 +1201,7 @@ function parseValues(schema, input) {
|
|
|
1223
1201
|
if (field === void 0) return void 0;
|
|
1224
1202
|
const value = parseValue(field, input[key]);
|
|
1225
1203
|
if (value === void 0) return void 0;
|
|
1226
|
-
|
|
1227
|
-
value,
|
|
1228
|
-
enumerable: true,
|
|
1229
|
-
configurable: false,
|
|
1230
|
-
writable: false
|
|
1231
|
-
});
|
|
1204
|
+
freezeEntry(values, key, value);
|
|
1232
1205
|
}
|
|
1233
1206
|
return Object.freeze(values);
|
|
1234
1207
|
});
|
|
@@ -1237,7 +1210,7 @@ function parseValues(schema, input) {
|
|
|
1237
1210
|
//#endregion
|
|
1238
1211
|
//#region src/core/Form.ts
|
|
1239
1212
|
/**
|
|
1240
|
-
*
|
|
1213
|
+
* Represents a form: a schema, the answers given against it, and the errors they carry.
|
|
1241
1214
|
*
|
|
1242
1215
|
* @remarks
|
|
1243
1216
|
* The form owns its schema, so a later edit to the schema the caller passed changes nothing here.
|
|
@@ -1277,7 +1250,7 @@ var Form = class {
|
|
|
1277
1250
|
#evaluation = 0;
|
|
1278
1251
|
#pending = false;
|
|
1279
1252
|
/**
|
|
1280
|
-
*
|
|
1253
|
+
* Opens a form against a schema.
|
|
1281
1254
|
*
|
|
1282
1255
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1283
1256
|
* @param options - The form's settings.
|
|
@@ -1286,93 +1259,76 @@ var Form = class {
|
|
|
1286
1259
|
* value is one its field's control cannot hold.
|
|
1287
1260
|
*/
|
|
1288
1261
|
constructor(schema, options) {
|
|
1289
|
-
const
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1262
|
+
const owned = attempt(() => cloneFormSchema(schema));
|
|
1263
|
+
if (!owned.success && isFormError(owned.error)) throw owned.error;
|
|
1264
|
+
const copy = owned.success && isFormSchema(owned.value) ? owned.value : void 0;
|
|
1265
|
+
const problems = copy === void 0 ? ["The schema is not a form schema"] : auditSchema(copy);
|
|
1266
|
+
if (copy === void 0 || problems.length > 0) throw new FormError("SCHEMA", `The form schema is unusable: ${problems.join("; ")}`, { problems: [...problems] });
|
|
1267
|
+
this.#schema = copy;
|
|
1292
1268
|
this.#messages = options?.messages === void 0 ? void 0 : Object.freeze({ ...options.messages });
|
|
1293
1269
|
const baseline = {};
|
|
1294
|
-
for (const [name, value] of Object.entries(computeDefaults(this.#schema)))
|
|
1295
|
-
value,
|
|
1296
|
-
enumerable: true,
|
|
1297
|
-
configurable: true,
|
|
1298
|
-
writable: true
|
|
1299
|
-
});
|
|
1270
|
+
for (const [name, value] of Object.entries(computeDefaults(this.#schema))) defineEntry(baseline, name, value);
|
|
1300
1271
|
for (const [name, value] of Object.entries(options?.values ?? {})) {
|
|
1301
1272
|
const field = this.#requireField(name);
|
|
1302
1273
|
if (!matchesField(field, value)) throw new FormError("CONTROL", `The ${field.control} field "${name}" cannot hold that value`, {
|
|
1303
1274
|
field: name,
|
|
1304
1275
|
control: field.control
|
|
1305
1276
|
});
|
|
1306
|
-
|
|
1307
|
-
value: cloneValue(value),
|
|
1308
|
-
enumerable: true,
|
|
1309
|
-
configurable: true,
|
|
1310
|
-
writable: true
|
|
1311
|
-
});
|
|
1277
|
+
defineEntry(baseline, name, cloneValue(value));
|
|
1312
1278
|
}
|
|
1313
1279
|
this.#baseline = Object.freeze(baseline);
|
|
1314
1280
|
this.#values = {};
|
|
1315
|
-
for (const [name, value] of Object.entries(baseline))
|
|
1316
|
-
value,
|
|
1317
|
-
enumerable: true,
|
|
1318
|
-
configurable: true,
|
|
1319
|
-
writable: true
|
|
1320
|
-
});
|
|
1281
|
+
for (const [name, value] of Object.entries(baseline)) defineEntry(this.#values, name, value);
|
|
1321
1282
|
this.#resolvers.promise.catch(() => void 0);
|
|
1322
1283
|
this.#emitter = new Emitter(options);
|
|
1323
1284
|
this.#evaluate();
|
|
1324
1285
|
}
|
|
1325
|
-
/**
|
|
1286
|
+
/** Holds the form's event emitter. */
|
|
1326
1287
|
get emitter() {
|
|
1327
1288
|
return this.#emitter;
|
|
1328
1289
|
}
|
|
1329
|
-
/**
|
|
1290
|
+
/** Holds the schema this form asks, owned and frozen. */
|
|
1330
1291
|
get schema() {
|
|
1331
1292
|
return this.#schema;
|
|
1332
1293
|
}
|
|
1333
|
-
/**
|
|
1294
|
+
/** Reports the answers the form holds. */
|
|
1334
1295
|
get values() {
|
|
1335
1296
|
const values = {};
|
|
1336
|
-
for (const name of Object.
|
|
1337
|
-
value: this.#values[name],
|
|
1338
|
-
enumerable: true,
|
|
1339
|
-
configurable: true,
|
|
1340
|
-
writable: true
|
|
1341
|
-
});
|
|
1297
|
+
for (const [name, value] of Object.entries(this.#values)) defineEntry(values, name, value);
|
|
1342
1298
|
return Object.freeze(values);
|
|
1343
1299
|
}
|
|
1344
|
-
/**
|
|
1300
|
+
/** Holds the answers the form opened with. */
|
|
1345
1301
|
get baseline() {
|
|
1346
1302
|
return this.#baseline;
|
|
1347
1303
|
}
|
|
1348
|
-
/**
|
|
1304
|
+
/** Holds every error the last completed evaluation produced. */
|
|
1349
1305
|
get errors() {
|
|
1350
1306
|
return this.#errors;
|
|
1351
1307
|
}
|
|
1352
|
-
/**
|
|
1308
|
+
/** Lists the names of the fields somebody has visited. */
|
|
1353
1309
|
get touched() {
|
|
1354
1310
|
return new Set(this.#touched);
|
|
1355
1311
|
}
|
|
1356
|
-
/**
|
|
1312
|
+
/** Lists the names of the fields that are out of the form. */
|
|
1357
1313
|
get disabled() {
|
|
1358
1314
|
const disabled = /* @__PURE__ */ new Set();
|
|
1359
1315
|
for (const field of this.#schema.fields) if ((this.#disabled.get(field.name) ?? field.disabled === true) === true) disabled.add(field.name);
|
|
1360
1316
|
return disabled;
|
|
1361
1317
|
}
|
|
1362
|
-
/**
|
|
1318
|
+
/** Reports where the form sits in its life. */
|
|
1363
1319
|
get status() {
|
|
1364
1320
|
return this.#status;
|
|
1365
1321
|
}
|
|
1366
|
-
/**
|
|
1322
|
+
/** Reports whether the last completed evaluation found no error. */
|
|
1367
1323
|
get valid() {
|
|
1368
1324
|
return this.#errors.length === 0;
|
|
1369
1325
|
}
|
|
1370
|
-
/**
|
|
1326
|
+
/** Reports whether any answer has moved since the form opened. */
|
|
1371
1327
|
get dirty() {
|
|
1372
1328
|
return !matchesValues(this.values, this.#baseline);
|
|
1373
1329
|
}
|
|
1374
1330
|
/**
|
|
1375
|
-
*
|
|
1331
|
+
* Holds the answers after the form settles.
|
|
1376
1332
|
*
|
|
1377
1333
|
* @remarks
|
|
1378
1334
|
* It resolves with the submitted values on the first valid submit, and rejects with a
|
|
@@ -1382,7 +1338,7 @@ var Form = class {
|
|
|
1382
1338
|
return this.#resolvers.promise;
|
|
1383
1339
|
}
|
|
1384
1340
|
/**
|
|
1385
|
-
*
|
|
1341
|
+
* Finds one field by name.
|
|
1386
1342
|
*
|
|
1387
1343
|
* @param name - The field's name.
|
|
1388
1344
|
* @returns The field, or `undefined` when the schema declares no such name.
|
|
@@ -1391,7 +1347,7 @@ var Form = class {
|
|
|
1391
1347
|
return this.#schema.fields.find((field) => field.name === name);
|
|
1392
1348
|
}
|
|
1393
1349
|
/**
|
|
1394
|
-
*
|
|
1350
|
+
* Answers one field or several.
|
|
1395
1351
|
*
|
|
1396
1352
|
* @param input - One field's name, or the answers to write keyed by field name.
|
|
1397
1353
|
* @param value - The answer to write when `input` names one field.
|
|
@@ -1414,12 +1370,7 @@ var Form = class {
|
|
|
1414
1370
|
for (const [name, answer] of entries) {
|
|
1415
1371
|
if (this.#differs(name, answer)) moved.push(name);
|
|
1416
1372
|
if (answer === void 0) delete this.#values[name];
|
|
1417
|
-
else
|
|
1418
|
-
value: cloneValue(answer),
|
|
1419
|
-
enumerable: true,
|
|
1420
|
-
configurable: true,
|
|
1421
|
-
writable: true
|
|
1422
|
-
});
|
|
1373
|
+
else defineEntry(this.#values, name, cloneValue(answer));
|
|
1423
1374
|
this.#invalidations.delete(name);
|
|
1424
1375
|
}
|
|
1425
1376
|
for (const name of moved) {
|
|
@@ -1430,7 +1381,7 @@ var Form = class {
|
|
|
1430
1381
|
});
|
|
1431
1382
|
}
|
|
1432
1383
|
/**
|
|
1433
|
-
*
|
|
1384
|
+
* Records that somebody has visited a field.
|
|
1434
1385
|
*
|
|
1435
1386
|
* @param name - The field's name.
|
|
1436
1387
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1442,7 +1393,7 @@ var Form = class {
|
|
|
1442
1393
|
this.#touched.add(name);
|
|
1443
1394
|
}
|
|
1444
1395
|
/**
|
|
1445
|
-
*
|
|
1396
|
+
* Fails a field from outside, for what the rules cannot see.
|
|
1446
1397
|
*
|
|
1447
1398
|
* @param name - The field's name.
|
|
1448
1399
|
* @param message - What to tell the person.
|
|
@@ -1461,7 +1412,7 @@ var Form = class {
|
|
|
1461
1412
|
});
|
|
1462
1413
|
}
|
|
1463
1414
|
/**
|
|
1464
|
-
*
|
|
1415
|
+
* Takes one or more fields out of the form.
|
|
1465
1416
|
*
|
|
1466
1417
|
* @param input - One field name, several names, or absence to select every field.
|
|
1467
1418
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1473,7 +1424,7 @@ var Form = class {
|
|
|
1473
1424
|
this.#change(input, true);
|
|
1474
1425
|
}
|
|
1475
1426
|
/**
|
|
1476
|
-
*
|
|
1427
|
+
* Puts one or more fields back into the form.
|
|
1477
1428
|
*
|
|
1478
1429
|
* @param input - One field name, several names, or absence to select every field.
|
|
1479
1430
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1485,7 +1436,7 @@ var Form = class {
|
|
|
1485
1436
|
this.#change(input, false);
|
|
1486
1437
|
}
|
|
1487
1438
|
/**
|
|
1488
|
-
*
|
|
1439
|
+
* Checks every answer and settles the form when they all pass.
|
|
1489
1440
|
*
|
|
1490
1441
|
* @returns The values on success, or every error that stopped them.
|
|
1491
1442
|
* @remarks
|
|
@@ -1538,7 +1489,7 @@ var Form = class {
|
|
|
1538
1489
|
});
|
|
1539
1490
|
}
|
|
1540
1491
|
/**
|
|
1541
|
-
*
|
|
1492
|
+
* Returns every answer to the ones the form opened with: the schema's defaults, overlaid with
|
|
1542
1493
|
* any seeded `values`. Reset the runtime disabled state to the schema's declarations.
|
|
1543
1494
|
*
|
|
1544
1495
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended.
|
|
@@ -1547,12 +1498,7 @@ var Form = class {
|
|
|
1547
1498
|
this.#gate();
|
|
1548
1499
|
this.#batch(() => {
|
|
1549
1500
|
for (const name of Object.keys(this.#values)) delete this.#values[name];
|
|
1550
|
-
for (const [name, value] of Object.entries(this.#baseline))
|
|
1551
|
-
value,
|
|
1552
|
-
enumerable: true,
|
|
1553
|
-
configurable: true,
|
|
1554
|
-
writable: true
|
|
1555
|
-
});
|
|
1501
|
+
for (const [name, value] of Object.entries(this.#baseline)) defineEntry(this.#values, name, value);
|
|
1556
1502
|
this.#touched.clear();
|
|
1557
1503
|
this.#disabled.clear();
|
|
1558
1504
|
this.#invalidations.clear();
|
|
@@ -1562,7 +1508,7 @@ var Form = class {
|
|
|
1562
1508
|
});
|
|
1563
1509
|
}
|
|
1564
1510
|
/**
|
|
1565
|
-
*
|
|
1511
|
+
* Tears the form down, abandoning it when it has not settled.
|
|
1566
1512
|
*
|
|
1567
1513
|
* @remarks
|
|
1568
1514
|
* Destroying twice does nothing the second time. A settled form keeps its `settled` status and
|
|
@@ -1640,12 +1586,7 @@ var Form = class {
|
|
|
1640
1586
|
const disabled = this.disabled;
|
|
1641
1587
|
for (const field of this.#schema.fields) {
|
|
1642
1588
|
const value = Object.hasOwn(this.#values, field.name) ? this.#values[field.name] : void 0;
|
|
1643
|
-
if (!disabled.has(field.name) && value !== void 0)
|
|
1644
|
-
value,
|
|
1645
|
-
enumerable: true,
|
|
1646
|
-
configurable: true,
|
|
1647
|
-
writable: true
|
|
1648
|
-
});
|
|
1589
|
+
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, value);
|
|
1649
1590
|
}
|
|
1650
1591
|
return Object.freeze(answers);
|
|
1651
1592
|
}
|
|
@@ -1670,7 +1611,7 @@ var Form = class {
|
|
|
1670
1611
|
//#endregion
|
|
1671
1612
|
//#region src/core/factories.ts
|
|
1672
1613
|
/**
|
|
1673
|
-
*
|
|
1614
|
+
* Opens a form against a schema.
|
|
1674
1615
|
*
|
|
1675
1616
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1676
1617
|
* @param options - The form's settings.
|
|
@@ -1699,6 +1640,6 @@ function createForm(schema, options) {
|
|
|
1699
1640
|
return new Form(schema, options);
|
|
1700
1641
|
}
|
|
1701
1642
|
//#endregion
|
|
1702
|
-
export { ALPHANUMERIC_PATTERN, CHOICE_LIMIT, COLOR_PATTERN, DATETIME_PATTERN, DATE_PATTERN, EMAIL_PATTERN, FIELD_CONTROLS, FIELD_LIMIT, FORM_STATUSES, Form, FormError, GROUP_LIMIT, INTEGER_PATTERN, LIST_LIMIT, NAME_LIMIT, NODE_LIMIT, PATTERN_LIMIT, RULE_MESSAGES, STRING_LIMIT, TEXT_LIMIT, TIME_PATTERN, URL_PATTERN, appliesRule, auditSchema, cloneChoices, cloneFormField, cloneFormSchema, cloneValue, computeDefaults, createForm, evaluateField, evaluateForm, extractChanges, extractGroups, formatMessage, isFieldChoice, isFieldControl, isFieldError, isFieldRule, isFieldValue, isFormError, isFormField, isFormGroup, isFormSchema, isFormStatus, isFormValues, matchesAnswer, matchesField, matchesValue, matchesValues, parseForm, parseValue, parseValues, serializeForm };
|
|
1643
|
+
export { ALPHANUMERIC_PATTERN, CHOICE_LIMIT, COLOR_PATTERN, DATETIME_PATTERN, DATE_PATTERN, EMAIL_PATTERN, FIELD_BASE_KEYS, FIELD_CONTROLS, FIELD_KEYS, FIELD_LIMIT, FORM_STATUSES, Form, FormError, GROUP_LIMIT, INTEGER_PATTERN, LIST_LIMIT, NAME_LIMIT, NODE_LIMIT, PATTERN_LIMIT, RULE_MESSAGES, STRING_LIMIT, TEXT_LIMIT, TIME_PATTERN, URL_PATTERN, appliesRule, auditSchema, cloneChoices, cloneFormField, cloneFormSchema, cloneValue, computeDefaults, createFieldError, createForm, defineEntry, evaluateField, evaluateForm, extractChanges, extractGroups, formatMessage, freezeEntry, isFieldChoice, isFieldControl, isFieldError, isFieldRule, isFieldValue, isFormError, isFormField, isFormGroup, isFormSchema, isFormStatus, isFormValues, matchesAnswer, matchesField, matchesValue, matchesValues, parseForm, parseValue, parseValues, serializeForm };
|
|
1703
1644
|
|
|
1704
1645
|
//# sourceMappingURL=index.js.map
|