@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/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,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 { reactive } from 'vue';
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.create({ value: 'John' }),
45
- lastName: Field.create({ value: 'Doe' }),
46
- age: Field.create({ value: 30 }),
47
- 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 })
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.create({ value: '' })
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.create()
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.create({
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.create({
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.create({
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.create({
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.create({
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="field.errors && field.errors.length > 0"
155
- :message="field.errors"
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/components/messages-widget.vue';
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.create({
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.create({ value: false }),
203
- companyName: Field.create(),
204
- firstName: Field.create(),
205
- lastName: Field.create()
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.create(),
234
- email: Field.create(),
235
- phone: Field.create()
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, IField } from '@dynamicforms/vue-forms';
314
+ import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
262
315
 
263
316
  // Define field types explicitly
264
- const usernameField = Field.create<string>({ value: '' });
265
- const emailField = Field.create<string>({ value: '' });
266
- const ageField = Field.create<number>({ value: 25 });
267
- 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 });
268
321
 
269
322
  // Type inference also works with initial values
270
- const implicitTypedField = Field.create({ value: 'string' }); // Type is inferred as string
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.create<string>({ value: '' }),
287
- email: Field.create<string>({ value: '' }),
288
- age: Field.create<number>({ value: 25 }),
289
- 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 }),
290
343
  preferences: new Group<{
291
344
  darkMode: Field<boolean>;
292
345
  notifications: Field<boolean>;
293
346
  }>({
294
- darkMode: Field.create<boolean>({ value: true }),
295
- notifications: Field.create<boolean>({ value: true })
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 (currently only locally runnable).
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