adaptform 2.0.5 → 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 +103 -2
- package/CHANGELOG.ru.md +152 -0
- package/LICENSE.md +3 -1
- package/LICENSE.ru.md +25 -0
- package/README.md +271 -303
- package/README.ru.md +388 -0
- package/dist/{Form-Be3I6LX0.d.cts → Form-kPYL4f-n.d.cts} +58 -36
- package/dist/{Form-Be3I6LX0.d.ts → Form-kPYL4f-n.d.ts} +58 -36
- package/dist/index.cjs +184 -102
- 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 +183 -103
- 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/README.md
CHANGED
|
@@ -1,420 +1,388 @@
|
|
|
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 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)**
|
|
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 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.
|
|
23
15
|
|
|
24
16
|
## Features
|
|
25
17
|
|
|
26
|
-
-
|
|
27
|
-
- Field-level
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
## Philosophy
|
|
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
|
-
````
|
|
47
|
-
|
|
48
|
-
AdaptForm treats forms as stateful domain models:
|
|
49
|
-
|
|
50
|
-
```TypeScript
|
|
51
|
-
class LoginForm extends Form {
|
|
52
|
-
login = new Field(StringType)
|
|
53
|
-
password = new Field(StringType)
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
field is not just a validator — it is a state container that owns:
|
|
58
|
-
|
|
59
|
-
- value
|
|
60
|
-
- validation
|
|
61
|
-
- errors
|
|
62
|
-
- transformation
|
|
63
|
-
- plugins
|
|
64
|
-
- type casting
|
|
65
|
-
|
|
66
|
-
The form itself becomes a single source of truth.
|
|
67
|
-
|
|
68
|
-
Simple Vue:
|
|
69
|
-
|
|
70
|
-
## Without AdaptForm
|
|
71
|
-
|
|
72
|
-
A typical Vue form consists of multiple disconnected parts:
|
|
73
|
-
|
|
74
|
-
- reactive state
|
|
75
|
-
- validation schema
|
|
76
|
-
- error store
|
|
77
|
-
- input masks
|
|
78
|
-
- submission mapping
|
|
79
|
-
|
|
80
|
-
## With AdaptForm
|
|
81
|
-
|
|
82
|
-
Everything is unified into a single form model:
|
|
83
|
-
|
|
84
|
-
```TypeScript
|
|
85
|
-
class RegistrationForm extends Form {
|
|
86
|
-
phone = new Field(StringType)
|
|
87
|
-
}
|
|
88
|
-
````
|
|
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.
|
|
89
28
|
|
|
90
29
|
## Installation
|
|
91
30
|
|
|
31
|
+
Install the published npm package:
|
|
32
|
+
|
|
92
33
|
```bash
|
|
93
34
|
npm install adaptform
|
|
94
35
|
```
|
|
95
36
|
|
|
96
|
-
|
|
37
|
+
Luxon integration is optional:
|
|
97
38
|
|
|
98
|
-
```
|
|
39
|
+
```bash
|
|
99
40
|
npm install luxon
|
|
100
41
|
```
|
|
101
42
|
|
|
102
|
-
|
|
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.
|
|
103
44
|
|
|
104
|
-
|
|
105
|
-
import {Form, Field, StringType, NumberType} from 'adaptform';
|
|
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.
|
|
106
46
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
})
|
|
112
58
|
|
|
113
|
-
|
|
59
|
+
email = new Field(StringType, null, {
|
|
60
|
+
isRequired: true,
|
|
61
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
62
|
+
})
|
|
114
63
|
|
|
64
|
+
acceptedTerms = new Field(BooleanType, false, {
|
|
65
|
+
mustBeTrue: true,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const form = new RegistrationForm()
|
|
115
70
|
form.fields = {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
71
|
+
name: 'Alex Morgan',
|
|
72
|
+
email: 'alex@example.com',
|
|
73
|
+
acceptedTerms: true,
|
|
74
|
+
}
|
|
119
75
|
|
|
120
|
-
|
|
121
|
-
console.log(form.fieldsValue)
|
|
76
|
+
if (form.isValid) {
|
|
77
|
+
console.log(form.fieldsValue)
|
|
78
|
+
console.log(form.toJSON())
|
|
79
|
+
} else {
|
|
80
|
+
console.log(form.allErrors)
|
|
81
|
+
}
|
|
122
82
|
```
|
|
123
83
|
|
|
124
|
-
|
|
84
|
+
`form.fields` assigns the supplied values and validates the completed form. Values for fields omitted from the assignment are left unchanged.
|
|
125
85
|
|
|
126
|
-
## Field
|
|
86
|
+
## Field types and options
|
|
127
87
|
|
|
128
|
-
|
|
129
|
-
import {Field, StringType, NumberType, BooleanType, DecimalType, DateType} from 'adaptform';
|
|
88
|
+
All field types are constructed using `new Field(TypeConstructor, defaultValue?, options?)`.
|
|
130
89
|
|
|
131
|
-
|
|
132
|
-
const name = new Field(StringType, null, {
|
|
133
|
-
minLength: 2,
|
|
134
|
-
maxLength: 50,
|
|
135
|
-
pattern: /^[a-zA-Z]+$/
|
|
136
|
-
});
|
|
90
|
+
### StringType
|
|
137
91
|
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Boolean with requirement
|
|
145
|
-
const agreed = new Field(BooleanType, false, {
|
|
146
|
-
mustBeTrue: true,
|
|
92
|
+
```typescript
|
|
93
|
+
const username = new Field(StringType, null, {
|
|
94
|
+
isRequired: true,
|
|
95
|
+
minLength: 3,
|
|
96
|
+
maxLength: 32,
|
|
97
|
+
pattern: /^[a-z0-9_]+$/i,
|
|
147
98
|
messages: {
|
|
148
|
-
|
|
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
|
-
});
|
|
99
|
+
isRequired: 'Enter a username.',
|
|
100
|
+
minLength: 'Use at least 3 characters.',
|
|
101
|
+
pattern: 'Use only letters, numbers, and underscores.',
|
|
102
|
+
},
|
|
103
|
+
})
|
|
168
104
|
```
|
|
169
105
|
|
|
170
|
-
|
|
106
|
+
Supported constraints: `isRequired`, `minLength`, `maxLength`, and `pattern`.
|
|
171
107
|
|
|
172
|
-
|
|
173
|
-
class SignUpForm extends Form {
|
|
174
|
-
country = new Field(StringType, null);
|
|
108
|
+
### NumberType
|
|
175
109
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
110
|
+
```typescript
|
|
111
|
+
const age = new Field(NumberType, null, {
|
|
112
|
+
isRequired: true,
|
|
113
|
+
ge: 18,
|
|
114
|
+
le: 120,
|
|
115
|
+
})
|
|
183
116
|
```
|
|
184
117
|
|
|
185
|
-
|
|
118
|
+
Supported constraints: `gt`, `ge`, `lt`, and `le`. Values must be finite integers; blank strings and non-finite numbers are rejected.
|
|
186
119
|
|
|
187
|
-
|
|
188
|
-
import {Field, StringType, MaskPlugin} from 'adaptform';
|
|
120
|
+
### DecimalType
|
|
189
121
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
})
|
|
197
|
-
]
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
phone.rawValue = '9991234567';
|
|
201
|
-
console.log(phone.valueClear); // '+7 (999) 123-45-67'
|
|
122
|
+
```typescript
|
|
123
|
+
const price = new Field(DecimalType, null, {
|
|
124
|
+
decimalPlaces: 2,
|
|
125
|
+
maxDigits: 8,
|
|
126
|
+
ge: 0,
|
|
127
|
+
})
|
|
202
128
|
```
|
|
203
129
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```TypeScript
|
|
207
|
-
import {ArrayField, Field, NumberType} from 'adaptform';
|
|
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.
|
|
208
131
|
|
|
209
|
-
|
|
210
|
-
(value?: number) => new Field(NumberType, value, {gt: 0}),
|
|
211
|
-
{minItems: 1}
|
|
212
|
-
);
|
|
132
|
+
### BooleanType
|
|
213
133
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
134
|
+
```typescript
|
|
135
|
+
const accepted = new Field(BooleanType, false, {
|
|
136
|
+
mustBeTrue: true,
|
|
137
|
+
})
|
|
217
138
|
```
|
|
218
139
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
```Bash
|
|
222
|
-
npm install luxon
|
|
223
|
-
```
|
|
140
|
+
Supports `mustBeTrue` and `mustBeFalse`.
|
|
224
141
|
|
|
225
|
-
|
|
226
|
-
import {DateLuxonType} from 'adaptform/luxon';
|
|
227
|
-
import {DateTime} from 'luxon';
|
|
142
|
+
### DateType
|
|
228
143
|
|
|
229
|
-
|
|
144
|
+
```typescript
|
|
145
|
+
const birthday = new Field(DateType, null, {
|
|
230
146
|
isDatetime: false,
|
|
231
|
-
minDate:
|
|
232
|
-
maxDate:
|
|
233
|
-
|
|
234
|
-
messages: {
|
|
235
|
-
minDate: 'Date must be after 1900',
|
|
236
|
-
maxDate: 'Date cannot be in the future'
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
birthdate.rawValue = '15.06.1990';
|
|
241
|
-
console.log(birthdate.valueClear); // Luxon DateTime object
|
|
147
|
+
minDate: new Date('1900-01-01T00:00:00.000Z'),
|
|
148
|
+
maxDate: new Date(),
|
|
149
|
+
})
|
|
242
150
|
```
|
|
243
151
|
|
|
244
|
-
|
|
152
|
+
Supports `minDate`, `maxDate`, and `isDatetime`. When `isDatetime` is false, comparisons use calendar dates; when true, timestamps are compared. Invalid dates are rejected.
|
|
245
153
|
|
|
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
|
-
}
|
|
154
|
+
### Custom validation and messages
|
|
254
155
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
// }
|
|
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
|
+
})
|
|
278
167
|
}
|
|
279
168
|
```
|
|
280
169
|
|
|
281
|
-
|
|
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.
|
|
282
171
|
|
|
283
|
-
|
|
284
|
-
class LoginForm extends Form {
|
|
285
|
-
login = new Field(StringType, null);
|
|
286
|
-
password = new Field(StringType, null);
|
|
287
|
-
}
|
|
172
|
+
## Validation lifecycle and state
|
|
288
173
|
|
|
289
|
-
|
|
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.
|
|
290
184
|
|
|
291
|
-
|
|
292
|
-
form.errors = {
|
|
293
|
-
login: 'Account not found',
|
|
294
|
-
password: 'Invalid password'
|
|
295
|
-
};
|
|
185
|
+
## Dynamic collections
|
|
296
186
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
187
|
+
### ArrayField
|
|
188
|
+
|
|
189
|
+
Use `ArrayField` for a dynamic list of similarly typed fields.
|
|
300
190
|
|
|
301
|
-
|
|
191
|
+
```typescript
|
|
192
|
+
import { ArrayField, Field, StringType } from 'adaptform'
|
|
302
193
|
|
|
303
|
-
|
|
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
|
+
)
|
|
304
201
|
|
|
305
|
-
|
|
202
|
+
tags.add('typescript')
|
|
203
|
+
tags.add('forms')
|
|
204
|
+
console.log(tags.itemsValue)
|
|
205
|
+
console.log(tags.isValid)
|
|
206
|
+
```
|
|
306
207
|
|
|
307
|
-
|
|
208
|
+
Options include `isRequired`, `minItems`, `maxItems`, `messages`, and a synchronous `validate(values, form)` callback. `itemsValue` returns an array whose entries may be `null`.
|
|
308
209
|
|
|
309
|
-
###
|
|
210
|
+
### ArrayFormField
|
|
310
211
|
|
|
311
|
-
|
|
312
|
-
import {BasePlugin} from 'adaptform';
|
|
313
|
-
import type {FieldPlugin} from 'adaptform';
|
|
212
|
+
Use `ArrayFormField` for a list of nested form instances.
|
|
314
213
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return value?.toUpperCase() || '';
|
|
318
|
-
}
|
|
214
|
+
```typescript
|
|
215
|
+
import { ArrayFormField, Field, Form, StringType } from 'adaptform'
|
|
319
216
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
217
|
+
class AddressForm extends Form {
|
|
218
|
+
city = new Field(StringType, null, { isRequired: true })
|
|
219
|
+
postalCode = new Field(StringType, null, { isRequired: true })
|
|
323
220
|
}
|
|
324
221
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
222
|
+
const addresses = new ArrayFormField(AddressForm, {
|
|
223
|
+
minForms: 1,
|
|
224
|
+
maxForms: 3,
|
|
225
|
+
})
|
|
329
226
|
|
|
330
|
-
|
|
227
|
+
addresses.add({ city: 'Berlin', postalCode: '10115' })
|
|
228
|
+
console.log(addresses.formsFieldsValue)
|
|
229
|
+
```
|
|
331
230
|
|
|
332
|
-
|
|
231
|
+
Options include `isRequired`, `minForms`, `maxForms`, `messages`, and a synchronous `validate(forms, form)` callback.
|
|
333
232
|
|
|
334
|
-
|
|
233
|
+
## Input masks
|
|
335
234
|
|
|
336
|
-
|
|
235
|
+
Plugins transform input before it is cast and validated. The built-in `MaskPlugin` supports fixed mask characters and digit or custom pattern placeholders.
|
|
337
236
|
|
|
338
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
import { Field, MaskPlugin, StringType } from 'adaptform'
|
|
339
239
|
|
|
340
|
-
|
|
240
|
+
const phone = new Field(StringType, null, {
|
|
241
|
+
plugins: [
|
|
242
|
+
new MaskPlugin({
|
|
243
|
+
maskFormat: '+1 (___) ___-____',
|
|
244
|
+
maskPlaceholder: '_',
|
|
245
|
+
digitPattern: /\d/,
|
|
246
|
+
}),
|
|
247
|
+
],
|
|
248
|
+
})
|
|
341
249
|
|
|
342
|
-
|
|
250
|
+
phone.rawValue = '4155550134'
|
|
251
|
+
console.log(phone.rawValue)
|
|
252
|
+
console.log(phone.valueClear)
|
|
253
|
+
```
|
|
343
254
|
|
|
344
|
-
|
|
255
|
+
## Server-side errors with ErrorsSchema
|
|
345
256
|
|
|
346
|
-
|
|
257
|
+
`ErrorsSchema` maps error keys returned by a backend to form fields. By default, unmatched keys fall back to their original names.
|
|
347
258
|
|
|
348
|
-
|
|
259
|
+
```typescript
|
|
260
|
+
import { ErrorsSchema, Form, Field, StringType } from 'adaptform'
|
|
349
261
|
|
|
350
|
-
|
|
262
|
+
class AccountForm extends Form {
|
|
263
|
+
email = new Field(StringType)
|
|
264
|
+
username = new Field(StringType)
|
|
265
|
+
}
|
|
351
266
|
|
|
352
|
-
|
|
267
|
+
const form = new AccountForm()
|
|
268
|
+
form.errorsShema = new ErrorsSchema()
|
|
269
|
+
.staticKey('email_address', 'email')
|
|
270
|
+
.staticKey('user_name', 'username')
|
|
271
|
+
.fallback(key => key)
|
|
353
272
|
|
|
354
|
-
|
|
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
|
+
}
|
|
355
278
|
|
|
356
|
-
|
|
279
|
+
console.log(form.allErrors)
|
|
280
|
+
```
|
|
357
281
|
|
|
358
|
-
|
|
282
|
+
Available schema methods:
|
|
359
283
|
|
|
360
|
-
|
|
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.
|
|
361
292
|
|
|
362
|
-
|
|
293
|
+
Assigning a new `form.errors` object clears previously applied errors before applying the new payload. Unknown field keys become global errors.
|
|
363
294
|
|
|
364
|
-
|
|
295
|
+
## Serialization
|
|
365
296
|
|
|
366
|
-
###
|
|
297
|
+
### JSON
|
|
367
298
|
|
|
368
|
-
|
|
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.
|
|
369
300
|
|
|
370
|
-
|
|
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
|
+
```
|
|
371
309
|
|
|
372
|
-
|
|
310
|
+
### FormData
|
|
373
311
|
|
|
374
|
-
|
|
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.
|
|
375
313
|
|
|
376
|
-
|
|
314
|
+
```typescript
|
|
315
|
+
const body = form.toFormData()
|
|
316
|
+
await fetch('/api/profile', {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
body,
|
|
319
|
+
})
|
|
320
|
+
```
|
|
377
321
|
|
|
378
|
-
|
|
322
|
+
Do not set the multipart `Content-Type` header manually; the browser supplies the boundary.
|
|
379
323
|
|
|
380
|
-
|
|
324
|
+
## Optional Luxon integration
|
|
381
325
|
|
|
382
|
-
|
|
326
|
+
Install Luxon separately, then import the additional date type from the dedicated subpath:
|
|
383
327
|
|
|
384
|
-
|
|
328
|
+
```bash
|
|
329
|
+
npm install luxon
|
|
330
|
+
```
|
|
385
331
|
|
|
386
|
-
|
|
332
|
+
```typescript
|
|
333
|
+
import { Field } from 'adaptform'
|
|
334
|
+
import { DateLuxonType } from 'adaptform/luxon'
|
|
335
|
+
import { DateTime } from 'luxon'
|
|
387
336
|
|
|
388
|
-
|
|
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
|
+
```
|
|
389
344
|
|
|
390
|
-
##
|
|
345
|
+
## API overview
|
|
391
346
|
|
|
392
|
-
|
|
393
|
-
|
|
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()` |
|
|
394
354
|
|
|
395
|
-
|
|
396
|
-
name = new Field(StringType, null);
|
|
397
|
-
age = new Field(NumberType, null);
|
|
398
|
-
}
|
|
355
|
+
## Development
|
|
399
356
|
|
|
400
|
-
|
|
357
|
+
Requirements: Node.js 22 (used by CI) and npm.
|
|
401
358
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
|
405
367
|
```
|
|
406
368
|
|
|
407
|
-
|
|
369
|
+
To run the interactive demo locally:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm run dev -- --config demo/vite.config.ts
|
|
373
|
+
```
|
|
408
374
|
|
|
409
|
-
|
|
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).
|
|
410
376
|
|
|
411
377
|
## Links
|
|
412
378
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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)
|
|
417
385
|
|
|
418
|
-
##
|
|
386
|
+
## License
|
|
419
387
|
|
|
420
|
-
|
|
388
|
+
MIT © Stanislav Orlov
|