@dynamicforms/vue-forms 0.5.1 → 0.6.1
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 +753 -606
- 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 +663 -0
- package/dist/index.d.ts +163 -93
- package/package.json +24 -8
- package/readme.md +145 -43
package/readme.md
CHANGED
|
@@ -5,21 +5,31 @@ 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**: `vue` (^3.4) is the only peer dependency, and `lodash` and `lodash-es` — one per build — the only runtime ones.
|
|
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**: `vue` (^3.4) is the only peer dependency; the runtime dependencies are `lodash` and `lodash-es`,
|
|
32
|
+
one for each of the two builds
|
|
23
33
|
- **Field types**: Core field types (Field, Action, Group, List) to represent any data structure
|
|
24
34
|
- **Validation**: Comprehensive validation system with built-in validators and extensible error handling
|
|
25
35
|
- **Conditional logic**: Dynamic form behavior based on field values and conditions
|
|
@@ -31,19 +41,45 @@ separates these concerns, allowing you to build forms that match your design sys
|
|
|
31
41
|
npm install @dynamicforms/vue-forms
|
|
32
42
|
```
|
|
33
43
|
|
|
44
|
+
The package ships two builds of the same source: an ESM build that imports `lodash-es` and a CJS/UMD build that
|
|
45
|
+
requires `lodash`. Both lodash packages are declared as dependencies and install with the package, so an `import` and
|
|
46
|
+
a `require()` both resolve without further setup. Node 18 or newer is required, and `vue` (^3.4) is the only peer
|
|
47
|
+
dependency. Type definitions ship for both entry points; the stylesheet is `@dynamicforms/vue-forms/style.css`.
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
The library ships a Vue plugin for its global options:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { forms } from '@dynamicforms/vue-forms';
|
|
55
|
+
|
|
56
|
+
app.use(forms, { useMarkdownInValidators: false });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The import must be a named one — the default export is a namespace of the library members, not the plugin.
|
|
60
|
+
|
|
61
|
+
`useMarkdownInValidators` defaults to `true`, which means the default messages of the built-in validators are markdown
|
|
62
|
+
(`MdString`). Rendering those requires a globally registered `vue-markdown` component; set the option to `false` if you
|
|
63
|
+
want plain text instead.
|
|
64
|
+
|
|
34
65
|
## Basic Usage Example
|
|
35
66
|
|
|
67
|
+
Every form element is created with the constructor: `new Field({ ... })`, `new Action({ ... })`,
|
|
68
|
+
`new Group({ ... })`, `new List(template)`. The instance is a Vue reactive object from that moment on, so reading
|
|
69
|
+
`field.value` in a template tracks it and `field.value = x` re-renders — there is no `ref` to unwrap and no
|
|
70
|
+
computed mirror to maintain.
|
|
71
|
+
|
|
36
72
|
Here's a simple example of how to create and use a form with fields and groups:
|
|
37
73
|
|
|
38
74
|
```typescript
|
|
39
|
-
import { Field, Group
|
|
75
|
+
import { Field, Group } from '@dynamicforms/vue-forms';
|
|
40
76
|
|
|
41
77
|
// Create a form with fields
|
|
42
78
|
const personForm = new Group({
|
|
43
|
-
firstName: Field
|
|
44
|
-
lastName: Field
|
|
45
|
-
age: Field
|
|
46
|
-
active: Field
|
|
79
|
+
firstName: new Field({ value: 'John' }),
|
|
80
|
+
lastName: new Field({ value: 'Doe' }),
|
|
81
|
+
age: new Field({ value: 30 }),
|
|
82
|
+
active: new Field({ value: true })
|
|
47
83
|
});
|
|
48
84
|
|
|
49
85
|
// Access values
|
|
@@ -59,6 +95,19 @@ personForm.fields.age.enabled = false;
|
|
|
59
95
|
console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
|
|
60
96
|
```
|
|
61
97
|
|
|
98
|
+
An `Action` is a field whose value is a label / icon pair and which can be executed:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { Action, ExecuteAction } from '@dynamicforms/vue-forms';
|
|
102
|
+
|
|
103
|
+
const saveAction = new Action({
|
|
104
|
+
value: { label: 'Save' },
|
|
105
|
+
actions: [new ExecuteAction((field, supr, params) => { console.log('saving', params); })]
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
saveAction.execute({ form: personForm }); // 'saving { form: ... }'
|
|
109
|
+
```
|
|
110
|
+
|
|
62
111
|
## Events Example
|
|
63
112
|
|
|
64
113
|
The library provides a powerful event system for field changes and other actions:
|
|
@@ -66,7 +115,7 @@ The library provides a powerful event system for field changes and other actions
|
|
|
66
115
|
```typescript
|
|
67
116
|
import { Field, Group, ValueChangedAction, ValidationErrorText } from '@dynamicforms/vue-forms';
|
|
68
117
|
|
|
69
|
-
const emailField = Field
|
|
118
|
+
const emailField = new Field({ value: '' })
|
|
70
119
|
.registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
71
120
|
// Custom validation on value change
|
|
72
121
|
if (!newValue.includes('@')) {
|
|
@@ -82,13 +131,16 @@ const emailField = Field.create({ value: '' })
|
|
|
82
131
|
// Or register events on a form
|
|
83
132
|
const form = new Group({
|
|
84
133
|
email: emailField,
|
|
85
|
-
username: Field
|
|
134
|
+
username: new Field()
|
|
86
135
|
}).registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
87
136
|
console.log('Form data changed:', newValue);
|
|
88
137
|
return supr(field, newValue, oldValue);
|
|
89
138
|
}));
|
|
90
139
|
```
|
|
91
140
|
|
|
141
|
+
Assigning `field.errors` directly replaces the whole array, including errors owned by validators — prefer a
|
|
142
|
+
`Validator` (see below) when all you want is to add a validation rule.
|
|
143
|
+
|
|
92
144
|
## Built-in Validators
|
|
93
145
|
|
|
94
146
|
The library provides several built-in validators for common validation scenarios:
|
|
@@ -98,12 +150,12 @@ import { Field, Group, Validators } from '@dynamicforms/vue-forms';
|
|
|
98
150
|
|
|
99
151
|
const validatedForm = new Group({
|
|
100
152
|
// Required field
|
|
101
|
-
username: Field
|
|
153
|
+
username: new Field({
|
|
102
154
|
validators: [new Validators.Required('Username is required')]
|
|
103
155
|
}),
|
|
104
156
|
|
|
105
157
|
// Email validation with pattern
|
|
106
|
-
email: Field
|
|
158
|
+
email: new Field({
|
|
107
159
|
validators: [
|
|
108
160
|
new Validators.Pattern(
|
|
109
161
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
@@ -113,7 +165,7 @@ const validatedForm = new Group({
|
|
|
113
165
|
}),
|
|
114
166
|
|
|
115
167
|
// Numeric range validation
|
|
116
|
-
age: Field
|
|
168
|
+
age: new Field({
|
|
117
169
|
value: 25,
|
|
118
170
|
validators: [
|
|
119
171
|
new Validators.ValueInRange(18, 100, 'Age must be between 18 and 100')
|
|
@@ -121,14 +173,14 @@ const validatedForm = new Group({
|
|
|
121
173
|
}),
|
|
122
174
|
|
|
123
175
|
// Allowed values validation
|
|
124
|
-
role: Field
|
|
176
|
+
role: new Field({
|
|
125
177
|
validators: [
|
|
126
178
|
new Validators.InAllowedValues(['admin', 'user', 'guest'])
|
|
127
179
|
]
|
|
128
180
|
}),
|
|
129
181
|
|
|
130
182
|
// Text length validation
|
|
131
|
-
bio: Field
|
|
183
|
+
bio: new Field({
|
|
132
184
|
validators: [
|
|
133
185
|
new Validators.LengthInRange(10, 200, 'Bio must be between 10 and 200 characters')
|
|
134
186
|
]
|
|
@@ -136,6 +188,15 @@ const validatedForm = new Group({
|
|
|
136
188
|
});
|
|
137
189
|
```
|
|
138
190
|
|
|
191
|
+
Validators run eagerly — a field is validated the moment it is created, so a form built from empty required fields is
|
|
192
|
+
invalid immediately. Use `field.touched` to decide when to show the errors in the UI.
|
|
193
|
+
|
|
194
|
+
A validation function may return a `Promise`. `field.validating` is `true` while such a run is pending, and the
|
|
195
|
+
verdict applied to the field is always the one belonging to the newest run, so a slow check cannot overwrite a faster
|
|
196
|
+
one started after it. A validator message given as a `Ref` or `computed` is resolved when the message is rendered, so
|
|
197
|
+
a translated message follows a locale switch without revalidating. `field.clearValidators()` drops the validators,
|
|
198
|
+
empties the errors and cancels whatever validation is still in flight.
|
|
199
|
+
|
|
139
200
|
## Messages Widget Component
|
|
140
201
|
|
|
141
202
|
The library includes a `messages-widget` Vue component for displaying validation errors and messages:
|
|
@@ -150,17 +211,20 @@ The library includes a `messages-widget` Vue component for displaying validation
|
|
|
150
211
|
|
|
151
212
|
<!-- Display field validation errors -->
|
|
152
213
|
<messages-widget
|
|
153
|
-
v-if="
|
|
154
|
-
:message="
|
|
214
|
+
v-if="emailField.errors.length > 0"
|
|
215
|
+
:message="emailField.errors"
|
|
155
216
|
:classes="['text-error', 'mt-2']"
|
|
156
217
|
/>
|
|
218
|
+
|
|
219
|
+
<!-- Markdown message -->
|
|
220
|
+
<messages-widget :message="markdownErrors" />
|
|
157
221
|
</template>
|
|
158
222
|
|
|
159
223
|
<script setup>
|
|
160
224
|
import { MessagesWidget, Field, Validators, ValidationErrorRenderContent, MdString } from '@dynamicforms/vue-forms';
|
|
161
225
|
|
|
162
226
|
// Example field with validation
|
|
163
|
-
const emailField = Field
|
|
227
|
+
const emailField = new Field({
|
|
164
228
|
value: '',
|
|
165
229
|
validators: [
|
|
166
230
|
new Validators.Pattern(
|
|
@@ -197,10 +261,10 @@ import {
|
|
|
197
261
|
} from '@dynamicforms/vue-forms';
|
|
198
262
|
|
|
199
263
|
const form = new Group({
|
|
200
|
-
isCompany: Field
|
|
201
|
-
companyName: Field
|
|
202
|
-
firstName: Field
|
|
203
|
-
lastName: Field
|
|
264
|
+
isCompany: new Field({ value: false }),
|
|
265
|
+
companyName: new Field(),
|
|
266
|
+
firstName: new Field(),
|
|
267
|
+
lastName: new Field()
|
|
204
268
|
});
|
|
205
269
|
|
|
206
270
|
// Show company name field only when isCompany is true
|
|
@@ -228,9 +292,9 @@ import { Field, Group, List } from '@dynamicforms/vue-forms';
|
|
|
228
292
|
|
|
229
293
|
// Define a template for list items
|
|
230
294
|
const contactTemplate = new Group({
|
|
231
|
-
name: Field
|
|
232
|
-
email: Field
|
|
233
|
-
phone: Field
|
|
295
|
+
name: new Field(),
|
|
296
|
+
email: new Field(),
|
|
297
|
+
phone: new Field()
|
|
234
298
|
});
|
|
235
299
|
|
|
236
300
|
// Create a list with the template
|
|
@@ -240,8 +304,8 @@ const contactsList = new List(contactTemplate);
|
|
|
240
304
|
contactsList.push({ name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' });
|
|
241
305
|
contactsList.push({ name: 'Jane Doe', email: 'jane@example.com', phone: '987-654-3210' });
|
|
242
306
|
|
|
243
|
-
// Access list items
|
|
244
|
-
const firstContact = contactsList.get(0)
|
|
307
|
+
// Access list items: get() returns undefined for an invalid index
|
|
308
|
+
const firstContact = contactsList.get(0)!;
|
|
245
309
|
console.log(firstContact.fields.name.value); // 'John Doe'
|
|
246
310
|
|
|
247
311
|
// Modify items
|
|
@@ -251,21 +315,24 @@ firstContact.fields.email.value = 'john.doe@example.com';
|
|
|
251
315
|
contactsList.remove(1);
|
|
252
316
|
```
|
|
253
317
|
|
|
318
|
+
Every list mutation is tracked, so a `v-for` over `contactsList.value` re-renders on `push()`, `insert()`,
|
|
319
|
+
`remove()`, `pop()` and `clear()` without any extra wiring.
|
|
320
|
+
|
|
254
321
|
## TypeScript Support
|
|
255
322
|
|
|
256
323
|
The library is written in TypeScript and provides full type definitions:
|
|
257
324
|
|
|
258
325
|
```typescript
|
|
259
|
-
import { Field, Group,
|
|
326
|
+
import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
|
|
260
327
|
|
|
261
328
|
// Define field types explicitly
|
|
262
|
-
const usernameField = Field
|
|
263
|
-
const emailField = Field
|
|
264
|
-
const ageField = Field
|
|
265
|
-
const isActiveField = Field
|
|
329
|
+
const usernameField = new Field<string>({ value: '' });
|
|
330
|
+
const emailField = new Field<string>({ value: '' });
|
|
331
|
+
const ageField = new Field<number>({ value: 25 });
|
|
332
|
+
const isActiveField = new Field<boolean>({ value: true });
|
|
266
333
|
|
|
267
334
|
// Type inference also works with initial values
|
|
268
|
-
const implicitTypedField = Field
|
|
335
|
+
const implicitTypedField = new Field({ value: 'string' }); // Type is inferred as string
|
|
269
336
|
|
|
270
337
|
// Define your form structure with types
|
|
271
338
|
interface UserFormData extends GenericFieldsInterface {
|
|
@@ -281,16 +348,16 @@ interface UserFormData extends GenericFieldsInterface {
|
|
|
281
348
|
|
|
282
349
|
// Create the form with type checking
|
|
283
350
|
const userForm = new Group<UserFormData>({
|
|
284
|
-
username: Field
|
|
285
|
-
email: Field
|
|
286
|
-
age: Field
|
|
287
|
-
isActive: Field
|
|
351
|
+
username: new Field<string>({ value: '' }),
|
|
352
|
+
email: new Field<string>({ value: '' }),
|
|
353
|
+
age: new Field<number>({ value: 25 }),
|
|
354
|
+
isActive: new Field<boolean>({ value: true }),
|
|
288
355
|
preferences: new Group<{
|
|
289
356
|
darkMode: Field<boolean>;
|
|
290
357
|
notifications: Field<boolean>;
|
|
291
358
|
}>({
|
|
292
|
-
darkMode: Field
|
|
293
|
-
notifications: Field
|
|
359
|
+
darkMode: new Field<boolean>({ value: true }),
|
|
360
|
+
notifications: new Field<boolean>({ value: true })
|
|
294
361
|
})
|
|
295
362
|
});
|
|
296
363
|
|
|
@@ -299,14 +366,49 @@ const email: string = userForm.fields.email.value;
|
|
|
299
366
|
const age: number = userForm.fields.age.value;
|
|
300
367
|
const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
|
|
301
368
|
|
|
369
|
+
// The serialized value is typed too, member by member
|
|
370
|
+
const values = userForm.value!;
|
|
371
|
+
const emailFromValue: string = values.email;
|
|
372
|
+
const prefs: { darkMode: boolean; notifications: boolean } | null = values.preferences;
|
|
373
|
+
|
|
302
374
|
// Type safety prevents errors
|
|
303
375
|
// userForm.fields.age.value = 'not a number'; // Error: Type 'string' is not assignable to type 'number'
|
|
304
376
|
```
|
|
305
377
|
|
|
378
|
+
The value shape is derived from the fields map by the exported `FieldsToValues<T>`, with `GroupValue<T>` and
|
|
379
|
+
`GroupValueInput<T>` as the group's read and write types, and `ListValue` for lists. Constructor parameters have
|
|
380
|
+
their own exported type, `IFieldConstructorParams<T>`, shared by all four element classes and by `clone()`:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { Field, IFieldConstructorParams } from '@dynamicforms/vue-forms';
|
|
384
|
+
|
|
385
|
+
const defaults: Partial<IFieldConstructorParams<string>> = { value: '', enabled: false };
|
|
386
|
+
const field = new Field(defaults);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
It admits only the writable members — `value`, `originalValue`, `enabled`, `visibility`, `touched`, `errors`,
|
|
390
|
+
`validators` and `actions`. Derived members such as `valid` and `isChanged` are getters, and passing one is a
|
|
391
|
+
compile error.
|
|
392
|
+
|
|
393
|
+
`FieldBase<T>` is the abstract base of `Field`, `Action`, `Group` and `List`, and the type to use in your own
|
|
394
|
+
signatures whenever you accept any form element:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { FieldBase } from '@dynamicforms/vue-forms';
|
|
398
|
+
|
|
399
|
+
function isDirty(field: FieldBase): boolean {
|
|
400
|
+
return field.isChanged;
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
306
404
|
## Documentation
|
|
307
405
|
|
|
308
406
|
For more detailed documentation and examples, check out the [documentation](https://docs.velis.si/dynamicforms/vue-forms).
|
|
309
407
|
|
|
408
|
+
Upgrading from 0.5.x? The
|
|
409
|
+
[migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration) lists every breaking change with
|
|
410
|
+
before/after code.
|
|
411
|
+
|
|
310
412
|
## Conclusion
|
|
311
413
|
|
|
312
414
|
`@dynamicforms/vue-forms` provides a clean, flexible approach to form management in Vue applications. By focusing on
|