@orkestrel/form 0.0.4 → 0.0.6
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 +16 -14
- package/dist/src/core/index.cjs +314 -340
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +301 -181
- package/dist/src/core/index.d.ts +301 -181
- package/dist/src/core/index.js +311 -342
- package/dist/src/core/index.js.map +1 -1
- package/package.json +8 -9
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, at 256. */
|
|
54
110
|
var PATTERN_LIMIT = 256;
|
|
55
|
-
/**
|
|
111
|
+
/** Caps the number of fields one schema may declare, at 512. */
|
|
56
112
|
var FIELD_LIMIT = 512;
|
|
57
|
-
/**
|
|
113
|
+
/** Caps the number of groups one schema may declare, at 64. */
|
|
58
114
|
var GROUP_LIMIT = 64;
|
|
59
|
-
/**
|
|
115
|
+
/** Caps the number of choices one `select` or `checkbox` field may offer, at 1024. */
|
|
60
116
|
var CHOICE_LIMIT = 1024;
|
|
61
|
-
/**
|
|
117
|
+
/** Caps the number of entries one list-valued answer may hold, at 1024. */
|
|
62
118
|
var LIST_LIMIT = 1024;
|
|
63
|
-
/**
|
|
119
|
+
/** Caps the length, in UTF-16 code units, of a schema, group, or field name, at 128. */
|
|
64
120
|
var NAME_LIMIT = 128;
|
|
65
|
-
/**
|
|
121
|
+
/** Caps the length, in UTF-16 code units, of any single retained string, at 65536. */
|
|
66
122
|
var STRING_LIMIT = 65536;
|
|
67
|
-
/**
|
|
123
|
+
/** Caps the total length, in UTF-16 code units, of every string one schema retains, at 1048576. */
|
|
68
124
|
var TEXT_LIMIT = 1048576;
|
|
69
|
-
/**
|
|
125
|
+
/** Caps the total number of records, arrays, and leaves one schema retains, at 16384. */
|
|
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,41 @@ 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.
|
|
192
|
+
*
|
|
193
|
+
* @remarks
|
|
194
|
+
* A string, a finite number, a boolean, and a list of strings each qualify. A number that is not
|
|
195
|
+
* finite does not, so `NaN` and `Infinity` are refused.
|
|
124
196
|
*
|
|
125
197
|
* @param input - The value to inspect.
|
|
126
|
-
* @returns
|
|
198
|
+
* @returns True if the value is a field value; false otherwise.
|
|
127
199
|
*/
|
|
128
200
|
function isFieldValue(input) {
|
|
129
201
|
return unionOf(isString, isFiniteNumber, isBoolean, arrayOf(isString))(input);
|
|
130
202
|
}
|
|
131
203
|
/**
|
|
132
|
-
*
|
|
204
|
+
* Determines whether an unknown value is one exact field choice record.
|
|
133
205
|
*
|
|
134
206
|
* @param input - The value to inspect.
|
|
135
|
-
* @returns
|
|
207
|
+
* @returns True if the value is a field choice; false otherwise.
|
|
136
208
|
*/
|
|
137
209
|
function isFieldChoice(input) {
|
|
138
210
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -145,10 +217,10 @@ function isFieldChoice(input) {
|
|
|
145
217
|
}, ["help", "disabled"])(input);
|
|
146
218
|
}
|
|
147
219
|
/**
|
|
148
|
-
*
|
|
220
|
+
* Determines whether an unknown value is one exact field rule record.
|
|
149
221
|
*
|
|
150
222
|
* @param input - The value to inspect.
|
|
151
|
-
* @returns
|
|
223
|
+
* @returns True if the value is a structurally valid field rule; false otherwise.
|
|
152
224
|
*/
|
|
153
225
|
function isFieldRule(input) {
|
|
154
226
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -167,7 +239,7 @@ function isFieldRule(input) {
|
|
|
167
239
|
}, true)(input);
|
|
168
240
|
}
|
|
169
241
|
/**
|
|
170
|
-
*
|
|
242
|
+
* Determines whether an unknown value is one exact discriminated form field.
|
|
171
243
|
*
|
|
172
244
|
* @remarks
|
|
173
245
|
* Metadata is admitted structurally as bounded JSON. An accessor-bearing metadata record is
|
|
@@ -175,120 +247,15 @@ function isFieldRule(input) {
|
|
|
175
247
|
* data properties only.
|
|
176
248
|
*
|
|
177
249
|
* @param input - The value to inspect.
|
|
178
|
-
* @returns
|
|
250
|
+
* @returns True if the value is a structurally valid form field; false otherwise.
|
|
179
251
|
*/
|
|
180
252
|
function isFormField(input) {
|
|
181
253
|
const outcome = attempt(() => {
|
|
182
254
|
if (!isRecord(input) || !Object.hasOwn(input, "control") || !Object.hasOwn(input, "name")) return false;
|
|
183
255
|
const control = input.control;
|
|
184
256
|
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;
|
|
257
|
+
const permitted = FIELD_KEYS[control];
|
|
258
|
+
if (!Reflect.ownKeys(input).every((key) => isString(key) && permitted.includes(key))) return false;
|
|
292
259
|
const name = input.name;
|
|
293
260
|
const hasLabel = Object.hasOwn(input, "label");
|
|
294
261
|
const label = hasLabel ? input.label : void 0;
|
|
@@ -369,10 +336,10 @@ function isFormField(input) {
|
|
|
369
336
|
return outcome.success && outcome.value;
|
|
370
337
|
}
|
|
371
338
|
/**
|
|
372
|
-
*
|
|
339
|
+
* Determines whether an unknown value is one exact form group record.
|
|
373
340
|
*
|
|
374
341
|
* @param input - The value to inspect.
|
|
375
|
-
* @returns
|
|
342
|
+
* @returns True if the value is a form group; false otherwise.
|
|
376
343
|
*/
|
|
377
344
|
function isFormGroup(input) {
|
|
378
345
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -384,10 +351,14 @@ function isFormGroup(input) {
|
|
|
384
351
|
}, ["help"])(input);
|
|
385
352
|
}
|
|
386
353
|
/**
|
|
387
|
-
*
|
|
354
|
+
* Determines whether an unknown value is one exact structural form schema.
|
|
355
|
+
*
|
|
356
|
+
* @remarks
|
|
357
|
+
* Structure alone is read. Domain soundness — a duplicate name, a bound no answer satisfies, a
|
|
358
|
+
* breached budget — is {@link auditSchema}'s question.
|
|
388
359
|
*
|
|
389
360
|
* @param input - The value to inspect.
|
|
390
|
-
* @returns
|
|
361
|
+
* @returns True if the value is a structurally valid form schema; false otherwise.
|
|
391
362
|
*/
|
|
392
363
|
function isFormSchema(input) {
|
|
393
364
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -406,10 +377,10 @@ function isFormSchema(input) {
|
|
|
406
377
|
])(input);
|
|
407
378
|
}
|
|
408
379
|
/**
|
|
409
|
-
*
|
|
380
|
+
* Determines whether an unknown value is a record of field values.
|
|
410
381
|
*
|
|
411
382
|
* @param input - The value to inspect.
|
|
412
|
-
* @returns
|
|
383
|
+
* @returns True if the value is a form values record; false otherwise.
|
|
413
384
|
*/
|
|
414
385
|
function isFormValues(input) {
|
|
415
386
|
const outcome = attempt(() => {
|
|
@@ -419,10 +390,10 @@ function isFormValues(input) {
|
|
|
419
390
|
return outcome.success && outcome.value;
|
|
420
391
|
}
|
|
421
392
|
/**
|
|
422
|
-
*
|
|
393
|
+
* Determines whether an unknown value is one exact field error record.
|
|
423
394
|
*
|
|
424
395
|
* @param input - The value to inspect.
|
|
425
|
-
* @returns
|
|
396
|
+
* @returns True if the value is a field error; false otherwise.
|
|
426
397
|
*/
|
|
427
398
|
function isFieldError(input) {
|
|
428
399
|
const keys = attempt(() => isRecord(input) && Reflect.ownKeys(input).every((key) => isString(key)));
|
|
@@ -430,7 +401,7 @@ function isFieldError(input) {
|
|
|
430
401
|
return recordOf({
|
|
431
402
|
field: isString,
|
|
432
403
|
message: isString,
|
|
433
|
-
rule:
|
|
404
|
+
rule: keyOf(RULE_MESSAGES)
|
|
434
405
|
}, ["rule"])(input);
|
|
435
406
|
}
|
|
436
407
|
//#endregion
|
|
@@ -439,7 +410,7 @@ function cloneValue(value) {
|
|
|
439
410
|
return isArray(value) ? Object.freeze(value.slice()) : value;
|
|
440
411
|
}
|
|
441
412
|
/**
|
|
442
|
-
*
|
|
413
|
+
* Clones a field's choices into an owned frozen snapshot.
|
|
443
414
|
*
|
|
444
415
|
* @param choices - The choices to own.
|
|
445
416
|
* @returns A frozen list of frozen choice records.
|
|
@@ -448,7 +419,7 @@ function cloneChoices(choices) {
|
|
|
448
419
|
return Object.freeze(choices.map((choice) => Object.freeze({ ...choice })));
|
|
449
420
|
}
|
|
450
421
|
/**
|
|
451
|
-
*
|
|
422
|
+
* Clones one form field into an owned frozen snapshot.
|
|
452
423
|
*
|
|
453
424
|
* @param field - The field to own.
|
|
454
425
|
* @returns A frozen field with every nested collection owned.
|
|
@@ -499,7 +470,7 @@ function cloneFormField(field) {
|
|
|
499
470
|
}
|
|
500
471
|
}
|
|
501
472
|
/**
|
|
502
|
-
*
|
|
473
|
+
* Clones a form schema into an owned frozen snapshot.
|
|
503
474
|
*
|
|
504
475
|
* @param schema - The schema to own.
|
|
505
476
|
* @returns A frozen schema with every nested record and list owned.
|
|
@@ -515,11 +486,68 @@ function cloneFormSchema(schema) {
|
|
|
515
486
|
//#endregion
|
|
516
487
|
//#region src/core/helpers.ts
|
|
517
488
|
/**
|
|
518
|
-
*
|
|
489
|
+
* Writes one own enumerable data property onto a record.
|
|
490
|
+
*
|
|
491
|
+
* @param target - The record to write into.
|
|
492
|
+
* @param name - The property name to write.
|
|
493
|
+
* @param value - The value to store.
|
|
494
|
+
*
|
|
495
|
+
* @remarks
|
|
496
|
+
* Plain assignment runs an inherited setter, so writing a `__proto__` key that way reaches
|
|
497
|
+
* `Object.prototype` and leaves the record without the entry. Defining the property writes the
|
|
498
|
+
* record itself, whatever the prototype chain declares. The entry stays writable and configurable.
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```ts
|
|
502
|
+
* const values: Record<string, number> = {}
|
|
503
|
+
* defineEntry(values, '__proto__', 1)
|
|
504
|
+
* Object.hasOwn(values, '__proto__') // true
|
|
505
|
+
* ```
|
|
506
|
+
*/
|
|
507
|
+
function defineEntry(target, name, value) {
|
|
508
|
+
Object.defineProperty(target, name, {
|
|
509
|
+
value,
|
|
510
|
+
enumerable: true,
|
|
511
|
+
configurable: true,
|
|
512
|
+
writable: true
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Writes one own enumerable data property that cannot be rewritten or removed.
|
|
517
|
+
*
|
|
518
|
+
* @param target - The record to write into.
|
|
519
|
+
* @param name - The property name to write.
|
|
520
|
+
* @param value - The value to store.
|
|
521
|
+
*
|
|
522
|
+
* @remarks
|
|
523
|
+
* The prototype-safe write of {@link defineEntry}, frozen: the entry is neither writable nor
|
|
524
|
+
* configurable, so the record a parser hands back cannot be edited through the key it filled.
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```ts
|
|
528
|
+
* const values: Record<string, number> = {}
|
|
529
|
+
* freezeEntry(values, '__proto__', 1)
|
|
530
|
+
* Object.getOwnPropertyDescriptor(values, '__proto__')?.writable // false
|
|
531
|
+
* ```
|
|
532
|
+
*/
|
|
533
|
+
function freezeEntry(target, name, value) {
|
|
534
|
+
Object.defineProperty(target, name, {
|
|
535
|
+
value,
|
|
536
|
+
enumerable: true,
|
|
537
|
+
configurable: false,
|
|
538
|
+
writable: false
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Checks whether a value has the shape required by one field control.
|
|
543
|
+
*
|
|
544
|
+
* @remarks
|
|
545
|
+
* Every write and every seeded value passes through this gate, and it reads `STRING_LIMIT` and
|
|
546
|
+
* `LIST_LIMIT` before it consults the control, so no regular expression sees an over-long value.
|
|
519
547
|
*
|
|
520
548
|
* @param field - The field that owns the value.
|
|
521
549
|
* @param value - The unknown value to inspect.
|
|
522
|
-
* @returns
|
|
550
|
+
* @returns True if the control can hold the value; false otherwise.
|
|
523
551
|
*/
|
|
524
552
|
function matchesField(field, value) {
|
|
525
553
|
if (isString(value) && value.length > 65536) return false;
|
|
@@ -548,7 +576,7 @@ function matchesField(field, value) {
|
|
|
548
576
|
}
|
|
549
577
|
}
|
|
550
578
|
/**
|
|
551
|
-
*
|
|
579
|
+
* Decides whether a raw binding value projects to an answered field.
|
|
552
580
|
*
|
|
553
581
|
* @remarks
|
|
554
582
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
@@ -557,13 +585,13 @@ function matchesField(field, value) {
|
|
|
557
585
|
* its `required` rule remains presence-only.
|
|
558
586
|
*
|
|
559
587
|
* @param value - The raw field value, or absence.
|
|
560
|
-
* @returns
|
|
588
|
+
* @returns True if the binding preserves the value as an answer; false otherwise.
|
|
561
589
|
*/
|
|
562
590
|
function matchesAnswer(value) {
|
|
563
591
|
return value !== void 0 && (!isString(value) || value.trim().length > 0);
|
|
564
592
|
}
|
|
565
593
|
/**
|
|
566
|
-
*
|
|
594
|
+
* Checks whether a named rule applies to one field control.
|
|
567
595
|
*
|
|
568
596
|
* @remarks
|
|
569
597
|
* The runtime control-membership check keeps this boundary total for JavaScript callers that
|
|
@@ -571,7 +599,7 @@ function matchesAnswer(value) {
|
|
|
571
599
|
*
|
|
572
600
|
* @param control - The field control to inspect.
|
|
573
601
|
* @param rule - The named rule to inspect.
|
|
574
|
-
* @returns
|
|
602
|
+
* @returns True if the control evaluates that rule; false otherwise.
|
|
575
603
|
*/
|
|
576
604
|
function appliesRule(control, rule) {
|
|
577
605
|
if (!FIELD_CONTROLS.some((candidate) => candidate === control)) return false;
|
|
@@ -589,23 +617,21 @@ function appliesRule(control, rule) {
|
|
|
589
617
|
return false;
|
|
590
618
|
}
|
|
591
619
|
/**
|
|
592
|
-
*
|
|
620
|
+
* Evaluates one field rule against its current value.
|
|
593
621
|
*
|
|
594
622
|
* @param field - The field and rule to evaluate.
|
|
595
623
|
* @param value - The current value, or absence.
|
|
596
624
|
* @param values - Every value available to a custom rule.
|
|
597
625
|
* @param messages - Optional rule-specific message replacements.
|
|
598
626
|
* @returns Every failure in rule order.
|
|
627
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
628
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
599
629
|
*/
|
|
600
630
|
function evaluateField(field, value, values, messages) {
|
|
601
631
|
const errors = [];
|
|
602
632
|
const rule = field.rule;
|
|
603
633
|
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
|
-
}));
|
|
634
|
+
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
|
|
609
635
|
}
|
|
610
636
|
if (rule === void 0) return Object.freeze(errors);
|
|
611
637
|
if (rule.minimum !== void 0 && appliesRule(field.control, "minimum")) {
|
|
@@ -627,11 +653,7 @@ function evaluateField(field, value, values, messages) {
|
|
|
627
653
|
case "checkbox":
|
|
628
654
|
case "file": failed = isArray(value) && isFiniteNumber(rule.minimum) && value.length < rule.minimum;
|
|
629
655
|
}
|
|
630
|
-
if (failed) errors.push(
|
|
631
|
-
field: field.name,
|
|
632
|
-
message: formatMessage("minimum", rule.minimum, messages),
|
|
633
|
-
rule: "minimum"
|
|
634
|
-
}));
|
|
656
|
+
if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
|
|
635
657
|
}
|
|
636
658
|
if (rule.maximum !== void 0 && appliesRule(field.control, "maximum")) {
|
|
637
659
|
let failed = false;
|
|
@@ -652,63 +674,27 @@ function evaluateField(field, value, values, messages) {
|
|
|
652
674
|
case "checkbox":
|
|
653
675
|
case "file": failed = isArray(value) && isFiniteNumber(rule.maximum) && value.length > rule.maximum;
|
|
654
676
|
}
|
|
655
|
-
if (failed) errors.push(
|
|
656
|
-
field: field.name,
|
|
657
|
-
message: formatMessage("maximum", rule.maximum, messages),
|
|
658
|
-
rule: "maximum"
|
|
659
|
-
}));
|
|
677
|
+
if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
|
|
660
678
|
}
|
|
661
679
|
if (rule.step !== void 0 && appliesRule(field.control, "step") && isFiniteNumber(value)) {
|
|
662
680
|
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
|
-
}));
|
|
681
|
+
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
682
|
}
|
|
669
683
|
if (isString(value)) {
|
|
670
684
|
if (rule.pattern !== void 0 && appliesRule(field.control, "pattern")) {
|
|
671
685
|
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
|
-
}));
|
|
686
|
+
if (pattern.length > 256) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
677
687
|
else {
|
|
678
688
|
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
|
-
}));
|
|
689
|
+
if (!outcome.success || !outcome.value) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
684
690
|
}
|
|
685
691
|
}
|
|
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
|
-
}));
|
|
692
|
+
if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(createFieldError(field, "email", void 0, messages));
|
|
693
|
+
if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(createFieldError(field, "url", void 0, messages));
|
|
694
|
+
if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(createFieldError(field, "alphanumeric", void 0, messages));
|
|
695
|
+
if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
706
696
|
}
|
|
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
|
-
}));
|
|
697
|
+
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
698
|
if (rule.custom !== void 0) {
|
|
713
699
|
const result = rule.custom(value, values);
|
|
714
700
|
if (isString(result)) errors.push(Object.freeze({
|
|
@@ -719,12 +705,14 @@ function evaluateField(field, value, values, messages) {
|
|
|
719
705
|
return Object.freeze(errors);
|
|
720
706
|
}
|
|
721
707
|
/**
|
|
722
|
-
*
|
|
708
|
+
* Evaluates every active field in schema order.
|
|
723
709
|
*
|
|
724
710
|
* @param schema - The form schema to evaluate.
|
|
725
711
|
* @param values - The values keyed by field name.
|
|
726
712
|
* @param options - Optional message replacements and the effective disabled field set.
|
|
727
713
|
* @returns Every field failure in schema and rule order.
|
|
714
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
715
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
728
716
|
*/
|
|
729
717
|
function evaluateForm(schema, values, options) {
|
|
730
718
|
const errors = [];
|
|
@@ -735,7 +723,11 @@ function evaluateForm(schema, values, options) {
|
|
|
735
723
|
return Object.freeze(errors);
|
|
736
724
|
}
|
|
737
725
|
/**
|
|
738
|
-
*
|
|
726
|
+
* Computes the values explicitly seeded by a schema.
|
|
727
|
+
*
|
|
728
|
+
* @remarks
|
|
729
|
+
* `password` and `file` declare no default, so a field of either control never appears in the
|
|
730
|
+
* result.
|
|
739
731
|
*
|
|
740
732
|
* @param schema - The schema whose defaults to collect.
|
|
741
733
|
* @returns A value record containing only fields with defaults.
|
|
@@ -744,12 +736,7 @@ function computeDefaults(schema) {
|
|
|
744
736
|
const defaults = {};
|
|
745
737
|
for (const field of schema.fields) switch (field.control) {
|
|
746
738
|
case "checkbox":
|
|
747
|
-
if (field.default !== void 0)
|
|
748
|
-
value: cloneValue(field.default),
|
|
749
|
-
enumerable: true,
|
|
750
|
-
configurable: true,
|
|
751
|
-
writable: true
|
|
752
|
-
});
|
|
739
|
+
if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
|
|
753
740
|
break;
|
|
754
741
|
case "password":
|
|
755
742
|
case "file": break;
|
|
@@ -761,35 +748,30 @@ function computeDefaults(schema) {
|
|
|
761
748
|
case "datetime":
|
|
762
749
|
case "color":
|
|
763
750
|
case "confirm":
|
|
764
|
-
case "select": if (field.default !== void 0)
|
|
765
|
-
value: field.default,
|
|
766
|
-
enumerable: true,
|
|
767
|
-
configurable: true,
|
|
768
|
-
writable: true
|
|
769
|
-
});
|
|
751
|
+
case "select": if (field.default !== void 0) defineEntry(defaults, field.name, field.default);
|
|
770
752
|
}
|
|
771
753
|
return Object.freeze(defaults);
|
|
772
754
|
}
|
|
773
755
|
/**
|
|
774
|
-
*
|
|
756
|
+
* Compares two field values by scalar identity or ordered list content.
|
|
775
757
|
*
|
|
776
758
|
* @param a - The first field value.
|
|
777
759
|
* @param b - The second field value.
|
|
778
|
-
* @returns
|
|
760
|
+
* @returns True if both values contain the same answer; false otherwise.
|
|
779
761
|
*/
|
|
780
762
|
function matchesValue(a, b) {
|
|
781
763
|
if (isArray(a) || isArray(b)) return isArray(a) && isArray(b) && a.length === b.length && a.every((entry, index) => entry === b[index]);
|
|
782
764
|
return a === b;
|
|
783
765
|
}
|
|
784
766
|
/**
|
|
785
|
-
*
|
|
767
|
+
* Extracts the names whose answers differ between two form value records.
|
|
786
768
|
*
|
|
787
769
|
* @remarks
|
|
788
770
|
* Presence is compared in both directions before present values are compared through
|
|
789
771
|
* {@link matchesValue}. The returned set is a new snapshot, exposed as readonly because later
|
|
790
772
|
* changes to either input never alter its membership.
|
|
791
773
|
*
|
|
792
|
-
* @param current - The values
|
|
774
|
+
* @param current - The values the form holds.
|
|
793
775
|
* @param opened - The values held when the form opened.
|
|
794
776
|
* @returns A readonly snapshot of changed field names.
|
|
795
777
|
*/
|
|
@@ -808,17 +790,21 @@ function extractChanges(current, opened) {
|
|
|
808
790
|
return changed;
|
|
809
791
|
}
|
|
810
792
|
/**
|
|
811
|
-
*
|
|
793
|
+
* Compares two form value records by keys and value content.
|
|
812
794
|
*
|
|
813
795
|
* @param a - The first value record.
|
|
814
796
|
* @param b - The second value record.
|
|
815
|
-
* @returns
|
|
797
|
+
* @returns True if both records contain the same answers; false otherwise.
|
|
816
798
|
*/
|
|
817
799
|
function matchesValues(a, b) {
|
|
818
800
|
return extractChanges(a, b).size === 0;
|
|
819
801
|
}
|
|
820
802
|
/**
|
|
821
|
-
*
|
|
803
|
+
* Resolves and interpolates one rule message.
|
|
804
|
+
*
|
|
805
|
+
* @remarks
|
|
806
|
+
* A replacement in `messages` is read first and {@link RULE_MESSAGES} supplies the copy otherwise,
|
|
807
|
+
* and `{limit}` in whichever text wins is replaced with the rule's operand.
|
|
822
808
|
*
|
|
823
809
|
* @param rule - The rule whose message to resolve.
|
|
824
810
|
* @param limit - The optional operand substituted for `{limit}`.
|
|
@@ -830,7 +816,34 @@ function formatMessage(rule, limit, messages) {
|
|
|
830
816
|
return limit === void 0 ? message : message.replaceAll("{limit}", String(limit));
|
|
831
817
|
}
|
|
832
818
|
/**
|
|
833
|
-
*
|
|
819
|
+
* Creates one named-rule failure against a field.
|
|
820
|
+
*
|
|
821
|
+
* @param field - The field the rule failed on.
|
|
822
|
+
* @param rule - The named rule that failed.
|
|
823
|
+
* @param limit - The rule's operand, substituted for `{limit}`, or absence when it carries none.
|
|
824
|
+
* @param messages - Optional rule-specific message replacements.
|
|
825
|
+
* @returns A frozen {@link FieldError} carrying the field's name, the resolved text, and the rule.
|
|
826
|
+
*
|
|
827
|
+
* @remarks
|
|
828
|
+
* A `custom` validator and the form's `invalidate` method both report a message of their own
|
|
829
|
+
* under no rule name, so neither builds its failure here.
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* ```ts
|
|
833
|
+
* const field: FormField = { control: 'text', name: 'nickname', rule: { minimum: 3 } }
|
|
834
|
+
* const error = createFieldError(field, 'minimum', 3)
|
|
835
|
+
* error.message // 'Must be at least 3'
|
|
836
|
+
* ```
|
|
837
|
+
*/
|
|
838
|
+
function createFieldError(field, rule, limit, messages) {
|
|
839
|
+
return Object.freeze({
|
|
840
|
+
field: field.name,
|
|
841
|
+
message: formatMessage(rule, limit, messages),
|
|
842
|
+
rule
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Projects a schema into JSON while removing custom validators and absent values.
|
|
834
847
|
*
|
|
835
848
|
* @param schema - The schema to project.
|
|
836
849
|
* @returns A deep JSON copy of the serializable schema.
|
|
@@ -932,7 +945,7 @@ function serializeForm(schema) {
|
|
|
932
945
|
return cloneJSONRecord(output);
|
|
933
946
|
}
|
|
934
947
|
/**
|
|
935
|
-
*
|
|
948
|
+
* Selects referenced groups in first-reference field order.
|
|
936
949
|
*
|
|
937
950
|
* @param schema - The schema whose group references to resolve.
|
|
938
951
|
* @returns The referenced schema groups without duplicates.
|
|
@@ -947,7 +960,7 @@ function extractGroups(schema) {
|
|
|
947
960
|
return Object.freeze(groups);
|
|
948
961
|
}
|
|
949
962
|
/**
|
|
950
|
-
*
|
|
963
|
+
* Audits a structurally valid schema for domain invariants.
|
|
951
964
|
*
|
|
952
965
|
* @param schema - The form schema to audit.
|
|
953
966
|
* @returns Human-readable invariant violations, or an empty list when the schema is sound.
|
|
@@ -1122,7 +1135,7 @@ function auditSchema(schema) {
|
|
|
1122
1135
|
//#endregion
|
|
1123
1136
|
//#region src/core/parsers.ts
|
|
1124
1137
|
/**
|
|
1125
|
-
*
|
|
1138
|
+
* Parses unknown wire data into an owned, semantically sound form schema.
|
|
1126
1139
|
*
|
|
1127
1140
|
* @param input - The unknown schema value to parse.
|
|
1128
1141
|
* @returns An owned schema with custom rules removed, or `undefined` on refusal.
|
|
@@ -1133,12 +1146,7 @@ function parseForm(input) {
|
|
|
1133
1146
|
const schema = {};
|
|
1134
1147
|
for (const key of Reflect.ownKeys(input)) {
|
|
1135
1148
|
if (!isString(key)) return void 0;
|
|
1136
|
-
|
|
1137
|
-
value: input[key],
|
|
1138
|
-
enumerable: true,
|
|
1139
|
-
configurable: true,
|
|
1140
|
-
writable: true
|
|
1141
|
-
});
|
|
1149
|
+
defineEntry(schema, key, input[key]);
|
|
1142
1150
|
}
|
|
1143
1151
|
const fields = schema.fields;
|
|
1144
1152
|
if (!isArray(fields)) return void 0;
|
|
@@ -1154,24 +1162,14 @@ function parseForm(input) {
|
|
|
1154
1162
|
const copy = {};
|
|
1155
1163
|
for (const key of Reflect.ownKeys(field)) {
|
|
1156
1164
|
if (!isString(key)) return void 0;
|
|
1157
|
-
|
|
1158
|
-
value: field[key],
|
|
1159
|
-
enumerable: true,
|
|
1160
|
-
configurable: true,
|
|
1161
|
-
writable: true
|
|
1162
|
-
});
|
|
1165
|
+
defineEntry(copy, key, field[key]);
|
|
1163
1166
|
}
|
|
1164
1167
|
const rule = copy.rule;
|
|
1165
1168
|
if (isRecord(rule)) {
|
|
1166
1169
|
const projected = {};
|
|
1167
1170
|
for (const key of Reflect.ownKeys(rule)) {
|
|
1168
1171
|
if (!isString(key)) return void 0;
|
|
1169
|
-
if (key !== "custom")
|
|
1170
|
-
value: rule[key],
|
|
1171
|
-
enumerable: true,
|
|
1172
|
-
configurable: true,
|
|
1173
|
-
writable: true
|
|
1174
|
-
});
|
|
1172
|
+
if (key !== "custom") defineEntry(projected, key, rule[key]);
|
|
1175
1173
|
}
|
|
1176
1174
|
copy.rule = projected;
|
|
1177
1175
|
}
|
|
@@ -1185,7 +1183,11 @@ function parseForm(input) {
|
|
|
1185
1183
|
return outcome.success ? outcome.value : void 0;
|
|
1186
1184
|
}
|
|
1187
1185
|
/**
|
|
1188
|
-
*
|
|
1186
|
+
* Parses one answer against its field control.
|
|
1187
|
+
*
|
|
1188
|
+
* @remarks
|
|
1189
|
+
* A numeric string coerces to a number for a `number` field, and `'true'` and `'false'` coerce to
|
|
1190
|
+
* a boolean for a `confirm` field. Every other value must already have its control's shape.
|
|
1189
1191
|
*
|
|
1190
1192
|
* @param field - The field that defines the accepted value.
|
|
1191
1193
|
* @param input - The unknown value to parse.
|
|
@@ -1207,7 +1209,7 @@ function parseValue(field, input) {
|
|
|
1207
1209
|
return outcome.success ? outcome.value : void 0;
|
|
1208
1210
|
}
|
|
1209
1211
|
/**
|
|
1210
|
-
*
|
|
1212
|
+
* Parses a strict answer record against the fields declared by a schema.
|
|
1211
1213
|
*
|
|
1212
1214
|
* @param schema - The schema that owns the accepted field names and controls.
|
|
1213
1215
|
* @param input - The unknown answer record to parse.
|
|
@@ -1223,12 +1225,7 @@ function parseValues(schema, input) {
|
|
|
1223
1225
|
if (field === void 0) return void 0;
|
|
1224
1226
|
const value = parseValue(field, input[key]);
|
|
1225
1227
|
if (value === void 0) return void 0;
|
|
1226
|
-
|
|
1227
|
-
value,
|
|
1228
|
-
enumerable: true,
|
|
1229
|
-
configurable: false,
|
|
1230
|
-
writable: false
|
|
1231
|
-
});
|
|
1228
|
+
freezeEntry(values, key, value);
|
|
1232
1229
|
}
|
|
1233
1230
|
return Object.freeze(values);
|
|
1234
1231
|
});
|
|
@@ -1237,7 +1234,8 @@ function parseValues(schema, input) {
|
|
|
1237
1234
|
//#endregion
|
|
1238
1235
|
//#region src/core/Form.ts
|
|
1239
1236
|
/**
|
|
1240
|
-
*
|
|
1237
|
+
* Implements `FormInterface` exactly, over an owned schema, the answers given against it, and the
|
|
1238
|
+
* errors they carry.
|
|
1241
1239
|
*
|
|
1242
1240
|
* @remarks
|
|
1243
1241
|
* The form owns its schema, so a later edit to the schema the caller passed changes nothing here.
|
|
@@ -1277,7 +1275,7 @@ var Form = class {
|
|
|
1277
1275
|
#evaluation = 0;
|
|
1278
1276
|
#pending = false;
|
|
1279
1277
|
/**
|
|
1280
|
-
*
|
|
1278
|
+
* Opens a form against a schema.
|
|
1281
1279
|
*
|
|
1282
1280
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1283
1281
|
* @param options - The form's settings.
|
|
@@ -1286,93 +1284,76 @@ var Form = class {
|
|
|
1286
1284
|
* value is one its field's control cannot hold.
|
|
1287
1285
|
*/
|
|
1288
1286
|
constructor(schema, options) {
|
|
1289
|
-
const
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1287
|
+
const owned = attempt(() => cloneFormSchema(schema));
|
|
1288
|
+
if (!owned.success && isFormError(owned.error)) throw owned.error;
|
|
1289
|
+
const copy = owned.success && isFormSchema(owned.value) ? owned.value : void 0;
|
|
1290
|
+
const problems = copy === void 0 ? ["The schema is not a form schema"] : auditSchema(copy);
|
|
1291
|
+
if (copy === void 0 || problems.length > 0) throw new FormError("SCHEMA", `The form schema is unusable: ${problems.join("; ")}`, { problems: [...problems] });
|
|
1292
|
+
this.#schema = copy;
|
|
1292
1293
|
this.#messages = options?.messages === void 0 ? void 0 : Object.freeze({ ...options.messages });
|
|
1293
1294
|
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
|
-
});
|
|
1295
|
+
for (const [name, value] of Object.entries(computeDefaults(this.#schema))) defineEntry(baseline, name, value);
|
|
1300
1296
|
for (const [name, value] of Object.entries(options?.values ?? {})) {
|
|
1301
1297
|
const field = this.#requireField(name);
|
|
1302
1298
|
if (!matchesField(field, value)) throw new FormError("CONTROL", `The ${field.control} field "${name}" cannot hold that value`, {
|
|
1303
1299
|
field: name,
|
|
1304
1300
|
control: field.control
|
|
1305
1301
|
});
|
|
1306
|
-
|
|
1307
|
-
value: cloneValue(value),
|
|
1308
|
-
enumerable: true,
|
|
1309
|
-
configurable: true,
|
|
1310
|
-
writable: true
|
|
1311
|
-
});
|
|
1302
|
+
defineEntry(baseline, name, cloneValue(value));
|
|
1312
1303
|
}
|
|
1313
1304
|
this.#baseline = Object.freeze(baseline);
|
|
1314
1305
|
this.#values = {};
|
|
1315
|
-
for (const [name, value] of Object.entries(baseline))
|
|
1316
|
-
value,
|
|
1317
|
-
enumerable: true,
|
|
1318
|
-
configurable: true,
|
|
1319
|
-
writable: true
|
|
1320
|
-
});
|
|
1306
|
+
for (const [name, value] of Object.entries(baseline)) defineEntry(this.#values, name, value);
|
|
1321
1307
|
this.#resolvers.promise.catch(() => void 0);
|
|
1322
1308
|
this.#emitter = new Emitter(options);
|
|
1323
1309
|
this.#evaluate();
|
|
1324
1310
|
}
|
|
1325
|
-
/**
|
|
1311
|
+
/** Holds the form's event emitter. */
|
|
1326
1312
|
get emitter() {
|
|
1327
1313
|
return this.#emitter;
|
|
1328
1314
|
}
|
|
1329
|
-
/**
|
|
1315
|
+
/** Holds the schema this form asks, owned and frozen. */
|
|
1330
1316
|
get schema() {
|
|
1331
1317
|
return this.#schema;
|
|
1332
1318
|
}
|
|
1333
|
-
/**
|
|
1319
|
+
/** Reports the answers the form holds. */
|
|
1334
1320
|
get values() {
|
|
1335
1321
|
const values = {};
|
|
1336
|
-
for (const name of Object.
|
|
1337
|
-
value: this.#values[name],
|
|
1338
|
-
enumerable: true,
|
|
1339
|
-
configurable: true,
|
|
1340
|
-
writable: true
|
|
1341
|
-
});
|
|
1322
|
+
for (const [name, value] of Object.entries(this.#values)) defineEntry(values, name, value);
|
|
1342
1323
|
return Object.freeze(values);
|
|
1343
1324
|
}
|
|
1344
|
-
/**
|
|
1325
|
+
/** Holds the answers the form opened with. */
|
|
1345
1326
|
get baseline() {
|
|
1346
1327
|
return this.#baseline;
|
|
1347
1328
|
}
|
|
1348
|
-
/**
|
|
1329
|
+
/** Holds every error the last completed evaluation produced. */
|
|
1349
1330
|
get errors() {
|
|
1350
1331
|
return this.#errors;
|
|
1351
1332
|
}
|
|
1352
|
-
/**
|
|
1333
|
+
/** Lists the names of the fields somebody has visited. */
|
|
1353
1334
|
get touched() {
|
|
1354
1335
|
return new Set(this.#touched);
|
|
1355
1336
|
}
|
|
1356
|
-
/**
|
|
1337
|
+
/** Lists the names of the fields that are out of the form. */
|
|
1357
1338
|
get disabled() {
|
|
1358
1339
|
const disabled = /* @__PURE__ */ new Set();
|
|
1359
1340
|
for (const field of this.#schema.fields) if ((this.#disabled.get(field.name) ?? field.disabled === true) === true) disabled.add(field.name);
|
|
1360
1341
|
return disabled;
|
|
1361
1342
|
}
|
|
1362
|
-
/**
|
|
1343
|
+
/** Reports where the form sits in its life. */
|
|
1363
1344
|
get status() {
|
|
1364
1345
|
return this.#status;
|
|
1365
1346
|
}
|
|
1366
|
-
/**
|
|
1347
|
+
/** Reports whether the last completed evaluation found no error. */
|
|
1367
1348
|
get valid() {
|
|
1368
1349
|
return this.#errors.length === 0;
|
|
1369
1350
|
}
|
|
1370
|
-
/**
|
|
1351
|
+
/** Reports whether any answer has moved since the form opened. */
|
|
1371
1352
|
get dirty() {
|
|
1372
1353
|
return !matchesValues(this.values, this.#baseline);
|
|
1373
1354
|
}
|
|
1374
1355
|
/**
|
|
1375
|
-
*
|
|
1356
|
+
* Holds the answers after the form settles.
|
|
1376
1357
|
*
|
|
1377
1358
|
* @remarks
|
|
1378
1359
|
* It resolves with the submitted values on the first valid submit, and rejects with a
|
|
@@ -1382,7 +1363,7 @@ var Form = class {
|
|
|
1382
1363
|
return this.#resolvers.promise;
|
|
1383
1364
|
}
|
|
1384
1365
|
/**
|
|
1385
|
-
*
|
|
1366
|
+
* Finds one field by name.
|
|
1386
1367
|
*
|
|
1387
1368
|
* @param name - The field's name.
|
|
1388
1369
|
* @returns The field, or `undefined` when the schema declares no such name.
|
|
@@ -1391,7 +1372,7 @@ var Form = class {
|
|
|
1391
1372
|
return this.#schema.fields.find((field) => field.name === name);
|
|
1392
1373
|
}
|
|
1393
1374
|
/**
|
|
1394
|
-
*
|
|
1375
|
+
* Answers one field or several.
|
|
1395
1376
|
*
|
|
1396
1377
|
* @param input - One field's name, or the answers to write keyed by field name.
|
|
1397
1378
|
* @param value - The answer to write when `input` names one field.
|
|
@@ -1414,12 +1395,7 @@ var Form = class {
|
|
|
1414
1395
|
for (const [name, answer] of entries) {
|
|
1415
1396
|
if (this.#differs(name, answer)) moved.push(name);
|
|
1416
1397
|
if (answer === void 0) delete this.#values[name];
|
|
1417
|
-
else
|
|
1418
|
-
value: cloneValue(answer),
|
|
1419
|
-
enumerable: true,
|
|
1420
|
-
configurable: true,
|
|
1421
|
-
writable: true
|
|
1422
|
-
});
|
|
1398
|
+
else defineEntry(this.#values, name, cloneValue(answer));
|
|
1423
1399
|
this.#invalidations.delete(name);
|
|
1424
1400
|
}
|
|
1425
1401
|
for (const name of moved) {
|
|
@@ -1430,7 +1406,7 @@ var Form = class {
|
|
|
1430
1406
|
});
|
|
1431
1407
|
}
|
|
1432
1408
|
/**
|
|
1433
|
-
*
|
|
1409
|
+
* Records that somebody has visited a field.
|
|
1434
1410
|
*
|
|
1435
1411
|
* @param name - The field's name.
|
|
1436
1412
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1442,7 +1418,7 @@ var Form = class {
|
|
|
1442
1418
|
this.#touched.add(name);
|
|
1443
1419
|
}
|
|
1444
1420
|
/**
|
|
1445
|
-
*
|
|
1421
|
+
* Fails a field from outside, for what the rules cannot see.
|
|
1446
1422
|
*
|
|
1447
1423
|
* @param name - The field's name.
|
|
1448
1424
|
* @param message - What to tell the person.
|
|
@@ -1461,7 +1437,7 @@ var Form = class {
|
|
|
1461
1437
|
});
|
|
1462
1438
|
}
|
|
1463
1439
|
/**
|
|
1464
|
-
*
|
|
1440
|
+
* Takes one or more fields out of the form.
|
|
1465
1441
|
*
|
|
1466
1442
|
* @param input - One field name, several names, or absence to select every field.
|
|
1467
1443
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1473,7 +1449,7 @@ var Form = class {
|
|
|
1473
1449
|
this.#change(input, true);
|
|
1474
1450
|
}
|
|
1475
1451
|
/**
|
|
1476
|
-
*
|
|
1452
|
+
* Puts one or more fields back into the form.
|
|
1477
1453
|
*
|
|
1478
1454
|
* @param input - One field name, several names, or absence to select every field.
|
|
1479
1455
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1485,7 +1461,7 @@ var Form = class {
|
|
|
1485
1461
|
this.#change(input, false);
|
|
1486
1462
|
}
|
|
1487
1463
|
/**
|
|
1488
|
-
*
|
|
1464
|
+
* Checks every answer and settles the form when they all pass.
|
|
1489
1465
|
*
|
|
1490
1466
|
* @returns The values on success, or every error that stopped them.
|
|
1491
1467
|
* @remarks
|
|
@@ -1538,7 +1514,7 @@ var Form = class {
|
|
|
1538
1514
|
});
|
|
1539
1515
|
}
|
|
1540
1516
|
/**
|
|
1541
|
-
*
|
|
1517
|
+
* Returns every answer to the ones the form opened with: the schema's defaults, overlaid with
|
|
1542
1518
|
* any seeded `values`. Reset the runtime disabled state to the schema's declarations.
|
|
1543
1519
|
*
|
|
1544
1520
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended.
|
|
@@ -1547,12 +1523,7 @@ var Form = class {
|
|
|
1547
1523
|
this.#gate();
|
|
1548
1524
|
this.#batch(() => {
|
|
1549
1525
|
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
|
-
});
|
|
1526
|
+
for (const [name, value] of Object.entries(this.#baseline)) defineEntry(this.#values, name, value);
|
|
1556
1527
|
this.#touched.clear();
|
|
1557
1528
|
this.#disabled.clear();
|
|
1558
1529
|
this.#invalidations.clear();
|
|
@@ -1562,7 +1533,7 @@ var Form = class {
|
|
|
1562
1533
|
});
|
|
1563
1534
|
}
|
|
1564
1535
|
/**
|
|
1565
|
-
*
|
|
1536
|
+
* Tears the form down, abandoning it when it has not settled.
|
|
1566
1537
|
*
|
|
1567
1538
|
* @remarks
|
|
1568
1539
|
* Destroying twice does nothing the second time. A settled form keeps its `settled` status and
|
|
@@ -1640,12 +1611,7 @@ var Form = class {
|
|
|
1640
1611
|
const disabled = this.disabled;
|
|
1641
1612
|
for (const field of this.#schema.fields) {
|
|
1642
1613
|
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
|
-
});
|
|
1614
|
+
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, value);
|
|
1649
1615
|
}
|
|
1650
1616
|
return Object.freeze(answers);
|
|
1651
1617
|
}
|
|
@@ -1670,7 +1636,7 @@ var Form = class {
|
|
|
1670
1636
|
//#endregion
|
|
1671
1637
|
//#region src/core/factories.ts
|
|
1672
1638
|
/**
|
|
1673
|
-
*
|
|
1639
|
+
* Opens a form against a schema.
|
|
1674
1640
|
*
|
|
1675
1641
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1676
1642
|
* @param options - The form's settings.
|
|
@@ -1681,8 +1647,10 @@ var Form = class {
|
|
|
1681
1647
|
* @throws A {@link FormError} coded `SCHEMA` when the schema is malformed, `FIELD` when
|
|
1682
1648
|
* `options.values` names a field the schema does not declare, and `CONTROL` when a seeded value
|
|
1683
1649
|
* is one its field's control cannot hold.
|
|
1684
|
-
* @example
|
|
1650
|
+
* @example Open a form, answer it, and settle it
|
|
1685
1651
|
* ```ts
|
|
1652
|
+
* import { createForm } from '@orkestrel/form'
|
|
1653
|
+
*
|
|
1686
1654
|
* const form = createForm({
|
|
1687
1655
|
* label: 'Sign up',
|
|
1688
1656
|
* fields: [
|
|
@@ -1692,13 +1660,14 @@ var Form = class {
|
|
|
1692
1660
|
* })
|
|
1693
1661
|
*
|
|
1694
1662
|
* form.fill({ email: 'ada@example.com', terms: true })
|
|
1695
|
-
* form.submit() // { success: true, value: { email: 'ada@example.com', terms: true } }
|
|
1663
|
+
* const result = form.submit() // { success: true, value: { email: 'ada@example.com', terms: true } }
|
|
1664
|
+
* const answers = await form.answer // { email: 'ada@example.com', terms: true }
|
|
1696
1665
|
* ```
|
|
1697
1666
|
*/
|
|
1698
1667
|
function createForm(schema, options) {
|
|
1699
1668
|
return new Form(schema, options);
|
|
1700
1669
|
}
|
|
1701
1670
|
//#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 };
|
|
1671
|
+
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
1672
|
|
|
1704
1673
|
//# sourceMappingURL=index.js.map
|