@dynamicforms/vue-forms 2.0.2 → 3.0.0-alpha.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
@@ -1,52 +1,57 @@
1
1
  # @dynamicforms/vue-forms
2
2
 
3
- A reactive data entry forms library for Vue.js that handles form state management without dictating your
4
- UI components.
3
+ A reactive library for Vue.js that holds forms and application state without dictating your UI components:
4
+ validation, rules between fields, transactions and lists.
5
5
 
6
6
  ## Introduction
7
7
 
8
- `@dynamicforms/vue-forms` manages form data, validation and state, and leaves rendering entirely to you.
8
+ `@dynamicforms/vue-forms` manages form data, validation and state; rendering is left to the application. The same
9
+ elements also hold application state that no screen displays, such as a shopping cart or the filters of a list view.
9
10
 
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
11
+ In addition to state, the library models the behaviour *between* fields. Visibility, enablement and values can be
12
+ declared as conditions over other fields, and every change passes through an action pipeline in which each handler
13
+ decides whether to pass the event on and may modify its result. Groups and lists compose recursively, so the same
13
14
  mechanism applies at every level of a nested form.
14
15
 
15
16
  ### Design Goals
16
17
 
17
- - **UI-Agnostic**: A logic layer for form state, validation and dynamic behaviour. Works with any Vue components, including your own. The few members that speak about the interface — `visibility`, `enabled`, `touched`, error render content and `Action` — are there because nearly every form needs them; [Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface) lists them with the reason for each.
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**: Every member of a field, group or list is a tracked read, 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.
18
+ - **UI-Agnostic**: a logic layer for state, validation and dynamic behaviour, rendered by any Vue components,
19
+ including your own. The few members that relate to the interface are listed in
20
+ [Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface).
21
+ - **Fields that react to each other**: the behaviour between fields is part of the form definition, not wiring in
22
+ components.
23
+ - **Reactive & Type-Safe**: every member is a tracked read, and the types follow the declared structure.
24
+ - **Structural serialization**: a group's value has the shape of its fields.
25
+ - **Application state, not only forms**: the same elements hold state that no screen displays, such as a shopping
26
+ cart, the filters of a list view or the settings of an editor, with the same features a form has.
21
27
 
22
28
  ## Features
23
29
 
24
- - **UI-agnostic**: a logic layer for form state, validation and dynamic behaviour. Any Vue components render it,
25
- your own included; the few members that speak about the interface are [listed with their reasons](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface)
26
- - **Transactional**: every mutating operation is atomic — events are announced once, over the net change, and a
27
- handler that throws leaves the form exactly as it was. `transaction()` makes several writes one operation, and
28
- `tx.rollback()` withdraws one without an error
29
- - **Lists that scale**: a `List` is meant to hold thousands of rows. Writing one field of one row costs that row
30
- and the depth it sits at, a `push()` costs one row, and reading `value` or `valid` again costs nothing
31
- - **Declared once, bound per record**: the element handed to `new List(template)` is the declaration every row is
32
- built from. One validator instance and one conditional rule serve every row, and each row answers for itself
33
- - **Reactive**: every member of a field, group or list is a tracked read — assign a property directly, with no
34
- `ref` to unwrap and no computed mirror to keep in sync
35
- - **Nested structures**: fields, groups and lists compose recursively, at every level
36
- - **Event system**: every change travels through an action pipeline; a handler may pass it on, reshape it or stop it
30
+ - **Transactional**: every mutating operation is atomic. Events are announced once, over the net change, and a
31
+ handler that throws leaves the form unchanged. `transaction()` makes several writes one operation, and
32
+ `tx.rollback()` reverts one without throwing
37
33
  - **Validation**: built-in validators, custom synchronous and asynchronous rules, cross-field comparisons, and
38
- errors that carry a `code` and render as text, markdown or a component of your own
34
+ errors that contain a `code` and render as text, markdown or a component of your own
39
35
  - **Conditional logic**: visibility, enablement and values declared as statements over other fields
40
- - **Extended properties**: a field carries whatever your UI renders it with — a label, a hint, a width — declared
41
- as a second type argument, checked by the compiler and read through `extra`
42
- - **Application state, not only forms**: the same elements hold state no screen shows — a cart, the filters of a list view, editor
43
- settings — with the reactivity, transactions and validation a form has
44
- - **Plain-data views**: `view(group)` reads an element as plain properties — `form.address.city`,
45
- `v-model="form.name"` — with the element itself as `form.$`
46
- - **Access**: `'editable'`, `'readonly'`, `'disabled'` and `'disabled-null'` decide whether an element accepts input
47
- and what it contributes to the value its form sends — its own value, `null`, or nothing — and validation follows
48
- what is sent; `visibility` states how it is drawn
49
- - **TypeScript support**: full type definitions, and a group's value type inferred from the fields it holds
36
+ - **Event system**: every change passes through an action pipeline; a handler may pass it on, modify it or stop it
37
+ - **Access**: `'editable'`, `'readonly'`, `'disabled'` and `'disabled-null'` determine whether an element accepts
38
+ input and what it sends in its form's value (its own value, `null`, or nothing); validation applies to what is
39
+ sent. `visibility` determines how it is drawn
40
+ - **Application state**: a cart, the filters of a list view or editor settings built from the same elements, with
41
+ per-element tracked reads, transactions, validation and rules for what is sent
42
+ - **Plain-data views**: `view(group)` reads an element as plain properties (`form.address.city`,
43
+ `v-model="form.name"`), with the element itself as `form.$`
44
+ - **Lists that scale**: a `List` is designed to hold thousands of rows. Writing one field of one row costs that row
45
+ and the depth of its nesting, a `push()` costs one row, and reading `value` or `valid` again with no change in
46
+ between costs nothing
47
+ - **Declared once, bound per record**: a list's item template is declared once; one validator instance and one
48
+ conditional rule serve every row, and each row has its own result
49
+ - **Nested structures**: fields, groups and lists compose recursively, at every level
50
+ - **Extended properties**: a field holds the data your UI renders it with (a label, a hint, a width), declared as a
51
+ second type argument, checked by the compiler and read through `extra`
52
+ - **Serialization**: `Group.createFromFormData()` builds a form from a plain object
53
+ - **TypeScript support**: full type definitions, and a group's value type inferred from the fields it holds,
54
+ nested structures included
50
55
 
51
56
  ## Installation
52
57
 
@@ -54,40 +59,12 @@ mechanism applies at every level of a nested form.
54
59
  npm install @dynamicforms/vue-forms
55
60
  ```
56
61
 
57
- The package is ESM-only: it ships one build, which imports `lodash-es`, its only runtime dependency. Node 22 or newer
58
- is required, and `vue` (^3.5.2) is the only peer dependency. A CommonJS consumer reaches it through `require()` of an
59
- ES module, which Node supports. Type definitions ship with the build; the stylesheet is
60
- `@dynamicforms/vue-forms/style.css`.
61
-
62
- ## Setup
63
-
64
- The library ships a Vue plugin for its global options:
65
-
66
- ```typescript
67
- import { forms } from '@dynamicforms/vue-forms';
68
-
69
- app.use(forms, { useMarkdownInValidators: false });
70
- ```
71
-
72
- The import must be a named one — the default export is a namespace of the library members, not the plugin.
73
-
74
- `useMarkdownInValidators` defaults to `true`, which means the default messages of the built-in validators are markdown
75
- (`MdString`). Rendering those requires a globally registered `vue-markdown` component; set the option to `false` if you
76
- want plain text instead.
77
-
78
- The configuration is module-global rather than per app, and `getConfig()`, `setConfig()` and the `FormsConfig` type
79
- are exported beside the plugin, so the options can be read and written where there is no app to install a plugin
80
- on.
62
+ The package is ESM-only and requires Node 22 or newer. A CommonJS consumer reaches it through `require()` of an ES
63
+ module, which Node supports without a flag from 22.12; on 22.0–22.11 it needs `--experimental-require-module`. Type definitions ship with the build. The package ships no components and no styles.
81
64
 
82
65
  ## Basic Usage Example
83
66
 
84
- Every form element is created with the constructor: `new Field({ ... })`, `new Action({ ... })`,
85
- `new Group({ ... })`, `new List(template)`. Every read through it is tracked from that moment on, so reading
86
- `field.value` in a template tracks it and `field.value = x` re-renders — there is no `ref` to unwrap and no
87
- computed mirror to maintain. The element itself is not a Vue proxy, so watch what you read:
88
- `watch(() => field.value, cb)`.
89
-
90
- Here's a simple example of how to create and use a form with fields and groups:
67
+ Creating and using a form with fields and groups:
91
68
 
92
69
  ```typescript
93
70
  import { Field, Group } from '@dynamicforms/vue-forms';
@@ -113,61 +90,78 @@ personForm.fields.age.access = 'disabled';
113
90
  console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
114
91
  ```
115
92
 
116
- An `Action` is a field whose value is a label / icon pair and which can be executed:
93
+ Every element is created with its constructor: `new Field({ ... })`, `new Action({ ... })`, `new Group({ ... })`,
94
+ `new List(template)`. Every read through it is tracked, so reading `field.value` in a template tracks it and
95
+ `field.value = x` re-renders. The element itself is not a Vue proxy, so `watch()` takes a getter of the member read:
96
+ `watch(() => field.value, cb)`.
97
+
98
+ An `Action` is a command: an element with a label and an icon that can be executed and sends nothing to its
99
+ container's value. `SubmitAction` and `RejectAction` submit and reset a form, and `form.confirm()` / `form.reject()`
100
+ execute them:
117
101
 
118
102
  ```typescript
119
- import { Action, ExecuteAction } from '@dynamicforms/vue-forms';
103
+ import { Action, CommandException, Field, Group, RejectAction, SubmitAction } from '@dynamicforms/vue-forms';
120
104
 
121
- const saveAction = new Action({
122
- value: { label: 'Save' },
123
- actions: [new ExecuteAction((field, supr, params) => { console.log('saving', params); })]
105
+ const form = new Group({
106
+ name: new Field({ value: 'John' }),
107
+ save: new Action({
108
+ value: { label: 'Save', defaultConfirm: true },
109
+ actions: [new SubmitAction((action) => action.parent, (value) => api.save(value))],
110
+ }),
111
+ cancel: new Action({
112
+ value: { label: 'Cancel', defaultReject: true },
113
+ actions: [new RejectAction((action) => action.parent)],
114
+ }),
124
115
  });
125
116
 
126
- await saveAction.execute({ form: personForm }); // 'saving { form: ... }'; saveAction.busy until it settles
117
+ form.value; // { name: 'John' }: actions send nothing
118
+ form.fields.save.executable; // false while the form is invalid or a submit is running
119
+
120
+ // waits for validation, sends a valid form and rebinds the form to the result
121
+ const result = await form.confirm();
122
+ if (result instanceof CommandException) {
123
+ // the submit was refused or failed
124
+ } else if (result !== undefined) {
125
+ const { sent, received } = result;
126
+ }
127
127
  ```
128
128
 
129
- `Action` is one of the few members that speak about the interface, listed with their reasons in
130
- [Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale#what-the-library-carries-for-the-interface):
131
- it names a label and an icon because it exists as the element a form's submit and cancel hang on, and that minimal
132
- pair is what makes the concept legible. The shape is
133
- minimal because a UI library is expected to extend it — [`@dynamicforms/vuetify-inputs`](https://docs.velis.si/dynamicforms/vuetify-inputs/examples/df-actions.html)
134
- widens the value with render options and per-breakpoint variants on top of it.
129
+ `confirm()` returns `undefined` where it executes nothing, such as while `save` is not executable. The cookbook's
130
+ [Submitting](https://docs.velis.si/dynamicforms/vue-forms/guide/cookbook#submitting) recipe describes every outcome.
131
+
132
+ An `Action` carries a label and an icon because a form's submit and cancel are drawn as buttons, and it is one of
133
+ the few members that relate to the interface. A UI library extends its value:
134
+ [`@dynamicforms/vuetify-inputs`](https://docs.velis.si/dynamicforms/vuetify-inputs/examples/df-actions.html) adds
135
+ render options and per-breakpoint variants.
135
136
 
136
137
  ## Events Example
137
138
 
138
- The library provides a powerful event system for field changes and other actions:
139
+ Field changes and other events are handled by actions:
139
140
 
140
141
  ```typescript
141
- import { Field, Group, ValueChangedAction, ValidationErrorText } from '@dynamicforms/vue-forms';
142
-
143
- const emailField = new Field({ value: '' })
144
- .registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
145
- // Custom validation on value change
146
- if (!newValue.includes('@')) {
147
- field.errors = [new ValidationErrorText('Invalid email format')];
148
- } else {
149
- field.errors = [];
150
- }
151
-
152
- // Always call supr to continue the action chain
153
- return supr(field, newValue, oldValue);
154
- }));
142
+ import { Field, Group, ValueChangedAction } from '@dynamicforms/vue-forms';
155
143
 
156
- // Or register events on a form
157
144
  const form = new Group({
158
- email: emailField,
159
- username: new Field()
160
- }).registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
145
+ email: new Field({ value: '' }),
146
+ username: new Field({ value: '' }),
147
+ });
148
+
149
+ // Fill in the username from the email while the username field is not touched
150
+ form.fields.email.registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
151
+ if (!form.fields.username.touched) form.fields.username.value = newValue.split('@')[0];
152
+ // calling supr passes the event on to the next handler; not calling it stops the chain
153
+ return supr(field, newValue, oldValue);
154
+ }));
155
+
156
+ // Or register events on a form
157
+ form.registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
161
158
  console.log('Form data changed:', newValue);
162
159
  return supr(field, newValue, oldValue);
163
160
  }));
164
161
  ```
165
162
 
166
- Assigning `field.errors` directly replaces the whole array, including errors owned by validators — prefer a
167
- `Validator` (see below) when all you want is to add a validation rule.
168
-
169
- Events are announced when the operation carrying them finishes, over the net change. Wrap several writes in a
170
- `transaction()` and they announce as one:
163
+ Events are announced over the net change when the operation that contains them finishes. Several writes wrapped
164
+ in a `transaction()` are announced as one:
171
165
 
172
166
  ```typescript
173
167
  import { transaction } from '@dynamicforms/vue-forms';
@@ -179,11 +173,11 @@ transaction(() => {
179
173
  });
180
174
  ```
181
175
 
182
- A throw out of the callback rolls the whole transaction back and rethrows, so a form is never left half-applied.
176
+ An exception thrown from the callback rolls back the whole transaction and is rethrown.
183
177
 
184
178
  ## Built-in Validators
185
179
 
186
- The library provides several built-in validators for common validation scenarios:
180
+ Built-in validators for common rules:
187
181
 
188
182
  ```typescript
189
183
  import { Field, Group, Validators } from '@dynamicforms/vue-forms';
@@ -191,7 +185,7 @@ import { Field, Group, Validators } from '@dynamicforms/vue-forms';
191
185
  const validatedForm = new Group({
192
186
  // Required field
193
187
  username: new Field({
194
- validators: [new Validators.Required('Username is required')]
188
+ validators: [new Validators.Required({ code: 'username_required', detail: 'Username is required' })]
195
189
  }),
196
190
 
197
191
  // Email validation with pattern
@@ -199,7 +193,7 @@ const validatedForm = new Group({
199
193
  validators: [
200
194
  new Validators.Pattern(
201
195
  /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
202
- 'Please enter a valid email address'
196
+ { code: 'invalid_email', detail: 'Please enter a valid email address' }
203
197
  )
204
198
  ]
205
199
  }),
@@ -208,7 +202,7 @@ const validatedForm = new Group({
208
202
  age: new Field({
209
203
  value: 25,
210
204
  validators: [
211
- new Validators.ValueInRange(18, 100, 'Age must be between 18 and 100')
205
+ new Validators.ValueInRange(18, 100, { code: 'age_range', detail: 'Age must be between {minValue} and {maxValue}' })
212
206
  ]
213
207
  }),
214
208
 
@@ -222,91 +216,38 @@ const validatedForm = new Group({
222
216
  // Text length validation
223
217
  bio: new Field({
224
218
  validators: [
225
- new Validators.LengthInRange(10, 200, 'Bio must be between 10 and 200 characters')
219
+ new Validators.LengthInRange(10, 200, { code: 'bio_length', detail: 'Bio must be between 10 and 200 characters' })
226
220
  ]
227
221
  })
228
222
  });
229
223
  ```
230
224
 
231
- Validators run eagerly — a field is validated the moment it is created, so a form built from empty required fields is
232
- invalid immediately. Use `field.touched` to decide when to show the errors in the UI. `Required` trims a string
233
- before it measures it, so a value of spaces alone is no value; `new Validators.Required({ trim: false })` keeps the
234
- spaces where they are part of what the field holds.
235
-
236
- Every error a built-in validator produces carries a `code` — `required`, `pattern`, `min-length`, … — so a program
237
- reacting to one particular failure need not match message text that is translated and configurable.
238
-
239
- A validation function may return a `Promise`. `field.validating` is `true` while such a run is pending — on the
240
- field and on every container above it, so a form answers for the whole tree — and the verdict applied to the field
241
- is always the one belonging to the newest run, so a slow check cannot overwrite a faster one started after it. The
242
- function receives an `AbortSignal` as its fourth argument, which aborts the moment the run's verdict stops
243
- counting, so the request behind it can be called off. `form.busy` is the same question with the `Action.execute()`
244
- runs below it included — what a submit button binds to. A validator message given as a `Ref` or `computed` is
245
- resolved when the message is rendered, so a translated message follows a locale switch without revalidating.
246
- `field.clearValidators()` drops the validators, empties the errors and cancels whatever validation is still in
247
- flight.
248
-
249
- ## Messages Widget Component
250
-
251
- The library includes a `messages-widget` Vue component for displaying validation errors and messages:
252
-
253
- ```vue
254
- <template>
255
- <!-- Simple string message -->
256
- <messages-widget
257
- message="This is an error message"
258
- classes="text-error"
259
- />
260
-
261
- <!-- Display field validation errors -->
262
- <messages-widget
263
- v-if="emailField.errors.length > 0"
264
- :message="emailField.errors"
265
- :classes="['text-error', 'mt-2']"
266
- />
267
-
268
- <!-- Markdown message -->
269
- <messages-widget :message="markdownErrors" />
270
- </template>
271
-
272
- <script setup>
273
- import { MessagesWidget, Field, Validators, ValidationErrorRenderContent, MdString } from '@dynamicforms/vue-forms';
274
-
275
- // Example field with validation
276
- const emailField = new Field({
277
- value: '',
278
- validators: [
279
- new Validators.Pattern(
280
- /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
281
- 'Please enter a valid email address'
282
- )
283
- ]
284
- });
225
+ Validators run eagerly: a field is validated when it is created, so a form built from empty required fields is
226
+ invalid immediately. `field.touched` determines when the UI shows the errors. `Required` trims a string before
227
+ measuring it, so a value of only spaces counts as empty; `new Validators.Required({ trim: false })` keeps spaces as
228
+ part of the value.
285
229
 
286
- // Markdown message support (requires VueMarkdown component)
287
- const markdownErrors = [
288
- new ValidationErrorRenderContent(
289
- new MdString('**Error**: This field contains *invalid* data.')
290
- )
291
- ];
292
- </script>
293
- ```
230
+ An error is data: a `code` (`required`, `pattern`, `min_length`, …), the `params` it failed with, and an English
231
+ `detail`. The library does not render it. The application, or a UI library such as `@dynamicforms/vuetify-inputs`,
232
+ converts it into text, translated by its code. A validator's last argument, `{ code, detail }`, replaces the
233
+ validator's default code and English detail.
294
234
 
295
- The messages widget supports:
296
- - **String messages**: Simple text messages
297
- - **ValidationError arrays**: Rich error objects with styling and components
298
- - **Markdown content**: Rich text formatting (requires VueMarkdown component)
299
- - **Custom components**: Render any Vue component as an error message
300
- - **Flexible styling**: Multiple ways to apply CSS classes
235
+ A validation function may return a `Promise`; `field.validating` is `true` while it runs, and only the newest run
236
+ determines the result. [Asynchronous validation](https://docs.velis.si/dynamicforms/vue-forms/api/validators#asynchronous-validation)
237
+ describes cancellation, `busy`, `pending` and `clearValidators()`.
301
238
 
302
239
  ## Conditional Form Behavior
303
240
 
304
- Create dynamic forms with conditional logic using Statements and Operators:
241
+ Conditional logic is declared with `Statement` and `Operator`:
305
242
 
306
243
  ```typescript
307
- import {
308
- Field, Group, Statement, Operator,
309
- ConditionalVisibilityAction, ConditionalAccessAction
244
+ import {
245
+ Field,
246
+ Group,
247
+ Statement,
248
+ Operator,
249
+ ConditionalVisibilityAction,
250
+ ConditionalAccessAction,
310
251
  } from '@dynamicforms/vue-forms';
311
252
 
312
253
  const form = new Group({
@@ -331,7 +272,7 @@ const isPerson = new Statement(form.fields.isCompany, Operator.EQUALS, false);
331
272
 
332
273
  ## Advanced Data Structures (Lists)
333
274
 
334
- Work with array data using the List component:
275
+ `List` holds array data:
335
276
 
336
277
  ```typescript
337
278
  import { Field, Group, List } from '@dynamicforms/vue-forms';
@@ -350,7 +291,7 @@ const contactsList = new List(contactTemplate);
350
291
  contactsList.push({ name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' });
351
292
  contactsList.push({ name: 'Jane Doe', email: 'jane@example.com', phone: '987-654-3210' });
352
293
 
353
- // Access list items: items hands out the rows, get() answers undefined for an invalid index
294
+ // Access list items: items returns the rows, get() returns undefined for an invalid index
354
295
  console.log(contactsList.length); // 2
355
296
  const firstContact = contactsList.get(0)!;
356
297
  console.log(firstContact.fields.name.value); // 'John Doe'
@@ -363,12 +304,12 @@ contactsList.remove(1);
363
304
  ```
364
305
 
365
306
  Every list mutation is tracked, so a `v-for` over `contactsList.items` re-renders on `push()`, `insert()`,
366
- `remove()`, `pop()` and `clear()` without any extra wiring. `items` hands out a frozen array of the live rows,
367
- rebuilt once per change of the set.
307
+ `remove()`, `pop()` and `clear()` without extra wiring. `items` returns a frozen array of the live rows, rebuilt
308
+ once per change of the set of rows.
368
309
 
369
310
  ## TypeScript Support
370
311
 
371
- The library is written in TypeScript and provides full type definitions:
312
+ The library is written in TypeScript and ships full type definitions:
372
313
 
373
314
  ```typescript
374
315
  import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
@@ -414,7 +355,7 @@ const email: string = userForm.fields.email.value;
414
355
  const age: number = userForm.fields.age.value;
415
356
  const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
416
357
 
417
- // The serialized value is typed too, member by member. Every member is optional, because a 'disabled' member is
358
+ // The group's value is typed too, member by member. Every member is optional, because a 'disabled' member is
418
359
  // left out of the object the group builds, and nullable, because a 'disabled-null' one is sent as null
419
360
  const values = userForm.value;
420
361
  const emailFromValue: string | null | undefined = values.email;
@@ -435,13 +376,13 @@ const defaults: IFieldParams<string> = { value: '', access: 'readonly' };
435
376
  const field = new Field(defaults);
436
377
  ```
437
378
 
438
- It admits only the writable members — `value`, `originalValue`, `access`, `visibility`, `touched`, `errors`,
439
- `validators` and `actions`, listed by `IFieldConstructorParams<T>`. Derived members such as `valid` and
379
+ It accepts only the writable members (`value`, `originalValue`, `access`, `visibility`, `touched`, `errors`,
380
+ `validators` and `actions`), listed by `IFieldConstructorParams<T>`. Derived members such as `valid` and
440
381
  `isChanged` are getters, and passing one is a compile error.
441
382
 
442
- `X` is what an element carries beyond those: declare it, and the parameter object takes those properties too,
443
- `extra` reads them back and `setExtendedValues()` writes them. It is how a form built from a server's description
444
- carries the label, hint or css class a UI layer binds to its inputs:
383
+ `X` declares the additional properties an element holds: the parameter object accepts them, `extra` reads them
384
+ and `setExtendedValues()` writes them. A form built from a server's description stores in them the label, hint or
385
+ css class a UI layer binds to its inputs:
445
386
 
446
387
  ```typescript
447
388
  interface Presentation { label: string; hint?: string }
@@ -452,7 +393,7 @@ name.setExtendedValues({ hint: 'as in your passport' });
452
393
  ```
453
394
 
454
395
  `FieldBase<T>` is the abstract base of `Field`, `Action`, `Group` and `List`, and the type to use in your own
455
- signatures whenever you accept any form element:
396
+ signatures that accept any form element:
456
397
 
457
398
  ```typescript
458
399
  import { FieldBase } from '@dynamicforms/vue-forms';
@@ -464,20 +405,16 @@ function isDirty(field: FieldBase): boolean {
464
405
 
465
406
  ## Documentation
466
407
 
467
- For more detailed documentation and examples, check out the [documentation](https://docs.velis.si/dynamicforms/vue-forms).
408
+ Full documentation and examples: [documentation](https://docs.velis.si/dynamicforms/vue-forms).
468
409
 
469
- [The model](https://docs.velis.si/dynamicforms/vue-forms/guide/model) is the whole library in one page: elements,
470
- declarations, transactions, where validity comes from and how a `List` builds its rows.
410
+ [The model](https://docs.velis.si/dynamicforms/vue-forms/guide/model) describes how the library fits together:
411
+ elements, declarations, transactions, where validity comes from and how a `List` builds its rows.
412
+ [Rationale](https://docs.velis.si/dynamicforms/vue-forms/guide/rationale) describes when the library fits, compared
413
+ with form libraries and with Pinia.
471
414
 
472
415
  Upgrading an existing project? The
473
- [migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration) walks the journey from 0.6.1
474
- onwards, silent breaks first, and keeps a section per release for a project crossing one.
475
-
476
- ## Conclusion
477
-
478
- `@dynamicforms/vue-forms` provides a clean, flexible approach to form management in Vue applications. By focusing on
479
- data structures and state management rather than UI components, it offers unparalleled flexibility while maintaining
480
- a simple, intuitive API.
416
+ [migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration) has one section per release, newest
417
+ first, from 3.0.0 back to 0.6.0, and a combined section for upgrading from a release before 0.12.
481
418
 
482
419
  ## License
483
420
 
@@ -1,2 +0,0 @@
1
- .df-messages-widget-markdown>:first-child,.df-messages-widget-markdown>:last-child{margin-top:0!important;margin-bottom:0!important;padding-top:0!important;padding-bottom:0!important;line-height:1.35em!important}
2
- /*$vite$:1*/