@dynamicforms/vue-forms 0.5.0 → 0.6.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/dynamicforms-vue-forms.js +530 -528
- package/dist/dynamicforms-vue-forms.js.map +1 -1
- package/dist/dynamicforms-vue-forms.umd.cjs +2 -2
- package/dist/dynamicforms-vue-forms.umd.cjs.map +1 -1
- package/dist/index.d.cts +603 -0
- package/dist/index.d.ts +103 -93
- package/package.json +26 -9
- package/readme.md +135 -47
package/readme.md
CHANGED
|
@@ -5,21 +5,30 @@ UI components.
|
|
|
5
5
|
|
|
6
6
|
## Introduction
|
|
7
7
|
|
|
8
|
-
`@dynamicforms/vue-forms`
|
|
9
|
-
applications. The library focuses on the logic layer of forms, giving you complete freedom to use any UI components
|
|
10
|
-
you prefer.
|
|
8
|
+
`@dynamicforms/vue-forms` manages form data, validation and state, and leaves rendering entirely to you.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
What it models beyond state is the behaviour *between* fields. Visibility, enablement and values can be declared
|
|
11
|
+
as conditions over other fields, and every change travels through an action pipeline in which each handler decides
|
|
12
|
+
whether to pass the event on and may reshape its result. Groups and lists compose recursively, so the same
|
|
13
|
+
mechanism applies at every level of a nested form.
|
|
14
|
+
|
|
15
|
+
### Design Goals
|
|
16
|
+
|
|
17
|
+
- **UI-Agnostic**: A logic layer for form state, validation and dynamic behaviour. Works with any Vue components, including your own.
|
|
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
|
+
- **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
|
+
- **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.
|
|
14
22
|
|
|
15
23
|
## Features
|
|
16
24
|
|
|
17
25
|
- **UI-agnostic**: Works with any Vue UI components or your custom ones
|
|
18
|
-
- **Reactive**:
|
|
26
|
+
- **Reactive**: Fields, groups and lists are Vue reactive objects — read and assign their properties directly, with
|
|
27
|
+
no `ref` to unwrap and no computed mirror to keep in sync
|
|
19
28
|
- **Nested structures**: Support for complex data with nested fields and groups
|
|
20
29
|
- **Event system**: Rich event handling for field changes, validation, and more
|
|
21
30
|
- **TypeScript support**: Full type definitions for excellent developer experience
|
|
22
|
-
- **Lightweight**:
|
|
31
|
+
- **Lightweight**: depends only on `vue` (^3.4) and `lodash-es` (^4.17)
|
|
23
32
|
- **Field types**: Core field types (Field, Action, Group, List) to represent any data structure
|
|
24
33
|
- **Validation**: Comprehensive validation system with built-in validators and extensible error handling
|
|
25
34
|
- **Conditional logic**: Dynamic form behavior based on field values and conditions
|
|
@@ -31,20 +40,40 @@ separates these concerns, allowing you to build forms that match your design sys
|
|
|
31
40
|
npm install @dynamicforms/vue-forms
|
|
32
41
|
```
|
|
33
42
|
|
|
43
|
+
## Setup
|
|
44
|
+
|
|
45
|
+
The library ships a Vue plugin for its global options:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { forms } from '@dynamicforms/vue-forms';
|
|
49
|
+
|
|
50
|
+
app.use(forms, { useMarkdownInValidators: false });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The import must be a named one — the default export is a namespace of the library members, not the plugin.
|
|
54
|
+
|
|
55
|
+
`useMarkdownInValidators` defaults to `true`, which means the default messages of the built-in validators are markdown
|
|
56
|
+
(`MdString`). Rendering those requires a globally registered `vue-markdown` component; set the option to `false` if you
|
|
57
|
+
want plain text instead.
|
|
58
|
+
|
|
34
59
|
## Basic Usage Example
|
|
35
60
|
|
|
61
|
+
Every form element is created with the constructor: `new Field({ ... })`, `new Action({ ... })`,
|
|
62
|
+
`new Group({ ... })`, `new List(template)`. The instance is a Vue reactive object from that moment on, so reading
|
|
63
|
+
`field.value` in a template tracks it and `field.value = x` re-renders — there is no `ref` to unwrap and no
|
|
64
|
+
computed mirror to maintain.
|
|
65
|
+
|
|
36
66
|
Here's a simple example of how to create and use a form with fields and groups:
|
|
37
67
|
|
|
38
68
|
```typescript
|
|
39
|
-
import {
|
|
40
|
-
import { Field, Group, ValueChangedAction } from '@dynamicforms/vue-forms';
|
|
69
|
+
import { Field, Group } from '@dynamicforms/vue-forms';
|
|
41
70
|
|
|
42
71
|
// Create a form with fields
|
|
43
72
|
const personForm = new Group({
|
|
44
|
-
firstName: Field
|
|
45
|
-
lastName: Field
|
|
46
|
-
age: Field
|
|
47
|
-
active: Field
|
|
73
|
+
firstName: new Field({ value: 'John' }),
|
|
74
|
+
lastName: new Field({ value: 'Doe' }),
|
|
75
|
+
age: new Field({ value: 30 }),
|
|
76
|
+
active: new Field({ value: true })
|
|
48
77
|
});
|
|
49
78
|
|
|
50
79
|
// Access values
|
|
@@ -60,6 +89,19 @@ personForm.fields.age.enabled = false;
|
|
|
60
89
|
console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
|
|
61
90
|
```
|
|
62
91
|
|
|
92
|
+
An `Action` is a field whose value is a label / icon pair and which can be executed:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { Action, ExecuteAction } from '@dynamicforms/vue-forms';
|
|
96
|
+
|
|
97
|
+
const saveAction = new Action({
|
|
98
|
+
value: { label: 'Save' },
|
|
99
|
+
actions: [new ExecuteAction((field, supr, params) => { console.log('saving', params); })]
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
saveAction.execute({ form: personForm }); // 'saving { form: ... }'
|
|
103
|
+
```
|
|
104
|
+
|
|
63
105
|
## Events Example
|
|
64
106
|
|
|
65
107
|
The library provides a powerful event system for field changes and other actions:
|
|
@@ -67,7 +109,7 @@ The library provides a powerful event system for field changes and other actions
|
|
|
67
109
|
```typescript
|
|
68
110
|
import { Field, Group, ValueChangedAction, ValidationErrorText } from '@dynamicforms/vue-forms';
|
|
69
111
|
|
|
70
|
-
const emailField = Field
|
|
112
|
+
const emailField = new Field({ value: '' })
|
|
71
113
|
.registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
72
114
|
// Custom validation on value change
|
|
73
115
|
if (!newValue.includes('@')) {
|
|
@@ -83,13 +125,16 @@ const emailField = Field.create({ value: '' })
|
|
|
83
125
|
// Or register events on a form
|
|
84
126
|
const form = new Group({
|
|
85
127
|
email: emailField,
|
|
86
|
-
username: Field
|
|
128
|
+
username: new Field()
|
|
87
129
|
}).registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
88
130
|
console.log('Form data changed:', newValue);
|
|
89
131
|
return supr(field, newValue, oldValue);
|
|
90
132
|
}));
|
|
91
133
|
```
|
|
92
134
|
|
|
135
|
+
Assigning `field.errors` directly replaces the whole array, including errors owned by validators — prefer a
|
|
136
|
+
`Validator` (see below) when all you want is to add a validation rule.
|
|
137
|
+
|
|
93
138
|
## Built-in Validators
|
|
94
139
|
|
|
95
140
|
The library provides several built-in validators for common validation scenarios:
|
|
@@ -99,12 +144,12 @@ import { Field, Group, Validators } from '@dynamicforms/vue-forms';
|
|
|
99
144
|
|
|
100
145
|
const validatedForm = new Group({
|
|
101
146
|
// Required field
|
|
102
|
-
username: Field
|
|
147
|
+
username: new Field({
|
|
103
148
|
validators: [new Validators.Required('Username is required')]
|
|
104
149
|
}),
|
|
105
150
|
|
|
106
151
|
// Email validation with pattern
|
|
107
|
-
email: Field
|
|
152
|
+
email: new Field({
|
|
108
153
|
validators: [
|
|
109
154
|
new Validators.Pattern(
|
|
110
155
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
@@ -114,7 +159,7 @@ const validatedForm = new Group({
|
|
|
114
159
|
}),
|
|
115
160
|
|
|
116
161
|
// Numeric range validation
|
|
117
|
-
age: Field
|
|
162
|
+
age: new Field({
|
|
118
163
|
value: 25,
|
|
119
164
|
validators: [
|
|
120
165
|
new Validators.ValueInRange(18, 100, 'Age must be between 18 and 100')
|
|
@@ -122,14 +167,14 @@ const validatedForm = new Group({
|
|
|
122
167
|
}),
|
|
123
168
|
|
|
124
169
|
// Allowed values validation
|
|
125
|
-
role: Field
|
|
170
|
+
role: new Field({
|
|
126
171
|
validators: [
|
|
127
172
|
new Validators.InAllowedValues(['admin', 'user', 'guest'])
|
|
128
173
|
]
|
|
129
174
|
}),
|
|
130
175
|
|
|
131
176
|
// Text length validation
|
|
132
|
-
bio: Field
|
|
177
|
+
bio: new Field({
|
|
133
178
|
validators: [
|
|
134
179
|
new Validators.LengthInRange(10, 200, 'Bio must be between 10 and 200 characters')
|
|
135
180
|
]
|
|
@@ -137,6 +182,9 @@ const validatedForm = new Group({
|
|
|
137
182
|
});
|
|
138
183
|
```
|
|
139
184
|
|
|
185
|
+
Validators run eagerly — a field is validated the moment it is created, so a form built from empty required fields is
|
|
186
|
+
invalid immediately. Use `field.touched` to decide when to show the errors in the UI.
|
|
187
|
+
|
|
140
188
|
## Messages Widget Component
|
|
141
189
|
|
|
142
190
|
The library includes a `messages-widget` Vue component for displaying validation errors and messages:
|
|
@@ -151,18 +199,20 @@ The library includes a `messages-widget` Vue component for displaying validation
|
|
|
151
199
|
|
|
152
200
|
<!-- Display field validation errors -->
|
|
153
201
|
<messages-widget
|
|
154
|
-
v-if="
|
|
155
|
-
:message="
|
|
202
|
+
v-if="emailField.errors.length > 0"
|
|
203
|
+
:message="emailField.errors"
|
|
156
204
|
:classes="['text-error', 'mt-2']"
|
|
157
205
|
/>
|
|
206
|
+
|
|
207
|
+
<!-- Markdown message -->
|
|
208
|
+
<messages-widget :message="markdownErrors" />
|
|
158
209
|
</template>
|
|
159
210
|
|
|
160
211
|
<script setup>
|
|
161
|
-
import MessagesWidget from '@dynamicforms/vue-forms
|
|
162
|
-
import { Field, Validators, ValidationErrorRenderContent, MdString } from '@dynamicforms/vue-forms';
|
|
212
|
+
import { MessagesWidget, Field, Validators, ValidationErrorRenderContent, MdString } from '@dynamicforms/vue-forms';
|
|
163
213
|
|
|
164
214
|
// Example field with validation
|
|
165
|
-
const emailField = Field
|
|
215
|
+
const emailField = new Field({
|
|
166
216
|
value: '',
|
|
167
217
|
validators: [
|
|
168
218
|
new Validators.Pattern(
|
|
@@ -199,10 +249,10 @@ import {
|
|
|
199
249
|
} from '@dynamicforms/vue-forms';
|
|
200
250
|
|
|
201
251
|
const form = new Group({
|
|
202
|
-
isCompany: Field
|
|
203
|
-
companyName: Field
|
|
204
|
-
firstName: Field
|
|
205
|
-
lastName: Field
|
|
252
|
+
isCompany: new Field({ value: false }),
|
|
253
|
+
companyName: new Field(),
|
|
254
|
+
firstName: new Field(),
|
|
255
|
+
lastName: new Field()
|
|
206
256
|
});
|
|
207
257
|
|
|
208
258
|
// Show company name field only when isCompany is true
|
|
@@ -230,9 +280,9 @@ import { Field, Group, List } from '@dynamicforms/vue-forms';
|
|
|
230
280
|
|
|
231
281
|
// Define a template for list items
|
|
232
282
|
const contactTemplate = new Group({
|
|
233
|
-
name: Field
|
|
234
|
-
email: Field
|
|
235
|
-
phone: Field
|
|
283
|
+
name: new Field(),
|
|
284
|
+
email: new Field(),
|
|
285
|
+
phone: new Field()
|
|
236
286
|
});
|
|
237
287
|
|
|
238
288
|
// Create a list with the template
|
|
@@ -242,8 +292,8 @@ const contactsList = new List(contactTemplate);
|
|
|
242
292
|
contactsList.push({ name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' });
|
|
243
293
|
contactsList.push({ name: 'Jane Doe', email: 'jane@example.com', phone: '987-654-3210' });
|
|
244
294
|
|
|
245
|
-
// Access list items
|
|
246
|
-
const firstContact = contactsList.get(0)
|
|
295
|
+
// Access list items: get() returns undefined for an invalid index
|
|
296
|
+
const firstContact = contactsList.get(0)!;
|
|
247
297
|
console.log(firstContact.fields.name.value); // 'John Doe'
|
|
248
298
|
|
|
249
299
|
// Modify items
|
|
@@ -253,21 +303,24 @@ firstContact.fields.email.value = 'john.doe@example.com';
|
|
|
253
303
|
contactsList.remove(1);
|
|
254
304
|
```
|
|
255
305
|
|
|
306
|
+
Every list mutation is tracked, so a `v-for` over `contactsList.value` re-renders on `push()`, `insert()`,
|
|
307
|
+
`remove()`, `pop()` and `clear()` without any extra wiring.
|
|
308
|
+
|
|
256
309
|
## TypeScript Support
|
|
257
310
|
|
|
258
311
|
The library is written in TypeScript and provides full type definitions:
|
|
259
312
|
|
|
260
313
|
```typescript
|
|
261
|
-
import { Field, Group,
|
|
314
|
+
import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
|
|
262
315
|
|
|
263
316
|
// Define field types explicitly
|
|
264
|
-
const usernameField = Field
|
|
265
|
-
const emailField = Field
|
|
266
|
-
const ageField = Field
|
|
267
|
-
const isActiveField = Field
|
|
317
|
+
const usernameField = new Field<string>({ value: '' });
|
|
318
|
+
const emailField = new Field<string>({ value: '' });
|
|
319
|
+
const ageField = new Field<number>({ value: 25 });
|
|
320
|
+
const isActiveField = new Field<boolean>({ value: true });
|
|
268
321
|
|
|
269
322
|
// Type inference also works with initial values
|
|
270
|
-
const implicitTypedField = Field
|
|
323
|
+
const implicitTypedField = new Field({ value: 'string' }); // Type is inferred as string
|
|
271
324
|
|
|
272
325
|
// Define your form structure with types
|
|
273
326
|
interface UserFormData extends GenericFieldsInterface {
|
|
@@ -283,16 +336,16 @@ interface UserFormData extends GenericFieldsInterface {
|
|
|
283
336
|
|
|
284
337
|
// Create the form with type checking
|
|
285
338
|
const userForm = new Group<UserFormData>({
|
|
286
|
-
username: Field
|
|
287
|
-
email: Field
|
|
288
|
-
age: Field
|
|
289
|
-
isActive: Field
|
|
339
|
+
username: new Field<string>({ value: '' }),
|
|
340
|
+
email: new Field<string>({ value: '' }),
|
|
341
|
+
age: new Field<number>({ value: 25 }),
|
|
342
|
+
isActive: new Field<boolean>({ value: true }),
|
|
290
343
|
preferences: new Group<{
|
|
291
344
|
darkMode: Field<boolean>;
|
|
292
345
|
notifications: Field<boolean>;
|
|
293
346
|
}>({
|
|
294
|
-
darkMode: Field
|
|
295
|
-
notifications: Field
|
|
347
|
+
darkMode: new Field<boolean>({ value: true }),
|
|
348
|
+
notifications: new Field<boolean>({ value: true })
|
|
296
349
|
})
|
|
297
350
|
});
|
|
298
351
|
|
|
@@ -301,13 +354,48 @@ const email: string = userForm.fields.email.value;
|
|
|
301
354
|
const age: number = userForm.fields.age.value;
|
|
302
355
|
const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
|
|
303
356
|
|
|
357
|
+
// The serialized value is typed too, member by member
|
|
358
|
+
const values = userForm.value!;
|
|
359
|
+
const emailFromValue: string = values.email;
|
|
360
|
+
const prefs: { darkMode: boolean; notifications: boolean } | null = values.preferences;
|
|
361
|
+
|
|
304
362
|
// Type safety prevents errors
|
|
305
363
|
// userForm.fields.age.value = 'not a number'; // Error: Type 'string' is not assignable to type 'number'
|
|
306
364
|
```
|
|
307
365
|
|
|
366
|
+
The value shape is derived from the fields map by the exported `FieldsToValues<T>`, with `GroupValue<T>` and
|
|
367
|
+
`GroupValueInput<T>` as the group's read and write types, and `ListValue` for lists. Constructor parameters have
|
|
368
|
+
their own exported type, `IFieldConstructorParams<T>`, shared by all four element classes and by `clone()`:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { Field, IFieldConstructorParams } from '@dynamicforms/vue-forms';
|
|
372
|
+
|
|
373
|
+
const defaults: Partial<IFieldConstructorParams<string>> = { value: '', enabled: false };
|
|
374
|
+
const field = new Field(defaults);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
It admits only the writable members — `value`, `originalValue`, `enabled`, `visibility`, `touched`, `errors`,
|
|
378
|
+
`validators` and `actions`. Derived members such as `valid` and `isChanged` are getters, and passing one is a
|
|
379
|
+
compile error.
|
|
380
|
+
|
|
381
|
+
`FieldBase<T>` is the abstract base of `Field`, `Action`, `Group` and `List`, and the type to use in your own
|
|
382
|
+
signatures whenever you accept any form element:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { FieldBase } from '@dynamicforms/vue-forms';
|
|
386
|
+
|
|
387
|
+
function isDirty(field: FieldBase): boolean {
|
|
388
|
+
return field.isChanged;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
308
392
|
## Documentation
|
|
309
393
|
|
|
310
|
-
For more detailed documentation and examples, check out the documentation
|
|
394
|
+
For more detailed documentation and examples, check out the [documentation](https://docs.velis.si/dynamicforms/vue-forms).
|
|
395
|
+
|
|
396
|
+
Upgrading from 0.5.x? The
|
|
397
|
+
[migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration) lists every breaking change with
|
|
398
|
+
before/after code.
|
|
311
399
|
|
|
312
400
|
## Conclusion
|
|
313
401
|
|