@dynamicforms/vue-forms 0.6.0 → 0.6.1

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.cts CHANGED
@@ -178,14 +178,46 @@ export declare abstract class FieldBase<T = any> {
178
178
  private _enabled;
179
179
  get enabled(): boolean;
180
180
  set enabled(newValue: boolean);
181
+ /**
182
+ * While true, validate() on this field does nothing at all: it neither recomputes _valid nor emits
183
+ * ValidChangedAction, and nothing climbs to the parent. A container that walks its members one by one - assigning
184
+ * them a value, or revalidating them - sets it for the duration of the walk, so a member's upward climb cannot
185
+ * announce the verdict of a half-applied value or of a half-revalidated set, and clears it in a finally block
186
+ * followed by its own validate(). That call recomputes over the finished state and emits the net transition, so
187
+ * the deferral cannot swallow a real change.
188
+ */
189
+ protected suppressValidation: boolean;
190
+ /**
191
+ * While true, a validity change is still recomputed and still announced on this field, but it is not passed up to
192
+ * the parent: the climb is only remembered and carried out by flushParentValidityClimb(). A setter that notifies
193
+ * the parent of the new value itself holds it over the notifying region, so the parent forms its verdict once,
194
+ * over the finished value, instead of once over the value being replaced and once over the value replacing it.
195
+ */
196
+ protected suppressParentValidityClimb: boolean;
197
+ private parentValidityClimbPending;
198
+ /** Carries out the climb that a validity change made while suppressParentValidityClimb was held, if there was one. */
199
+ protected flushParentValidityClimb(): void;
181
200
  validate(revalidate?: boolean): void;
182
201
  get valid(): boolean;
183
202
  get fullValue(): any;
184
203
  get isChanged(): boolean;
204
+ /**
205
+ * Registers the actions a constructor received in its parameters: registration without the eager trigger,
206
+ * because a constructor ends with a single this.actions.triggerEager(...) over the finished value, and an
207
+ * eager action registered here would otherwise also run once per registration, over a half-built field.
208
+ */
209
+ protected registerInitialActions(actions: FieldActionBase[]): void;
185
210
  registerAction(action: FieldActionBase): this;
186
211
  triggerAction<T2 extends FieldActionBase>(actionClass: (abstract new (...args: any[]) => T2) & {
187
212
  classIdentifier: symbol;
188
213
  }, ...params: any[]): any;
214
+ private _validationEpoch;
215
+ /**
216
+ * Generation counter of the validators attached to this field. A Validator reads it when a run starts and drops
217
+ * a result whose epoch no longer matches, so a validation still in flight when clearValidators() is called
218
+ * cannot push an error onto a field that no longer has the validator that produced it.
219
+ */
220
+ get validationEpoch(): number;
189
221
  clearValidators(): void;
190
222
  }
191
223
 
@@ -279,6 +311,14 @@ export declare class Group<T extends GenericFieldsInterface = GenericFieldsInter
279
311
  field<K extends keyof T>(fieldName: K): T[K] | null;
280
312
  get fields(): T;
281
313
  get value(): GroupValue<T>;
314
+ /**
315
+ * Writes the members that the given value carries, without letting any single member announce the state in
316
+ * between: the group's own value notification and its own validation are both held back for the duration, so
317
+ * neither a ValueChangedAction nor a ValidChangedAction reports a verdict over a half-applied value. The
318
+ * caller decides what to announce once the whole value is in place. Both flags are restored to what they were,
319
+ * so a caller that is itself holding the group back stays in charge.
320
+ */
321
+ private assignMembers;
282
322
  set value(newValue: GroupValueInput<T>);
283
323
  get touched(): boolean;
284
324
  set touched(touched: boolean);
@@ -339,6 +379,11 @@ export declare class List<T extends GenericFieldsInterface = GenericFieldsInterf
339
379
  private _previousValue;
340
380
  constructor(itemTemplate?: Group<T>, params?: Partial<IFieldConstructorParams<ListValue>>);
341
381
  private processSetValueItem;
382
+ /**
383
+ * Builds the item that fills a gap left by an insert beyond the end of the list: a copy of the item template with
384
+ * the template's own values, or an empty group when the list has no template.
385
+ */
386
+ private createPaddingItem;
342
387
  private setValueInternal;
343
388
  get value(): ListValue;
344
389
  set value(newValue: Record<string, any>[]);
@@ -543,11 +588,26 @@ export declare type ValidationFunctionResult = ValidationError[] | null;
543
588
  */
544
589
  export declare class Validator<T = any> extends ValueChangedAction {
545
590
  private readonly source;
591
+ /**
592
+ * Sequence number of the newest run of this validator, per field. Every execution takes the next number, and a
593
+ * result is applied only while its number is still the newest one, so a slow run cannot overwrite the verdict of
594
+ * a faster one that started after it. The key is the field because one validator instance may be registered on
595
+ * several fields, each with a sequence of its own.
596
+ */
597
+ private readonly runs;
546
598
  /**
547
599
  * Creates a new validator
548
600
  * @param validationFn Function that validates the field value and returns errors or null
549
601
  */
550
602
  constructor(validationFn: ValidationFunction<T>);
603
+ /**
604
+ * Returns the error instance this validator owns and may later withdraw: the argument itself when it carries no
605
+ * ownership stamp or already carries this validator's, a copy of it when it belongs to another validator. The
606
+ * stamp is not configurable, so an instance one validator owns can never be reassigned to another; the copy is
607
+ * what makes an error instance shareable between validators, and it keeps the prototype and every own property,
608
+ * including the non-enumerable ones, so it renders exactly like the instance it was made from.
609
+ */
610
+ private claim;
551
611
  static get classIdentifier(): symbol;
552
612
  get eager(): boolean;
553
613
  protected replacePlaceholdersFunction(text: RenderContentRef, replace: Record<string, any>): RenderContentRef;
package/dist/index.d.ts CHANGED
@@ -178,14 +178,46 @@ export declare abstract class FieldBase<T = any> {
178
178
  private _enabled;
179
179
  get enabled(): boolean;
180
180
  set enabled(newValue: boolean);
181
+ /**
182
+ * While true, validate() on this field does nothing at all: it neither recomputes _valid nor emits
183
+ * ValidChangedAction, and nothing climbs to the parent. A container that walks its members one by one - assigning
184
+ * them a value, or revalidating them - sets it for the duration of the walk, so a member's upward climb cannot
185
+ * announce the verdict of a half-applied value or of a half-revalidated set, and clears it in a finally block
186
+ * followed by its own validate(). That call recomputes over the finished state and emits the net transition, so
187
+ * the deferral cannot swallow a real change.
188
+ */
189
+ protected suppressValidation: boolean;
190
+ /**
191
+ * While true, a validity change is still recomputed and still announced on this field, but it is not passed up to
192
+ * the parent: the climb is only remembered and carried out by flushParentValidityClimb(). A setter that notifies
193
+ * the parent of the new value itself holds it over the notifying region, so the parent forms its verdict once,
194
+ * over the finished value, instead of once over the value being replaced and once over the value replacing it.
195
+ */
196
+ protected suppressParentValidityClimb: boolean;
197
+ private parentValidityClimbPending;
198
+ /** Carries out the climb that a validity change made while suppressParentValidityClimb was held, if there was one. */
199
+ protected flushParentValidityClimb(): void;
181
200
  validate(revalidate?: boolean): void;
182
201
  get valid(): boolean;
183
202
  get fullValue(): any;
184
203
  get isChanged(): boolean;
204
+ /**
205
+ * Registers the actions a constructor received in its parameters: registration without the eager trigger,
206
+ * because a constructor ends with a single this.actions.triggerEager(...) over the finished value, and an
207
+ * eager action registered here would otherwise also run once per registration, over a half-built field.
208
+ */
209
+ protected registerInitialActions(actions: FieldActionBase[]): void;
185
210
  registerAction(action: FieldActionBase): this;
186
211
  triggerAction<T2 extends FieldActionBase>(actionClass: (abstract new (...args: any[]) => T2) & {
187
212
  classIdentifier: symbol;
188
213
  }, ...params: any[]): any;
214
+ private _validationEpoch;
215
+ /**
216
+ * Generation counter of the validators attached to this field. A Validator reads it when a run starts and drops
217
+ * a result whose epoch no longer matches, so a validation still in flight when clearValidators() is called
218
+ * cannot push an error onto a field that no longer has the validator that produced it.
219
+ */
220
+ get validationEpoch(): number;
189
221
  clearValidators(): void;
190
222
  }
191
223
 
@@ -279,6 +311,14 @@ export declare class Group<T extends GenericFieldsInterface = GenericFieldsInter
279
311
  field<K extends keyof T>(fieldName: K): T[K] | null;
280
312
  get fields(): T;
281
313
  get value(): GroupValue<T>;
314
+ /**
315
+ * Writes the members that the given value carries, without letting any single member announce the state in
316
+ * between: the group's own value notification and its own validation are both held back for the duration, so
317
+ * neither a ValueChangedAction nor a ValidChangedAction reports a verdict over a half-applied value. The
318
+ * caller decides what to announce once the whole value is in place. Both flags are restored to what they were,
319
+ * so a caller that is itself holding the group back stays in charge.
320
+ */
321
+ private assignMembers;
282
322
  set value(newValue: GroupValueInput<T>);
283
323
  get touched(): boolean;
284
324
  set touched(touched: boolean);
@@ -339,6 +379,11 @@ export declare class List<T extends GenericFieldsInterface = GenericFieldsInterf
339
379
  private _previousValue;
340
380
  constructor(itemTemplate?: Group<T>, params?: Partial<IFieldConstructorParams<ListValue>>);
341
381
  private processSetValueItem;
382
+ /**
383
+ * Builds the item that fills a gap left by an insert beyond the end of the list: a copy of the item template with
384
+ * the template's own values, or an empty group when the list has no template.
385
+ */
386
+ private createPaddingItem;
342
387
  private setValueInternal;
343
388
  get value(): ListValue;
344
389
  set value(newValue: Record<string, any>[]);
@@ -543,11 +588,26 @@ export declare type ValidationFunctionResult = ValidationError[] | null;
543
588
  */
544
589
  export declare class Validator<T = any> extends ValueChangedAction {
545
590
  private readonly source;
591
+ /**
592
+ * Sequence number of the newest run of this validator, per field. Every execution takes the next number, and a
593
+ * result is applied only while its number is still the newest one, so a slow run cannot overwrite the verdict of
594
+ * a faster one that started after it. The key is the field because one validator instance may be registered on
595
+ * several fields, each with a sequence of its own.
596
+ */
597
+ private readonly runs;
546
598
  /**
547
599
  * Creates a new validator
548
600
  * @param validationFn Function that validates the field value and returns errors or null
549
601
  */
550
602
  constructor(validationFn: ValidationFunction<T>);
603
+ /**
604
+ * Returns the error instance this validator owns and may later withdraw: the argument itself when it carries no
605
+ * ownership stamp or already carries this validator's, a copy of it when it belongs to another validator. The
606
+ * stamp is not configurable, so an instance one validator owns can never be reassigned to another; the copy is
607
+ * what makes an error instance shareable between validators, and it keeps the prototype and every own property,
608
+ * including the non-enumerable ones, so it renders exactly like the instance it was made from.
609
+ */
610
+ private claim;
551
611
  static get classIdentifier(): symbol;
552
612
  get eager(): boolean;
553
613
  protected replacePlaceholdersFunction(text: RenderContentRef, replace: Record<string, any>): RenderContentRef;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dynamicforms/vue-forms",
3
3
  "private": false,
4
- "version": "0.6.0",
4
+ "version": "0.6.1",
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",
@@ -29,6 +29,7 @@
29
29
  ],
30
30
  "scripts": {
31
31
  "build": "vite build && node -e \"require('fs').copyFileSync('dist/index.d.ts', 'dist/index.d.cts')\"",
32
+ "prepack": "npm run build",
32
33
  "test": "vitest run --coverage",
33
34
  "lint": "eslint src --fix && vue-tsc --noEmit",
34
35
  "lint:ci": "eslint src && vue-tsc --noEmit",
@@ -52,13 +53,19 @@
52
53
  "homepage": "https://docs.velis.si/dynamicforms/vue-forms",
53
54
  "repository": {
54
55
  "type": "git",
55
- "url": "git+https://github.com/velis74/dynamicforms-vue-forms.git"
56
+ "url": "git+https://github.com/dynamicforms/vue-forms.git"
56
57
  },
57
58
  "bugs": {
58
- "url": "https://github.com/velis74/dynamicforms-vue-forms/issues"
59
+ "url": "https://github.com/dynamicforms/vue-forms/issues"
60
+ },
61
+ "engines": {
62
+ "node": ">=18"
63
+ },
64
+ "dependencies": {
65
+ "lodash": "^4.17.21",
66
+ "lodash-es": "^4.17.21"
59
67
  },
60
68
  "peerDependencies": {
61
- "lodash-es": "^4.17.12",
62
69
  "vue": "^3.4"
63
70
  },
64
71
  "devDependencies": {
package/readme.md CHANGED
@@ -18,7 +18,7 @@ mechanism applies at every level of a nested form.
18
18
  - **Fields that react to each other**: Conditional visibility, enablement and values are declared as statements over other fields, and an action pipeline lets a handler intercept, transform or abort an event.
19
19
  - **Reactive & Type-Safe**: Fields, groups and lists are Vue reactive objects, and a group's value type is inferred from the fields it holds, nested structures included.
20
20
  - **Structural serialization**: A group's value is the shape of its fields, and `Group.createFromFormData()` turns a plain object back into a form.
21
- - **Lightweight**: Depends on Vue and lodash-es, and nothing else.
21
+ - **Lightweight**: `vue` (^3.4) is the only peer dependency, and `lodash` and `lodash-es` — one per build — the only runtime ones.
22
22
 
23
23
  ## Features
24
24
 
@@ -28,7 +28,8 @@ mechanism applies at every level of a nested form.
28
28
  - **Nested structures**: Support for complex data with nested fields and groups
29
29
  - **Event system**: Rich event handling for field changes, validation, and more
30
30
  - **TypeScript support**: Full type definitions for excellent developer experience
31
- - **Lightweight**: depends only on `vue` (^3.4) and `lodash-es` (^4.17)
31
+ - **Lightweight**: `vue` (^3.4) is the only peer dependency; the runtime dependencies are `lodash` and `lodash-es`,
32
+ one for each of the two builds
32
33
  - **Field types**: Core field types (Field, Action, Group, List) to represent any data structure
33
34
  - **Validation**: Comprehensive validation system with built-in validators and extensible error handling
34
35
  - **Conditional logic**: Dynamic form behavior based on field values and conditions
@@ -40,6 +41,11 @@ mechanism applies at every level of a nested form.
40
41
  npm install @dynamicforms/vue-forms
41
42
  ```
42
43
 
44
+ The package ships two builds of the same source: an ESM build that imports `lodash-es` and a CJS/UMD build that
45
+ requires `lodash`. Both lodash packages are declared as dependencies and install with the package, so an `import` and
46
+ a `require()` both resolve without further setup. Node 18 or newer is required, and `vue` (^3.4) is the only peer
47
+ dependency. Type definitions ship for both entry points; the stylesheet is `@dynamicforms/vue-forms/style.css`.
48
+
43
49
  ## Setup
44
50
 
45
51
  The library ships a Vue plugin for its global options:
@@ -185,6 +191,12 @@ const validatedForm = new Group({
185
191
  Validators run eagerly — a field is validated the moment it is created, so a form built from empty required fields is
186
192
  invalid immediately. Use `field.touched` to decide when to show the errors in the UI.
187
193
 
194
+ A validation function may return a `Promise`. `field.validating` is `true` while such a run is pending, and the
195
+ verdict applied to the field is always the one belonging to the newest run, so a slow check cannot overwrite a faster
196
+ one started after it. A validator message given as a `Ref` or `computed` is resolved when the message is rendered, so
197
+ a translated message follows a locale switch without revalidating. `field.clearValidators()` drops the validators,
198
+ empties the errors and cancels whatever validation is still in flight.
199
+
188
200
  ## Messages Widget Component
189
201
 
190
202
  The library includes a `messages-widget` Vue component for displaying validation errors and messages: