@dynamicforms/vue-forms 0.12.0 → 0.13.0

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/dist/index.d.ts CHANGED
@@ -110,6 +110,12 @@ export declare interface ActionValue {
110
110
  icon?: string;
111
111
  }
112
112
 
113
+ /**
114
+ * The values a field may hold: a fixed list, or a reference or a callback for a list that is filled in or replaced
115
+ * after the validator is built - the options a server answers with, or the ones another field's value leaves open.
116
+ */
117
+ declare type AllowedValues<T> = T[] | Ref<T[]> | (() => T[]);
118
+
113
119
  /**
114
120
  * Depending on the useMarkdownInValidators setting, return either markdown or a plain string error message
115
121
  * @param mdErrorString markdown source to optionally be stripped
@@ -269,8 +275,8 @@ export declare namespace DisplayMode {
269
275
  * walk their way down a structure over own keys, enumerable symbols included, and the `parent` slot is in the
270
276
  * state: reachable, it would take either walker back into the container the element came from.
271
277
  *
272
- * What they do reach on an element is `_actions` once something is registered, `_fields` on a `Group` and
273
- * `_itemTemplate` on a `List`. All three lead downwards only.
278
+ * What they do reach on an element is `_actions` once something is registered, `_fields` and the guarded view
279
+ * over it on a `Group`, and `_itemTemplate` on a `List`. All of them lead downwards only.
274
280
  */
275
281
  declare interface ElementSlots<T = any> {
276
282
  /** the value the element was given at construction; isChanged compares against it */
@@ -284,6 +290,8 @@ declare interface ElementSlots<T = any> {
284
290
  valueVersion: number;
285
291
  /** how many asynchronous validation runs are in flight on this element */
286
292
  validatingCount: number;
293
+ /** how many direct children answer `validating` with true; a child that starts or stops running moves it */
294
+ validatingChildren: number;
287
295
  /** the container that holds this element, absent while none does; takeChild writes it, releaseChild clears it */
288
296
  parent: FieldBase | undefined;
289
297
  /** the name the containing Group holds this element under; a List row carries none */
@@ -499,16 +507,56 @@ export declare abstract class FieldBase<T = any, X extends object = {}> {
499
507
  * Puts a captured state back. It writes through the tracked view, so an effect that ran on the state the
500
508
  * transaction produced runs again on the state it is being returned to, and only for the slots that differ.
501
509
  *
502
- * validatingCount is the one slot the element keeps: it counts runs that are in flight, and a rollback cannot
503
- * un-start one. Put back, the count would no longer match the endValidating calls still to come.
510
+ * The two validation counters are the slots the element keeps: they count runs that are in flight, and a
511
+ * rollback cannot un-start one. Put back, the counts would no longer match the endValidating calls still to come.
504
512
  */
505
513
  protected [TxRestore](snapshot: TxSnapshot): void;
506
- /** true while at least one asynchronous validator is still running for this field */
514
+ /**
515
+ * True while an asynchronous validation is in flight on this element or on anything below it, so a form answers
516
+ * for the whole tree it holds. The answer is a pair of counters rather than a walk: the element's own runs, and
517
+ * how many of its children answer the same question with true. A child that starts running while it was idle,
518
+ * or stops while it was the last one running, moves its container's tally, and a tally that changes the
519
+ * container's own answer moves the one above it - the transition costs the nesting depth and the read costs
520
+ * nothing.
521
+ */
507
522
  get validating(): boolean;
508
523
  /** announces the start of one asynchronous validation run; validators pair it with endValidating */
509
524
  beginValidating(): void;
510
525
  /** announces the end of one asynchronous validation run */
511
526
  endValidating(): void;
527
+ /**
528
+ * Records that a child started or stopped answering `validating` with true, and carries the transition further
529
+ * up where it changes this element's own answer.
530
+ */
531
+ protected childValidatingChanged(started: boolean): void;
532
+ /**
533
+ * True while an `Action.execute()` at or below this element has yet to settle. An `Action` answers for its own
534
+ * runs; a container answers for the actions below it; anything else answers false, because an element that is
535
+ * not an action has nothing to execute.
536
+ *
537
+ * It states one thing and `validating` states the other, so a form that gates a submit button on the tree being
538
+ * idle reads both.
539
+ */
540
+ get busy(): boolean;
541
+ /**
542
+ * The composed answer a container gives `busy`, memoised by Vue. An `Action` counts its executions in a counter
543
+ * of its own, which no container is told about, so the answer is composed over the members instead of tallied;
544
+ * the computed keeps the walk from repeating while nothing it read has moved, and a member that is itself a
545
+ * container answers from its own computed.
546
+ */
547
+ protected get busyRead(): boolean;
548
+ /** What busyRead computes. A container answers over its members; anything else executes nothing. */
549
+ protected composeBusy(): boolean;
550
+ /**
551
+ * Resolves once nothing at or below this element is running - no asynchronous validation, no `Action.execute()`
552
+ * that has yet to settle. It is what a submit path awaits instead of polling `validating` and `busy`, and it
553
+ * resolves at once where nothing is running to begin with.
554
+ *
555
+ * It answers for the moment it resolves and makes no promise about the one after: work started later leaves the
556
+ * element running again, so a caller that has to act on a settled tree reads what it needs immediately after
557
+ * awaiting rather than at its leisure.
558
+ */
559
+ settled(): Promise<void>;
512
560
  /**
513
561
  * List of errors. The array handed out is the one the element holds, and a validator writes into it in place,
514
562
  * so a transaction that is open takes the element into its record here: a rollback that put back everything
@@ -670,7 +718,7 @@ export declare abstract class FieldBase<T = any, X extends object = {}> {
670
718
  protected [TxSettleValidity](tx: Transaction): void;
671
719
  validate(revalidate?: boolean): void;
672
720
  get valid(): boolean;
673
- get fullValue(): any;
721
+ get fullValue(): T;
674
722
  get isChanged(): boolean;
675
723
  /**
676
724
  * Brings a fresh element into the state a binding of `source` is in: it records what it was declared as, takes
@@ -725,6 +773,15 @@ declare interface FieldSlots<T = any> extends ElementSlots<T> {
725
773
  touched: boolean;
726
774
  }
727
775
 
776
+ /**
777
+ * What Group.fullValue reads back. The indexed access reads each field's fullValue getter, so a nested group
778
+ * contributes its own full structure rather than the partial one its `value` builds, and the recursion carries
779
+ * the guarantee all the way down: every key is present and none of them is null.
780
+ */
781
+ export declare type FieldsToFullValues<T extends GenericFieldsInterface> = {
782
+ [K in keyof T]: T[K]['fullValue'];
783
+ };
784
+
728
785
  export declare type FieldsToValues<T extends GenericFieldsInterface> = {
729
786
  [K in keyof T]: T[K]['value'];
730
787
  };
@@ -768,6 +825,7 @@ declare namespace Form {
768
825
  FieldBase,
769
826
  GenericFieldsInterface,
770
827
  FieldsToValues,
828
+ FieldsToFullValues,
771
829
  GroupValue,
772
830
  GroupValueInput,
773
831
  Group,
@@ -803,26 +861,79 @@ export declare const forms: {
803
861
  install(app: any, options?: Partial<FormsConfig>): void;
804
862
  };
805
863
 
806
- declare interface FormsConfig {
864
+ /**
865
+ * Global configuration for @dynamicforms/vue-forms.
866
+ *
867
+ * The configuration is module-global: `setConfig` and the plugin's `install` write the single record this module
868
+ * holds, so in a process running several Vue apps the configuration applied last is the one all of them read.
869
+ */
870
+ export declare interface FormsConfig {
871
+ /** Whether the built-in validators phrase their error messages as `MdString` markdown instead of plain text. */
807
872
  useMarkdownInValidators: boolean;
808
873
  }
809
874
 
810
875
  export declare type GenericFieldsInterface = Record<string, FieldBase>;
811
876
 
877
+ /** The current configuration. The object is the module's own record, and reading it again reports later writes. */
878
+ export declare function getConfig(): FormsConfig;
879
+
812
880
  export declare class Group<T extends GenericFieldsInterface = GenericFieldsInterface, X extends object = {}> extends FieldBase<GroupValue<T>, X> {
813
- protected get state(): ContainerSlots<GroupValue<T>>;
814
- protected get raw(): ContainerSlots<GroupValue<T>>;
881
+ protected get state(): GroupSlots<GroupValue<T>>;
882
+ protected get raw(): GroupSlots<GroupValue<T>>;
815
883
  private readonly _fields;
816
884
  constructor(fields: T, params?: IFieldParams<GroupValueInput<T>, X>);
885
+ /**
886
+ * The name array is copied as well: a rollback puts back the set of members the group held, not the array it
887
+ * went on to hold.
888
+ */
889
+ protected [TxCapture](): TxSnapshot;
817
890
  /**
818
891
  * The copy of a built value that serves as a baseline. The value getter hands out one object per version, and
819
892
  * a baseline holding that same object would report every value as its own original.
820
893
  */
821
894
  private static baseline;
822
- private addField;
895
+ /**
896
+ * Takes `field` into this group under `fieldName`. The group ends up holding it exactly as it holds a field the
897
+ * constructor was given: the field carries the back-reference and the name, its verdict counts towards the
898
+ * group's, and a rule of the field's that names another member of the form reaches it here.
899
+ *
900
+ * The change is announced through the ordinary path, so a group whose value the new field adds to says so once
901
+ * the transaction closes, and its verdict is re-formed over the members it now holds. The baseline behind
902
+ * `isChanged` is not rewritten: a group that gains a field holds something its original value does not carry,
903
+ * and reports itself changed until `originalValue` is written.
904
+ *
905
+ * @throws Error where this group already holds a field under `fieldName`.
906
+ * @throws TypeError where `field` already belongs to a container - pass a `bind()` of it instead.
907
+ */
908
+ addField(fieldName: string, field: FieldBase): this;
909
+ /**
910
+ * Takes the field held under `fieldName` out of this group and hands it back, answering undefined where the
911
+ * group holds no field of that name. The field is released whole: the back-reference and the name are gone, its
912
+ * verdict and its runs in flight no longer count towards the group's, and it is free to be taken by another
913
+ * container. What it holds - its value, its errors, the change history behind `isChanged` - is its own to report.
914
+ *
915
+ * The change is announced through the ordinary path, so the group's value and verdict settle over the members it
916
+ * has left. The baseline behind `isChanged` is not rewritten, the same way it is not on `addField`.
917
+ */
918
+ removeField(fieldName: string): FieldBase | undefined;
919
+ /**
920
+ * Writes one entry of the member map. The map is typed by the fields interface, which names the members a group
921
+ * of that type holds and no others, so both here and in dropEntry it is reached as the plain record it is.
922
+ */
923
+ private static setEntry;
924
+ /** Drops one entry out of the member map. */
925
+ private static dropEntry;
823
926
  private static isValidFields;
824
927
  static createFromFormData(data: Record<string, any> | null): Group;
825
928
  field<K extends keyof T>(fieldName: K): T[K] | null;
929
+ /**
930
+ * The typed map of this group's members. What it hands out is a view over the map the group holds: reading it
931
+ * reaches the members themselves, and every write to it is refused - addField() and removeField() are what
932
+ * change the set. The view is built the first time something asks for it.
933
+ *
934
+ * Reading through it is a tracked read of the set of members, so a template rendering off `group.fields`
935
+ * re-renders when a field is added or removed; what a member itself holds is tracked by that member.
936
+ */
826
937
  get fields(): T;
827
938
  protected get members(): FieldBase[];
828
939
  get value(): GroupValue<T>;
@@ -841,7 +952,7 @@ export declare class Group<T extends GenericFieldsInterface = GenericFieldsInter
841
952
  protected resetTo(source: FieldBase, value: any): void;
842
953
  get touched(): boolean;
843
954
  set touched(touched: boolean);
844
- get fullValue(): Record<string, any>;
955
+ get fullValue(): FieldsToFullValues<T>;
845
956
  /**
846
957
  * Records that a member changed its value, so that the transaction in progress works out at commit what this
847
958
  * group's own value became and announces it once. A mutation method calls it itself; you rarely need to.
@@ -856,6 +967,8 @@ export declare class Group<T extends GenericFieldsInterface = GenericFieldsInter
856
967
  protected refreshPreviousValue(): void;
857
968
  get valid(): boolean;
858
969
  protected composeValid(): boolean;
970
+ get busy(): boolean;
971
+ protected composeBusy(): boolean;
859
972
  validate(revalidate?: boolean): void;
860
973
  bind(data?: GroupValueInput<T>, overrides?: IBindParams<GroupValueInput<T>, X>): Group<T, X>;
861
974
  /**
@@ -865,8 +978,21 @@ export declare class Group<T extends GenericFieldsInterface = GenericFieldsInter
865
978
  rebind(data: GroupValueInput<T>): this;
866
979
  }
867
980
 
868
- /** what Group.value reads back: the full field map, or null when no field serializes */
869
- export declare type GroupValue<T extends GenericFieldsInterface> = FieldsToValues<T> | null;
981
+ /** what a Group holds beyond the container slots: the names of its members, in the order it took them */
982
+ declare interface GroupSlots<T = any> extends ContainerSlots<T> {
983
+ /**
984
+ * The names the group holds its members under. The member map itself is a plain object beside the state, so
985
+ * this array is what a reader inside an effect depends on: a member added or removed re-runs the effect, and a
986
+ * rolled-back transaction puts the set back with the rest of the slots.
987
+ */
988
+ fieldNames: string[];
989
+ }
990
+
991
+ /**
992
+ * What Group.value reads back: the values of the fields that serialize, or null when none does. Every key is
993
+ * optional, because a field that is disabled is left out of the object the group builds.
994
+ */
995
+ export declare type GroupValue<T extends GenericFieldsInterface> = Partial<FieldsToValues<T>> | null;
870
996
 
871
997
  /** what Group.value and the Group constructor accept: keys left out are simply not assigned */
872
998
  export declare type GroupValueInput<T extends GenericFieldsInterface> = Partial<FieldsToValues<T>> | null;
@@ -887,7 +1013,7 @@ export declare interface IFieldConstructorActionsList {
887
1013
  /**
888
1014
  * Parameters accepted by field constructors and by bind overrides.
889
1015
  *
890
- * Only writable members are listed. valid, validating, fullValue and isChanged are getter-only, so assigning
1016
+ * Only writable members are listed. valid, validating, busy, fullValue and isChanged are getter-only, so assigning
891
1017
  * them throws a TypeError. So are parent and fieldName: a container writes the slots behind them when it takes
892
1018
  * an element, and nobody else can, which is why the type rejects them.
893
1019
  */
@@ -915,7 +1041,7 @@ export declare type IFieldConstructorParams<T = any> = {
915
1041
  export declare type IFieldParams<T = any, X extends object = {}> = Partial<IFieldConstructorParams<T>> & Partial<NoInfer<X>>;
916
1042
 
917
1043
  declare class InAllowedValues<T = any> extends Validator {
918
- constructor(allowedValues: T[], message?: RenderContentRef);
1044
+ constructor(allowedValues: AllowedValues<T>, message?: RenderContentRef);
919
1045
  }
920
1046
 
921
1047
  export declare function isCallableFunction(msg?: RenderContentRef): msg is RenderContentCallable;
@@ -927,7 +1053,7 @@ export declare function isCallableFunction(msg?: RenderContentRef): msg is Rende
927
1053
  */
928
1054
  export declare function isSimpleComponentDef(msg?: RenderContentRef): msg is SimpleComponentDef;
929
1055
 
930
- declare class LengthInRange<T = any> extends Validator {
1056
+ declare class LengthInRange extends Validator {
931
1057
  constructor(minLength: number, maxLength: number, message?: RenderContentRef);
932
1058
  }
933
1059
 
@@ -949,9 +1075,13 @@ export declare class List<T extends GenericFieldsInterface = GenericFieldsInterf
949
1075
  * own values, or an empty group when the list has no template.
950
1076
  */
951
1077
  private createPaddingItem;
1078
+ /** Records that the set of rows changed, so `items` rebuilds the frozen array it hands out at the next read. */
1079
+ private rowsChanged;
1080
+ /** True where `next` is a different set of rows than `previous`: another count, or another row at a position. */
1081
+ private static rowsDiffer;
952
1082
  private setValueInternal;
953
1083
  get value(): ListValue;
954
- set value(newValue: Record<string, any>[]);
1084
+ set value(newValue: ListValue);
955
1085
  /**
956
1086
  * The rows are taken from `source` where the caller supplied no value of its own, so a list nested in a row that
957
1087
  * a whole-list assignment reuses ends up holding what the template gives it rather than what it held before.
@@ -974,8 +1104,31 @@ export declare class List<T extends GenericFieldsInterface = GenericFieldsInterf
974
1104
  protected refreshPreviousValue(): void;
975
1105
  get valid(): boolean;
976
1106
  protected composeValid(): boolean;
1107
+ get busy(): boolean;
1108
+ protected composeBusy(): boolean;
977
1109
  validate(revalidate?: boolean): void;
978
1110
  protected get members(): FieldBase[];
1111
+ /**
1112
+ * How many rows this list holds. The read is tracked, so a template rendering off it re-renders as rows come and
1113
+ * go.
1114
+ */
1115
+ /**
1116
+ * Every row of this list, each one built from all of its fields. Where `value` states what the list serializes -
1117
+ * rows composed of the fields that are enabled, and null where the list is empty - this states what the list
1118
+ * holds: the disabled fields are in it too, and an empty list reads back as an empty array rather than as null.
1119
+ */
1120
+ get fullValue(): FieldsToFullValues<T>[];
1121
+ get length(): number;
1122
+ /**
1123
+ * The rows this list holds, oldest position first. The array is a frozen copy: it states what the list held at
1124
+ * the read and nothing writes back through it - `push`, `insert`, `remove` and `clear` are what change the set -
1125
+ * so a caller may hold on to it. The rows in it are the live elements, so reading one reports what it holds now.
1126
+ *
1127
+ * The copy is built once per change of the set and handed to every reader until the next one: a write inside a
1128
+ * row changes what the list serializes without changing which rows it holds, and the array a reader took stays
1129
+ * the same one across such a write.
1130
+ */
1131
+ get items(): readonly Group<T>[];
979
1132
  get(index: number): Group<T> | undefined;
980
1133
  push(item: any): number;
981
1134
  pop(): Group<T> | undefined;
@@ -1001,12 +1154,20 @@ export declare class ListItemRemovedAction extends FieldActionBase {
1001
1154
  /** what a List holds beyond the container slots: the rows themselves */
1002
1155
  declare interface ListSlots<T extends GenericFieldsInterface = GenericFieldsInterface> extends ContainerSlots<ListValue> {
1003
1156
  rows: Group<T>[] | null;
1157
+ /**
1158
+ * Counts the changes to the set of rows. `items` rebuilds the frozen array it hands out when it moves, and
1159
+ * only then: a write inside a row changes what the list serializes without changing which rows it holds.
1160
+ */
1161
+ rowsVersion: number;
1162
+ /** the frozen array `items` last handed out, together with the rows version it was built from */
1163
+ cachedItems: readonly Group<T>[] | null;
1164
+ cachedItemsVersion: number;
1004
1165
  }
1005
1166
 
1006
1167
  /** what List.value reads back: one plain object per item, or null when the list is empty */
1007
1168
  export declare type ListValue = Record<string, any>[] | null;
1008
1169
 
1009
- declare class MaxLength<T = any> extends Validator {
1170
+ declare class MaxLength extends Validator {
1010
1171
  constructor(maxLength: number, message?: RenderContentRef);
1011
1172
  }
1012
1173
 
@@ -1025,7 +1186,7 @@ export declare class MdString extends String {
1025
1186
 
1026
1187
  export declare const MessagesWidget: DefineComponent<Props, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
1027
1188
 
1028
- declare class MinLength<T = any> extends Validator {
1189
+ declare class MinLength extends Validator {
1029
1190
  constructor(minLength: number, message?: RenderContentRef);
1030
1191
  }
1031
1192
 
@@ -1073,7 +1234,7 @@ export declare namespace Operator {
1073
1234
  export function isLogicOperator(operator: Operator): boolean;
1074
1235
  }
1075
1236
 
1076
- declare class Pattern<T = any> extends Validator {
1237
+ declare class Pattern extends Validator {
1077
1238
  constructor(pattern: RegExp, message?: RenderContentRef);
1078
1239
  }
1079
1240
 
@@ -1102,10 +1263,23 @@ export declare type RenderContentNonCallable = string | MdString | SimpleCompone
1102
1263
  */
1103
1264
  export declare type RenderContentRef = RenderContent | Ref<RenderContent>;
1104
1265
 
1105
- declare class Required_2<T = any> extends Validator {
1106
- constructor(message?: RenderContentRef);
1266
+ declare class Required_2 extends Validator {
1267
+ constructor(options?: RequiredOptions);
1268
+ constructor(message?: RenderContentRef, options?: RequiredOptions);
1107
1269
  }
1108
1270
 
1271
+ /** How the value is read before its length is taken. */
1272
+ declare interface RequiredOptions {
1273
+ /**
1274
+ * Whether a string is trimmed before it is measured, so that whitespace alone is no value. Defaults to true.
1275
+ * Set it to false where the spaces are part of what the field holds.
1276
+ */
1277
+ trim?: boolean;
1278
+ }
1279
+
1280
+ /** Writes the members `newConfig` names and leaves the rest as they stand. */
1281
+ export declare function setConfig(newConfig: Partial<FormsConfig>): void;
1282
+
1109
1283
  /**
1110
1284
  * Interface for custom component content definition
1111
1285
  */
@@ -1119,6 +1293,14 @@ export declare class Statement {
1119
1293
  private readonly operand1;
1120
1294
  private readonly operator;
1121
1295
  private readonly operand2;
1296
+ /**
1297
+ * An operand is a nested `Statement`, a `FieldBase` whose value is compared, or a literal. `undefined` and a
1298
+ * function are neither: they are what a misspelled field name and a field accessor handed over uncalled answer
1299
+ * with, and a statement built from one compares nothing and never fires. The position names which of the two
1300
+ * operands is at fault, so the error points at the place the name was written.
1301
+ */
1302
+ private static validateOperand;
1303
+ /** @throws TypeError where an operand the statement reads is undefined or a function. */
1122
1304
  constructor(operand1: OperandType, operator: Operator, operand2: OperandType);
1123
1305
  /**
1124
1306
  * The value an operand contributes. A field operand names a field of the record the statement is evaluated in:
@@ -1145,6 +1327,8 @@ declare class Transaction {
1145
1327
  private readonly participants;
1146
1328
  /** what a rollback has to put back beyond the state slots, newest last */
1147
1329
  private readonly undo;
1330
+ /** what the commit has to do once the change stands, in the order it was handed in */
1331
+ private readonly settled;
1148
1332
  /** true once the transaction has been unwound, which is what tells a run started inside it to say nothing */
1149
1333
  private unwound;
1150
1334
  /** the depth the announcement in progress is working at, -1 while none is */
@@ -1177,6 +1361,12 @@ declare class Transaction {
1177
1361
  * hands its own undo in here rather than making every snapshot carry the thing.
1178
1362
  */
1179
1363
  whenRolledBack(work: () => void): void;
1364
+ /**
1365
+ * Registers work that runs once the transaction has committed, and not at all where it is rolled back. It is
1366
+ * how an operation defers a step that cannot be taken back - cancelling work in flight, for one - until the
1367
+ * change it belongs to actually stands.
1368
+ */
1369
+ whenCommitted(work: () => void): void;
1180
1370
  /**
1181
1371
  * Notes that an element at or below the depth the pass in progress is working at has been enrolled. The pass
1182
1372
  * then gives up the rest of its batch and is started again over what is now owed, so the newcomer is reached
@@ -1286,6 +1476,13 @@ declare interface TxStructuralEvent {
1286
1476
  * Base validation error class with component rendering capabilities
1287
1477
  */
1288
1478
  export declare class ValidationError {
1479
+ code?: string | undefined;
1480
+ /**
1481
+ * Machine-readable identifier of what failed, in kebab-case. Every validator this library ships states one, so
1482
+ * code that reacts to a particular failure does not have to match the message text, which is translated and
1483
+ * configurable. It is optional: an error built by hand carries whatever its author gives it, or nothing.
1484
+ */
1485
+ constructor(code?: string | undefined);
1289
1486
  get componentName(): string;
1290
1487
  get componentBindings(): {};
1291
1488
  get componentBody(): string;
@@ -1299,7 +1496,7 @@ export declare class ValidationErrorRenderContent extends ValidationError {
1299
1496
  classes: ClassTypes;
1300
1497
  private text;
1301
1498
  private textType;
1302
- constructor(text: RenderContentRef, classes?: ClassTypes);
1499
+ constructor(text: RenderContentRef, classes?: ClassTypes, code?: string);
1303
1500
  get resolvedText(): RenderContentNonCallable;
1304
1501
  get getTextType(): "string" | "md" | "component";
1305
1502
  get componentName(): string;
@@ -1314,13 +1511,19 @@ export declare class ValidationErrorRenderContent extends ValidationError {
1314
1511
  export declare class ValidationErrorText extends ValidationError {
1315
1512
  text: string;
1316
1513
  classes: ClassTypes;
1317
- constructor(text: string, classes?: ClassTypes);
1514
+ constructor(text: string, classes?: ClassTypes, code?: string);
1318
1515
  get componentName(): string;
1319
1516
  get componentBody(): string;
1320
1517
  get extraClasses(): ClassTypes;
1321
1518
  }
1322
1519
 
1323
- export declare type ValidationFunction<T = any> = (newValue: T, oldValue: T, field: FieldBase<T>) => ValidationFunctionResult | Promise<ValidationFunctionResult>;
1520
+ /**
1521
+ * What a validator does with a value. `signal` aborts when the verdict this call would reach stops counting - a
1522
+ * newer run over the same field, a field the validator was taken off once that removal stands, a transaction that
1523
+ * was unwound - so an asynchronous check hands it to the work it commissions and stops paying for an answer nobody
1524
+ * will read. A function that has nothing to cancel ignores it.
1525
+ */
1526
+ export declare type ValidationFunction<T = any> = (newValue: T, oldValue: T, field: FieldBase<T>, signal: AbortSignal) => ValidationFunctionResult | Promise<ValidationFunctionResult>;
1324
1527
 
1325
1528
  export declare type ValidationFunctionResult = ValidationError[] | null;
1326
1529
 
@@ -1349,8 +1552,19 @@ export declare class Validator<T = any> extends ValueChangedAction {
1349
1552
  * Takes this validator off `field`: the errors it put there are withdrawn and the verdict is re-formed over what
1350
1553
  * the validators the field still holds have to say. Withdrawing them is what keeps a field from staying invalid
1351
1554
  * on an error no validator is left to take back.
1555
+ *
1556
+ * A run in flight over the field is cancelled once the removal stands, and not before: a transaction that
1557
+ * unwinds puts the validator and its epoch back, and the run it never cancelled goes on to reach the verdict the
1558
+ * field is then owed. Cancelling it as the removal is made would leave the field reporting itself valid over a
1559
+ * value nothing checked, on the strength of a transaction that never happened.
1352
1560
  */
1353
1561
  unregisterFrom(field: FieldBase): void;
1562
+ /**
1563
+ * Cancels the asynchronous run in flight over `field`, where there is one. It is called once the field has
1564
+ * stopped holding this validator: the field's validation epoch has moved on by then, so the run's verdict is
1565
+ * already dropped and the signal it handed its check aborts with it.
1566
+ */
1567
+ protected abandonRun(field: FieldBase): void;
1354
1568
  /**
1355
1569
  * What this validator remembers about one of the fields it validates. One instance may be registered on several
1356
1570
  * fields - every row of a list carries the instances the item template carries - so each field has a record of
@@ -1370,6 +1584,8 @@ export declare class Validator<T = any> extends ValueChangedAction {
1370
1584
  /** What a validator remembers about one field it is registered on; a subclass widens it. */
1371
1585
  export declare interface ValidatorBindingState {
1372
1586
  run: number;
1587
+ /** cancels the asynchronous run in flight over the field, absent while no run is waiting for its verdict */
1588
+ abandon?: () => void;
1373
1589
  }
1374
1590
 
1375
1591
  export declare namespace Validators {
@@ -1377,7 +1593,9 @@ export declare namespace Validators {
1377
1593
  CompareTo,
1378
1594
  CompareToTarget,
1379
1595
  InAllowedValues,
1596
+ AllowedValues,
1380
1597
  Required_2 as Required,
1598
+ RequiredOptions,
1381
1599
  Pattern,
1382
1600
  ValidationFunctionResult,
1383
1601
  ValidationFunction,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dynamicforms/vue-forms",
3
3
  "private": false,
4
- "version": "0.12.0",
4
+ "version": "0.13.0",
5
5
  "type": "module",
6
6
  "description": "A lightweight Vue.js library for form state management with built-in validation, conditional logic, nested structures, and a rich event system. UI-agnostic and TypeScript-first.",
7
7
  "author": "Jure Erznožnik",
package/readme.md CHANGED
@@ -22,19 +22,25 @@ mechanism applies at every level of a nested form.
22
22
 
23
23
  ## Features
24
24
 
25
- - **UI-agnostic**: Works with any Vue UI components or your custom ones
25
+ - **UI-agnostic**: a logic layer for form state, validation and dynamic behaviour. Any Vue components render it,
26
+ your own included
27
+ - **Transactional**: every mutating operation is atomic — events are announced once, over the net change, and a
28
+ handler that throws leaves the form exactly as it was. `transaction()` makes several writes one operation, and
29
+ `tx.rollback()` withdraws one without an error
30
+ - **Lists that scale**: a `List` is meant to hold thousands of rows. Writing one field of one row costs that row
31
+ and the depth it sits at, a `push()` costs one row, and reading `value` or `valid` again costs nothing
32
+ - **Declared once, bound per record**: the `Group` handed to `new List(template)` is the declaration every row is
33
+ built from. One validator instance and one conditional rule serve every row, and each row answers for itself
26
34
  - **Reactive**: every member of a field, group or list is a tracked read — assign a property directly, with no
27
35
  `ref` to unwrap and no computed mirror to keep in sync
28
- - **Nested structures**: Support for complex data with nested fields and groups
29
- - **Event system**: Rich event handling for field changes, validation, and more
30
- - **Transactional**: every mutating operation is atomic — events are announced once, over the net change, and a
31
- handler that throws leaves the form exactly as it was
32
- - **TypeScript support**: Full type definitions for excellent developer experience
36
+ - **Nested structures**: fields, groups and lists compose recursively, at every level
37
+ - **Event system**: every change travels through an action pipeline; a handler may pass it on, reshape it or stop it
38
+ - **Validation**: built-in validators, custom synchronous and asynchronous rules, cross-field comparisons, and
39
+ errors that carry a `code` and render as text, markdown or a component of your own
40
+ - **Conditional logic**: visibility, enablement and values declared as statements over other fields
41
+ - **Display modes**: control field visibility with different display modes (Full, Hidden, Invisible, Suppress)
42
+ - **TypeScript support**: full type definitions, and a group's value type inferred from the fields it holds
33
43
  - **Lightweight**: `vue` (^3.5.2) is the only peer dependency and `lodash-es` the only runtime one
34
- - **Field types**: Core field types (Field, Action, Group, List) to represent any data structure
35
- - **Validation**: Comprehensive validation system with built-in validators and extensible error handling
36
- - **Conditional logic**: Dynamic form behavior based on field values and conditions
37
- - **Display modes**: Control field visibility with different display modes (Full, Hidden, Invisible, Suppress)
38
44
 
39
45
  ## Installation
40
46
 
@@ -63,6 +69,10 @@ The import must be a named one — the default export is a namespace of the libr
63
69
  (`MdString`). Rendering those requires a globally registered `vue-markdown` component; set the option to `false` if you
64
70
  want plain text instead.
65
71
 
72
+ The configuration is module-global rather than per app, and `getConfig()`, `setConfig()` and the `FormsConfig` type
73
+ are exported beside the plugin, so the options can be read and written where there is no app to install a plugin
74
+ on.
75
+
66
76
  ## Basic Usage Example
67
77
 
68
78
  Every form element is created with the constructor: `new Field({ ... })`, `new Action({ ... })`,
@@ -211,13 +221,22 @@ const validatedForm = new Group({
211
221
  ```
212
222
 
213
223
  Validators run eagerly — a field is validated the moment it is created, so a form built from empty required fields is
214
- invalid immediately. Use `field.touched` to decide when to show the errors in the UI.
215
-
216
- A validation function may return a `Promise`. `field.validating` is `true` while such a run is pending, and the
217
- verdict applied to the field is always the one belonging to the newest run, so a slow check cannot overwrite a faster
218
- one started after it. A validator message given as a `Ref` or `computed` is resolved when the message is rendered, so
219
- a translated message follows a locale switch without revalidating. `field.clearValidators()` drops the validators,
220
- empties the errors and cancels whatever validation is still in flight.
224
+ invalid immediately. Use `field.touched` to decide when to show the errors in the UI. `Required` trims a string
225
+ before it measures it, so a value of spaces alone is no value; `new Validators.Required({ trim: false })` keeps the
226
+ spaces where they are part of what the field holds.
227
+
228
+ Every error a built-in validator produces carries a `code` — `required`, `pattern`, `min-length`, … — so a program
229
+ reacting to one particular failure need not match message text that is translated and configurable.
230
+
231
+ A validation function may return a `Promise`. `field.validating` is `true` while such a run is pending — on the
232
+ field and on every container above it, so a form answers for the whole tree — and the verdict applied to the field
233
+ is always the one belonging to the newest run, so a slow check cannot overwrite a faster one started after it. The
234
+ function receives an `AbortSignal` as its fourth argument, which aborts the moment the run's verdict stops
235
+ counting, so the request behind it can be called off. `form.busy` is the same question with the `Action.execute()`
236
+ runs below it included — what a submit button binds to. A validator message given as a `Ref` or `computed` is
237
+ resolved when the message is rendered, so a translated message follows a locale switch without revalidating.
238
+ `field.clearValidators()` drops the validators, empties the errors and cancels whatever validation is still in
239
+ flight.
221
240
 
222
241
  ## Messages Widget Component
223
242
 
@@ -326,7 +345,8 @@ const contactsList = new List(contactTemplate);
326
345
  contactsList.push({ name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' });
327
346
  contactsList.push({ name: 'Jane Doe', email: 'jane@example.com', phone: '987-654-3210' });
328
347
 
329
- // Access list items: get() returns undefined for an invalid index
348
+ // Access list items: items hands out the rows, get() answers undefined for an invalid index
349
+ console.log(contactsList.length); // 2
330
350
  const firstContact = contactsList.get(0)!;
331
351
  console.log(firstContact.fields.name.value); // 'John Doe'
332
352
 
@@ -337,8 +357,9 @@ firstContact.fields.email.value = 'john.doe@example.com';
337
357
  contactsList.remove(1);
338
358
  ```
339
359
 
340
- Every list mutation is tracked, so a `v-for` over `contactsList.value` re-renders on `push()`, `insert()`,
341
- `remove()`, `pop()` and `clear()` without any extra wiring.
360
+ Every list mutation is tracked, so a `v-for` over `contactsList.items` re-renders on `push()`, `insert()`,
361
+ `remove()`, `pop()` and `clear()` without any extra wiring. `items` hands out a frozen array of the live rows,
362
+ rebuilt once per change of the set.
342
363
 
343
364
  ## TypeScript Support
344
365
 
@@ -388,10 +409,11 @@ const email: string = userForm.fields.email.value;
388
409
  const age: number = userForm.fields.age.value;
389
410
  const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
390
411
 
391
- // The serialized value is typed too, member by member
412
+ // The serialized value is typed too, member by member. Every member is optional, because a disabled field is
413
+ // left out of the object the group builds
392
414
  const values = userForm.value!;
393
- const emailFromValue: string = values.email;
394
- const prefs: { darkMode: boolean; notifications: boolean } | null = values.preferences;
415
+ const emailFromValue: string | undefined = values.email;
416
+ const prefs: { darkMode?: boolean; notifications?: boolean } | null | undefined = values.preferences;
395
417
 
396
418
  // Type safety prevents errors
397
419
  // userForm.fields.age.value = 'not a number'; // Error: Type 'string' is not assignable to type 'number'
@@ -443,8 +465,8 @@ For more detailed documentation and examples, check out the [documentation](http
443
465
  declarations, transactions, where validity comes from and how a `List` builds its rows.
444
466
 
445
467
  Upgrading an existing project? The
446
- [migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration) walks the journey from 0.6.1 to
447
- 0.10.x, silent breaks first, and keeps a section per release for a project crossing one.
468
+ [migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration) walks the journey from 0.6.1
469
+ onwards, silent breaks first, and keeps a section per release for a project crossing one.
448
470
 
449
471
  ## Conclusion
450
472