adaptform 2.0.4 → 2.1.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 +152 -0
- package/CHANGELOG.ru.md +152 -0
- package/LICENSE.md +3 -1
- package/LICENSE.ru.md +25 -0
- package/README.md +388 -1
- package/README.ru.md +388 -0
- package/dist/{Form-DOdrOtbT.d.cts → Form-kPYL4f-n.d.cts} +117 -15
- package/dist/{Form-DOdrOtbT.d.ts → Form-kPYL4f-n.d.ts} +117 -15
- package/dist/index.cjs +483 -80
- 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 +482 -81
- package/dist/index.mjs.map +1 -1
- package/dist/luxon.cjs.map +1 -1
- package/dist/luxon.d.cts +1 -1
- package/dist/luxon.d.ts +1 -1
- package/dist/luxon.mjs.map +1 -1
- package/package.json +41 -12
package/README.md
CHANGED
|
@@ -1 +1,388 @@
|
|
|
1
|
-
|
|
1
|
+
# AdaptForm
|
|
2
|
+
|
|
3
|
+
[English](README.md) · [Русский](README.ru.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/adaptform)
|
|
6
|
+
[](https://www.npmjs.com/package/adaptform)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
|
|
10
|
+
**[Open the interactive demo](https://arhimen.github.io/AdaptForm/)** · **[Read the 2.1.0 changelog](https://github.com/ArhiMEN/AdaptForm/blob/master/CHANGELOG.md)** · **[Browse the demo source](https://github.com/ArhiMEN/AdaptForm/tree/master/demo)**
|
|
11
|
+
|
|
12
|
+
Maintainers: [automatic npm publication setup](docs/NPM_PUBLISH.md).
|
|
13
|
+
|
|
14
|
+
AdaptForm is a TypeScript-first form modeling library inspired by Django Forms and Pydantic. Define forms as classes with typed fields. Each field owns its value, casting, validation, transformation plugins, errors, and state.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
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
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install the published npm package:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install adaptform
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Luxon integration is optional:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install luxon
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The [interactive demo](https://arhimen.github.io/AdaptForm/) runs from repository source, with automatic deployment from `v2.1.0` and `master`. Version `2.1.0` is prepared in the repository; npm continues to serve the last published version until the maintainer publishes the release.
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { BooleanType, Field, Form, StringType } from 'adaptform'
|
|
51
|
+
|
|
52
|
+
class RegistrationForm extends Form {
|
|
53
|
+
name = new Field(StringType, null, {
|
|
54
|
+
isRequired: true,
|
|
55
|
+
minLength: 2,
|
|
56
|
+
maxLength: 50,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
email = new Field(StringType, null, {
|
|
60
|
+
isRequired: true,
|
|
61
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
acceptedTerms = new Field(BooleanType, false, {
|
|
65
|
+
mustBeTrue: true,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const form = new RegistrationForm()
|
|
70
|
+
form.fields = {
|
|
71
|
+
name: 'Alex Morgan',
|
|
72
|
+
email: 'alex@example.com',
|
|
73
|
+
acceptedTerms: true,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (form.isValid) {
|
|
77
|
+
console.log(form.fieldsValue)
|
|
78
|
+
console.log(form.toJSON())
|
|
79
|
+
} else {
|
|
80
|
+
console.log(form.allErrors)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`form.fields` assigns the supplied values and validates the completed form. Values for fields omitted from the assignment are left unchanged.
|
|
85
|
+
|
|
86
|
+
## Field types and options
|
|
87
|
+
|
|
88
|
+
All field types are constructed using `new Field(TypeConstructor, defaultValue?, options?)`.
|
|
89
|
+
|
|
90
|
+
### StringType
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const username = new Field(StringType, null, {
|
|
94
|
+
isRequired: true,
|
|
95
|
+
minLength: 3,
|
|
96
|
+
maxLength: 32,
|
|
97
|
+
pattern: /^[a-z0-9_]+$/i,
|
|
98
|
+
messages: {
|
|
99
|
+
isRequired: 'Enter a username.',
|
|
100
|
+
minLength: 'Use at least 3 characters.',
|
|
101
|
+
pattern: 'Use only letters, numbers, and underscores.',
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Supported constraints: `isRequired`, `minLength`, `maxLength`, and `pattern`.
|
|
107
|
+
|
|
108
|
+
### NumberType
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const age = new Field(NumberType, null, {
|
|
112
|
+
isRequired: true,
|
|
113
|
+
ge: 18,
|
|
114
|
+
le: 120,
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Supported constraints: `gt`, `ge`, `lt`, and `le`. Values must be finite integers; blank strings and non-finite numbers are rejected.
|
|
119
|
+
|
|
120
|
+
### DecimalType
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const price = new Field(DecimalType, null, {
|
|
124
|
+
decimalPlaces: 2,
|
|
125
|
+
maxDigits: 8,
|
|
126
|
+
ge: 0,
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
### BooleanType
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const accepted = new Field(BooleanType, false, {
|
|
136
|
+
mustBeTrue: true,
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Supports `mustBeTrue` and `mustBeFalse`.
|
|
141
|
+
|
|
142
|
+
### DateType
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const birthday = new Field(DateType, null, {
|
|
146
|
+
isDatetime: false,
|
|
147
|
+
minDate: new Date('1900-01-01T00:00:00.000Z'),
|
|
148
|
+
maxDate: new Date(),
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Supports `minDate`, `maxDate`, and `isDatetime`. When `isDatetime` is false, comparisons use calendar dates; when true, timestamps are compared. Invalid dates are rejected.
|
|
153
|
+
|
|
154
|
+
### Custom validation and messages
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
class PasswordForm extends Form {
|
|
160
|
+
password = new Field(StringType, null, { minLength: 8 })
|
|
161
|
+
|
|
162
|
+
confirmation = new Field(StringType, null, {
|
|
163
|
+
isRequired: true,
|
|
164
|
+
validate: (value, form) =>
|
|
165
|
+
value === form?.password.valueClear || 'Passwords do not match.',
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
## Validation lifecycle and state
|
|
173
|
+
|
|
174
|
+
- Constructor defaults begin untouched.
|
|
175
|
+
- Empty strings and whitespace-only strings count as empty values.
|
|
176
|
+
- Global and sticky regular-expression flags do not make repeated string validation stateful.
|
|
177
|
+
- `form.checkValid()` validates every field and returns `false` when a field is invalid or global server errors remain.
|
|
178
|
+
- 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.
|
|
179
|
+
- Bulk assignment validates after direct field values have been assigned, so conditional-required rules can inspect the complete form state.
|
|
180
|
+
- If you change dependent fields one at a time, call `form.checkValid()` after the changes to recompute cross-field rules.
|
|
181
|
+
- `field.setRawValue(value, false)` and `form.setFields(data, false)` allow advanced callers to defer validation. Call `checkValid()` before relying on the validity state.
|
|
182
|
+
- `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.
|
|
183
|
+
- Adding, removing, or clearing collection items refreshes collection validation. Invalid removal indices are ignored.
|
|
184
|
+
|
|
185
|
+
## Dynamic collections
|
|
186
|
+
|
|
187
|
+
### ArrayField
|
|
188
|
+
|
|
189
|
+
Use `ArrayField` for a dynamic list of similarly typed fields.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { ArrayField, Field, StringType } from 'adaptform'
|
|
193
|
+
|
|
194
|
+
const tags = new ArrayField(
|
|
195
|
+
(value?: string) => new Field(StringType, value, {
|
|
196
|
+
isRequired: true,
|
|
197
|
+
minLength: 2,
|
|
198
|
+
}),
|
|
199
|
+
{ minItems: 1, maxItems: 5 },
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
tags.add('typescript')
|
|
203
|
+
tags.add('forms')
|
|
204
|
+
console.log(tags.itemsValue)
|
|
205
|
+
console.log(tags.isValid)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Options include `isRequired`, `minItems`, `maxItems`, `messages`, and a synchronous `validate(values, form)` callback. `itemsValue` returns an array whose entries may be `null`.
|
|
209
|
+
|
|
210
|
+
### ArrayFormField
|
|
211
|
+
|
|
212
|
+
Use `ArrayFormField` for a list of nested form instances.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { ArrayFormField, Field, Form, StringType } from 'adaptform'
|
|
216
|
+
|
|
217
|
+
class AddressForm extends Form {
|
|
218
|
+
city = new Field(StringType, null, { isRequired: true })
|
|
219
|
+
postalCode = new Field(StringType, null, { isRequired: true })
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const addresses = new ArrayFormField(AddressForm, {
|
|
223
|
+
minForms: 1,
|
|
224
|
+
maxForms: 3,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
addresses.add({ city: 'Berlin', postalCode: '10115' })
|
|
228
|
+
console.log(addresses.formsFieldsValue)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Options include `isRequired`, `minForms`, `maxForms`, `messages`, and a synchronous `validate(forms, form)` callback.
|
|
232
|
+
|
|
233
|
+
## Input masks
|
|
234
|
+
|
|
235
|
+
Plugins transform input before it is cast and validated. The built-in `MaskPlugin` supports fixed mask characters and digit or custom pattern placeholders.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { Field, MaskPlugin, StringType } from 'adaptform'
|
|
239
|
+
|
|
240
|
+
const phone = new Field(StringType, null, {
|
|
241
|
+
plugins: [
|
|
242
|
+
new MaskPlugin({
|
|
243
|
+
maskFormat: '+1 (___) ___-____',
|
|
244
|
+
maskPlaceholder: '_',
|
|
245
|
+
digitPattern: /\d/,
|
|
246
|
+
}),
|
|
247
|
+
],
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
phone.rawValue = '4155550134'
|
|
251
|
+
console.log(phone.rawValue)
|
|
252
|
+
console.log(phone.valueClear)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Server-side errors with ErrorsSchema
|
|
256
|
+
|
|
257
|
+
`ErrorsSchema` maps error keys returned by a backend to form fields. By default, unmatched keys fall back to their original names.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { ErrorsSchema, Form, Field, StringType } from 'adaptform'
|
|
261
|
+
|
|
262
|
+
class AccountForm extends Form {
|
|
263
|
+
email = new Field(StringType)
|
|
264
|
+
username = new Field(StringType)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const form = new AccountForm()
|
|
268
|
+
form.errorsShema = new ErrorsSchema()
|
|
269
|
+
.staticKey('email_address', 'email')
|
|
270
|
+
.staticKey('user_name', 'username')
|
|
271
|
+
.fallback(key => key)
|
|
272
|
+
|
|
273
|
+
form.errors = {
|
|
274
|
+
email_address: 'This email is already registered.',
|
|
275
|
+
user_name: 'This username is unavailable.',
|
|
276
|
+
request_id: 'The request could not be completed.',
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log(form.allErrors)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Available schema methods:
|
|
283
|
+
|
|
284
|
+
- `staticKey(errorKey, fieldName)` maps an exact key.
|
|
285
|
+
- `dynamicKey(pattern, mapping, options?)` maps keys using a regular expression and optional priority.
|
|
286
|
+
- `fallback(handler)` handles keys that do not match configured rules.
|
|
287
|
+
- `transformValue(transformer)` normalizes server error values before mapping.
|
|
288
|
+
- `nest(prefix)` creates a schema for nested field paths.
|
|
289
|
+
- `clone()` and `merge(other)` compose schemas.
|
|
290
|
+
- `ErrorsSchema.fromObject(mapping)` creates a schema from a key-to-field map.
|
|
291
|
+
- `ErrorsSchema.forArray(itemSchema, arrayFieldName, indexExtractor?)` maps array-item errors, including bracket and dotted index notation.
|
|
292
|
+
|
|
293
|
+
Assigning a new `form.errors` object clears previously applied errors before applying the new payload. Unknown field keys become global errors.
|
|
294
|
+
|
|
295
|
+
## Serialization
|
|
296
|
+
|
|
297
|
+
### JSON
|
|
298
|
+
|
|
299
|
+
`toJSON()` returns a JSON-compatible object. JavaScript `Date` instances are recursively converted to ISO 8601 strings, including dates nested in arrays and plain objects.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
const payload = form.toJSON()
|
|
303
|
+
await fetch('/api/profile', {
|
|
304
|
+
method: 'POST',
|
|
305
|
+
headers: { 'Content-Type': 'application/json' },
|
|
306
|
+
body: JSON.stringify(payload),
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### FormData
|
|
311
|
+
|
|
312
|
+
`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.
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
const body = form.toFormData()
|
|
316
|
+
await fetch('/api/profile', {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
body,
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Do not set the multipart `Content-Type` header manually; the browser supplies the boundary.
|
|
323
|
+
|
|
324
|
+
## Optional Luxon integration
|
|
325
|
+
|
|
326
|
+
Install Luxon separately, then import the additional date type from the dedicated subpath:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
npm install luxon
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { Field } from 'adaptform'
|
|
334
|
+
import { DateLuxonType } from 'adaptform/luxon'
|
|
335
|
+
import { DateTime } from 'luxon'
|
|
336
|
+
|
|
337
|
+
const date = new Field(DateLuxonType, null, {
|
|
338
|
+
isDatetime: false,
|
|
339
|
+
minDate: DateTime.fromISO('1900-01-01'),
|
|
340
|
+
maxDate: DateTime.now(),
|
|
341
|
+
inputFormat: 'dd.MM.yyyy',
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## API overview
|
|
346
|
+
|
|
347
|
+
| Class | Main API |
|
|
348
|
+
| --- | --- |
|
|
349
|
+
| `Form` | `fields`, `setFields()`, `fieldsValue`, `checkValid()`, `isValid`, `allErrors`, `globalErrors`, `errors`, `setGlobalError()`, `clearGlobalErrors()`, `reset()`, `toJSON()`, `toFormData()` |
|
|
350
|
+
| `Field` | `rawValue`, `setRawValue()`, `valueClear`, `errors`, `error`, `isValid`, `isTouched`, `isEmpty`, `checkValid()`, `reset()` |
|
|
351
|
+
| `ArrayField` | `items`, `itemsValue`, `add()`, `remove()`, `clear()`, `checkValid()`, `reset()` |
|
|
352
|
+
| `ArrayFormField` | `forms`, `formsFieldsValue`, `add()`, `remove()`, `clear()`, `checkValid()`, `reset()` |
|
|
353
|
+
| `ErrorsSchema` | `staticKey()`, `dynamicKey()`, `fallback()`, `transformValue()`, `parseErrors()`, `parseErrorsDetailed()`, `nest()`, `clone()`, `merge()`, `forArray()`, `fromObject()` |
|
|
354
|
+
|
|
355
|
+
## Development
|
|
356
|
+
|
|
357
|
+
Requirements: Node.js 22 (used by CI) and npm.
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
npm ci
|
|
361
|
+
npm run lint
|
|
362
|
+
npm test
|
|
363
|
+
npm run build
|
|
364
|
+
npx tsc --noEmit -p tsconfig.json
|
|
365
|
+
npm run check:demo
|
|
366
|
+
npm run build:demo
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
To run the interactive demo locally:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm run dev -- --config demo/vite.config.ts
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
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).
|
|
376
|
+
|
|
377
|
+
## Links
|
|
378
|
+
|
|
379
|
+
- **[Interactive demo](https://arhimen.github.io/AdaptForm/)**
|
|
380
|
+
- [GitHub repository](https://github.com/ArhiMEN/AdaptForm)
|
|
381
|
+
- [npm package](https://www.npmjs.com/package/adaptform)
|
|
382
|
+
- [Issue tracker](https://github.com/ArhiMEN/AdaptForm/issues)
|
|
383
|
+
- [Changelog](https://github.com/ArhiMEN/AdaptForm/blob/master/CHANGELOG.md)
|
|
384
|
+
- [License](https://github.com/ArhiMEN/AdaptForm/blob/master/LICENSE.md)
|
|
385
|
+
|
|
386
|
+
## License
|
|
387
|
+
|
|
388
|
+
MIT © Stanislav Orlov
|