@orkestrel/form 0.0.11 → 0.0.12
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 +5 -0
- package/dist/src/browser/index.d.ts +26 -5
- package/dist/src/browser/index.js +86 -6
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +62 -14
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +27 -6
- package/dist/src/core/index.d.ts +27 -6
- package/dist/src/core/index.js +62 -14
- package/dist/src/core/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -354,7 +354,7 @@ export declare interface FieldChoice {
|
|
|
354
354
|
* const control: FieldControl = 'select'
|
|
355
355
|
* ```
|
|
356
356
|
*/
|
|
357
|
-
export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'file';
|
|
357
|
+
export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'list' | 'file';
|
|
358
358
|
|
|
359
359
|
/**
|
|
360
360
|
* Represents one failed check against one field.
|
|
@@ -377,11 +377,12 @@ export declare interface FieldError {
|
|
|
377
377
|
* `minimum` and `maximum` measure whatever the control makes countable: characters for
|
|
378
378
|
* `text`, `editor`, and `password`; magnitude for `number`; chronology for `date`, `time`,
|
|
379
379
|
* and `datetime`, whose operand is a string written in that control's own format; and the
|
|
380
|
-
* number of
|
|
380
|
+
* number of entries for `checkbox`, `list`, and `file`.
|
|
381
381
|
*
|
|
382
382
|
* `step` is the interval a numeric value must land on. `pattern` is regular-expression
|
|
383
383
|
* source. `email`, `url`, `integer`, and `alphanumeric` each assert one shape over the whole
|
|
384
|
-
* value
|
|
384
|
+
* value, or each entry of a `list`. `required` requires an entry for `list` and presence
|
|
385
|
+
* for other controls. `custom` runs last and is the only rule that sees the rest of the form.
|
|
385
386
|
*
|
|
386
387
|
* @example
|
|
387
388
|
* ```ts
|
|
@@ -440,7 +441,7 @@ export declare type FieldValidator = (value: FieldValue | undefined, values: For
|
|
|
440
441
|
*
|
|
441
442
|
* @remarks
|
|
442
443
|
* The variant follows the control: text-like controls hold a `string`, `number` holds a
|
|
443
|
-
* `number`, `confirm` holds a `boolean`, and `checkbox`
|
|
444
|
+
* `number`, `confirm` holds a `boolean`, and `checkbox`, `list`, and `file` hold a
|
|
444
445
|
* `readonly string[]`.
|
|
445
446
|
*/
|
|
446
447
|
export declare type FieldValue = string | number | boolean | readonly string[];
|
|
@@ -753,7 +754,7 @@ export declare type FormEventMap = {
|
|
|
753
754
|
* }
|
|
754
755
|
* ```
|
|
755
756
|
*/
|
|
756
|
-
export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | FileField;
|
|
757
|
+
export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | ListField | FileField;
|
|
757
758
|
|
|
758
759
|
/**
|
|
759
760
|
* Represents a named section of a form.
|
|
@@ -1202,6 +1203,26 @@ export declare function isFormValues(input: unknown): input is FormValues;
|
|
|
1202
1203
|
/** Caps the number of entries one list-valued answer may hold, at 1024. */
|
|
1203
1204
|
export declare const LIST_LIMIT = 1024;
|
|
1204
1205
|
|
|
1206
|
+
/**
|
|
1207
|
+
* Represents an ordered list of free-text strings.
|
|
1208
|
+
*
|
|
1209
|
+
* @remarks
|
|
1210
|
+
* `required` requires at least one entry. `minimum` and `maximum` count entries.
|
|
1211
|
+
* `pattern`, `email`, `url`, `integer`, and `alphanumeric` check each entry and name
|
|
1212
|
+
* the failing value in its message. `custom` checks the whole list. Duplicates are retained.
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* const to: ListField = {
|
|
1217
|
+
* control: 'list', name: 'to', rule: { required: true, email: true },
|
|
1218
|
+
* }
|
|
1219
|
+
* ```
|
|
1220
|
+
*/
|
|
1221
|
+
export declare interface ListField extends FieldBase {
|
|
1222
|
+
readonly control: 'list';
|
|
1223
|
+
readonly default?: readonly string[];
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1205
1226
|
/**
|
|
1206
1227
|
* Decides whether a raw binding value projects to an answered field.
|
|
1207
1228
|
*
|
|
@@ -1209,7 +1230,7 @@ export declare const LIST_LIMIT = 1024;
|
|
|
1209
1230
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
1210
1231
|
* value and a string containing only whitespace as unanswered. Every other field value is an
|
|
1211
1232
|
* answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
|
|
1212
|
-
* its `required` rule remains presence-only.
|
|
1233
|
+
* its `required` rule remains presence-only except that `list` requires an entry.
|
|
1213
1234
|
*
|
|
1214
1235
|
* @param value - The raw field value, or absence.
|
|
1215
1236
|
* @returns True if the binding preserves the value as an answer; false otherwise.
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -354,7 +354,7 @@ export declare interface FieldChoice {
|
|
|
354
354
|
* const control: FieldControl = 'select'
|
|
355
355
|
* ```
|
|
356
356
|
*/
|
|
357
|
-
export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'file';
|
|
357
|
+
export declare type FieldControl = 'text' | 'editor' | 'password' | 'number' | 'date' | 'time' | 'datetime' | 'color' | 'confirm' | 'select' | 'checkbox' | 'list' | 'file';
|
|
358
358
|
|
|
359
359
|
/**
|
|
360
360
|
* Represents one failed check against one field.
|
|
@@ -377,11 +377,12 @@ export declare interface FieldError {
|
|
|
377
377
|
* `minimum` and `maximum` measure whatever the control makes countable: characters for
|
|
378
378
|
* `text`, `editor`, and `password`; magnitude for `number`; chronology for `date`, `time`,
|
|
379
379
|
* and `datetime`, whose operand is a string written in that control's own format; and the
|
|
380
|
-
* number of
|
|
380
|
+
* number of entries for `checkbox`, `list`, and `file`.
|
|
381
381
|
*
|
|
382
382
|
* `step` is the interval a numeric value must land on. `pattern` is regular-expression
|
|
383
383
|
* source. `email`, `url`, `integer`, and `alphanumeric` each assert one shape over the whole
|
|
384
|
-
* value
|
|
384
|
+
* value, or each entry of a `list`. `required` requires an entry for `list` and presence
|
|
385
|
+
* for other controls. `custom` runs last and is the only rule that sees the rest of the form.
|
|
385
386
|
*
|
|
386
387
|
* @example
|
|
387
388
|
* ```ts
|
|
@@ -440,7 +441,7 @@ export declare type FieldValidator = (value: FieldValue | undefined, values: For
|
|
|
440
441
|
*
|
|
441
442
|
* @remarks
|
|
442
443
|
* The variant follows the control: text-like controls hold a `string`, `number` holds a
|
|
443
|
-
* `number`, `confirm` holds a `boolean`, and `checkbox`
|
|
444
|
+
* `number`, `confirm` holds a `boolean`, and `checkbox`, `list`, and `file` hold a
|
|
444
445
|
* `readonly string[]`.
|
|
445
446
|
*/
|
|
446
447
|
export declare type FieldValue = string | number | boolean | readonly string[];
|
|
@@ -753,7 +754,7 @@ export declare type FormEventMap = {
|
|
|
753
754
|
* }
|
|
754
755
|
* ```
|
|
755
756
|
*/
|
|
756
|
-
export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | FileField;
|
|
757
|
+
export declare type FormField = TextField | EditorField | PasswordField | NumberField | DateField | TimeField | DatetimeField | ColorField | ConfirmField | SelectField | CheckboxField | ListField | FileField;
|
|
757
758
|
|
|
758
759
|
/**
|
|
759
760
|
* Represents a named section of a form.
|
|
@@ -1202,6 +1203,26 @@ export declare function isFormValues(input: unknown): input is FormValues;
|
|
|
1202
1203
|
/** Caps the number of entries one list-valued answer may hold, at 1024. */
|
|
1203
1204
|
export declare const LIST_LIMIT = 1024;
|
|
1204
1205
|
|
|
1206
|
+
/**
|
|
1207
|
+
* Represents an ordered list of free-text strings.
|
|
1208
|
+
*
|
|
1209
|
+
* @remarks
|
|
1210
|
+
* `required` requires at least one entry. `minimum` and `maximum` count entries.
|
|
1211
|
+
* `pattern`, `email`, `url`, `integer`, and `alphanumeric` check each entry and name
|
|
1212
|
+
* the failing value in its message. `custom` checks the whole list. Duplicates are retained.
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* ```ts
|
|
1216
|
+
* const to: ListField = {
|
|
1217
|
+
* control: 'list', name: 'to', rule: { required: true, email: true },
|
|
1218
|
+
* }
|
|
1219
|
+
* ```
|
|
1220
|
+
*/
|
|
1221
|
+
export declare interface ListField extends FieldBase {
|
|
1222
|
+
readonly control: 'list';
|
|
1223
|
+
readonly default?: readonly string[];
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1205
1226
|
/**
|
|
1206
1227
|
* Decides whether a raw binding value projects to an answered field.
|
|
1207
1228
|
*
|
|
@@ -1209,7 +1230,7 @@ export declare const LIST_LIMIT = 1024;
|
|
|
1209
1230
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
1210
1231
|
* value and a string containing only whitespace as unanswered. Every other field value is an
|
|
1211
1232
|
* answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
|
|
1212
|
-
* its `required` rule remains presence-only.
|
|
1233
|
+
* its `required` rule remains presence-only except that `list` requires an entry.
|
|
1213
1234
|
*
|
|
1214
1235
|
* @param value - The raw field value, or absence.
|
|
1215
1236
|
* @returns True if the binding preserves the value as an answer; false otherwise.
|
package/dist/src/core/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var FIELD_CONTROLS = Object.freeze([
|
|
|
14
14
|
"confirm",
|
|
15
15
|
"select",
|
|
16
16
|
"checkbox",
|
|
17
|
+
"list",
|
|
17
18
|
"file"
|
|
18
19
|
]);
|
|
19
20
|
/** Lists the members every field declares, whatever its control. */
|
|
@@ -66,6 +67,7 @@ var FIELD_KEYS = Object.freeze({
|
|
|
66
67
|
"choices",
|
|
67
68
|
"default"
|
|
68
69
|
]),
|
|
70
|
+
list: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
69
71
|
file: Object.freeze([
|
|
70
72
|
...FIELD_BASE_KEYS,
|
|
71
73
|
"accept",
|
|
@@ -324,6 +326,7 @@ function isFormField(input) {
|
|
|
324
326
|
const fallback = hasDefault ? input.default : void 0;
|
|
325
327
|
return arrayOf(isFieldChoice)(choices) && (!hasDefault || arrayOf(isString)(fallback));
|
|
326
328
|
}
|
|
329
|
+
case "list": return !Object.hasOwn(input, "default") || arrayOf(isString)(input.default);
|
|
327
330
|
case "file": {
|
|
328
331
|
const hasAccept = Object.hasOwn(input, "accept");
|
|
329
332
|
const accept = hasAccept ? input.accept : void 0;
|
|
@@ -448,6 +451,12 @@ function cloneFormField(field) {
|
|
|
448
451
|
choices: cloneChoices(field.choices),
|
|
449
452
|
...field.default === void 0 ? {} : { default: cloneValue(field.default) }
|
|
450
453
|
});
|
|
454
|
+
case "list": return Object.freeze({
|
|
455
|
+
...field,
|
|
456
|
+
...rule,
|
|
457
|
+
...meta,
|
|
458
|
+
...field.default === void 0 ? {} : { default: cloneValue(field.default) }
|
|
459
|
+
});
|
|
451
460
|
case "file": return Object.freeze({
|
|
452
461
|
...field,
|
|
453
462
|
...rule,
|
|
@@ -572,6 +581,7 @@ function matchesField(field, value) {
|
|
|
572
581
|
case "confirm": return isBoolean(value);
|
|
573
582
|
case "select": return isString(value) && !field.choices.some((choice) => choice.value === value && choice.disabled === true) && (field.open === true || field.choices.some((choice) => choice.value === value && choice.disabled !== true));
|
|
574
583
|
case "checkbox": return entries !== void 0 && entries.every((entry) => isString(entry) && field.choices.some((choice) => choice.value === entry && choice.disabled !== true)) && new Set(entries).size === entries.length;
|
|
584
|
+
case "list": return entries !== void 0;
|
|
575
585
|
case "file": return entries !== void 0 && (field.multiple === true || entries.length <= 1);
|
|
576
586
|
}
|
|
577
587
|
}
|
|
@@ -582,7 +592,7 @@ function matchesField(field, value) {
|
|
|
582
592
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
583
593
|
* value and a string containing only whitespace as unanswered. Every other field value is an
|
|
584
594
|
* answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
|
|
585
|
-
* its `required` rule remains presence-only.
|
|
595
|
+
* its `required` rule remains presence-only except that `list` requires an entry.
|
|
586
596
|
*
|
|
587
597
|
* @param value - The raw field value, or absence.
|
|
588
598
|
* @returns True if the binding preserves the value as an answer; false otherwise.
|
|
@@ -630,7 +640,7 @@ function appliesRule(control, rule) {
|
|
|
630
640
|
function evaluateField(field, value, values, messages) {
|
|
631
641
|
const errors = [];
|
|
632
642
|
const rule = field.rule;
|
|
633
|
-
if (value === void 0) {
|
|
643
|
+
if (value === void 0 || field.control === "list" && isArray(value) && value.length === 0) {
|
|
634
644
|
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
|
|
635
645
|
}
|
|
636
646
|
if (rule === void 0) return Object.freeze(errors);
|
|
@@ -651,6 +661,7 @@ function evaluateField(field, value, values, messages) {
|
|
|
651
661
|
failed = isString(value) && isString(rule.minimum) && value < rule.minimum;
|
|
652
662
|
break;
|
|
653
663
|
case "checkbox":
|
|
664
|
+
case "list":
|
|
654
665
|
case "file": failed = isArray(value) && isFiniteNumber(rule.minimum) && value.length < rule.minimum;
|
|
655
666
|
}
|
|
656
667
|
if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
|
|
@@ -672,6 +683,7 @@ function evaluateField(field, value, values, messages) {
|
|
|
672
683
|
failed = isString(value) && isString(rule.maximum) && value > rule.maximum;
|
|
673
684
|
break;
|
|
674
685
|
case "checkbox":
|
|
686
|
+
case "list":
|
|
675
687
|
case "file": failed = isArray(value) && isFiniteNumber(rule.maximum) && value.length > rule.maximum;
|
|
676
688
|
}
|
|
677
689
|
if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
|
|
@@ -680,19 +692,45 @@ function evaluateField(field, value, values, messages) {
|
|
|
680
692
|
const multiple = (value - (isFiniteNumber(rule.minimum) ? rule.minimum : 0)) / rule.step;
|
|
681
693
|
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));
|
|
682
694
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
695
|
+
const entries = isString(value) ? [value] : field.control === "list" && isArray(value) ? value : [];
|
|
696
|
+
for (const constraint of [
|
|
697
|
+
"pattern",
|
|
698
|
+
"email",
|
|
699
|
+
"url",
|
|
700
|
+
"alphanumeric",
|
|
701
|
+
"integer"
|
|
702
|
+
]) {
|
|
703
|
+
if (!appliesRule(field.control, constraint)) continue;
|
|
704
|
+
for (const entry of entries) {
|
|
705
|
+
let failed = false;
|
|
706
|
+
switch (constraint) {
|
|
707
|
+
case "pattern": {
|
|
708
|
+
const pattern = rule.pattern;
|
|
709
|
+
if (pattern !== void 0) {
|
|
710
|
+
const outcome = attempt(() => pattern.length <= 256 && new RegExp(pattern).test(entry));
|
|
711
|
+
failed = !outcome.success || !outcome.value;
|
|
712
|
+
}
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
case "email":
|
|
716
|
+
failed = rule.email === true && !EMAIL_PATTERN.test(entry);
|
|
717
|
+
break;
|
|
718
|
+
case "url":
|
|
719
|
+
failed = rule.url === true && !URL_PATTERN.test(entry);
|
|
720
|
+
break;
|
|
721
|
+
case "alphanumeric":
|
|
722
|
+
failed = rule.alphanumeric === true && !ALPHANUMERIC_PATTERN.test(entry);
|
|
723
|
+
break;
|
|
724
|
+
case "integer": failed = rule.integer === true && !INTEGER_PATTERN.test(entry);
|
|
725
|
+
}
|
|
726
|
+
if (failed) {
|
|
727
|
+
const error = createFieldError(field, constraint, void 0, messages);
|
|
728
|
+
errors.push(field.control === "list" ? Object.freeze({
|
|
729
|
+
...error,
|
|
730
|
+
message: `${JSON.stringify(entry)}: ${error.message}`
|
|
731
|
+
}) : error);
|
|
690
732
|
}
|
|
691
733
|
}
|
|
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));
|
|
696
734
|
}
|
|
697
735
|
if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !isInteger(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
698
736
|
if (rule.custom !== void 0) {
|
|
@@ -736,6 +774,7 @@ function computeDefaults(schema) {
|
|
|
736
774
|
const defaults = {};
|
|
737
775
|
for (const field of schema.fields) switch (field.control) {
|
|
738
776
|
case "checkbox":
|
|
777
|
+
case "list":
|
|
739
778
|
if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
|
|
740
779
|
break;
|
|
741
780
|
case "password":
|
|
@@ -923,6 +962,9 @@ function serializeForm(schema) {
|
|
|
923
962
|
});
|
|
924
963
|
if (field.default !== void 0) entry.default = field.default;
|
|
925
964
|
break;
|
|
965
|
+
case "list":
|
|
966
|
+
if (field.default !== void 0) entry.default = field.default;
|
|
967
|
+
break;
|
|
926
968
|
case "file":
|
|
927
969
|
if (field.accept !== void 0) entry.accept = field.accept;
|
|
928
970
|
if (field.multiple !== void 0) entry.multiple = field.multiple;
|
|
@@ -1074,7 +1116,8 @@ function auditSchema(schema) {
|
|
|
1074
1116
|
case "color":
|
|
1075
1117
|
case "confirm":
|
|
1076
1118
|
case "select":
|
|
1077
|
-
case "checkbox":
|
|
1119
|
+
case "checkbox":
|
|
1120
|
+
case "list": if (field.default !== void 0 && !matchesField(field, field.default)) faults.push(`Field "${field.name}" has an invalid default`);
|
|
1078
1121
|
}
|
|
1079
1122
|
if (field.control === "select" || field.control === "checkbox") {
|
|
1080
1123
|
const choices = /* @__PURE__ */ new Set();
|
|
@@ -1093,6 +1136,11 @@ function auditSchema(schema) {
|
|
|
1093
1136
|
}
|
|
1094
1137
|
const rule = field.rule;
|
|
1095
1138
|
if (rule === void 0) continue;
|
|
1139
|
+
if (field.control === "list") {
|
|
1140
|
+
for (const bound of [rule.minimum, rule.maximum]) if (isFiniteNumber(bound) && (!isInteger(bound) || bound < 0)) faults.push(`Field "${field.name}" has a count that is not a nonnegative integer`);
|
|
1141
|
+
if (isFiniteNumber(rule.minimum) && rule.minimum > 1024) faults.push(`Field "${field.name}" has minimum greater than ${LIST_LIMIT}`);
|
|
1142
|
+
if (rule.required === true && isFiniteNumber(rule.maximum) && rule.maximum < 1) faults.push(`Field "${field.name}" is required but permits no entries`);
|
|
1143
|
+
}
|
|
1096
1144
|
const temporal = field.control === "date" || field.control === "time" || field.control === "datetime";
|
|
1097
1145
|
if (rule.minimum !== void 0 && !appliesRule(field.control, "minimum")) faults.push(`Field "${field.name}" has minimum on ${field.control}`);
|
|
1098
1146
|
else if (isString(rule.minimum) && !temporal) faults.push(`Field "${field.name}" has a string minimum on ${field.control}`);
|