@dynamicforms/vue-forms 0.5.1 → 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 +535 -539
- 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 +14 -5
- package/readme.md +133 -43
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,19 +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 { Field, Group
|
|
69
|
+
import { Field, Group } from '@dynamicforms/vue-forms';
|
|
40
70
|
|
|
41
71
|
// Create a form with fields
|
|
42
72
|
const personForm = new Group({
|
|
43
|
-
firstName: Field
|
|
44
|
-
lastName: Field
|
|
45
|
-
age: Field
|
|
46
|
-
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 })
|
|
47
77
|
});
|
|
48
78
|
|
|
49
79
|
// Access values
|
|
@@ -59,6 +89,19 @@ personForm.fields.age.enabled = false;
|
|
|
59
89
|
console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
|
|
60
90
|
```
|
|
61
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
|
+
|
|
62
105
|
## Events Example
|
|
63
106
|
|
|
64
107
|
The library provides a powerful event system for field changes and other actions:
|
|
@@ -66,7 +109,7 @@ The library provides a powerful event system for field changes and other actions
|
|
|
66
109
|
```typescript
|
|
67
110
|
import { Field, Group, ValueChangedAction, ValidationErrorText } from '@dynamicforms/vue-forms';
|
|
68
111
|
|
|
69
|
-
const emailField = Field
|
|
112
|
+
const emailField = new Field({ value: '' })
|
|
70
113
|
.registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
71
114
|
// Custom validation on value change
|
|
72
115
|
if (!newValue.includes('@')) {
|
|
@@ -82,13 +125,16 @@ const emailField = Field.create({ value: '' })
|
|
|
82
125
|
// Or register events on a form
|
|
83
126
|
const form = new Group({
|
|
84
127
|
email: emailField,
|
|
85
|
-
username: Field
|
|
128
|
+
username: new Field()
|
|
86
129
|
}).registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
87
130
|
console.log('Form data changed:', newValue);
|
|
88
131
|
return supr(field, newValue, oldValue);
|
|
89
132
|
}));
|
|
90
133
|
```
|
|
91
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
|
+
|
|
92
138
|
## Built-in Validators
|
|
93
139
|
|
|
94
140
|
The library provides several built-in validators for common validation scenarios:
|
|
@@ -98,12 +144,12 @@ import { Field, Group, Validators } from '@dynamicforms/vue-forms';
|
|
|
98
144
|
|
|
99
145
|
const validatedForm = new Group({
|
|
100
146
|
// Required field
|
|
101
|
-
username: Field
|
|
147
|
+
username: new Field({
|
|
102
148
|
validators: [new Validators.Required('Username is required')]
|
|
103
149
|
}),
|
|
104
150
|
|
|
105
151
|
// Email validation with pattern
|
|
106
|
-
email: Field
|
|
152
|
+
email: new Field({
|
|
107
153
|
validators: [
|
|
108
154
|
new Validators.Pattern(
|
|
109
155
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
@@ -113,7 +159,7 @@ const validatedForm = new Group({
|
|
|
113
159
|
}),
|
|
114
160
|
|
|
115
161
|
// Numeric range validation
|
|
116
|
-
age: Field
|
|
162
|
+
age: new Field({
|
|
117
163
|
value: 25,
|
|
118
164
|
validators: [
|
|
119
165
|
new Validators.ValueInRange(18, 100, 'Age must be between 18 and 100')
|
|
@@ -121,14 +167,14 @@ const validatedForm = new Group({
|
|
|
121
167
|
}),
|
|
122
168
|
|
|
123
169
|
// Allowed values validation
|
|
124
|
-
role: Field
|
|
170
|
+
role: new Field({
|
|
125
171
|
validators: [
|
|
126
172
|
new Validators.InAllowedValues(['admin', 'user', 'guest'])
|
|
127
173
|
]
|
|
128
174
|
}),
|
|
129
175
|
|
|
130
176
|
// Text length validation
|
|
131
|
-
bio: Field
|
|
177
|
+
bio: new Field({
|
|
132
178
|
validators: [
|
|
133
179
|
new Validators.LengthInRange(10, 200, 'Bio must be between 10 and 200 characters')
|
|
134
180
|
]
|
|
@@ -136,6 +182,9 @@ const validatedForm = new Group({
|
|
|
136
182
|
});
|
|
137
183
|
```
|
|
138
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
|
+
|
|
139
188
|
## Messages Widget Component
|
|
140
189
|
|
|
141
190
|
The library includes a `messages-widget` Vue component for displaying validation errors and messages:
|
|
@@ -150,17 +199,20 @@ The library includes a `messages-widget` Vue component for displaying validation
|
|
|
150
199
|
|
|
151
200
|
<!-- Display field validation errors -->
|
|
152
201
|
<messages-widget
|
|
153
|
-
v-if="
|
|
154
|
-
:message="
|
|
202
|
+
v-if="emailField.errors.length > 0"
|
|
203
|
+
:message="emailField.errors"
|
|
155
204
|
:classes="['text-error', 'mt-2']"
|
|
156
205
|
/>
|
|
206
|
+
|
|
207
|
+
<!-- Markdown message -->
|
|
208
|
+
<messages-widget :message="markdownErrors" />
|
|
157
209
|
</template>
|
|
158
210
|
|
|
159
211
|
<script setup>
|
|
160
212
|
import { MessagesWidget, Field, Validators, ValidationErrorRenderContent, MdString } from '@dynamicforms/vue-forms';
|
|
161
213
|
|
|
162
214
|
// Example field with validation
|
|
163
|
-
const emailField = Field
|
|
215
|
+
const emailField = new Field({
|
|
164
216
|
value: '',
|
|
165
217
|
validators: [
|
|
166
218
|
new Validators.Pattern(
|
|
@@ -197,10 +249,10 @@ import {
|
|
|
197
249
|
} from '@dynamicforms/vue-forms';
|
|
198
250
|
|
|
199
251
|
const form = new Group({
|
|
200
|
-
isCompany: Field
|
|
201
|
-
companyName: Field
|
|
202
|
-
firstName: Field
|
|
203
|
-
lastName: Field
|
|
252
|
+
isCompany: new Field({ value: false }),
|
|
253
|
+
companyName: new Field(),
|
|
254
|
+
firstName: new Field(),
|
|
255
|
+
lastName: new Field()
|
|
204
256
|
});
|
|
205
257
|
|
|
206
258
|
// Show company name field only when isCompany is true
|
|
@@ -228,9 +280,9 @@ import { Field, Group, List } from '@dynamicforms/vue-forms';
|
|
|
228
280
|
|
|
229
281
|
// Define a template for list items
|
|
230
282
|
const contactTemplate = new Group({
|
|
231
|
-
name: Field
|
|
232
|
-
email: Field
|
|
233
|
-
phone: Field
|
|
283
|
+
name: new Field(),
|
|
284
|
+
email: new Field(),
|
|
285
|
+
phone: new Field()
|
|
234
286
|
});
|
|
235
287
|
|
|
236
288
|
// Create a list with the template
|
|
@@ -240,8 +292,8 @@ const contactsList = new List(contactTemplate);
|
|
|
240
292
|
contactsList.push({ name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' });
|
|
241
293
|
contactsList.push({ name: 'Jane Doe', email: 'jane@example.com', phone: '987-654-3210' });
|
|
242
294
|
|
|
243
|
-
// Access list items
|
|
244
|
-
const firstContact = contactsList.get(0)
|
|
295
|
+
// Access list items: get() returns undefined for an invalid index
|
|
296
|
+
const firstContact = contactsList.get(0)!;
|
|
245
297
|
console.log(firstContact.fields.name.value); // 'John Doe'
|
|
246
298
|
|
|
247
299
|
// Modify items
|
|
@@ -251,21 +303,24 @@ firstContact.fields.email.value = 'john.doe@example.com';
|
|
|
251
303
|
contactsList.remove(1);
|
|
252
304
|
```
|
|
253
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
|
+
|
|
254
309
|
## TypeScript Support
|
|
255
310
|
|
|
256
311
|
The library is written in TypeScript and provides full type definitions:
|
|
257
312
|
|
|
258
313
|
```typescript
|
|
259
|
-
import { Field, Group,
|
|
314
|
+
import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
|
|
260
315
|
|
|
261
316
|
// Define field types explicitly
|
|
262
|
-
const usernameField = Field
|
|
263
|
-
const emailField = Field
|
|
264
|
-
const ageField = Field
|
|
265
|
-
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 });
|
|
266
321
|
|
|
267
322
|
// Type inference also works with initial values
|
|
268
|
-
const implicitTypedField = Field
|
|
323
|
+
const implicitTypedField = new Field({ value: 'string' }); // Type is inferred as string
|
|
269
324
|
|
|
270
325
|
// Define your form structure with types
|
|
271
326
|
interface UserFormData extends GenericFieldsInterface {
|
|
@@ -281,16 +336,16 @@ interface UserFormData extends GenericFieldsInterface {
|
|
|
281
336
|
|
|
282
337
|
// Create the form with type checking
|
|
283
338
|
const userForm = new Group<UserFormData>({
|
|
284
|
-
username: Field
|
|
285
|
-
email: Field
|
|
286
|
-
age: Field
|
|
287
|
-
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 }),
|
|
288
343
|
preferences: new Group<{
|
|
289
344
|
darkMode: Field<boolean>;
|
|
290
345
|
notifications: Field<boolean>;
|
|
291
346
|
}>({
|
|
292
|
-
darkMode: Field
|
|
293
|
-
notifications: Field
|
|
347
|
+
darkMode: new Field<boolean>({ value: true }),
|
|
348
|
+
notifications: new Field<boolean>({ value: true })
|
|
294
349
|
})
|
|
295
350
|
});
|
|
296
351
|
|
|
@@ -299,14 +354,49 @@ const email: string = userForm.fields.email.value;
|
|
|
299
354
|
const age: number = userForm.fields.age.value;
|
|
300
355
|
const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
|
|
301
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
|
+
|
|
302
362
|
// Type safety prevents errors
|
|
303
363
|
// userForm.fields.age.value = 'not a number'; // Error: Type 'string' is not assignable to type 'number'
|
|
304
364
|
```
|
|
305
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
|
+
|
|
306
392
|
## Documentation
|
|
307
393
|
|
|
308
394
|
For more detailed documentation and examples, check out the [documentation](https://docs.velis.si/dynamicforms/vue-forms).
|
|
309
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.
|
|
399
|
+
|
|
310
400
|
## Conclusion
|
|
311
401
|
|
|
312
402
|
`@dynamicforms/vue-forms` provides a clean, flexible approach to form management in Vue applications. By focusing on
|