@dynamicforms/vue-forms 1.1.0 → 2.0.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dynamicforms/vue-forms",
3
3
  "private": false,
4
- "version": "1.1.0",
4
+ "version": "2.0.3",
5
5
  "type": "module",
6
6
  "description": "A 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",
@@ -55,10 +55,10 @@
55
55
  "node": ">=22"
56
56
  },
57
57
  "dependencies": {
58
- "@dynamicforms/translatable": "^0.1.0",
59
58
  "lodash-es": "^4.17.21"
60
59
  },
61
60
  "peerDependencies": {
61
+ "@dynamicforms/translatable": "^0.1.0",
62
62
  "vue": "^3.5.2"
63
63
  },
64
64
  "devDependencies": {
package/readme.md CHANGED
@@ -14,7 +14,7 @@ mechanism applies at every level of a nested form.
14
14
 
15
15
  ### Design Goals
16
16
 
17
- - **UI-Agnostic**: A logic layer for form state, validation and dynamic behaviour. Works with any Vue components, including your own.
17
+ - **UI-Agnostic**: A logic layer for form state, validation and dynamic behaviour. Works with any Vue components, including your own. The few members that speak about the interface — `visibility`, `enabled`, `touched`, error render content and `Action` — are there because nearly every form needs them; [Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface) lists them with the reason for each.
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**: Every member of a field, group or list is a tracked read, 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.
@@ -22,13 +22,13 @@ mechanism applies at every level of a nested form.
22
22
  ## Features
23
23
 
24
24
  - **UI-agnostic**: a logic layer for form state, validation and dynamic behaviour. Any Vue components render it,
25
- your own included
25
+ your own included; the few members that speak about the interface are [listed with their reasons](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface)
26
26
  - **Transactional**: every mutating operation is atomic — events are announced once, over the net change, and a
27
27
  handler that throws leaves the form exactly as it was. `transaction()` makes several writes one operation, and
28
28
  `tx.rollback()` withdraws one without an error
29
29
  - **Lists that scale**: a `List` is meant to hold thousands of rows. Writing one field of one row costs that row
30
30
  and the depth it sits at, a `push()` costs one row, and reading `value` or `valid` again costs nothing
31
- - **Declared once, bound per record**: the `Group` handed to `new List(template)` is the declaration every row is
31
+ - **Declared once, bound per record**: the element handed to `new List(template)` is the declaration every row is
32
32
  built from. One validator instance and one conditional rule serve every row, and each row answers for itself
33
33
  - **Reactive**: every member of a field, group or list is a tracked read — assign a property directly, with no
34
34
  `ref` to unwrap and no computed mirror to keep in sync
@@ -39,7 +39,13 @@ mechanism applies at every level of a nested form.
39
39
  - **Conditional logic**: visibility, enablement and values declared as statements over other fields
40
40
  - **Extended properties**: a field carries whatever your UI renders it with — a label, a hint, a width — declared
41
41
  as a second type argument, checked by the compiler and read through `extra`
42
- - **Display modes**: control field visibility with different display modes (Full, Hidden, Invisible, Suppress)
42
+ - **Application state, not only forms**: the same elements hold state no screen shows — a cart, the filters of a list view, editor
43
+ settings — with the reactivity, transactions and validation a form has
44
+ - **Plain-data views**: `view(group)` reads an element as plain properties — `form.address.city`,
45
+ `v-model="form.name"` — with the element itself as `form.$`
46
+ - **Access**: `'editable'`, `'readonly'`, `'disabled'` and `'disabled-null'` decide whether an element accepts input
47
+ and what it contributes to the value its form sends — its own value, `null`, or nothing — and validation follows
48
+ what is sent; `visibility` states how it is drawn
43
49
  - **TypeScript support**: full type definitions, and a group's value type inferred from the fields it holds
44
50
 
45
51
  ## Installation
@@ -48,9 +54,8 @@ mechanism applies at every level of a nested form.
48
54
  npm install @dynamicforms/vue-forms
49
55
  ```
50
56
 
51
- The package is ESM-only: it ships one build, which imports `lodash-es`, its only runtime dependency. Node 22 or newer
52
- is required, and `vue` (^3.5.2) is the only peer dependency. A CommonJS consumer reaches it through `require()` of an
53
- ES module, which Node supports. Type definitions ship with the build; the stylesheet is
57
+ The package is ESM-only and requires Node 22 or newer. A CommonJS consumer reaches it through `require()` of an ES
58
+ module, which Node supports. Type definitions ship with the build; the stylesheet is
54
59
  `@dynamicforms/vue-forms/style.css`.
55
60
 
56
61
  ## Setup
@@ -101,9 +106,9 @@ console.log(personForm.value); // { firstName: 'John', lastName: 'Doe', age: 30
101
106
  personForm.fields.firstName.value = 'Jane';
102
107
 
103
108
  // Disable a field
104
- personForm.fields.age.enabled = false;
109
+ personForm.fields.age.access = 'disabled';
105
110
 
106
- // Form serializes only enabled fields
111
+ // The form leaves a disabled field out of what it sends
107
112
  console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
108
113
  ```
109
114
 
@@ -120,8 +125,10 @@ const saveAction = new Action({
120
125
  await saveAction.execute({ form: personForm }); // 'saving { form: ... }'; saveAction.busy until it settles
121
126
  ```
122
127
 
123
- `Action` is the one deliberate exception to "UI-agnostic": it names a label and an icon because it exists as the
124
- element a form's submit and cancel hang on, and that minimal pair is what makes the concept legible. The shape is
128
+ `Action` is one of the few members that speak about the interface, listed with their reasons in
129
+ [Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface):
130
+ it names a label and an icon because it exists as the element a form's submit and cancel hang on, and that minimal
131
+ pair is what makes the concept legible. The shape is
125
132
  minimal because a UI library is expected to extend it — [`@dynamicforms/vuetify-inputs`](https://docs.velis.si/dynamicforms/vuetify-inputs/examples/df-actions.html)
126
133
  widens the value with render options and per-breakpoint variants on top of it.
127
134
 
@@ -298,7 +305,7 @@ Create dynamic forms with conditional logic using Statements and Operators:
298
305
  ```typescript
299
306
  import {
300
307
  Field, Group, Statement, Operator,
301
- ConditionalVisibilityAction, ConditionalEnabledAction
308
+ ConditionalVisibilityAction, ConditionalAccessAction
302
309
  } from '@dynamicforms/vue-forms';
303
310
 
304
311
  const form = new Group({
@@ -308,20 +315,17 @@ const form = new Group({
308
315
  lastName: new Field()
309
316
  });
310
317
 
311
- // Show company name field only when isCompany is true
312
- const showCompanyNameStatement = new Statement(form.fields.isCompany, Operator.EQUALS, true);
313
- form.fields.companyName.registerAction(
314
- new ConditionalVisibilityAction(showCompanyNameStatement)
315
- );
316
-
317
- // Show personal name fields only when isCompany is false
318
- const showPersonalFieldsStatement = new Statement(form.fields.isCompany, Operator.EQUALS, false);
319
- form.fields.firstName.registerAction(
320
- new ConditionalVisibilityAction(showPersonalFieldsStatement)
321
- );
322
- form.fields.lastName.registerAction(
323
- new ConditionalVisibilityAction(showPersonalFieldsStatement)
324
- );
318
+ // Show and send the company name only when isCompany is true
319
+ const isCompany = new Statement(form.fields.isCompany, Operator.EQUALS, true);
320
+ form.fields.companyName.registerAction(new ConditionalVisibilityAction(isCompany));
321
+ form.fields.companyName.registerAction(new ConditionalAccessAction(isCompany));
322
+
323
+ // Show and send the personal name fields only when isCompany is false
324
+ const isPerson = new Statement(form.fields.isCompany, Operator.EQUALS, false);
325
+ [form.fields.firstName, form.fields.lastName].forEach((field) => {
326
+ field.registerAction(new ConditionalVisibilityAction(isPerson));
327
+ field.registerAction(new ConditionalAccessAction(isPerson));
328
+ });
325
329
  ```
326
330
 
327
331
  ## Advanced Data Structures (Lists)
@@ -409,28 +413,28 @@ const email: string = userForm.fields.email.value;
409
413
  const age: number = userForm.fields.age.value;
410
414
  const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
411
415
 
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
414
- const values = userForm.value!;
415
- const emailFromValue: string | undefined = values.email;
416
- const prefs: { darkMode?: boolean; notifications?: boolean } | null | undefined = values.preferences;
416
+ // The serialized value is typed too, member by member. Every member is optional, because a 'disabled' member is
417
+ // left out of the object the group builds, and nullable, because a 'disabled-null' one is sent as null
418
+ const values = userForm.value;
419
+ const emailFromValue: string | null | undefined = values.email;
420
+ const prefs: { darkMode?: boolean | null; notifications?: boolean | null } | null | undefined = values.preferences;
417
421
 
418
422
  // Type safety prevents errors
419
423
  // userForm.fields.age.value = 'not a number'; // Error: Type 'string' is not assignable to type 'number'
420
424
  ```
421
425
 
422
426
  The value shape is derived from the fields map by the exported `FieldsToValues<T>`, with `GroupValue<T>` and
423
- `GroupValueInput<T>` as the group's read and write types, and `ListValue` for lists. Constructor parameters have
427
+ `GroupValueInput<T>` as the group's read and write types, and `ListValue<R>` for lists. Constructor parameters have
424
428
  their own exported type, `IFieldParams<T, X>`, shared by all four element classes:
425
429
 
426
430
  ```typescript
427
431
  import { Field, IFieldParams } from '@dynamicforms/vue-forms';
428
432
 
429
- const defaults: IFieldParams<string> = { value: '', enabled: false };
433
+ const defaults: IFieldParams<string> = { value: '', access: 'readonly' };
430
434
  const field = new Field(defaults);
431
435
  ```
432
436
 
433
- It admits only the writable members — `value`, `originalValue`, `enabled`, `visibility`, `touched`, `errors`,
437
+ It admits only the writable members — `value`, `originalValue`, `access`, `visibility`, `touched`, `errors`,
434
438
  `validators` and `actions`, listed by `IFieldConstructorParams<T>`. Derived members such as `valid` and
435
439
  `isChanged` are getters, and passing one is a compile error.
436
440