@reformer/core 1.0.0-beta.9 → 1.1.0-beta.1
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.
|
@@ -13,13 +13,19 @@ import { TreeValidatorFn, ValidateTreeOptions } from '../../types';
|
|
|
13
13
|
* @group Validation
|
|
14
14
|
* @category Core Functions
|
|
15
15
|
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Параметр `ctx` в callback требует явной типизации для корректного вывода типов:
|
|
18
|
+
* ```typescript
|
|
19
|
+
* validateTree((ctx: { form: MyFormType }) => { ... });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
16
22
|
* @example
|
|
17
23
|
* ```typescript
|
|
24
|
+
* // Явная типизация ctx для избежания implicit any
|
|
18
25
|
* validateTree(
|
|
19
|
-
* (ctx:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* if (form.initialPayment > form.propertyValue) {
|
|
26
|
+
* (ctx: { form: MyForm }) => {
|
|
27
|
+
* if (ctx.form.initialPayment && ctx.form.propertyValue) {
|
|
28
|
+
* if (ctx.form.initialPayment > ctx.form.propertyValue) {
|
|
23
29
|
* return {
|
|
24
30
|
* code: 'initialPaymentTooHigh',
|
|
25
31
|
* message: 'Первоначальный взнос не может превышать стоимость',
|
|
@@ -13,13 +13,19 @@ import { getCurrentValidationRegistry } from '../../utils/registry-helpers';
|
|
|
13
13
|
* @group Validation
|
|
14
14
|
* @category Core Functions
|
|
15
15
|
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Параметр `ctx` в callback требует явной типизации для корректного вывода типов:
|
|
18
|
+
* ```typescript
|
|
19
|
+
* validateTree((ctx: { form: MyFormType }) => { ... });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
16
22
|
* @example
|
|
17
23
|
* ```typescript
|
|
24
|
+
* // Явная типизация ctx для избежания implicit any
|
|
18
25
|
* validateTree(
|
|
19
|
-
* (ctx:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* if (form.initialPayment > form.propertyValue) {
|
|
26
|
+
* (ctx: { form: MyForm }) => {
|
|
27
|
+
* if (ctx.form.initialPayment && ctx.form.propertyValue) {
|
|
28
|
+
* if (ctx.form.initialPayment > ctx.form.propertyValue) {
|
|
23
29
|
* return {
|
|
24
30
|
* code: 'initialPaymentTooHigh',
|
|
25
31
|
* message: 'Первоначальный взнос не может превышать стоимость',
|
package/llms.txt
CHANGED
|
@@ -29,7 +29,7 @@ maxLength(path, length: number, options?: { message?: string })
|
|
|
29
29
|
email(path, options?: { message?: string })
|
|
30
30
|
validate(path, validator: (value) => ValidationError | null)
|
|
31
31
|
validateTree(validator: (ctx) => ValidationError | null)
|
|
32
|
-
|
|
32
|
+
applyWhen(fieldPath, condition: (fieldValue) => boolean, validatorsFn: (path) => void)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
### Behaviors
|
|
@@ -38,8 +38,15 @@ when(condition: (form) => boolean, validatorsFn: () => void)
|
|
|
38
38
|
enableWhen(path, condition: (form) => boolean, options?: { resetOnDisable?: boolean })
|
|
39
39
|
disableWhen(path, condition: (form) => boolean)
|
|
40
40
|
computeFrom(sourcePaths[], targetPath, compute: (values) => result)
|
|
41
|
-
watchField(path, callback: (value, ctx) => void)
|
|
41
|
+
watchField(path, callback: (value, ctx: BehaviorContext) => void)
|
|
42
42
|
copyFrom(sourcePath, targetPath, options?: { when?, fields?, transform? })
|
|
43
|
+
|
|
44
|
+
// BehaviorContext interface:
|
|
45
|
+
interface BehaviorContext<TForm> {
|
|
46
|
+
form: TForm; // Current form state
|
|
47
|
+
setFieldValue: (path, value) => void; // Set field value
|
|
48
|
+
getFieldValue: (path) => unknown; // Get field value
|
|
49
|
+
}
|
|
43
50
|
```
|
|
44
51
|
|
|
45
52
|
## 3. COMMON PATTERNS
|
|
@@ -141,7 +148,7 @@ import type {
|
|
|
141
148
|
import { createForm, useFormControl } from '@reformer/core';
|
|
142
149
|
|
|
143
150
|
// Validators - from /validators submodule
|
|
144
|
-
import { required, min, max, email, validate,
|
|
151
|
+
import { required, min, max, email, validate, applyWhen } from '@reformer/core/validators';
|
|
145
152
|
|
|
146
153
|
// Behaviors - from /behaviors submodule
|
|
147
154
|
import { computeFrom, enableWhen, watchField, copyFrom } from '@reformer/core/behaviors';
|
|
@@ -169,10 +176,248 @@ interface MyForm {
|
|
|
169
176
|
city: string;
|
|
170
177
|
};
|
|
171
178
|
|
|
172
|
-
// Arrays
|
|
179
|
+
// Arrays - use tuple format for schema
|
|
173
180
|
items: Array<{
|
|
174
181
|
id: string;
|
|
175
182
|
name: string;
|
|
176
183
|
}>;
|
|
177
184
|
}
|
|
178
185
|
```
|
|
186
|
+
|
|
187
|
+
## 8. CREATEFORM API
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Full config with behavior and validation
|
|
191
|
+
const form = createForm<MyForm>({
|
|
192
|
+
form: formSchema, // Required: form schema
|
|
193
|
+
behavior: behaviorSchema, // Optional: behavior rules
|
|
194
|
+
validation: validationSchema, // Optional: validation rules
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Legacy format (schema only)
|
|
198
|
+
const form = createForm<MyForm>(formSchema);
|
|
199
|
+
|
|
200
|
+
// Form schema example
|
|
201
|
+
const formSchema: FormSchema<MyForm> = {
|
|
202
|
+
name: '',
|
|
203
|
+
email: '',
|
|
204
|
+
address: {
|
|
205
|
+
street: '',
|
|
206
|
+
city: '',
|
|
207
|
+
},
|
|
208
|
+
// Arrays use tuple format
|
|
209
|
+
items: [{ id: '', name: '' }] as [{ id: string; name: string }],
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 9. ARRAY SCHEMA FORMAT
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// ✅ CORRECT - use tuple format for arrays
|
|
217
|
+
const schema = {
|
|
218
|
+
items: [itemSchema] as [typeof itemSchema],
|
|
219
|
+
properties: [propertySchema] as [typeof propertySchema],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ❌ WRONG - object format is NOT supported
|
|
223
|
+
const schema = {
|
|
224
|
+
items: { schema: itemSchema, initialItems: [] }, // This will NOT work
|
|
225
|
+
};
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 10. ASYNC WATCHFIELD (CRITICALLY IMPORTANT)
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// ✅ CORRECT - async watchField with ALL safeguards
|
|
232
|
+
watchField(
|
|
233
|
+
path.parentField,
|
|
234
|
+
async (value, ctx) => {
|
|
235
|
+
if (!value) return; // Guard clause
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const { data } = await fetchData(value);
|
|
239
|
+
ctx.form.dependentField.updateComponentProps({ options: data });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('Failed:', error);
|
|
242
|
+
ctx.form.dependentField.updateComponentProps({ options: [] });
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{ immediate: false, debounce: 300 } // REQUIRED options
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// ❌ WRONG - missing safeguards
|
|
249
|
+
watchField(path.field, async (value, ctx) => {
|
|
250
|
+
const { data } = await fetchData(value); // Will fail silently!
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Required Options for async watchField:
|
|
255
|
+
- `immediate: false` - prevents execution during initialization
|
|
256
|
+
- `debounce: 300` - prevents excessive API calls (300-500ms recommended)
|
|
257
|
+
- Guard clause - skip if value is empty
|
|
258
|
+
- try-catch - handle errors explicitly
|
|
259
|
+
|
|
260
|
+
## 11. ARRAY CLEANUP PATTERN
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// ✅ CORRECT - cleanup array when checkbox unchecked
|
|
264
|
+
watchField(
|
|
265
|
+
path.hasItems,
|
|
266
|
+
(hasItems, ctx) => {
|
|
267
|
+
if (!hasItems && ctx.form.items) {
|
|
268
|
+
ctx.form.items.clear();
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{ immediate: false }
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// ❌ WRONG - no immediate: false, no null check
|
|
275
|
+
watchField(path.hasItems, (hasItems, ctx) => {
|
|
276
|
+
if (!hasItems) ctx.form.items.clear(); // May crash on init!
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## 12. MULTI-STEP FORM VALIDATION
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Step-specific validation schemas
|
|
284
|
+
const step1Validation: ValidationSchemaFn<Form> = (path) => {
|
|
285
|
+
required(path.loanType);
|
|
286
|
+
required(path.loanAmount);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const step2Validation: ValidationSchemaFn<Form> = (path) => {
|
|
290
|
+
required(path.personalData.firstName);
|
|
291
|
+
required(path.personalData.lastName);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// STEP_VALIDATIONS map for useStepForm hook
|
|
295
|
+
export const STEP_VALIDATIONS = {
|
|
296
|
+
1: step1Validation,
|
|
297
|
+
2: step2Validation,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Full validation (combines all steps)
|
|
301
|
+
export const fullValidation: ValidationSchemaFn<Form> = (path) => {
|
|
302
|
+
step1Validation(path);
|
|
303
|
+
step2Validation(path);
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## 13. ⚠️ EXTENDED COMMON MISTAKES
|
|
308
|
+
|
|
309
|
+
### Behavior Composition (Cycle Error)
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// ❌ WRONG - apply() in behavior causes "Cycle detected"
|
|
313
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
314
|
+
apply(addressBehavior, path.address); // WILL FAIL!
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// ✅ CORRECT - inline or use setup function
|
|
318
|
+
const setupAddressBehavior = (path: FieldPath<Address>) => {
|
|
319
|
+
watchField(path.region, async (region, ctx) => {
|
|
320
|
+
// ...
|
|
321
|
+
}, { immediate: false });
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const mainBehavior: BehaviorSchemaFn<Form> = (path) => {
|
|
325
|
+
setupAddressBehavior(path.address); // Works!
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Infinite Loop in watchField
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// ❌ WRONG - causes infinite loop
|
|
333
|
+
watchField(path.field, (value, ctx) => {
|
|
334
|
+
ctx.form.field.setValue(value.toUpperCase()); // Loop!
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ✅ CORRECT - write to different field OR add guard
|
|
338
|
+
watchField(path.input, (value, ctx) => {
|
|
339
|
+
const upper = value?.toUpperCase() || '';
|
|
340
|
+
if (ctx.form.display.value.value !== upper) {
|
|
341
|
+
ctx.form.display.setValue(upper);
|
|
342
|
+
}
|
|
343
|
+
}, { immediate: false });
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### validateTree Typing
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// ❌ WRONG - implicit any
|
|
350
|
+
validateTree((ctx) => { ... });
|
|
351
|
+
|
|
352
|
+
// ✅ CORRECT - explicit typing
|
|
353
|
+
validateTree((ctx: { form: MyForm }) => {
|
|
354
|
+
if (ctx.form.field1 > ctx.form.field2) {
|
|
355
|
+
return { code: 'error', message: 'Invalid' };
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## 14. PROJECT STRUCTURE (COLOCATION)
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
src/
|
|
365
|
+
├── components/ui/ # Reusable UI components
|
|
366
|
+
│ ├── FormField.tsx
|
|
367
|
+
│ └── FormArrayManager.tsx
|
|
368
|
+
│
|
|
369
|
+
├── forms/
|
|
370
|
+
│ └── [form-name]/ # Form module
|
|
371
|
+
│ ├── type.ts # Main form type
|
|
372
|
+
│ ├── schema.ts # Main schema
|
|
373
|
+
│ ├── validators.ts # Validators
|
|
374
|
+
│ ├── behaviors.ts # Behaviors
|
|
375
|
+
│ ├── [FormName]Form.tsx # Main component
|
|
376
|
+
│ │
|
|
377
|
+
│ ├── steps/ # Multi-step wizard
|
|
378
|
+
│ │ ├── loan-info/
|
|
379
|
+
│ │ │ ├── type.ts
|
|
380
|
+
│ │ │ ├── schema.ts
|
|
381
|
+
│ │ │ ├── validators.ts
|
|
382
|
+
│ │ │ ├── behaviors.ts
|
|
383
|
+
│ │ │ └── LoanInfoForm.tsx
|
|
384
|
+
│ │ └── ...
|
|
385
|
+
│ │
|
|
386
|
+
│ └── sub-forms/ # Reusable sub-forms
|
|
387
|
+
│ ├── address/
|
|
388
|
+
│ └── personal-data/
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Key Files
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// forms/credit-application/type.ts
|
|
395
|
+
export type { LoanInfoStep } from './steps/loan-info/type';
|
|
396
|
+
export interface CreditApplicationForm {
|
|
397
|
+
loanType: LoanType;
|
|
398
|
+
loanAmount: number;
|
|
399
|
+
// ...
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// forms/credit-application/schema.ts
|
|
403
|
+
import { loanInfoSchema } from './steps/loan-info/schema';
|
|
404
|
+
export const creditApplicationSchema = {
|
|
405
|
+
...loanInfoSchema,
|
|
406
|
+
monthlyPayment: { value: 0, disabled: true },
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// forms/credit-application/validators.ts
|
|
410
|
+
import { loanValidation } from './steps/loan-info/validators';
|
|
411
|
+
export const creditApplicationValidation: ValidationSchemaFn<Form> = (path) => {
|
|
412
|
+
loanValidation(path);
|
|
413
|
+
// Cross-step validation...
|
|
414
|
+
};
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Scaling
|
|
418
|
+
|
|
419
|
+
| Complexity | Structure |
|
|
420
|
+
|------------|-----------|
|
|
421
|
+
| Simple | Single file: `ContactForm.tsx` |
|
|
422
|
+
| Medium | Separate files: `type.ts`, `schema.ts`, `validators.ts`, `Form.tsx` |
|
|
423
|
+
| Complex | Full colocation with `steps/` and `sub-forms/` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reformer/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0-beta.1",
|
|
4
4
|
"description": "Reactive form state management library for React with signals-based architecture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -77,8 +77,8 @@
|
|
|
77
77
|
"@types/uuid": "^10.0.0",
|
|
78
78
|
"@vitejs/plugin-react": "^5.1.0",
|
|
79
79
|
"@vitest/utils": "^4.0.8",
|
|
80
|
-
"react": "^19.2.
|
|
81
|
-
"react-dom": "^19.2.
|
|
80
|
+
"react": "^19.2.1",
|
|
81
|
+
"react-dom": "^19.2.1",
|
|
82
82
|
"typescript": "^5.9.3",
|
|
83
83
|
"vite": "^7.2.2",
|
|
84
84
|
"vite-plugin-dts": "^4.5.4",
|