@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/readme.md CHANGED
@@ -5,21 +5,30 @@ UI components.
5
5
 
6
6
  ## Introduction
7
7
 
8
- `@dynamicforms/vue-forms` provides a powerful yet simple way to manage form data, validation, and state in Vue
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
- Unlike other form libraries that couple data management with specific UI components, `@dynamicforms/vue-forms`
13
- separates these concerns, allowing you to build forms that match your design system perfectly.
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**: Built on Vue's reactivity system for seamless integration
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**: No dependencies besides Vue and lodash
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, ValueChangedAction } from '@dynamicforms/vue-forms';
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.create({ value: 'John' }),
44
- lastName: Field.create({ value: 'Doe' }),
45
- age: Field.create({ value: 30 }),
46
- active: Field.create({ value: true })
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.create({ value: '' })
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.create()
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.create({
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.create({
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.create({
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.create({
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.create({
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="field.errors && field.errors.length > 0"
154
- :message="field.errors"
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.create({
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.create({ value: false }),
201
- companyName: Field.create(),
202
- firstName: Field.create(),
203
- lastName: Field.create()
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.create(),
232
- email: Field.create(),
233
- phone: Field.create()
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, IField } from '@dynamicforms/vue-forms';
314
+ import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
260
315
 
261
316
  // Define field types explicitly
262
- const usernameField = Field.create<string>({ value: '' });
263
- const emailField = Field.create<string>({ value: '' });
264
- const ageField = Field.create<number>({ value: 25 });
265
- const isActiveField = Field.create<boolean>({ value: true });
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.create({ value: 'string' }); // Type is inferred as string
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.create<string>({ value: '' }),
285
- email: Field.create<string>({ value: '' }),
286
- age: Field.create<number>({ value: 25 }),
287
- isActive: Field.create<boolean>({ value: true }),
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.create<boolean>({ value: true }),
293
- notifications: Field.create<boolean>({ value: true })
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