adaptform 2.0.3 → 2.0.5

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 CHANGED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [2.0.0 - 2.0.4] - 2026-05-29
6
+
7
+ ### Added
8
+
9
+ - Complete TypeScript rewrite with full type inference
10
+ - Plugin system with MaskPlugin, FieldPlugin, and DecoratorPlugin
11
+ - ArrayField for dynamic field arrays
12
+ - ArrayFormField for nested form arrays
13
+ - New field types: DecimalType, DateLuxonType, BooleanType
14
+ - Optional Luxon integration via `adaptform/luxon` subpath export
15
+ - Input mask support with flexible configuration
16
+ - Conditional required fields based on form state
17
+ - Custom validation messages for all field types
18
+ - Global error handling for server-side errors
19
+ - Form serialization (`toJSON()`, `toFormData()`)
20
+ - Tree-shakeable exports for smaller bundles
21
+ - ESM and CJS module support
22
+ - Full TypeScript declarations with proper types
23
+
24
+ ### Changed
25
+
26
+ - **Complete API redesign** - new class-based architecture
27
+ - Field creation now uses `new Field(TypeClass, defaultValue, options)` pattern
28
+ - Form validation now requires explicit field declarations in class body
29
+ - Plugin system replaces old middleware approach
30
+ - Error handling unified across all field types
31
+ - Validation methods now return string arrays instead of boolean
32
+ - Options interface restructured for better type inference
33
+
34
+ ### Breaking Changes
35
+
36
+ - **Full rewrite from v1.x** - no backward compatibility
37
+ - Form must extend base `Form` class with field declarations
38
+ - Field constructors require TypeClass as first argument
39
+ - `validate()` now returns `string[]` instead of `boolean`
40
+ - Plugin API completely redesigned
41
+ - Import paths changed for tree-shaking support
42
+ - Luxon date type moved to separate entry point `adaptform/luxon`
43
+
44
+ ### Removed
45
+
46
+ - Legacy function-based API
47
+ - Old validation middleware system
48
+ - Global configuration singleton
49
+ - Automatic Luxon dependency (now optional)
50
+
51
+ [2.0.4]: https://github.com/ArhiMEN/adaptform/releases/tag/v2.0.4
package/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 ArhiMEN (Stanislav Orlov)
3
+ Copyright (c) 2025 Stanislav Orlov (ArhiMEN)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,420 @@
1
- Library of adaptive forms of validation
1
+ # AdaptForm
2
+
3
+ [![npm version](https://img.shields.io/npm/v/adaptform.svg)](https://www.npmjs.com/package/adaptform)
4
+ [![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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+
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.
13
+
14
+ ## AdaptForm provides:
15
+
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
23
+
24
+ ## Features
25
+
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
+ ````
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
+ ````
89
+
90
+ ## Installation
91
+
92
+ ```bash
93
+ npm install adaptform
94
+ ```
95
+
96
+ ## Optional: Luxon integration
97
+
98
+ ```Bash
99
+ npm install luxon
100
+ ```
101
+
102
+ ## Quick Start
103
+
104
+ ```TypeScript
105
+ import {Form, Field, StringType, NumberType} from 'adaptform';
106
+
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
+ }
112
+
113
+ const form = new LoginForm();
114
+
115
+ form.fields = {
116
+ login: 'john_doe',
117
+ password: 'securepass123'
118
+ };
119
+
120
+ console.log(form.isValid); // true
121
+ console.log(form.fieldsValue); // { login: 'john_doe', password: 'securepass123', remember: false }
122
+ ```
123
+
124
+ ## Usage Examples
125
+
126
+ ## Field Types
127
+
128
+ ```TypeScript
129
+ import {Field, StringType, NumberType, BooleanType, DecimalType, DateType} from 'adaptform';
130
+
131
+ // String with validation
132
+ const name = new Field(StringType, null, {
133
+ minLength: 2,
134
+ maxLength: 50,
135
+ pattern: /^[a-zA-Z]+$/
136
+ });
137
+
138
+ // Number with range
139
+ const age = new Field(NumberType, null, {
140
+ gt: 0, // greater than
141
+ lt: 120 // less than
142
+ });
143
+
144
+ // Boolean with requirement
145
+ const agreed = new Field(BooleanType, false, {
146
+ 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
+ });
168
+ ```
169
+
170
+ ## Conditional Required Fields
171
+
172
+ ```TypeScript
173
+ class SignUpForm extends Form {
174
+ country = new Field(StringType, null);
175
+
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
+ }
183
+ ```
184
+
185
+ ## Input Masks
186
+
187
+ ```TypeScript
188
+ import {Field, StringType, MaskPlugin} from 'adaptform';
189
+
190
+ const phone = new Field(StringType, null, {
191
+ plugins: [
192
+ new MaskPlugin({
193
+ maskFormat: '+7 (___) ___-__-__',
194
+ maskPlaceholder: '_',
195
+ digitPattern: /\d/
196
+ })
197
+ ]
198
+ });
199
+
200
+ phone.rawValue = '9991234567';
201
+ console.log(phone.valueClear); // '+7 (999) 123-45-67'
202
+ ```
203
+
204
+ ## Nested Forms
205
+
206
+ ```TypeScript
207
+ import {ArrayField, Field, NumberType} from 'adaptform';
208
+
209
+ const gameIds = new ArrayField(
210
+ (value?: number) => new Field(NumberType, value, {gt: 0}),
211
+ {minItems: 1}
212
+ );
213
+
214
+ gameIds.itemsValue = [1, 2, 3];
215
+ gameIds.add(4);
216
+ console.log(gameIds.itemsValue); // [1, 2, 3, 4]
217
+ ```
218
+
219
+ ## Luxon Date Integration
220
+
221
+ ```Bash
222
+ npm install luxon
223
+ ```
224
+
225
+ ```TypeScript
226
+ import {DateLuxonType} from 'adaptform/luxon';
227
+ import {DateTime} from 'luxon';
228
+
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
+ });
239
+
240
+ birthdate.rawValue = '15.06.1990';
241
+ console.log(birthdate.valueClear); // Luxon DateTime object
242
+ ```
243
+
244
+ ## Form Submission
245
+
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
+ }
254
+
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
+ ```
280
+
281
+ ## Global Errors
282
+
283
+ ```TypeScript
284
+ class LoginForm extends Form {
285
+ login = new Field(StringType, null);
286
+ password = new Field(StringType, null);
287
+ }
288
+
289
+ const form = new LoginForm();
290
+
291
+ // Add server-side errors
292
+ form.errors = {
293
+ login: 'Account not found',
294
+ password: 'Invalid password'
295
+ };
296
+
297
+ // Or set global error
298
+ form.setGlobalError('auth', 'Authentication failed');
299
+ ```
300
+
301
+ ## Plugin System
302
+
303
+ ### Built-in Plugins
304
+
305
+ * **MaskPlugin** - Input masking (phone, date, credit card)
306
+
307
+ * **FieldPlugin** - Base plugin for custom extensions
308
+
309
+ ### Creating Custom Plugins
310
+
311
+ ```TypeScript
312
+ import {BasePlugin} from 'adaptform';
313
+ import type {FieldPlugin} from 'adaptform';
314
+
315
+ class UppercasePlugin extends BasePlugin implements FieldPlugin<string> {
316
+ toRawValue(value: string): string {
317
+ return value?.toUpperCase() || '';
318
+ }
319
+
320
+ toValueClear(value: string): string {
321
+ return value?.toLowerCase() || '';
322
+ }
323
+ }
324
+
325
+ const field = new Field(StringType, null, {
326
+ plugins: [new UppercasePlugin()]
327
+ });
328
+ ```
329
+
330
+ ## API Reference
331
+
332
+ ### Form
333
+
334
+ * `checkValid(form?)` - Validate all fields
335
+
336
+ * `fieldsValue` - Get all field values
337
+
338
+ * `allErrors` - Get all validation errors
339
+
340
+ * `globalErrors` - Get global errors
341
+
342
+ * `toFormData()` - Convert to FormData
343
+
344
+ * `toJSON()` - Convert to JSON
345
+
346
+ * `reset()` - Reset all fields
347
+
348
+ ### Field
349
+
350
+ * `rawValue` - Set raw input value
351
+
352
+ * `valueClear` - Get cleaned/validated value
353
+
354
+ * `isValid` - Check if field is valid
355
+
356
+ * `error` - Get first error message
357
+
358
+ * `errors` - Get all error messages
359
+
360
+ * `isTouched` - Check if field was modified
361
+
362
+ * `isEmpty` - Check if field is empty
363
+
364
+ * `reset()` - Reset to default value
365
+
366
+ ### ArrayField
367
+
368
+ * `add(value?)` - Add new field
369
+
370
+ * `remove(index)` - Remove field at index
371
+
372
+ * `clear()` - Remove all fields
373
+
374
+ * `itemsValue` - Get array of values
375
+
376
+ * `items` - Get array of Field instances
377
+
378
+ ### ArrayFormField
379
+
380
+ * `add(data?)` - Add new form
381
+
382
+ * `remove(index)` - Remove form at index
383
+
384
+ * `clear()` - Remove all forms
385
+
386
+ * `forms` - Get array of Form instances
387
+
388
+ * `formsFieldsValue` - Get array of form values
389
+
390
+ ## TypeScript Support
391
+
392
+ ```TypeScript
393
+ import {Form, Field, StringType, NumberType} from 'adaptform';
394
+
395
+ class UserForm extends Form {
396
+ name = new Field(StringType, null);
397
+ age = new Field(NumberType, null);
398
+ }
399
+
400
+ const form = new UserForm();
401
+
402
+ // Full type inference
403
+ type UserData = typeof form.fieldsValue;
404
+ // { name: string | null; age: number | null; }
405
+ ```
406
+
407
+ ## License
408
+
409
+ MIT © [Stanislav Orlov](https://github.com/ArhiMEN)
410
+
411
+ ## Links
412
+
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)
417
+
418
+ ## Support
419
+
420
+ If you find this project useful, please give it a star on GitHub!
@@ -170,7 +170,7 @@ type FieldTypeConstructor<T, O extends FieldOptions = FieldOptions> = new (optio
170
170
  declare class Field<T = any, O extends FieldOptions = FieldOptions> {
171
171
  protected _rawValue: any;
172
172
  protected _valueClear: T | null;
173
- protected filed_type: FieldType<T, O>;
173
+ protected fieldType: FieldType<T, O>;
174
174
  protected defaultValue: T | null;
175
175
  protected _errors: string[];
176
176
  isValid: boolean | null;
@@ -192,15 +192,95 @@ declare class Field<T = any, O extends FieldOptions = FieldOptions> {
192
192
  reset(): void;
193
193
  }
194
194
 
195
+ interface ParsedError {
196
+ fieldName: string;
197
+ message: string;
198
+ sourceKey: string;
199
+ matchedBy: 'static' | 'dynamic' | 'fallback';
200
+ }
201
+ declare class ErrorsSchema {
202
+ private _staticRules;
203
+ private _dynamicRules;
204
+ private _fallbackHandler?;
205
+ private _valueTransformers;
206
+ /**
207
+ * Добавляет статическое правило - точное соответствие ключа ошибки
208
+ * @param errorKey - точный ключ ошибки из ответа сервера
209
+ * @param fieldName - имя поля в форме или функция для динамического определения
210
+ */
211
+ staticKey(errorKey: string, fieldName: string | ((errorValue: any) => string)): this;
212
+ /**
213
+ * Добавляет динамическое правило с регулярным выражением
214
+ * @param pattern - регулярное выражение для сопоставления ключа ошибки
215
+ * @param mapping - имя поля или функция для извлечения имени поля из совпадения
216
+ * @param options - дополнительные опции (приоритет и т.д.)
217
+ */
218
+ dynamicKey(pattern: RegExp, mapping: string | ((match: RegExpMatchArray) => string), options?: {
219
+ priority?: number;
220
+ }): this;
221
+ /**
222
+ * Устанавливает обработчик для ошибок, которые не подошли ни под одно правило
223
+ */
224
+ fallback(handler: (key: string, value: any) => string | null): this;
225
+ /**
226
+ * Добавляет трансформер для значений ошибок
227
+ * Полезно для нормализации сообщений (например, если сервер возвращает массив ошибок)
228
+ */
229
+ transformValue(transformer: (value: any) => any): this;
230
+ /**
231
+ * Быстрый метод для создания схемы из простого объекта маппинга
232
+ */
233
+ static fromObject(mapping: Record<string, string>): ErrorsSchema;
234
+ /**
235
+ * Создает схему для массива однотипных форм
236
+ */
237
+ static forArray(itemSchema: ErrorsSchema, arrayFieldName: string, indexExtractor?: (key: string) => number): ErrorsSchema;
238
+ /**
239
+ * Основной метод парсинга ошибок с поддержкой трансформеров
240
+ * Поддерживает вложенные структуры через точечную нотацию
241
+ */
242
+ parseErrors(errors: Record<string, any>, prefix?: string): Record<string, string>;
243
+ /**
244
+ * Парсит ошибки сервера и возвращает расширенную информацию
245
+ */
246
+ parseErrorsDetailed(errors: Record<string, any>, prefix?: string): Record<string, ParsedError>;
247
+ /**
248
+ * Создает схему для вложенной формы
249
+ */
250
+ nest(prefix: string): ErrorsSchema;
251
+ /**
252
+ * Клонирует текущую схему
253
+ */
254
+ clone(): ErrorsSchema;
255
+ /**
256
+ * Объединяет текущую схему с другой
257
+ */
258
+ merge(other: ErrorsSchema): ErrorsSchema;
259
+ private isNestedObject;
260
+ private findMappingDetailed;
261
+ private findMapping;
262
+ }
263
+
195
264
  declare abstract class Form {
196
265
  private _globalErrors;
266
+ errorsShema: ErrorsSchema;
197
267
  protected get _fields(): Record<string, Field<any> | ArrayField<any> | ArrayFormField<any>>;
198
268
  checkValid(form?: this): boolean;
199
269
  get fieldsValue(): Record<string, any>;
200
270
  set fields(data: Record<string, any>);
201
271
  get allErrors(): Record<string, string[]>;
272
+ /**
273
+ * Рекурсивно собирает все ошибки из ArrayFormField
274
+ */
275
+ private collectArrayFormErrors;
202
276
  get globalErrors(): Record<string, string>;
203
- set errors(errors: Record<string, string>);
277
+ set errors(errors: Record<string, any>);
278
+ /**
279
+ * Применяет ошибку к соответствующему полю
280
+ * Поддерживает вложенные поля через точечную нотацию
281
+ */
282
+ private applyErrorToField;
283
+ private applyNestedError;
204
284
  setGlobalError(key: string, message: string): void;
205
285
  clearGlobalErrors(): void;
206
286
  reset(): void;
@@ -210,4 +290,4 @@ declare abstract class Form {
210
290
  toJSON(): Record<string, any>;
211
291
  }
212
292
 
213
- export { type ArrayFieldOptions as A, BasePlugin as B, type DecimalOptions as D, Field as F, type MaskPluginOptions as M, type NumberOptions as N, type PluginOptions as P, type StringOptions as S, FieldType as a, type BooleanOptions as b, type DateOptions as c, Form as d, type messagesStringOptions as e, type messagesNumberOptions as f, type messagesBooleanOptions as g, type messagesDateOptions as h, type messagesDecimalOptions as i, type messagesArrayFieldOptions as j, type messagesArrayFormFieldOptions as k, type FieldOptions as l, type messagesOptions as m, type ArrayFormFieldOptions as n, ArrayField as o, ArrayFormField as p, type FieldPlugin as q };
293
+ export { ArrayField as A, BasePlugin as B, type DecimalOptions as D, Field as F, type MaskPluginOptions as M, type NumberOptions as N, type PluginOptions as P, type StringOptions as S, FieldType as a, type BooleanOptions as b, type DateOptions as c, type ArrayFieldOptions as d, ArrayFormField as e, type ArrayFormFieldOptions as f, type FieldOptions as g, type FieldPlugin as h, Form as i, type messagesArrayFormFieldOptions as j, type messagesBooleanOptions as k, type messagesDateOptions as l, type messagesArrayFieldOptions as m, type messagesDecimalOptions as n, type messagesNumberOptions as o, type messagesOptions as p, type messagesStringOptions as q };