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/README.md CHANGED
@@ -1,420 +1,422 @@
1
1
  # AdaptForm
2
2
 
3
+ [English](README.md) · [Русский](README.ru.md)
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/adaptform.svg)](https://www.npmjs.com/package/adaptform)
4
6
  [![npm downloads](https://img.shields.io/npm/dm/adaptform.svg)](https://www.npmjs.com/package/adaptform)
5
- [![bundle size](https://img.shields.io/bundlephobia/minzip/adaptform)](https://bundlephobia.com/package/adaptform)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
9
 
9
- AdaptForm is a TypeScript-first form modeling framework inspired by Django Forms and Pydantic.
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
- ## AdaptForm provides:
12
+ Maintainers: [automatic npm publication setup](docs/NPM_PUBLISH.md).
15
13
 
16
- - Typed form models
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
- - Form-as-a-domain-model architecture
27
- - Field-level state + validation ownership
28
- - Cross-field validation support
29
- - Plugin-based transformation pipeline
30
- - Strong TypeScript inference
31
- - Nested forms & collections
32
- - Built-in serialization (JSON / FormData)
33
- - Optional date engine (Luxon)
34
- - Framework-agnostic design
35
- - Zero-core-dependencies
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
- ````
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
- AdaptForm treats forms as stateful domain models:
32
+ ## Installation
49
33
 
50
- ```TypeScript
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
- field is not just a validator — it is a state container that owns:
36
+ ```bash
37
+ npm install adaptform
38
+ ```
58
39
 
59
- - value
60
- - validation
61
- - errors
62
- - transformation
63
- - plugins
64
- - type casting
40
+ Luxon integration is optional:
65
41
 
66
- The form itself becomes a single source of truth.
42
+ ```bash
43
+ npm install luxon
44
+ ```
67
45
 
68
- Simple Vue:
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
- ## Without AdaptForm
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
- A typical Vue form consists of multiple disconnected parts:
50
+ ## Quick start
73
51
 
74
- - reactive state
75
- - validation schema
76
- - error store
77
- - input masks
78
- - submission mapping
52
+ ```typescript
53
+ import { BooleanType, Field, Form, StringType } from 'adaptform'
79
54
 
80
- ## With AdaptForm
55
+ class RegistrationForm extends Form {
56
+ name = new Field(StringType, null, {
57
+ isRequired: true,
58
+ minLength: 2,
59
+ maxLength: 50,
60
+ })
81
61
 
82
- Everything is unified into a single form model:
62
+ email = new Field(StringType, null, {
63
+ isRequired: true,
64
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
65
+ })
83
66
 
84
- ```TypeScript
85
- class RegistrationForm extends Form {
86
- phone = new Field(StringType)
67
+ acceptedTerms = new Field(BooleanType, false, {
68
+ mustBeTrue: true,
69
+ })
87
70
  }
88
- ````
89
71
 
90
- ## Installation
72
+ const form = new RegistrationForm()
73
+ form.fields = {
74
+ name: 'Alex Morgan',
75
+ email: 'alex@example.com',
76
+ acceptedTerms: true,
77
+ }
91
78
 
92
- ```bash
93
- npm install adaptform
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
- ## Optional: Luxon integration
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
- ```Bash
99
- npm install luxon
100
- ```
89
+ ## Field types and options
101
90
 
102
- ## Quick Start
91
+ All field types are constructed using `new Field(TypeConstructor, defaultValue?, options?)`.
103
92
 
104
- ```TypeScript
105
- import {Form, Field, StringType, NumberType} from 'adaptform';
93
+ ### StringType
106
94
 
107
- class LoginForm extends Form {
108
- login = new Field(StringType, null, {maxLength: 50});
109
- password = new Field(StringType, null, {minLength: 8});
110
- remember = new Field(BooleanType, false);
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
- const form = new LoginForm();
109
+ Supported constraints: `isRequired`, `minLength`, `maxLength`, and `pattern`.
114
110
 
115
- form.fields = {
116
- login: 'john_doe',
117
- password: 'securepass123'
118
- };
111
+ ### NumberType
119
112
 
120
- console.log(form.isValid); // true
121
- console.log(form.fieldsValue); // { login: 'john_doe', password: 'securepass123', remember: false }
113
+ ```typescript
114
+ const age = new Field(NumberType, null, {
115
+ isRequired: true,
116
+ ge: 18,
117
+ le: 120,
118
+ })
122
119
  ```
123
120
 
124
- ## Usage Examples
121
+ Supported constraints: `gt`, `ge`, `lt`, and `le`. Values must be finite integers; blank strings and non-finite numbers are rejected.
125
122
 
126
- ## Field Types
123
+ ### DecimalType
127
124
 
128
- ```TypeScript
129
- import {Field, StringType, NumberType, BooleanType, DecimalType, DateType} from 'adaptform';
125
+ ```typescript
126
+ const price = new Field(DecimalType, null, {
127
+ decimalPlaces: 2,
128
+ maxDigits: 8,
129
+ ge: 0,
130
+ })
131
+ ```
130
132
 
131
- // String with validation
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
- // Number with range
139
- const age = new Field(NumberType, null, {
140
- gt: 0, // greater than
141
- lt: 120 // less than
142
- });
135
+ ### BooleanType
143
136
 
144
- // Boolean with requirement
145
- const agreed = new Field(BooleanType, false, {
137
+ ```typescript
138
+ const accepted = new Field(BooleanType, false, {
146
139
  mustBeTrue: true,
147
- messages: {
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
- ## Conditional Required Fields
143
+ Supports `mustBeTrue` and `mustBeFalse`.
171
144
 
172
- ```TypeScript
173
- class SignUpForm extends Form {
174
- country = new Field(StringType, null);
145
+ ### DateType
175
146
 
176
- state = new Field(StringType, null, {
177
- isRequired: (form) => form?.country?.valueClear === 'USA',
178
- messages: {
179
- isRequired: 'State is required for USA residents'
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
- ## Input Masks
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
- ```TypeScript
188
- import {Field, StringType, MaskPlugin} from 'adaptform';
157
+ ### Custom validation and messages
189
158
 
190
- const phone = new Field(StringType, null, {
191
- plugins: [
192
- new MaskPlugin({
193
- maskFormat: '+7 (___) ___-__-__',
194
- maskPlaceholder: '_',
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
- phone.rawValue = '9991234567';
201
- console.log(phone.valueClear); // '+7 (999) 123-45-67'
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
- ## Nested Forms
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
- ```TypeScript
207
- import {ArrayField, Field, NumberType} from 'adaptform';
175
+ ## Validation lifecycle and state
208
176
 
209
- const gameIds = new ArrayField(
210
- (value?: number) => new Field(NumberType, value, {gt: 0}),
211
- {minItems: 1}
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
- gameIds.itemsValue = [1, 2, 3];
215
- gameIds.add(4);
216
- console.log(gameIds.itemsValue); // [1, 2, 3, 4]
217
- ```
188
+ ## Dynamic collections
218
189
 
219
- ## Luxon Date Integration
190
+ ### ArrayField
220
191
 
221
- ```Bash
222
- npm install luxon
223
- ```
192
+ Use `ArrayField` for a dynamic list of similarly typed fields.
224
193
 
225
- ```TypeScript
226
- import {DateLuxonType} from 'adaptform/luxon';
227
- import {DateTime} from 'luxon';
194
+ ```typescript
195
+ import { ArrayField, Field, StringType } from 'adaptform'
228
196
 
229
- const birthdate = new Field(DateLuxonType, null, {
230
- isDatetime: false,
231
- minDate: DateTime.fromISO('1900-01-01'),
232
- maxDate: DateTime.now(),
233
- inputFormat: 'dd.MM.yyyy',
234
- messages: {
235
- minDate: 'Date must be after 1900',
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
- birthdate.rawValue = '15.06.1990';
241
- console.log(birthdate.valueClear); // Luxon DateTime object
205
+ tags.add('typescript')
206
+ tags.add('forms')
207
+ console.log(tags.itemsValue)
208
+ console.log(tags.isValid)
242
209
  ```
243
210
 
244
- ## Form Submission
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
- ```TypeScript
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
- const form = new ContactForm();
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
- ## Global Errors
217
+ ```typescript
218
+ import { ArrayFormField, Field, Form, StringType } from 'adaptform'
282
219
 
283
- ```TypeScript
284
- class LoginForm extends Form {
285
- login = new Field(StringType, null);
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 form = new LoginForm();
290
-
291
- // Add server-side errors
292
- form.errors = {
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
- // Or set global error
298
- form.setGlobalError('auth', 'Authentication failed');
230
+ addresses.add({ city: 'Berlin', postalCode: '10115' })
231
+ console.log(addresses.formsFieldsValue)
299
232
  ```
300
233
 
301
- ## Plugin System
234
+ Options include `isRequired`, `minForms`, `maxForms`, `messages`, and a synchronous `validate(forms, form)` callback.
302
235
 
303
- ### Built-in Plugins
236
+ ## Input masks
304
237
 
305
- * **MaskPlugin** - Input masking (phone, date, credit card)
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
- * **FieldPlugin** - Base plugin for custom extensions
240
+ ```typescript
241
+ import { Field, MaskPlugin, StringType } from 'adaptform'
308
242
 
309
- ### Creating Custom Plugins
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
- ```TypeScript
312
- import {BasePlugin} from 'adaptform';
313
- import type {FieldPlugin} from 'adaptform';
253
+ phone.rawValue = '4155550134'
254
+ console.log(phone.rawValue)
255
+ console.log(phone.valueClear)
256
+ ```
314
257
 
315
- class UppercasePlugin extends BasePlugin implements FieldPlugin<string> {
316
- toRawValue(value: string): string {
317
- return value?.toUpperCase() || '';
318
- }
258
+ ## Server-side errors with ErrorsSchema
319
259
 
320
- toValueClear(value: string): string {
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
- const field = new Field(StringType, null, {
326
- plugins: [new UppercasePlugin()]
327
- });
328
- ```
262
+ ```typescript
263
+ import { ErrorsSchema, Form, Field, StringType } from 'adaptform'
329
264
 
330
- ## API Reference
265
+ class AccountForm extends Form {
266
+ email = new Field(StringType)
267
+ username = new Field(StringType)
268
+ }
331
269
 
332
- ### Form
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
- * `checkValid(form?)` - Validate all fields
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
- * `fieldsValue` - Get all field values
282
+ console.log(form.allErrors)
283
+ ```
337
284
 
338
- * `allErrors` - Get all validation errors
285
+ Available schema methods:
339
286
 
340
- * `globalErrors` - Get global errors
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
- * `toFormData()` - Convert to FormData
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
- * `toJSON()` - Convert to JSON
298
+ ## Serialization
345
299
 
346
- * `reset()` - Reset all fields
300
+ ### JSON
347
301
 
348
- ### Field
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
- * `rawValue` - Set raw input value
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
- * `valueClear` - Get cleaned/validated value
313
+ ### FormData
353
314
 
354
- * `isValid` - Check if field is valid
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
- * `error` - Get first error message
317
+ ```typescript
318
+ const body = form.toFormData()
319
+ await fetch('/api/profile', {
320
+ method: 'POST',
321
+ body,
322
+ })
323
+ ```
357
324
 
358
- * `errors` - Get all error messages
325
+ Do not set the multipart `Content-Type` header manually; the browser supplies the boundary.
359
326
 
360
- * `isTouched` - Check if field was modified
327
+ ## Optional Luxon integration
361
328
 
362
- * `isEmpty` - Check if field is empty
329
+ Install Luxon separately, then import the additional date type from the dedicated subpath:
363
330
 
364
- * `reset()` - Reset to default value
331
+ ```bash
332
+ npm install luxon
333
+ ```
365
334
 
366
- ### ArrayField
335
+ ```typescript
336
+ import { Field } from 'adaptform'
337
+ import { DateLuxonType } from 'adaptform/luxon'
338
+ import { DateTime } from 'luxon'
367
339
 
368
- * `add(value?)` - Add new field
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
- * `remove(index)` - Remove field at index
348
+ ## API overview
371
349
 
372
- * `clear()` - Remove all fields
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
- * `itemsValue` - Get array of values
358
+ ## Development
375
359
 
376
- * `items` - Get array of Field instances
360
+ Requirements: Node.js 22 (used by CI) and npm.
377
361
 
378
- ### ArrayFormField
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
- * `add(data?)` - Add new form
372
+ To run the interactive demo locally:
381
373
 
382
- * `remove(index)` - Remove form at index
374
+ ```bash
375
+ npm run dev -- --config demo/vite.config.ts
376
+ ```
383
377
 
384
- * `clear()` - Remove all forms
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
- * `forms` - Get array of Form instances
380
+ ## Links
387
381
 
388
- * `formsFieldsValue` - Get array of form values
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
- ## TypeScript Support
389
+ ## License
391
390
 
392
- ```TypeScript
393
- import {Form, Field, StringType, NumberType} from 'adaptform';
391
+ MIT © Stanislav Orlov
394
392
 
395
- class UserForm extends Form {
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
- const form = new UserForm();
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
- // Full type inference
403
- type UserData = typeof form.fieldsValue;
404
- // { name: string | null; age: number | null; }
405
- ```
397
+ ```typescript
398
+ import {Field, Form, NumberType, StringType} from 'adaptform'
406
399
 
407
- ## License
400
+ class SettingsForm extends Form {
401
+ displayName = new Field(StringType, 'Alex')
402
+ notifications = new Field(NumberType, 1)
403
+ }
408
404
 
409
- MIT © [Stanislav Orlov](https://github.com/ArhiMEN)
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
- ## Links
411
+ form.batch(() => {
412
+ form.displayName.rawValue = 'Alexandra'
413
+ form.notifications.rawValue = 2
414
+ }) // One coherent notification after final automatic validation
412
415
 
413
- * [GitHub Repository](https://github.com/ArhiMEN/adaptform)
414
- * [npm Package](https://www.npmjs.com/package/adaptform)
415
- * [Bug Reports](https://github.com/ArhiMEN/adaptform/issues)
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
- ## Support
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
- If you find this project useful, please give it a star on GitHub!
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).