adaptform 2.0.5 → 2.2.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/CHANGELOG.md +151 -2
- package/CHANGELOG.ru.md +200 -0
- package/LICENSE.md +3 -1
- package/LICENSE.ru.md +25 -0
- package/README.md +299 -297
- package/README.ru.md +422 -0
- package/dist/Form-uHUaTUZS.d.cts +387 -0
- package/dist/Form-uHUaTUZS.d.ts +387 -0
- package/dist/index.cjs +864 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.mjs +863 -284
- package/dist/index.mjs.map +1 -1
- package/dist/luxon.d.cts +1 -1
- package/dist/luxon.d.ts +1 -1
- package/package.json +28 -9
- package/dist/Form-Be3I6LX0.d.cts +0 -293
- package/dist/Form-Be3I6LX0.d.ts +0 -293
package/README.md
CHANGED
|
@@ -1,420 +1,422 @@
|
|
|
1
1
|
# AdaptForm
|
|
2
2
|
|
|
3
|
+
[English](README.md) · [Русский](README.ru.md)
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/adaptform)
|
|
4
6
|
[](https://www.npmjs.com/package/adaptform)
|
|
5
|
-
[](https://bundlephobia.com/package/adaptform)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[](https://www.typescriptlang.org/)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Instead of describing validation schemas, you describe forms as classes with typed fields. Each field is responsible for
|
|
12
|
-
its own state lifecycle, validation, transformation pipeline, and serialization logic.
|
|
10
|
+
**[Open the interactive demo](https://arhimen.github.io/AdaptForm/)** · **[Read the changelog](https://github.com/ArhiMEN/AdaptForm/blob/master/CHANGELOG.md)** · **[Browse the demo source](https://github.com/ArhiMEN/AdaptForm/tree/master/demo)**
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
Maintainers: [automatic npm publication setup](docs/NPM_PUBLISH.md).
|
|
15
13
|
|
|
16
|
-
-
|
|
17
|
-
- Field-level validation
|
|
18
|
-
- Cross-field validation
|
|
19
|
-
- Input transformation plugins
|
|
20
|
-
- Nested forms and arrays
|
|
21
|
-
- Automatic TypeScript inference
|
|
22
|
-
- Framework-agnostic integration
|
|
14
|
+
AdaptForm is a TypeScript-first, framework-independent, class-based headless form engine inspired by Django Forms and Pydantic. Forms and fields manage values, casting, validation, errors, baselines, subscriptions and observable state without requiring UI framework components.
|
|
23
15
|
|
|
24
16
|
## Features
|
|
25
17
|
|
|
26
|
-
-
|
|
27
|
-
- Field-level
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Most form libraries treat forms as validation schemas:
|
|
40
|
-
|
|
41
|
-
```TypeScript
|
|
42
|
-
const schema = z.object({
|
|
43
|
-
login: z.string(),
|
|
44
|
-
password: z.string(),
|
|
45
|
-
})
|
|
46
|
-
````
|
|
18
|
+
- Class-based form models with inferred TypeScript value types.
|
|
19
|
+
- Field-level and cross-field validation.
|
|
20
|
+
- Conditional required rules and custom synchronous validators.
|
|
21
|
+
- String, number, decimal, boolean, and JavaScript Date field types.
|
|
22
|
+
- Dynamic arrays of fields and arrays of nested forms.
|
|
23
|
+
- Input transformation plugins, including `MaskPlugin`.
|
|
24
|
+
- Server-error mapping with `ErrorsSchema`.
|
|
25
|
+
- JSON and `FormData` serialization.
|
|
26
|
+
- Optional Luxon integration through the `adaptform/luxon` entry point.
|
|
27
|
+
- Framework-agnostic core with no required runtime dependencies.
|
|
28
|
+
- Clean-value baselines with `isDirty` and `commitBaseline()` on fields, forms and collections.
|
|
29
|
+
- Stable immutable `getSnapshot()` data, `subscribe()` and unsubscribe for reactive UIs.
|
|
30
|
+
- Synchronous nested `Form.batch()` and propagation of nested form/collection updates.
|
|
47
31
|
|
|
48
|
-
|
|
32
|
+
## Installation
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
class LoginForm extends Form {
|
|
52
|
-
login = new Field(StringType)
|
|
53
|
-
password = new Field(StringType)
|
|
54
|
-
}
|
|
55
|
-
```
|
|
34
|
+
Install the published npm package:
|
|
56
35
|
|
|
57
|
-
|
|
36
|
+
```bash
|
|
37
|
+
npm install adaptform
|
|
38
|
+
```
|
|
58
39
|
|
|
59
|
-
|
|
60
|
-
- validation
|
|
61
|
-
- errors
|
|
62
|
-
- transformation
|
|
63
|
-
- plugins
|
|
64
|
-
- type casting
|
|
40
|
+
Luxon integration is optional:
|
|
65
41
|
|
|
66
|
-
|
|
42
|
+
```bash
|
|
43
|
+
npm install luxon
|
|
44
|
+
```
|
|
67
45
|
|
|
68
|
-
|
|
46
|
+
The [interactive demo](https://arhimen.github.io/AdaptForm/) runs from repository source, with automatic deployment from `v2.1.0` and `master`. **Headless APIs in the 2.2.0 source candidate are not yet published to npm.** Check the [npm package](https://www.npmjs.com/package/adaptform) for available published versions.
|
|
69
47
|
|
|
70
|
-
|
|
48
|
+
The demo supports English and Russian with a persistent language selector: [English](https://arhimen.github.io/AdaptForm/?lang=en) · [Русский](https://arhimen.github.io/AdaptForm/?lang=ru). Switching languages preserves form data and validation state.
|
|
71
49
|
|
|
72
|
-
|
|
50
|
+
## Quick start
|
|
73
51
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
- error store
|
|
77
|
-
- input masks
|
|
78
|
-
- submission mapping
|
|
52
|
+
```typescript
|
|
53
|
+
import { BooleanType, Field, Form, StringType } from 'adaptform'
|
|
79
54
|
|
|
80
|
-
|
|
55
|
+
class RegistrationForm extends Form {
|
|
56
|
+
name = new Field(StringType, null, {
|
|
57
|
+
isRequired: true,
|
|
58
|
+
minLength: 2,
|
|
59
|
+
maxLength: 50,
|
|
60
|
+
})
|
|
81
61
|
|
|
82
|
-
|
|
62
|
+
email = new Field(StringType, null, {
|
|
63
|
+
isRequired: true,
|
|
64
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
65
|
+
})
|
|
83
66
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
67
|
+
acceptedTerms = new Field(BooleanType, false, {
|
|
68
|
+
mustBeTrue: true,
|
|
69
|
+
})
|
|
87
70
|
}
|
|
88
|
-
````
|
|
89
71
|
|
|
90
|
-
|
|
72
|
+
const form = new RegistrationForm()
|
|
73
|
+
form.fields = {
|
|
74
|
+
name: 'Alex Morgan',
|
|
75
|
+
email: 'alex@example.com',
|
|
76
|
+
acceptedTerms: true,
|
|
77
|
+
}
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
if (form.isValid) {
|
|
80
|
+
console.log(form.fieldsValue)
|
|
81
|
+
console.log(form.toJSON())
|
|
82
|
+
} else {
|
|
83
|
+
console.log(form.allErrors)
|
|
84
|
+
}
|
|
94
85
|
```
|
|
95
86
|
|
|
96
|
-
|
|
87
|
+
`form.fields` assigns the supplied values and validates the completed form. Values for fields omitted from the assignment are left unchanged.
|
|
97
88
|
|
|
98
|
-
|
|
99
|
-
npm install luxon
|
|
100
|
-
```
|
|
89
|
+
## Field types and options
|
|
101
90
|
|
|
102
|
-
|
|
91
|
+
All field types are constructed using `new Field(TypeConstructor, defaultValue?, options?)`.
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
import {Form, Field, StringType, NumberType} from 'adaptform';
|
|
93
|
+
### StringType
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
```typescript
|
|
96
|
+
const username = new Field(StringType, null, {
|
|
97
|
+
isRequired: true,
|
|
98
|
+
minLength: 3,
|
|
99
|
+
maxLength: 32,
|
|
100
|
+
pattern: /^[a-z0-9_]+$/i,
|
|
101
|
+
messages: {
|
|
102
|
+
isRequired: 'Enter a username.',
|
|
103
|
+
minLength: 'Use at least 3 characters.',
|
|
104
|
+
pattern: 'Use only letters, numbers, and underscores.',
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
```
|
|
112
108
|
|
|
113
|
-
|
|
109
|
+
Supported constraints: `isRequired`, `minLength`, `maxLength`, and `pattern`.
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
login: 'john_doe',
|
|
117
|
-
password: 'securepass123'
|
|
118
|
-
};
|
|
111
|
+
### NumberType
|
|
119
112
|
|
|
120
|
-
|
|
121
|
-
|
|
113
|
+
```typescript
|
|
114
|
+
const age = new Field(NumberType, null, {
|
|
115
|
+
isRequired: true,
|
|
116
|
+
ge: 18,
|
|
117
|
+
le: 120,
|
|
118
|
+
})
|
|
122
119
|
```
|
|
123
120
|
|
|
124
|
-
|
|
121
|
+
Supported constraints: `gt`, `ge`, `lt`, and `le`. Values must be finite integers; blank strings and non-finite numbers are rejected.
|
|
125
122
|
|
|
126
|
-
|
|
123
|
+
### DecimalType
|
|
127
124
|
|
|
128
|
-
```
|
|
129
|
-
|
|
125
|
+
```typescript
|
|
126
|
+
const price = new Field(DecimalType, null, {
|
|
127
|
+
decimalPlaces: 2,
|
|
128
|
+
maxDigits: 8,
|
|
129
|
+
ge: 0,
|
|
130
|
+
})
|
|
131
|
+
```
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
const name = new Field(StringType, null, {
|
|
133
|
-
minLength: 2,
|
|
134
|
-
maxLength: 50,
|
|
135
|
-
pattern: /^[a-zA-Z]+$/
|
|
136
|
-
});
|
|
133
|
+
Supports `gt`, `ge`, `lt`, `le`, `decimalPlaces`, and `maxDigits`. Numeric inputs with excess precision are rejected rather than silently rounded. String inputs may use a comma or a period as the decimal separator. Incomplete values such as `"123."` retain their editable text but fail explicit validation; call `Form.checkValid()` before submitting. `toJSON()` does not validate automatically.
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
const age = new Field(NumberType, null, {
|
|
140
|
-
gt: 0, // greater than
|
|
141
|
-
lt: 120 // less than
|
|
142
|
-
});
|
|
135
|
+
### BooleanType
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
const
|
|
137
|
+
```typescript
|
|
138
|
+
const accepted = new Field(BooleanType, false, {
|
|
146
139
|
mustBeTrue: true,
|
|
147
|
-
|
|
148
|
-
mustBeTrue: 'You must agree to the terms'
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Custom Validation
|
|
154
|
-
|
|
155
|
-
```TypeScript
|
|
156
|
-
const email = new Field(StringType, null, {
|
|
157
|
-
validate: (value) => {
|
|
158
|
-
return value.includes('@') || 'Invalid email format';
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// Async validation with form context
|
|
163
|
-
const passwordConfirmation = new Field(StringType, null, {
|
|
164
|
-
validate: (value, form) => {
|
|
165
|
-
return value === form?.password?.valueClear || 'Passwords do not match';
|
|
166
|
-
}
|
|
167
|
-
});
|
|
140
|
+
})
|
|
168
141
|
```
|
|
169
142
|
|
|
170
|
-
|
|
143
|
+
Supports `mustBeTrue` and `mustBeFalse`.
|
|
171
144
|
|
|
172
|
-
|
|
173
|
-
class SignUpForm extends Form {
|
|
174
|
-
country = new Field(StringType, null);
|
|
145
|
+
### DateType
|
|
175
146
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
147
|
+
```typescript
|
|
148
|
+
const birthday = new Field(DateType, null, {
|
|
149
|
+
isDatetime: false,
|
|
150
|
+
minDate: new Date('1900-01-01T00:00:00.000Z'),
|
|
151
|
+
maxDate: new Date(),
|
|
152
|
+
})
|
|
183
153
|
```
|
|
184
154
|
|
|
185
|
-
|
|
155
|
+
Supports `minDate`, `maxDate`, and `isDatetime`. When `isDatetime` is false, comparisons use calendar dates; when true, timestamps are compared. Invalid dates are rejected.
|
|
186
156
|
|
|
187
|
-
|
|
188
|
-
import {Field, StringType, MaskPlugin} from 'adaptform';
|
|
157
|
+
### Custom validation and messages
|
|
189
158
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
digitPattern: /\d/
|
|
196
|
-
})
|
|
197
|
-
]
|
|
198
|
-
});
|
|
159
|
+
Field options support `isRequired`, `messages`, and a synchronous `validate(value, form)` callback. A custom validator returns `true` for success, `false` for the configured validation message, or a string for a specific error.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
class PasswordForm extends Form {
|
|
163
|
+
password = new Field(StringType, null, { minLength: 8 })
|
|
199
164
|
|
|
200
|
-
|
|
201
|
-
|
|
165
|
+
confirmation = new Field(StringType, null, {
|
|
166
|
+
isRequired: true,
|
|
167
|
+
validate: (value, form) =>
|
|
168
|
+
value === form?.password.valueClear || 'Passwords do not match.',
|
|
169
|
+
})
|
|
170
|
+
}
|
|
202
171
|
```
|
|
203
172
|
|
|
204
|
-
|
|
173
|
+
Validators are synchronous; returning a Promise is not supported. For asynchronous checks such as server-side uniqueness, call the service separately and apply the response through the form's error API.
|
|
205
174
|
|
|
206
|
-
|
|
207
|
-
import {ArrayField, Field, NumberType} from 'adaptform';
|
|
175
|
+
## Validation lifecycle and state
|
|
208
176
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
177
|
+
- Constructor defaults begin untouched.
|
|
178
|
+
- Empty strings and whitespace-only strings count as empty values.
|
|
179
|
+
- Global and sticky regular-expression flags do not make repeated string validation stateful.
|
|
180
|
+
- `form.checkValid()` validates every field and returns `false` when a field is invalid or global server errors remain.
|
|
181
|
+
- Reading `isValid` does not run validators. Fields without a non-null default start with `isValid === null`; non-null constructor defaults pass through casting and validation but still start untouched. After `reset()`, field validity is unset until validation runs again.
|
|
182
|
+
- Bulk assignment validates after direct field values have been assigned, so conditional-required rules can inspect the complete form state.
|
|
183
|
+
- If you change dependent fields one at a time, call `form.checkValid()` after the changes to recompute cross-field rules.
|
|
184
|
+
- `field.setRawValue(value, false)` and `form.setFields(data, false)` allow advanced callers to defer validation. Call `checkValid()` before relying on the validity state.
|
|
185
|
+
- `reset()` restores defaults, clears errors, and leaves validity unset until the next validation. Collection validity is false after reset until the collection is checked again.
|
|
186
|
+
- Adding, removing, or clearing collection items refreshes collection validation outside `Form.batch()`; inside a batch automatic validation is deferred. Invalid removal indices are ignored.
|
|
213
187
|
|
|
214
|
-
|
|
215
|
-
gameIds.add(4);
|
|
216
|
-
console.log(gameIds.itemsValue); // [1, 2, 3, 4]
|
|
217
|
-
```
|
|
188
|
+
## Dynamic collections
|
|
218
189
|
|
|
219
|
-
|
|
190
|
+
### ArrayField
|
|
220
191
|
|
|
221
|
-
|
|
222
|
-
npm install luxon
|
|
223
|
-
```
|
|
192
|
+
Use `ArrayField` for a dynamic list of similarly typed fields.
|
|
224
193
|
|
|
225
|
-
```
|
|
226
|
-
import {
|
|
227
|
-
import {DateTime} from 'luxon';
|
|
194
|
+
```typescript
|
|
195
|
+
import { ArrayField, Field, StringType } from 'adaptform'
|
|
228
196
|
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
maxDate: 'Date cannot be in the future'
|
|
237
|
-
}
|
|
238
|
-
});
|
|
197
|
+
const tags = new ArrayField(
|
|
198
|
+
(value?: string) => new Field(StringType, value, {
|
|
199
|
+
isRequired: true,
|
|
200
|
+
minLength: 2,
|
|
201
|
+
}),
|
|
202
|
+
{ minItems: 1, maxItems: 5 },
|
|
203
|
+
)
|
|
239
204
|
|
|
240
|
-
|
|
241
|
-
|
|
205
|
+
tags.add('typescript')
|
|
206
|
+
tags.add('forms')
|
|
207
|
+
console.log(tags.itemsValue)
|
|
208
|
+
console.log(tags.isValid)
|
|
242
209
|
```
|
|
243
210
|
|
|
244
|
-
|
|
211
|
+
Options include `isRequired`, `minItems`, `maxItems`, `messages`, and a synchronous `validate(values, form)` callback. `itemsValue` returns an array whose entries may be `null`.
|
|
245
212
|
|
|
246
|
-
|
|
247
|
-
class ContactForm extends Form {
|
|
248
|
-
name = new Field(StringType, null, {maxLength: 100});
|
|
249
|
-
email = new Field(StringType, null, {
|
|
250
|
-
pattern: /^[^@]+@[^@.]+\.[^@.]+$/
|
|
251
|
-
});
|
|
252
|
-
message = new Field(StringType, null, {maxLength: 1000});
|
|
253
|
-
}
|
|
213
|
+
### ArrayFormField
|
|
254
214
|
|
|
255
|
-
|
|
256
|
-
form.fields = {
|
|
257
|
-
name: 'John',
|
|
258
|
-
email: 'john@example.com',
|
|
259
|
-
message: 'Hello!'
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
if (form.checkValid()) {
|
|
263
|
-
// Submit form
|
|
264
|
-
const formData = form.toFormData();
|
|
265
|
-
const json = form.toJSON();
|
|
266
|
-
|
|
267
|
-
await fetch('/api/contact', {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
body: formData
|
|
270
|
-
});
|
|
271
|
-
} else {
|
|
272
|
-
console.log(form.allErrors);
|
|
273
|
-
// {
|
|
274
|
-
// name: [],
|
|
275
|
-
// email: [],
|
|
276
|
-
// message: []
|
|
277
|
-
// }
|
|
278
|
-
}
|
|
279
|
-
```
|
|
215
|
+
Use `ArrayFormField` for a list of nested form instances.
|
|
280
216
|
|
|
281
|
-
|
|
217
|
+
```typescript
|
|
218
|
+
import { ArrayFormField, Field, Form, StringType } from 'adaptform'
|
|
282
219
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
password = new Field(StringType, null);
|
|
220
|
+
class AddressForm extends Form {
|
|
221
|
+
city = new Field(StringType, null, { isRequired: true })
|
|
222
|
+
postalCode = new Field(StringType, null, { isRequired: true })
|
|
287
223
|
}
|
|
288
224
|
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
login: 'Account not found',
|
|
294
|
-
password: 'Invalid password'
|
|
295
|
-
};
|
|
225
|
+
const addresses = new ArrayFormField(AddressForm, {
|
|
226
|
+
minForms: 1,
|
|
227
|
+
maxForms: 3,
|
|
228
|
+
})
|
|
296
229
|
|
|
297
|
-
|
|
298
|
-
|
|
230
|
+
addresses.add({ city: 'Berlin', postalCode: '10115' })
|
|
231
|
+
console.log(addresses.formsFieldsValue)
|
|
299
232
|
```
|
|
300
233
|
|
|
301
|
-
|
|
234
|
+
Options include `isRequired`, `minForms`, `maxForms`, `messages`, and a synchronous `validate(forms, form)` callback.
|
|
302
235
|
|
|
303
|
-
|
|
236
|
+
## Input masks
|
|
304
237
|
|
|
305
|
-
|
|
238
|
+
Plugins transform input before it is cast and validated. The built-in `MaskPlugin` supports fixed mask characters and digit or custom pattern placeholders.
|
|
306
239
|
|
|
307
|
-
|
|
240
|
+
```typescript
|
|
241
|
+
import { Field, MaskPlugin, StringType } from 'adaptform'
|
|
308
242
|
|
|
309
|
-
|
|
243
|
+
const phone = new Field(StringType, null, {
|
|
244
|
+
plugins: [
|
|
245
|
+
new MaskPlugin({
|
|
246
|
+
maskFormat: '+1 (___) ___-____',
|
|
247
|
+
maskPlaceholder: '_',
|
|
248
|
+
digitPattern: /\d/,
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
})
|
|
310
252
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
253
|
+
phone.rawValue = '4155550134'
|
|
254
|
+
console.log(phone.rawValue)
|
|
255
|
+
console.log(phone.valueClear)
|
|
256
|
+
```
|
|
314
257
|
|
|
315
|
-
|
|
316
|
-
toRawValue(value: string): string {
|
|
317
|
-
return value?.toUpperCase() || '';
|
|
318
|
-
}
|
|
258
|
+
## Server-side errors with ErrorsSchema
|
|
319
259
|
|
|
320
|
-
|
|
321
|
-
return value?.toLowerCase() || '';
|
|
322
|
-
}
|
|
323
|
-
}
|
|
260
|
+
`ErrorsSchema` maps error keys returned by a backend to form fields. By default, unmatched keys fall back to their original names.
|
|
324
261
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
});
|
|
328
|
-
```
|
|
262
|
+
```typescript
|
|
263
|
+
import { ErrorsSchema, Form, Field, StringType } from 'adaptform'
|
|
329
264
|
|
|
330
|
-
|
|
265
|
+
class AccountForm extends Form {
|
|
266
|
+
email = new Field(StringType)
|
|
267
|
+
username = new Field(StringType)
|
|
268
|
+
}
|
|
331
269
|
|
|
332
|
-
|
|
270
|
+
const form = new AccountForm()
|
|
271
|
+
form.errorsShema = new ErrorsSchema()
|
|
272
|
+
.staticKey('email_address', 'email')
|
|
273
|
+
.staticKey('user_name', 'username')
|
|
274
|
+
.fallback(key => key)
|
|
333
275
|
|
|
334
|
-
|
|
276
|
+
form.errors = {
|
|
277
|
+
email_address: 'This email is already registered.',
|
|
278
|
+
user_name: 'This username is unavailable.',
|
|
279
|
+
request_id: 'The request could not be completed.',
|
|
280
|
+
}
|
|
335
281
|
|
|
336
|
-
|
|
282
|
+
console.log(form.allErrors)
|
|
283
|
+
```
|
|
337
284
|
|
|
338
|
-
|
|
285
|
+
Available schema methods:
|
|
339
286
|
|
|
340
|
-
|
|
287
|
+
- `staticKey(errorKey, fieldName)` maps an exact key.
|
|
288
|
+
- `dynamicKey(pattern, mapping, options?)` maps keys using a regular expression and optional priority.
|
|
289
|
+
- `fallback(handler)` handles keys that do not match configured rules.
|
|
290
|
+
- `transformValue(transformer)` normalizes server error values before mapping.
|
|
291
|
+
- `nest(prefix)` creates a schema for nested field paths.
|
|
292
|
+
- `clone()` and `merge(other)` compose schemas.
|
|
293
|
+
- `ErrorsSchema.fromObject(mapping)` creates a schema from a key-to-field map.
|
|
294
|
+
- `ErrorsSchema.forArray(itemSchema, arrayFieldName, indexExtractor?)` maps array-item errors, including bracket and dotted index notation.
|
|
341
295
|
|
|
342
|
-
|
|
296
|
+
Assigning a new `form.errors` object clears previously applied errors before applying the new payload. Unknown field keys become global errors.
|
|
343
297
|
|
|
344
|
-
|
|
298
|
+
## Serialization
|
|
345
299
|
|
|
346
|
-
|
|
300
|
+
### JSON
|
|
347
301
|
|
|
348
|
-
|
|
302
|
+
`toJSON()` returns a JSON-compatible object. JavaScript `Date` instances are recursively converted to ISO 8601 strings, including dates nested in arrays and plain objects.
|
|
349
303
|
|
|
350
|
-
|
|
304
|
+
```typescript
|
|
305
|
+
const payload = form.toJSON()
|
|
306
|
+
await fetch('/api/profile', {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json' },
|
|
309
|
+
body: JSON.stringify(payload),
|
|
310
|
+
})
|
|
311
|
+
```
|
|
351
312
|
|
|
352
|
-
|
|
313
|
+
### FormData
|
|
353
314
|
|
|
354
|
-
|
|
315
|
+
`toFormData()` omits `null` and `undefined` values, appends scalar values as strings, serializes dates as ISO 8601 strings, preserves `Blob` and `File` values, and JSON-encodes arrays and nested objects.
|
|
355
316
|
|
|
356
|
-
|
|
317
|
+
```typescript
|
|
318
|
+
const body = form.toFormData()
|
|
319
|
+
await fetch('/api/profile', {
|
|
320
|
+
method: 'POST',
|
|
321
|
+
body,
|
|
322
|
+
})
|
|
323
|
+
```
|
|
357
324
|
|
|
358
|
-
|
|
325
|
+
Do not set the multipart `Content-Type` header manually; the browser supplies the boundary.
|
|
359
326
|
|
|
360
|
-
|
|
327
|
+
## Optional Luxon integration
|
|
361
328
|
|
|
362
|
-
|
|
329
|
+
Install Luxon separately, then import the additional date type from the dedicated subpath:
|
|
363
330
|
|
|
364
|
-
|
|
331
|
+
```bash
|
|
332
|
+
npm install luxon
|
|
333
|
+
```
|
|
365
334
|
|
|
366
|
-
|
|
335
|
+
```typescript
|
|
336
|
+
import { Field } from 'adaptform'
|
|
337
|
+
import { DateLuxonType } from 'adaptform/luxon'
|
|
338
|
+
import { DateTime } from 'luxon'
|
|
367
339
|
|
|
368
|
-
|
|
340
|
+
const date = new Field(DateLuxonType, null, {
|
|
341
|
+
isDatetime: false,
|
|
342
|
+
minDate: DateTime.fromISO('1900-01-01'),
|
|
343
|
+
maxDate: DateTime.now(),
|
|
344
|
+
inputFormat: 'dd.MM.yyyy',
|
|
345
|
+
})
|
|
346
|
+
```
|
|
369
347
|
|
|
370
|
-
|
|
348
|
+
## API overview
|
|
371
349
|
|
|
372
|
-
|
|
350
|
+
| Class | Main API |
|
|
351
|
+
| --- | --- |
|
|
352
|
+
| `Form` | `fields`, `setFields()`, `fieldsValue`, `checkValid()`, `isValid`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `batch()`, `allErrors`, `globalErrors`, `errors`, `setGlobalError()`, `clearGlobalErrors()`, `reset()`, `toJSON()`, `toFormData()` |
|
|
353
|
+
| `Field` | `rawValue`, `setRawValue()`, `valueClear`, `errors`, `error`, `isValid`, `isTouched`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `isEmpty`, `checkValid()`, `reset()` |
|
|
354
|
+
| `ArrayField` | `items`, `itemsValue`, `add()`, `remove()`, `clear()`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `checkValid()`, `reset()` |
|
|
355
|
+
| `ArrayFormField` | `forms`, `formsFieldsValue`, `add()`, `remove()`, `clear()`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `checkValid()`, `reset()` |
|
|
356
|
+
| `ErrorsSchema` | `staticKey()`, `dynamicKey()`, `fallback()`, `transformValue()`, `parseErrors()`, `parseErrorsDetailed()`, `nest()`, `clone()`, `merge()`, `forArray()`, `fromObject()` |
|
|
373
357
|
|
|
374
|
-
|
|
358
|
+
## Development
|
|
375
359
|
|
|
376
|
-
|
|
360
|
+
Requirements: Node.js 22 (used by CI) and npm.
|
|
377
361
|
|
|
378
|
-
|
|
362
|
+
```bash
|
|
363
|
+
npm ci
|
|
364
|
+
npm run lint
|
|
365
|
+
npm test
|
|
366
|
+
npm run build
|
|
367
|
+
npx tsc --noEmit -p tsconfig.json
|
|
368
|
+
npm run check:demo
|
|
369
|
+
npm run build:demo
|
|
370
|
+
```
|
|
379
371
|
|
|
380
|
-
|
|
372
|
+
To run the interactive demo locally:
|
|
381
373
|
|
|
382
|
-
|
|
374
|
+
```bash
|
|
375
|
+
npm run dev -- --config demo/vite.config.ts
|
|
376
|
+
```
|
|
383
377
|
|
|
384
|
-
|
|
378
|
+
The demo is built by GitHub Actions from `v2.1.0` and `master`. GitHub Pages is configured to allow both branches to publish to the same demo URL; see [Demo and GitHub Pages notes](https://github.com/ArhiMEN/AdaptForm/blob/master/docs/DEMO.md).
|
|
385
379
|
|
|
386
|
-
|
|
380
|
+
## Links
|
|
387
381
|
|
|
388
|
-
|
|
382
|
+
- **[Interactive demo](https://arhimen.github.io/AdaptForm/)**
|
|
383
|
+
- [GitHub repository](https://github.com/ArhiMEN/AdaptForm)
|
|
384
|
+
- [npm package](https://www.npmjs.com/package/adaptform)
|
|
385
|
+
- [Issue tracker](https://github.com/ArhiMEN/AdaptForm/issues)
|
|
386
|
+
- [Changelog](https://github.com/ArhiMEN/AdaptForm/blob/master/CHANGELOG.md)
|
|
387
|
+
- [License](https://github.com/ArhiMEN/AdaptForm/blob/master/LICENSE.md)
|
|
389
388
|
|
|
390
|
-
##
|
|
389
|
+
## License
|
|
391
390
|
|
|
392
|
-
|
|
393
|
-
import {Form, Field, StringType, NumberType} from 'adaptform';
|
|
391
|
+
MIT © Stanislav Orlov
|
|
394
392
|
|
|
395
|
-
|
|
396
|
-
name = new Field(StringType, null);
|
|
397
|
-
age = new Field(NumberType, null);
|
|
398
|
-
}
|
|
393
|
+
## Headless form state (2.2.0 source candidate)
|
|
399
394
|
|
|
400
|
-
|
|
395
|
+
HF-005–HF-010 were merged after independent review. The **source candidate is 2.2.0**, but npm publication is separate, so these APIs may not yet be available from `npm install adaptform`.
|
|
401
396
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// { name: string | null; age: number | null; }
|
|
405
|
-
```
|
|
397
|
+
```typescript
|
|
398
|
+
import {Field, Form, NumberType, StringType} from 'adaptform'
|
|
406
399
|
|
|
407
|
-
|
|
400
|
+
class SettingsForm extends Form {
|
|
401
|
+
displayName = new Field(StringType, 'Alex')
|
|
402
|
+
notifications = new Field(NumberType, 1)
|
|
403
|
+
}
|
|
408
404
|
|
|
409
|
-
|
|
405
|
+
const form = new SettingsForm()
|
|
406
|
+
const unsubscribe = form.subscribe(() => {
|
|
407
|
+
const snapshot = form.getSnapshot()
|
|
408
|
+
console.log(snapshot.isDirty, snapshot.isValid, snapshot.fieldsValue)
|
|
409
|
+
})
|
|
410
410
|
|
|
411
|
-
|
|
411
|
+
form.batch(() => {
|
|
412
|
+
form.displayName.rawValue = 'Alexandra'
|
|
413
|
+
form.notifications.rawValue = 2
|
|
414
|
+
}) // One coherent notification after final automatic validation
|
|
412
415
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
* [Changelog](https://github.com/ArhiMEN/adaptform/blob/master/CHANGELOG.md)
|
|
416
|
+
form.commitBaseline() // Accept current cleaned values; isDirty becomes false
|
|
417
|
+
unsubscribe() // Release the listener
|
|
418
|
+
```
|
|
417
419
|
|
|
418
|
-
|
|
420
|
+
`isTouched` keeps its former semantics. `commitBaseline()` changes the dirty-comparison baseline, **not** the constructor defaults restored by `reset()`; a reset may leave `isDirty=true`. Snapshots are detached read-only descriptions, not mutable source objects; `toJSON()` still does not validate.
|
|
419
421
|
|
|
420
|
-
|
|
422
|
+
Automatic validation inside `Form.batch()` is deferred, whereas explicit `checkValid()` is immediate. Batch callbacks must be synchronous and do not roll back changes on errors. Direct in-place changes to legacy public arrays or arbitrary form properties are not automatically observed. See [usage and limitations](docs/ai/HEADLESS_STATE_USAGE.md) and the [2.1.1 compatibility table](docs/ai/HEADLESS_STATE_CONTRACT.md#compatibility-with-211).
|