@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/readme.md CHANGED
@@ -5,21 +5,31 @@ 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**: `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**: 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**: `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, ValueChangedAction } from '@dynamicforms/vue-forms';
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.create({ value: 'John' }),
44
- lastName: Field.create({ value: 'Doe' }),
45
- age: Field.create({ value: 30 }),
46
- active: Field.create({ value: true })
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.create({ value: '' })
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.create()
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.create({
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.create({
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.create({
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.create({
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.create({
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="field.errors && field.errors.length > 0"
154
- :message="field.errors"
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.create({
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.create({ value: false }),
201
- companyName: Field.create(),
202
- firstName: Field.create(),
203
- lastName: Field.create()
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.create(),
232
- email: Field.create(),
233
- phone: Field.create()
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, IField } from '@dynamicforms/vue-forms';
326
+ import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
260
327
 
261
328
  // 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 });
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.create({ value: 'string' }); // Type is inferred as string
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.create<string>({ value: '' }),
285
- email: Field.create<string>({ value: '' }),
286
- age: Field.create<number>({ value: 25 }),
287
- isActive: Field.create<boolean>({ value: true }),
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.create<boolean>({ value: true }),
293
- notifications: Field.create<boolean>({ value: true })
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