@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/dist/dynamicforms-vue-forms.js +861 -531
- package/dist/dynamicforms-vue-forms.js.map +1 -1
- package/dist/index.d.ts +447 -183
- package/package.json +2 -2
- package/readme.md +38 -34
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamicforms/vue-forms",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "
|
|
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
|
|
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
|
-
- **
|
|
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
|
|
52
|
-
|
|
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.
|
|
109
|
+
personForm.fields.age.access = 'disabled';
|
|
105
110
|
|
|
106
|
-
//
|
|
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
|
|
124
|
-
|
|
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,
|
|
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
|
|
312
|
-
const
|
|
313
|
-
form.fields.companyName.registerAction(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
new
|
|
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
|
|
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
|
|
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: '',
|
|
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`, `
|
|
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
|
|