@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/dist/chunks/plugin-uXEQYne6.js +271 -0
- package/dist/chunks/plugin-uXEQYne6.js.map +1 -0
- package/dist/chunks/registry-_vVZW3p9.js +105 -0
- package/dist/chunks/registry-_vVZW3p9.js.map +1 -0
- package/dist/dynamicforms-vue-forms.js +1320 -938
- package/dist/dynamicforms-vue-forms.js.map +1 -1
- package/dist/index.d.ts +1343 -906
- package/dist/plugins/tracking.d.ts +61 -0
- package/dist/plugins/tracking.js +279 -0
- package/dist/plugins/tracking.js.map +1 -0
- package/package.json +12 -9
- package/readme.md +139 -202
- package/dist/dynamicforms-vue-forms.css +0 -2
package/readme.md
CHANGED
|
@@ -1,52 +1,57 @@
|
|
|
1
1
|
# @dynamicforms/vue-forms
|
|
2
2
|
|
|
3
|
-
A reactive
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
11
|
-
as conditions over other fields, and every change
|
|
12
|
-
whether to pass the event on and may
|
|
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**:
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
- **
|
|
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
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
- **
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
|
|
46
|
-
- **
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
103
|
+
import { Action, CommandException, Field, Group, RejectAction, SubmitAction } from '@dynamicforms/vue-forms';
|
|
120
104
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
130
|
-
[
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
+
Field changes and other events are handled by actions:
|
|
139
140
|
|
|
140
141
|
```typescript
|
|
141
|
-
import { Field, Group, ValueChangedAction
|
|
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:
|
|
159
|
-
username: new Field()
|
|
160
|
-
})
|
|
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
|
-
|
|
167
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
232
|
-
invalid immediately.
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
241
|
+
Conditional logic is declared with `Statement` and `Operator`:
|
|
305
242
|
|
|
306
243
|
```typescript
|
|
307
|
-
import {
|
|
308
|
-
Field,
|
|
309
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
367
|
-
|
|
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
|
|
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
|
|
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
|
|
439
|
-
`validators` and `actions
|
|
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`
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
474
|
-
|
|
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
|
|