@dynamicforms/vue-forms 2.0.3 → 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 +138 -200
- 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
|
|
|
@@ -55,38 +60,11 @@ npm install @dynamicforms/vue-forms
|
|
|
55
60
|
```
|
|
56
61
|
|
|
57
62
|
The package is ESM-only and requires Node 22 or newer. A CommonJS consumer reaches it through `require()` of an ES
|
|
58
|
-
module, which Node supports. Type definitions ship with the build
|
|
59
|
-
`@dynamicforms/vue-forms/style.css`.
|
|
60
|
-
|
|
61
|
-
## Setup
|
|
62
|
-
|
|
63
|
-
The library ships a Vue plugin for its global options:
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
import { forms } from '@dynamicforms/vue-forms';
|
|
67
|
-
|
|
68
|
-
app.use(forms, { useMarkdownInValidators: false });
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
The import must be a named one — the default export is a namespace of the library members, not the plugin.
|
|
72
|
-
|
|
73
|
-
`useMarkdownInValidators` defaults to `true`, which means the default messages of the built-in validators are markdown
|
|
74
|
-
(`MdString`). Rendering those requires a globally registered `vue-markdown` component; set the option to `false` if you
|
|
75
|
-
want plain text instead.
|
|
76
|
-
|
|
77
|
-
The configuration is module-global rather than per app, and `getConfig()`, `setConfig()` and the `FormsConfig` type
|
|
78
|
-
are exported beside the plugin, so the options can be read and written where there is no app to install a plugin
|
|
79
|
-
on.
|
|
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.
|
|
80
64
|
|
|
81
65
|
## Basic Usage Example
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
`new Group({ ... })`, `new List(template)`. Every read through it is tracked from that moment on, so reading
|
|
85
|
-
`field.value` in a template tracks it and `field.value = x` re-renders — there is no `ref` to unwrap and no
|
|
86
|
-
computed mirror to maintain. The element itself is not a Vue proxy, so watch what you read:
|
|
87
|
-
`watch(() => field.value, cb)`.
|
|
88
|
-
|
|
89
|
-
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:
|
|
90
68
|
|
|
91
69
|
```typescript
|
|
92
70
|
import { Field, Group } from '@dynamicforms/vue-forms';
|
|
@@ -112,61 +90,78 @@ personForm.fields.age.access = 'disabled';
|
|
|
112
90
|
console.log(personForm.value); // { firstName: 'Jane', lastName: 'Doe', active: true }
|
|
113
91
|
```
|
|
114
92
|
|
|
115
|
-
|
|
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:
|
|
116
101
|
|
|
117
102
|
```typescript
|
|
118
|
-
import { Action,
|
|
103
|
+
import { Action, CommandException, Field, Group, RejectAction, SubmitAction } from '@dynamicforms/vue-forms';
|
|
119
104
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}),
|
|
123
115
|
});
|
|
124
116
|
|
|
125
|
-
|
|
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
|
+
}
|
|
126
127
|
```
|
|
127
128
|
|
|
128
|
-
`
|
|
129
|
-
[
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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.
|
|
134
136
|
|
|
135
137
|
## Events Example
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
Field changes and other events are handled by actions:
|
|
138
140
|
|
|
139
141
|
```typescript
|
|
140
|
-
import { Field, Group, ValueChangedAction
|
|
141
|
-
|
|
142
|
-
const emailField = new Field({ value: '' })
|
|
143
|
-
.registerAction(new ValueChangedAction((field, supr, newValue, oldValue) => {
|
|
144
|
-
// Custom validation on value change
|
|
145
|
-
if (!newValue.includes('@')) {
|
|
146
|
-
field.errors = [new ValidationErrorText('Invalid email format')];
|
|
147
|
-
} else {
|
|
148
|
-
field.errors = [];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Always call supr to continue the action chain
|
|
152
|
-
return supr(field, newValue, oldValue);
|
|
153
|
-
}));
|
|
142
|
+
import { Field, Group, ValueChangedAction } from '@dynamicforms/vue-forms';
|
|
154
143
|
|
|
155
|
-
// Or register events on a form
|
|
156
144
|
const form = new Group({
|
|
157
|
-
email:
|
|
158
|
-
username: new Field()
|
|
159
|
-
})
|
|
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) => {
|
|
160
158
|
console.log('Form data changed:', newValue);
|
|
161
159
|
return supr(field, newValue, oldValue);
|
|
162
160
|
}));
|
|
163
161
|
```
|
|
164
162
|
|
|
165
|
-
|
|
166
|
-
`
|
|
167
|
-
|
|
168
|
-
Events are announced when the operation carrying them finishes, over the net change. Wrap several writes in a
|
|
169
|
-
`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:
|
|
170
165
|
|
|
171
166
|
```typescript
|
|
172
167
|
import { transaction } from '@dynamicforms/vue-forms';
|
|
@@ -178,11 +173,11 @@ transaction(() => {
|
|
|
178
173
|
});
|
|
179
174
|
```
|
|
180
175
|
|
|
181
|
-
|
|
176
|
+
An exception thrown from the callback rolls back the whole transaction and is rethrown.
|
|
182
177
|
|
|
183
178
|
## Built-in Validators
|
|
184
179
|
|
|
185
|
-
|
|
180
|
+
Built-in validators for common rules:
|
|
186
181
|
|
|
187
182
|
```typescript
|
|
188
183
|
import { Field, Group, Validators } from '@dynamicforms/vue-forms';
|
|
@@ -190,7 +185,7 @@ import { Field, Group, Validators } from '@dynamicforms/vue-forms';
|
|
|
190
185
|
const validatedForm = new Group({
|
|
191
186
|
// Required field
|
|
192
187
|
username: new Field({
|
|
193
|
-
validators: [new Validators.Required('Username is required')]
|
|
188
|
+
validators: [new Validators.Required({ code: 'username_required', detail: 'Username is required' })]
|
|
194
189
|
}),
|
|
195
190
|
|
|
196
191
|
// Email validation with pattern
|
|
@@ -198,7 +193,7 @@ const validatedForm = new Group({
|
|
|
198
193
|
validators: [
|
|
199
194
|
new Validators.Pattern(
|
|
200
195
|
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
201
|
-
'Please enter a valid email address'
|
|
196
|
+
{ code: 'invalid_email', detail: 'Please enter a valid email address' }
|
|
202
197
|
)
|
|
203
198
|
]
|
|
204
199
|
}),
|
|
@@ -207,7 +202,7 @@ const validatedForm = new Group({
|
|
|
207
202
|
age: new Field({
|
|
208
203
|
value: 25,
|
|
209
204
|
validators: [
|
|
210
|
-
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}' })
|
|
211
206
|
]
|
|
212
207
|
}),
|
|
213
208
|
|
|
@@ -221,91 +216,38 @@ const validatedForm = new Group({
|
|
|
221
216
|
// Text length validation
|
|
222
217
|
bio: new Field({
|
|
223
218
|
validators: [
|
|
224
|
-
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' })
|
|
225
220
|
]
|
|
226
221
|
})
|
|
227
222
|
});
|
|
228
223
|
```
|
|
229
224
|
|
|
230
|
-
Validators run eagerly
|
|
231
|
-
invalid immediately.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
Every error a built-in validator produces carries a `code` — `required`, `pattern`, `min-length`, … — so a program
|
|
236
|
-
reacting to one particular failure need not match message text that is translated and configurable.
|
|
237
|
-
|
|
238
|
-
A validation function may return a `Promise`. `field.validating` is `true` while such a run is pending — on the
|
|
239
|
-
field and on every container above it, so a form answers for the whole tree — and the verdict applied to the field
|
|
240
|
-
is always the one belonging to the newest run, so a slow check cannot overwrite a faster one started after it. The
|
|
241
|
-
function receives an `AbortSignal` as its fourth argument, which aborts the moment the run's verdict stops
|
|
242
|
-
counting, so the request behind it can be called off. `form.busy` is the same question with the `Action.execute()`
|
|
243
|
-
runs below it included — what a submit button binds to. A validator message given as a `Ref` or `computed` is
|
|
244
|
-
resolved when the message is rendered, so a translated message follows a locale switch without revalidating.
|
|
245
|
-
`field.clearValidators()` drops the validators, empties the errors and cancels whatever validation is still in
|
|
246
|
-
flight.
|
|
247
|
-
|
|
248
|
-
## Messages Widget Component
|
|
249
|
-
|
|
250
|
-
The library includes a `messages-widget` Vue component for displaying validation errors and messages:
|
|
251
|
-
|
|
252
|
-
```vue
|
|
253
|
-
<template>
|
|
254
|
-
<!-- Simple string message -->
|
|
255
|
-
<messages-widget
|
|
256
|
-
message="This is an error message"
|
|
257
|
-
classes="text-error"
|
|
258
|
-
/>
|
|
259
|
-
|
|
260
|
-
<!-- Display field validation errors -->
|
|
261
|
-
<messages-widget
|
|
262
|
-
v-if="emailField.errors.length > 0"
|
|
263
|
-
:message="emailField.errors"
|
|
264
|
-
:classes="['text-error', 'mt-2']"
|
|
265
|
-
/>
|
|
266
|
-
|
|
267
|
-
<!-- Markdown message -->
|
|
268
|
-
<messages-widget :message="markdownErrors" />
|
|
269
|
-
</template>
|
|
270
|
-
|
|
271
|
-
<script setup>
|
|
272
|
-
import { MessagesWidget, Field, Validators, ValidationErrorRenderContent, MdString } from '@dynamicforms/vue-forms';
|
|
273
|
-
|
|
274
|
-
// Example field with validation
|
|
275
|
-
const emailField = new Field({
|
|
276
|
-
value: '',
|
|
277
|
-
validators: [
|
|
278
|
-
new Validators.Pattern(
|
|
279
|
-
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
|
|
280
|
-
'Please enter a valid email address'
|
|
281
|
-
)
|
|
282
|
-
]
|
|
283
|
-
});
|
|
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.
|
|
284
229
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
290
|
-
];
|
|
291
|
-
</script>
|
|
292
|
-
```
|
|
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.
|
|
293
234
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
- **Markdown content**: Rich text formatting (requires VueMarkdown component)
|
|
298
|
-
- **Custom components**: Render any Vue component as an error message
|
|
299
|
-
- **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()`.
|
|
300
238
|
|
|
301
239
|
## Conditional Form Behavior
|
|
302
240
|
|
|
303
|
-
|
|
241
|
+
Conditional logic is declared with `Statement` and `Operator`:
|
|
304
242
|
|
|
305
243
|
```typescript
|
|
306
|
-
import {
|
|
307
|
-
Field,
|
|
308
|
-
|
|
244
|
+
import {
|
|
245
|
+
Field,
|
|
246
|
+
Group,
|
|
247
|
+
Statement,
|
|
248
|
+
Operator,
|
|
249
|
+
ConditionalVisibilityAction,
|
|
250
|
+
ConditionalAccessAction,
|
|
309
251
|
} from '@dynamicforms/vue-forms';
|
|
310
252
|
|
|
311
253
|
const form = new Group({
|
|
@@ -330,7 +272,7 @@ const isPerson = new Statement(form.fields.isCompany, Operator.EQUALS, false);
|
|
|
330
272
|
|
|
331
273
|
## Advanced Data Structures (Lists)
|
|
332
274
|
|
|
333
|
-
|
|
275
|
+
`List` holds array data:
|
|
334
276
|
|
|
335
277
|
```typescript
|
|
336
278
|
import { Field, Group, List } from '@dynamicforms/vue-forms';
|
|
@@ -349,7 +291,7 @@ const contactsList = new List(contactTemplate);
|
|
|
349
291
|
contactsList.push({ name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' });
|
|
350
292
|
contactsList.push({ name: 'Jane Doe', email: 'jane@example.com', phone: '987-654-3210' });
|
|
351
293
|
|
|
352
|
-
// Access list items: items
|
|
294
|
+
// Access list items: items returns the rows, get() returns undefined for an invalid index
|
|
353
295
|
console.log(contactsList.length); // 2
|
|
354
296
|
const firstContact = contactsList.get(0)!;
|
|
355
297
|
console.log(firstContact.fields.name.value); // 'John Doe'
|
|
@@ -362,12 +304,12 @@ contactsList.remove(1);
|
|
|
362
304
|
```
|
|
363
305
|
|
|
364
306
|
Every list mutation is tracked, so a `v-for` over `contactsList.items` re-renders on `push()`, `insert()`,
|
|
365
|
-
`remove()`, `pop()` and `clear()` without
|
|
366
|
-
|
|
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.
|
|
367
309
|
|
|
368
310
|
## TypeScript Support
|
|
369
311
|
|
|
370
|
-
The library is written in TypeScript and
|
|
312
|
+
The library is written in TypeScript and ships full type definitions:
|
|
371
313
|
|
|
372
314
|
```typescript
|
|
373
315
|
import { Field, Group, GenericFieldsInterface } from '@dynamicforms/vue-forms';
|
|
@@ -413,7 +355,7 @@ const email: string = userForm.fields.email.value;
|
|
|
413
355
|
const age: number = userForm.fields.age.value;
|
|
414
356
|
const darkMode: boolean = userForm.fields.preferences.fields.darkMode.value;
|
|
415
357
|
|
|
416
|
-
// The
|
|
358
|
+
// The group's value is typed too, member by member. Every member is optional, because a 'disabled' member is
|
|
417
359
|
// left out of the object the group builds, and nullable, because a 'disabled-null' one is sent as null
|
|
418
360
|
const values = userForm.value;
|
|
419
361
|
const emailFromValue: string | null | undefined = values.email;
|
|
@@ -434,13 +376,13 @@ const defaults: IFieldParams<string> = { value: '', access: 'readonly' };
|
|
|
434
376
|
const field = new Field(defaults);
|
|
435
377
|
```
|
|
436
378
|
|
|
437
|
-
It
|
|
438
|
-
`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
|
|
439
381
|
`isChanged` are getters, and passing one is a compile error.
|
|
440
382
|
|
|
441
|
-
`X`
|
|
442
|
-
|
|
443
|
-
|
|
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:
|
|
444
386
|
|
|
445
387
|
```typescript
|
|
446
388
|
interface Presentation { label: string; hint?: string }
|
|
@@ -451,7 +393,7 @@ name.setExtendedValues({ hint: 'as in your passport' });
|
|
|
451
393
|
```
|
|
452
394
|
|
|
453
395
|
`FieldBase<T>` is the abstract base of `Field`, `Action`, `Group` and `List`, and the type to use in your own
|
|
454
|
-
signatures
|
|
396
|
+
signatures that accept any form element:
|
|
455
397
|
|
|
456
398
|
```typescript
|
|
457
399
|
import { FieldBase } from '@dynamicforms/vue-forms';
|
|
@@ -463,20 +405,16 @@ function isDirty(field: FieldBase): boolean {
|
|
|
463
405
|
|
|
464
406
|
## Documentation
|
|
465
407
|
|
|
466
|
-
|
|
408
|
+
Full documentation and examples: [documentation](https://docs.velis.si/dynamicforms/vue-forms).
|
|
467
409
|
|
|
468
|
-
[The model](https://docs.velis.si/dynamicforms/vue-forms/guide/model)
|
|
469
|
-
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.
|
|
470
414
|
|
|
471
415
|
Upgrading an existing project? The
|
|
472
|
-
[migration guide](https://docs.velis.si/dynamicforms/vue-forms/guide/migration)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
## Conclusion
|
|
476
|
-
|
|
477
|
-
`@dynamicforms/vue-forms` provides a clean, flexible approach to form management in Vue applications. By focusing on
|
|
478
|
-
data structures and state management rather than UI components, it offers unparalleled flexibility while maintaining
|
|
479
|
-
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.
|
|
480
418
|
|
|
481
419
|
## License
|
|
482
420
|
|