@dynamicforms/vue-forms 1.0.0 → 2.0.2
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 +860 -517
- package/dist/dynamicforms-vue-forms.js.map +1 -1
- package/dist/index.d.ts +458 -183
- package/package.json +1 -1
- package/readme.md +36 -31
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.2",
|
|
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",
|
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
|
|
@@ -101,9 +107,9 @@ console.log(personForm.value); // { firstName: 'John', lastName: 'Doe', age: 30
|
|
|
101
107
|
personForm.fields.firstName.value = 'Jane';
|
|
102
108
|
|
|
103
109
|
// Disable a field
|
|
104
|
-
personForm.fields.age.
|
|
110
|
+
personForm.fields.age.access = 'disabled';
|
|
105
111
|
|
|
106
|
-
//
|
|
112
|
+
// The form leaves a disabled field out of what it sends
|
|
107
113
|
console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
|
|
108
114
|
```
|
|
109
115
|
|
|
@@ -120,8 +126,10 @@ const saveAction = new Action({
|
|
|
120
126
|
await saveAction.execute({ form: personForm }); // 'saving { form: ... }'; saveAction.busy until it settles
|
|
121
127
|
```
|
|
122
128
|
|
|
123
|
-
`Action` is
|
|
124
|
-
|
|
129
|
+
`Action` is one of the few members that speak about the interface, listed with their reasons in
|
|
130
|
+
[Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface):
|
|
131
|
+
it names a label and an icon because it exists as the element a form's submit and cancel hang on, and that minimal
|
|
132
|
+
pair is what makes the concept legible. The shape is
|
|
125
133
|
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
134
|
widens the value with render options and per-breakpoint variants on top of it.
|
|
127
135
|
|
|
@@ -298,7 +306,7 @@ Create dynamic forms with conditional logic using Statements and Operators:
|
|
|
298
306
|
```typescript
|
|
299
307
|
import {
|
|
300
308
|
Field, Group, Statement, Operator,
|
|
301
|
-
ConditionalVisibilityAction,
|
|
309
|
+
ConditionalVisibilityAction, ConditionalAccessAction
|
|
302
310
|
} from '@dynamicforms/vue-forms';
|
|
303
311
|
|
|
304
312
|
const form = new Group({
|
|
@@ -308,20 +316,17 @@ const form = new Group({
|
|
|
308
316
|
lastName: new Field()
|
|
309
317
|
});
|
|
310
318
|
|
|
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
|
-
);
|
|
319
|
+
// Show and send the company name only when isCompany is true
|
|
320
|
+
const isCompany = new Statement(form.fields.isCompany, Operator.EQUALS, true);
|
|
321
|
+
form.fields.companyName.registerAction(new ConditionalVisibilityAction(isCompany));
|
|
322
|
+
form.fields.companyName.registerAction(new ConditionalAccessAction(isCompany));
|
|
323
|
+
|
|
324
|
+
// Show and send the personal name fields only when isCompany is false
|
|
325
|
+
const isPerson = new Statement(form.fields.isCompany, Operator.EQUALS, false);
|
|
326
|
+
[form.fields.firstName, form.fields.lastName].forEach((field) => {
|
|
327
|
+
field.registerAction(new ConditionalVisibilityAction(isPerson));
|
|
328
|
+
field.registerAction(new ConditionalAccessAction(isPerson));
|
|
329
|
+
});
|
|
325
330
|
```
|
|
326
331
|
|
|
327
332
|
## Advanced Data Structures (Lists)
|
|
@@ -409,28 +414,28 @@ const email: string = userForm.fields.email.value;
|
|
|
409
414
|
const age: number = userForm.fields.age.value;
|
|
410
415
|
const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
|
|
411
416
|
|
|
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;
|
|
417
|
+
// The serialized value is typed too, member by member. Every member is optional, because a 'disabled' member is
|
|
418
|
+
// left out of the object the group builds, and nullable, because a 'disabled-null' one is sent as null
|
|
419
|
+
const values = userForm.value;
|
|
420
|
+
const emailFromValue: string | null | undefined = values.email;
|
|
421
|
+
const prefs: { darkMode?: boolean | null; notifications?: boolean | null } | null | undefined = values.preferences;
|
|
417
422
|
|
|
418
423
|
// Type safety prevents errors
|
|
419
424
|
// userForm.fields.age.value = 'not a number'; // Error: Type 'string' is not assignable to type 'number'
|
|
420
425
|
```
|
|
421
426
|
|
|
422
427
|
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
|
|
428
|
+
`GroupValueInput<T>` as the group's read and write types, and `ListValue<R>` for lists. Constructor parameters have
|
|
424
429
|
their own exported type, `IFieldParams<T, X>`, shared by all four element classes:
|
|
425
430
|
|
|
426
431
|
```typescript
|
|
427
432
|
import { Field, IFieldParams } from '@dynamicforms/vue-forms';
|
|
428
433
|
|
|
429
|
-
const defaults: IFieldParams<string> = { value: '',
|
|
434
|
+
const defaults: IFieldParams<string> = { value: '', access: 'readonly' };
|
|
430
435
|
const field = new Field(defaults);
|
|
431
436
|
```
|
|
432
437
|
|
|
433
|
-
It admits only the writable members — `value`, `originalValue`, `
|
|
438
|
+
It admits only the writable members — `value`, `originalValue`, `access`, `visibility`, `touched`, `errors`,
|
|
434
439
|
`validators` and `actions`, listed by `IFieldConstructorParams<T>`. Derived members such as `valid` and
|
|
435
440
|
`isChanged` are getters, and passing one is a compile error.
|
|
436
441
|
|