@reformer/core 11.0.0-beta.3 → 11.0.0-beta.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/dist/behaviors.d.ts +1 -1
- package/dist/behaviors.js +37 -37
- package/dist/create-model-Dxh9cfLp.js +97 -0
- package/dist/form/behaviors/collections.d.ts +50 -0
- package/dist/form/behaviors/context.d.ts +23 -0
- package/dist/form/behaviors/index.d.ts +28 -0
- package/dist/form/behaviors/internals.d.ts +26 -0
- package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +3 -3
- package/dist/form/behaviors/operators.d.ts +51 -0
- package/dist/form/behaviors/types.d.ts +17 -0
- package/dist/form/create-core-form.d.ts +3 -3
- package/dist/form/create-form.d.ts +12 -10
- package/dist/form/factories/node-factory.d.ts +17 -16
- package/dist/form/index.d.ts +10 -16
- package/dist/form/nodes/array-node.d.ts +3 -3
- package/dist/form/nodes/field-node.d.ts +6 -3
- package/dist/form/nodes/group-node.d.ts +7 -7
- package/dist/form/nodes/model-array-node.d.ts +3 -3
- package/dist/form/types/contracts.d.ts +9 -6
- package/dist/form/types/deep-schema.d.ts +5 -5
- package/dist/form/types/validation-schema.d.ts +1 -1
- package/dist/form/{validation-config.d.ts → validation/config.d.ts} +3 -3
- package/dist/form/validation/context.d.ts +27 -0
- package/dist/form/{error-handler.d.ts → validation/error-handler.d.ts} +1 -1
- package/dist/form/validation/index.d.ts +23 -28
- package/dist/form/validation/operators.d.ts +42 -0
- package/dist/form/validation/run.d.ts +45 -0
- package/dist/form/{validation-strategy.d.ts → validation/strategy.d.ts} +2 -2
- package/dist/form/validation/types.d.ts +28 -0
- package/dist/form/{validation/validators → validators}/date-utils.d.ts +1 -1
- package/dist/form/{validation/validators → validators}/email.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/file-type.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/file-utils.d.ts +1 -1
- package/dist/form/{validation/validators → validators}/future-date.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
- package/dist/form/{validation/validators → validators}/integer.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/is-date.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/is-number.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/max-age.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/max-date.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/max-file-size.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/max-files.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/max-length.d.ts +7 -8
- package/dist/form/{validation/validators → validators}/max-total-file-size.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/max.d.ts +7 -8
- package/dist/form/{validation/validators → validators}/min-age.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/min-date.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/min-file-size.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/min-files.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/min-length.d.ts +7 -8
- package/dist/form/{validation/validators → validators}/min.d.ts +7 -8
- package/dist/form/{validation/validators → validators}/multiple-of.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/non-negative.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/non-zero.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/past-date.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/pattern.d.ts +7 -15
- package/dist/form/{validation/validators → validators}/phone.d.ts +6 -7
- package/dist/form/{validation/validators → validators}/required.d.ts +8 -13
- package/dist/form/{validation/validators → validators}/url.d.ts +8 -13
- package/dist/index.d.ts +7 -4
- package/dist/index.js +272 -94
- package/dist/{state/form-model.d.ts → model/create-model.d.ts} +0 -14
- package/dist/{state → model}/index.d.ts +10 -11
- package/dist/model/model-nodes.d.ts +60 -0
- package/dist/model/model-signals-proxy.d.ts +32 -0
- package/dist/model/model-value-proxy.d.ts +3 -0
- package/dist/{state → model}/safe-effect.d.ts +1 -1
- package/dist/model-signals-proxy-mV3WGdlO.js +233 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +21 -0
- package/dist/{form → platforms/react}/hooks/types.d.ts +1 -1
- package/dist/{form → platforms/react}/hooks/use-form-bundle.d.ts +1 -1
- package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +3 -3
- package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +1 -1
- package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +3 -3
- package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +2 -2
- package/dist/platforms/react/index.d.ts +23 -0
- package/dist/signals.d.ts +1 -1
- package/dist/strategy-DA3JwxsR.js +170 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +10 -165
- package/dist/validators/email.d.ts +1 -1
- package/dist/validators/future-date.d.ts +1 -1
- package/dist/validators/integer.d.ts +1 -1
- package/dist/validators/is-date.d.ts +1 -1
- package/dist/validators/is-number.d.ts +1 -1
- package/dist/validators/max-age.d.ts +1 -1
- package/dist/validators/max-date.d.ts +1 -1
- package/dist/validators/max-length.d.ts +1 -1
- package/dist/validators/max.d.ts +1 -1
- package/dist/validators/min-age.d.ts +1 -1
- package/dist/validators/min-date.d.ts +1 -1
- package/dist/validators/min-length.d.ts +1 -1
- package/dist/validators/min.d.ts +1 -1
- package/dist/validators/multiple-of.d.ts +1 -1
- package/dist/validators/non-negative.d.ts +1 -1
- package/dist/validators/non-zero.d.ts +1 -1
- package/dist/validators/past-date.d.ts +1 -1
- package/dist/validators/pattern.d.ts +1 -1
- package/dist/validators/phone.d.ts +1 -1
- package/dist/validators/required.d.ts +1 -1
- package/dist/validators/url.d.ts +1 -1
- package/dist/validators.d.ts +1 -1
- package/docs/llms/02-quick-start.md +6 -6
- package/docs/llms/03-api-signatures.md +3 -3
- package/docs/llms/05-common-mistakes.md +1 -1
- package/docs/llms/09-formschema.md +11 -11
- package/docs/llms/10-arrays.md +4 -4
- package/docs/llms/14-extended-mistakes.md +1 -1
- package/docs/llms/15-project-structure.md +8 -7
- package/docs/llms/16-ui-components.md +6 -6
- package/docs/llms/17-nonexistent-api.md +2 -2
- package/docs/llms/29-async-preload.md +4 -4
- package/docs/llms/30-type-safety-recipes.md +1 -1
- package/llms-index.json +1 -0
- package/llms.txt +2102 -262
- package/package.json +5 -4
- package/dist/form/behaviors.d.ts +0 -133
- package/dist/form/validation-schema.d.ts +0 -106
- package/dist/form/validators.d.ts +0 -11
- package/dist/form-model-DhW2HZOB.js +0 -323
- package/dist/state.d.ts +0 -2
- package/dist/state.js +0 -21
- package/dist/subscription-manager-705bls97.js +0 -171
- /package/dist/{state → form/nodes}/subscription-manager.d.ts +0 -0
- /package/dist/{state → model}/behaviors-value.d.ts +0 -0
- /package/dist/{state → model}/derived-registry.d.ts +0 -0
- /package/dist/{state → model}/types.d.ts +0 -0
- /package/dist/{behaviors-node-DGEFMTQu.js → node-DGEFMTQu.js} +0 -0
- /package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +0 -0
|
@@ -11,7 +11,7 @@ import { ConfigWithSchema, ConfigWithValue, UnknownRecord } from '../types/index
|
|
|
11
11
|
* import { NodeFactory } from '@reformer/core';
|
|
12
12
|
*
|
|
13
13
|
* const factory = new NodeFactory();
|
|
14
|
-
* const node = factory.createNode({ value: '', component:
|
|
14
|
+
* const node = factory.createNode({ value: '', component: InputField }); // → FieldNode<string>
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
export declare class NodeFactory {
|
|
@@ -33,28 +33,29 @@ export declare class NodeFactory {
|
|
|
33
33
|
* ```typescript
|
|
34
34
|
* const factory = new NodeFactory();
|
|
35
35
|
*
|
|
36
|
+
* Конфиг node-level (не layout-схема M1, где `validators` уже нет).
|
|
36
37
|
* // FieldNode
|
|
37
38
|
* const field = factory.createNode({
|
|
38
39
|
* value: 'test@mail.com',
|
|
39
|
-
* component:
|
|
40
|
+
* component: InputField,
|
|
40
41
|
* validators: [required, email]
|
|
41
42
|
* });
|
|
42
43
|
*
|
|
43
44
|
* // GroupNode
|
|
44
45
|
* const group = factory.createNode({
|
|
45
|
-
* email: { value: '', component:
|
|
46
|
-
* password: { value: '', component:
|
|
46
|
+
* email: { value: '', component: InputField },
|
|
47
|
+
* password: { value: '', component: InputField }
|
|
47
48
|
* });
|
|
48
49
|
*
|
|
49
50
|
* // ArrayNode (объект)
|
|
50
51
|
* const array = factory.createNode({
|
|
51
|
-
* schema: { title: { value: '', component:
|
|
52
|
+
* schema: { title: { value: '', component: InputField } },
|
|
52
53
|
* initialItems: [{ title: 'Item 1' }]
|
|
53
54
|
* });
|
|
54
55
|
*
|
|
55
56
|
* // ArrayNode (массив) - новый формат
|
|
56
57
|
* const array2 = factory.createNode([
|
|
57
|
-
* { title: { value: '', component:
|
|
58
|
+
* { title: { value: '', component: InputField } }, // schema
|
|
58
59
|
* { title: 'Item 1' }, // initial item 1
|
|
59
60
|
* { title: 'Item 2' } // initial item 2
|
|
60
61
|
* ]);
|
|
@@ -79,7 +80,7 @@ export declare class NodeFactory {
|
|
|
79
80
|
*
|
|
80
81
|
* // Массив с начальными элементами
|
|
81
82
|
* const array = factory.createArrayNodeFromArray([
|
|
82
|
-
* { title: { value: '', component:
|
|
83
|
+
* { title: { value: '', component: InputField } }, // schema
|
|
83
84
|
* { title: 'Item 1' }, // initial value
|
|
84
85
|
* { title: 'Item 2' } // initial value
|
|
85
86
|
* ]);
|
|
@@ -93,7 +94,7 @@ export declare class NodeFactory {
|
|
|
93
94
|
* ✅ НОВОЕ: Извлечено из GroupNode для централизации логики
|
|
94
95
|
*
|
|
95
96
|
* Преобразует схему формы в объект со значениями:
|
|
96
|
-
* - `{ name: { value: 'John', component:
|
|
97
|
+
* - `{ name: { value: 'John', component: InputField } } → { name: 'John' }`
|
|
97
98
|
* - Поддерживает вложенные группы
|
|
98
99
|
* - Поддерживает массивы
|
|
99
100
|
*
|
|
@@ -105,10 +106,10 @@ export declare class NodeFactory {
|
|
|
105
106
|
* const factory = new NodeFactory();
|
|
106
107
|
*
|
|
107
108
|
* const schema = {
|
|
108
|
-
* name: { value: 'John', component:
|
|
109
|
-
* age: { value: 30, component:
|
|
109
|
+
* name: { value: 'John', component: InputField },
|
|
110
|
+
* age: { value: 30, component: InputField },
|
|
110
111
|
* address: {
|
|
111
|
-
* city: { value: 'Moscow', component:
|
|
112
|
+
* city: { value: 'Moscow', component: InputField }
|
|
112
113
|
* }
|
|
113
114
|
* };
|
|
114
115
|
*
|
|
@@ -131,7 +132,7 @@ export declare class NodeFactory {
|
|
|
131
132
|
* ```typescript
|
|
132
133
|
* const factory = new NodeFactory();
|
|
133
134
|
*
|
|
134
|
-
* factory.isFieldConfig({ value: '', component:
|
|
135
|
+
* factory.isFieldConfig({ value: '', component: InputField }); // true
|
|
135
136
|
* factory.isFieldConfig({ email: { value: '' } }); // false
|
|
136
137
|
* factory.isFieldConfig(null); // false
|
|
137
138
|
* ```
|
|
@@ -154,7 +155,7 @@ export declare class NodeFactory {
|
|
|
154
155
|
* const factory = new NodeFactory();
|
|
155
156
|
*
|
|
156
157
|
* factory.isArrayConfig({ schema: {}, initialItems: [] }); // true
|
|
157
|
-
* factory.isArrayConfig({ value: '', component:
|
|
158
|
+
* factory.isArrayConfig({ value: '', component: InputField }); // false
|
|
158
159
|
* factory.isArrayConfig({ email: { value: '' } }); // false
|
|
159
160
|
* ```
|
|
160
161
|
*/
|
|
@@ -175,11 +176,11 @@ export declare class NodeFactory {
|
|
|
175
176
|
* const factory = new NodeFactory();
|
|
176
177
|
*
|
|
177
178
|
* factory.isGroupConfig({
|
|
178
|
-
* email: { value: '', component:
|
|
179
|
-
* password: { value: '', component:
|
|
179
|
+
* email: { value: '', component: InputField },
|
|
180
|
+
* password: { value: '', component: InputField }
|
|
180
181
|
* }); // true
|
|
181
182
|
*
|
|
182
|
-
* factory.isGroupConfig({ value: '', component:
|
|
183
|
+
* factory.isGroupConfig({ value: '', component: InputField }); // false
|
|
183
184
|
* factory.isGroupConfig({ schema: {} }); // false
|
|
184
185
|
* factory.isGroupConfig(null); // false
|
|
185
186
|
* ```
|
package/dist/form/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Модуль `form` — доменный слой формы поверх
|
|
2
|
+
* Модуль `form` — доменный слой формы поверх реактивной модели (`model`).
|
|
3
3
|
*
|
|
4
4
|
* Ноды (value/touched/dirty/status/errors/componentProps), сборка формы из модели по идентичности
|
|
5
|
-
* сигнала (`createForm`), state-операции над нодами (`enableWhen`/`disableWhen`), submit
|
|
6
|
-
* React
|
|
7
|
-
*
|
|
5
|
+
* сигнала (`createForm`), state-операции над нодами (`enableWhen`/`disableWhen`), submit/статус.
|
|
6
|
+
* React-биндинги вынесены в `platforms/react` — этот слой от React не зависит.
|
|
7
|
+
* Schema-валидация — внешний контракт `@reformer/core/validation` (`validateModel`),
|
|
8
|
+
* роутит ошибки в ноды через реестр сигнал→нода. Зависит от `model` (form→model разрешено);
|
|
8
9
|
* обратной зависимости нет.
|
|
9
10
|
*
|
|
10
11
|
* @group Form
|
|
@@ -21,19 +22,12 @@ export { createForm, createFormFromModel } from './create-form';
|
|
|
21
22
|
export type { CreateFormFromModelArgs } from './create-form';
|
|
22
23
|
export { createCoreForm } from './create-core-form';
|
|
23
24
|
export type { CoreForm, CreateCoreFormConfig, CreateFormConfigBase } from './create-core-form';
|
|
24
|
-
export { buildValidation } from './validation
|
|
25
|
-
export type { FormValidation, FormValidationBundle } from './validation
|
|
26
|
-
export {
|
|
27
|
-
export
|
|
28
|
-
export { enableWhen, disableWhen } from './behaviors-node';
|
|
29
|
-
export { FormErrorHandler, ErrorStrategy } from './error-handler';
|
|
25
|
+
export { buildValidation } from './validation/config';
|
|
26
|
+
export type { FormValidation, FormValidationBundle } from './validation/config';
|
|
27
|
+
export { enableWhen, disableWhen } from './behaviors/node';
|
|
28
|
+
export { FormErrorHandler, ErrorStrategy } from './validation/error-handler';
|
|
30
29
|
export { registerSignalNode, getNodeForSignal } from './signal-node-registry';
|
|
31
30
|
export { FormSubmitter } from './form-submitter';
|
|
31
|
+
export { SubscriptionManager } from './nodes/subscription-manager';
|
|
32
32
|
export type { SubmittableForm, SubmitOptions, SubmitResult } from './form-submitter';
|
|
33
33
|
export { isFormNode, isFieldNode, isGroupNode, isArrayNode, getNodeType } from './type-guards';
|
|
34
|
-
export { useFormControl } from './hooks/useFormControl';
|
|
35
|
-
export { useFormControlValue } from './hooks/useFormControlValue';
|
|
36
|
-
export { useArrayLength } from './hooks/useArrayLength';
|
|
37
|
-
export { useFormValidation } from './hooks/use-form-validation';
|
|
38
|
-
export type { UseFormValidationArgs, UseFormValidationResult } from './hooks/use-form-validation';
|
|
39
|
-
export type { FieldControlState, ArrayControlState } from './hooks/types';
|
|
@@ -11,8 +11,8 @@ import { FormProxy } from '../types/form-proxy';
|
|
|
11
11
|
* @example
|
|
12
12
|
* ```typescript
|
|
13
13
|
* const array = new ArrayNode({
|
|
14
|
-
* title: { value: '', component:
|
|
15
|
-
* price: { value: 0, component:
|
|
14
|
+
* title: { value: '', component: InputField },
|
|
15
|
+
* price: { value: 0, component: InputField },
|
|
16
16
|
* });
|
|
17
17
|
*
|
|
18
18
|
* array.push({ title: 'Item 1', price: 100 });
|
|
@@ -141,7 +141,7 @@ export declare class ArrayNode<T extends object> extends FormNode<T[]> {
|
|
|
141
141
|
* @example
|
|
142
142
|
* ```typescript
|
|
143
143
|
* const arrayNode = new ArrayNode(
|
|
144
|
-
* { name: { value: '', component:
|
|
144
|
+
* { name: { value: '', component: InputField } },
|
|
145
145
|
* [{ name: 'Initial 1' }, { name: 'Initial 2' }]
|
|
146
146
|
* );
|
|
147
147
|
*
|
|
@@ -7,10 +7,11 @@ import { FieldConfig, FieldStatus, ValidationError } from '../types/index';
|
|
|
7
7
|
* @group Nodes
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
|
+
* Пример node-level: конфиг УЗЛА, а не layout-схема M1 (в ней поля `validators` нет).
|
|
10
11
|
* ```typescript
|
|
11
12
|
* const field = new FieldNode({
|
|
12
13
|
* value: '',
|
|
13
|
-
* component:
|
|
14
|
+
* component: InputField,
|
|
14
15
|
* validators: [required, email],
|
|
15
16
|
* });
|
|
16
17
|
*
|
|
@@ -102,7 +103,7 @@ export declare class FieldNode<T> extends FormNode<T> {
|
|
|
102
103
|
*
|
|
103
104
|
* @example
|
|
104
105
|
* ```typescript
|
|
105
|
-
* const field = new FieldNode({ value: 'initial', component:
|
|
106
|
+
* const field = new FieldNode({ value: 'initial', component: InputField });
|
|
106
107
|
*
|
|
107
108
|
* field.setValue('changed');
|
|
108
109
|
* field.reset('temp value');
|
|
@@ -194,12 +195,14 @@ export declare class FieldNode<T> extends FormNode<T> {
|
|
|
194
195
|
* @param updateOn - новый триггер валидации: 'change' | 'blur' | 'submit'
|
|
195
196
|
*
|
|
196
197
|
* @example
|
|
198
|
+
* Пример node-level: плоская схема с инлайн-значениями (back-compat путь до M1). В layout-схеме
|
|
199
|
+
* `createForm({ model, schema })` поля `validators` нет — правила живут в `defineValidationSchema`.
|
|
197
200
|
* ```typescript
|
|
198
201
|
* // Сценарий 1: Instant feedback после submit
|
|
199
202
|
* const form = createForm({
|
|
200
203
|
* email: {
|
|
201
204
|
* value: '',
|
|
202
|
-
* component:
|
|
205
|
+
* component: InputField,
|
|
203
206
|
* updateOn: 'submit', // Изначально валидация только при submit
|
|
204
207
|
* validators: [required, email],
|
|
205
208
|
* },
|
|
@@ -15,8 +15,8 @@ import { SubmitOptions, SubmitResult } from '../form-submitter';
|
|
|
15
15
|
* @example
|
|
16
16
|
* ```typescript
|
|
17
17
|
* const form = new GroupNode({
|
|
18
|
-
* email: { valueSignal: model.$.email, component:
|
|
19
|
-
* password: { valueSignal: model.$.password, component:
|
|
18
|
+
* email: { valueSignal: model.$.email, component: InputField },
|
|
19
|
+
* password: { valueSignal: model.$.password, component: InputField },
|
|
20
20
|
* });
|
|
21
21
|
*
|
|
22
22
|
* // Прямой доступ к полям через Proxy
|
|
@@ -136,8 +136,8 @@ export declare class GroupNode<T> extends FormNode<T> {
|
|
|
136
136
|
* @example
|
|
137
137
|
* ```typescript
|
|
138
138
|
* const form = new GroupNode({
|
|
139
|
-
* email: { value: '', component:
|
|
140
|
-
* name: { value: '', component:
|
|
139
|
+
* email: { value: '', component: InputField },
|
|
140
|
+
* name: { value: '', component: InputField },
|
|
141
141
|
* });
|
|
142
142
|
*
|
|
143
143
|
* const proxy = form.getProxy();
|
|
@@ -189,11 +189,11 @@ export declare class GroupNode<T> extends FormNode<T> {
|
|
|
189
189
|
* @example
|
|
190
190
|
* ```typescript
|
|
191
191
|
* const form = new GroupNode({
|
|
192
|
-
* email: { value: '', component:
|
|
192
|
+
* email: { value: '', component: InputField },
|
|
193
193
|
* address: {
|
|
194
|
-
* city: { value: '', component:
|
|
194
|
+
* city: { value: '', component: InputField }
|
|
195
195
|
* },
|
|
196
|
-
* items: [{ name: { value: '', component:
|
|
196
|
+
* items: [{ name: { value: '', component: InputField } }]
|
|
197
197
|
* });
|
|
198
198
|
*
|
|
199
199
|
* form.getFieldByPath('email'); // FieldNode
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Signal, ReadonlySignal } from '@preact/signals-core';
|
|
2
2
|
import { FormNode } from './form-node';
|
|
3
3
|
import { FieldStatus, ValidationError } from '../types/index';
|
|
4
|
-
import { FormModel } from '../../
|
|
4
|
+
import { FormModel } from '../../model/types';
|
|
5
5
|
import { FormProxy } from '../types/form-proxy';
|
|
6
6
|
/** Минимальный контракт реактивного массива модели, используемый узлом. */
|
|
7
7
|
export interface ModelArrayControl<TItem extends object> {
|
|
@@ -36,8 +36,8 @@ export interface ModelArrayControl<TItem extends object> {
|
|
|
36
36
|
* const model = createModel<{ rows: { name: string; qty: number }[] }>({ rows: [] });
|
|
37
37
|
*
|
|
38
38
|
* const rowItem = (item: FormModel<{ name: string; qty: number }>) => ({
|
|
39
|
-
* name: { value: item.$.name, component:
|
|
40
|
-
* qty: { value: item.$.qty, component:
|
|
39
|
+
* name: { value: item.$.name, component: InputField },
|
|
40
|
+
* qty: { value: item.$.qty, component: InputField },
|
|
41
41
|
* });
|
|
42
42
|
*
|
|
43
43
|
* const form = createForm({
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Общие контракт-типы значения и
|
|
2
|
+
* Общие контракт-типы значения и валидации.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Базовый словарь «значение + ошибка + статус», на который опираются узлы формы
|
|
5
|
+
* (`FormNode.getErrors`, `FieldNode`, `FormStatusMachine`, `aggregate-signals`), слой валидации
|
|
6
|
+
* и платформенные биндинги. Файл не импортирует ничего — это лист графа зависимостей.
|
|
7
|
+
*
|
|
8
|
+
* Живёт в `form/types/`, а не в слое модели: `FieldStatus`/`ErrorFilterOptions` — понятия формы,
|
|
9
|
+
* а модель этих типов не использует (и импортировать их оттуда запрещено границей model⇏form).
|
|
10
|
+
* Наружу отдаётся через `form/types/index`.
|
|
8
11
|
*
|
|
9
12
|
* @group Types
|
|
10
|
-
* @module
|
|
13
|
+
* @module form/types/contracts
|
|
11
14
|
*/
|
|
12
15
|
/**
|
|
13
16
|
* Represents any valid form value type
|
|
@@ -89,14 +89,14 @@ type NonEmptyFieldConfig<T> = RequireAtLeastOne<FieldConfig<T>>;
|
|
|
89
89
|
* }
|
|
90
90
|
*
|
|
91
91
|
* const schema: FormSchema<Form> = {
|
|
92
|
-
* name: { value: '', component:
|
|
92
|
+
* name: { value: '', component: InputField },
|
|
93
93
|
* address: {
|
|
94
|
-
* city: { value: '', component:
|
|
95
|
-
* street: { value: '', component:
|
|
94
|
+
* city: { value: '', component: InputField },
|
|
95
|
+
* street: { value: '', component: InputField },
|
|
96
96
|
* },
|
|
97
97
|
* items: [{
|
|
98
|
-
* title: { value: '', component:
|
|
99
|
-
* price: { value: 0, component:
|
|
98
|
+
* title: { value: '', component: InputField },
|
|
99
|
+
* price: { value: 0, component: InputField },
|
|
100
100
|
* }],
|
|
101
101
|
* };
|
|
102
102
|
* ```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
-
import { FormModel } from '
|
|
3
|
-
import { ValidationSchema } from './
|
|
4
|
-
import { FormValidationController, ValidationStrategyKind } from './
|
|
2
|
+
import { FormModel } from '../../model/types';
|
|
3
|
+
import { ValidationSchema } from './types';
|
|
4
|
+
import { FormValidationController, ValidationStrategyKind } from './strategy';
|
|
5
5
|
/**
|
|
6
6
|
* Правила валидации формы — ДАННЫЕ (без привязки к рендеру).
|
|
7
7
|
*
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { FormModel, PathAwareSignal, ValidationError } from '../../index';
|
|
2
|
+
/** Сток одного прогона: модель scope, накопленные ошибки, незавершённые async-правила, гейты. */
|
|
3
|
+
export interface VContext {
|
|
4
|
+
/** Модель текущего scope — источник снапшота для `cross` и модель для `apply`. */
|
|
5
|
+
model: FormModel<unknown>;
|
|
6
|
+
/** Накопленные ошибки по сигналу (ключ — идентичность `PathAwareSignal`). */
|
|
7
|
+
errors: Map<PathAwareSignal<unknown>, ValidationError[]>;
|
|
8
|
+
/** Незавершённые async-правила (раннер их дожидается). */
|
|
9
|
+
pending: Promise<void>[];
|
|
10
|
+
/** Стек активных условий `validateWhen` (все должны быть истинны, чтобы правило сработало). */
|
|
11
|
+
whenStack: Array<() => boolean>;
|
|
12
|
+
/** Отмена устаревшего прогона: прокидывается в async-правила, гасит роутинг после await. */
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Открыть ambient-окно на время `fn` и закрыть его в `finally`.
|
|
17
|
+
*
|
|
18
|
+
* Единственный способ установить контекст: прямое присваивание `current` из другого модуля
|
|
19
|
+
* невозможно. Сохранение/восстановление `prev` делает вложенный прогон безопасным.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runWithContext<R>(ctx: VContext, fn: () => R): R;
|
|
22
|
+
/** Контекст активного прогона либо понятная ошибка, если оператор вызван вне схемы. */
|
|
23
|
+
export declare function requireCtx(op: string): VContext;
|
|
24
|
+
/** «Тронуть» сигнал: гарантирует его наличие в `errors` (даже пустым) → он попадёт в очистку. */
|
|
25
|
+
export declare function touch(ctx: VContext, sig: PathAwareSignal<unknown>): ValidationError[];
|
|
26
|
+
/** Активна ли текущая ветка (все `validateWhen`-условия истинны). */
|
|
27
|
+
export declare const gated: (ctx: VContext) => boolean;
|
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
export {
|
|
22
|
-
export {
|
|
23
|
-
export
|
|
24
|
-
export { fileType } from './validators/file-type';
|
|
25
|
-
export { maxFiles } from './validators/max-files';
|
|
26
|
-
export { minFiles } from './validators/min-files';
|
|
27
|
-
export { maxTotalFileSize } from './validators/max-total-file-size';
|
|
28
|
-
export { isFileLike, matchesFileAccept, toFileArray, type FileLike } from './validators/file-utils';
|
|
1
|
+
/**
|
|
2
|
+
* Декларативный контракт СХЕМЫ ВАЛИДАЦИИ — `@reformer/core/validation`.
|
|
3
|
+
*
|
|
4
|
+
* Схема валидации — обычная функция `(ctx: { model }) => void`. Внутри вызываются свободные операторы
|
|
5
|
+
* (`validate`/`validateAsync`/`validateWhen`/`cross`/`each`/`apply`), которые САМИ пишут ошибки в ambient-сток
|
|
6
|
+
* текущего прогона — автор не видит ни коллектора, ни `getNodeForSignal`, ни `.push`. Внешний раннер
|
|
7
|
+
* {@link validateModel} открывает ambient-окно на время СИНХРОННОГО прогона схемы, дожидается async-правил
|
|
8
|
+
* и разносит ошибки по нодам формы (`getNodeForSignal(sig).setErrors(...)`), гася поля, ставшие валидными.
|
|
9
|
+
*
|
|
10
|
+
* Зеркалит контракт поведения (`@reformer/core/behaviors`): тот же ambient-стиль, но отдельный слой —
|
|
11
|
+
* валидация НЕ реактивна (прогон по требованию: submit/шаг), поведение — реактивно (живые подписки).
|
|
12
|
+
*
|
|
13
|
+
* Модуль разложен по зонам ответственности: `./types` — контракт правил и схемы, `./context` —
|
|
14
|
+
* ambient-сток прогона, `./operators` — операторы схемы, `./run` — раннер, `./strategy` — КОГДА
|
|
15
|
+
* запускать (реэкспортируется отсюда, тот же сабпат).
|
|
16
|
+
*
|
|
17
|
+
* @group Validation
|
|
18
|
+
* @module form/validation
|
|
19
|
+
*/
|
|
20
|
+
export type { Rule, AsyncRule, ValidationSchema } from './types';
|
|
21
|
+
export { validate, validateAsync, validateWhen, cross, each, apply } from './operators';
|
|
22
|
+
export { defineValidationSchema, validateModel } from './run';
|
|
23
|
+
export * from './strategy';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FormModel, ModelArray, PathAwareSignal, ValidationError } from '../../index';
|
|
2
|
+
import { AsyncRule, Rule, ValidationSchema } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Синхронные правила поля.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* validate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* Асинхронные правила поля (зеркалит движковое разделение `validators` / `asyncValidators`).
|
|
14
|
+
* Раннер дожидается их и прокидывает `AbortSignal` для отмены устаревших ответов.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* validateAsync(model.$.username, [async (v, { signal }) => {
|
|
19
|
+
* const r = await fetch(`/api/free?u=${v}`, { signal });
|
|
20
|
+
* return (await r.json()).free ? null : { code: 'taken', message: 'Занято' };
|
|
21
|
+
* }]);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateAsync<TField>(sig: PathAwareSignal<TField>, rules: AsyncRule<TField>[]): void;
|
|
25
|
+
/**
|
|
26
|
+
* Условная валидация: правила внутри `cb` активны, только пока `cond()` истинно; иначе их поля
|
|
27
|
+
* ГАСЯТСЯ (пустой bucket → `setErrors([])`). Не трогает включение/сброс поля — это дело поведения (`enableWhen`).
|
|
28
|
+
*/
|
|
29
|
+
export declare function validateWhen(cond: () => boolean, cb: () => void): void;
|
|
30
|
+
/**
|
|
31
|
+
* Cross-field правило: `fn` получает СНАПШОТ модели текущего scope (`model.get()`) и вешает ошибку на `sig`.
|
|
32
|
+
* Для элементов массива / под-моделей захватывайте нужный снапшот в замыкание (`const item = im.get()`),
|
|
33
|
+
* т.к. `fn` всегда получает модель ТЕКУЩЕГО scope (корень прогона), а не под-модель.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cross<TSnapshot>(sig: PathAwareSignal<unknown>, fn: (form: TSnapshot) => ValidationError | null): void;
|
|
36
|
+
/**
|
|
37
|
+
* Применить под-правила к КАЖДОМУ элементу текущего массива модели.
|
|
38
|
+
* `U extends object` — элементы должны быть под-моделями (объектами); для массива примитивов валидируйте лист напрямую.
|
|
39
|
+
*/
|
|
40
|
+
export declare function each<U extends object>(arr: ModelArray<U>, itemFn: (item: FormModel<U>) => void): void;
|
|
41
|
+
/** Композиция под-схем в текущую (над той же моделью scope). Заменяет пошаговую группировку. */
|
|
42
|
+
export declare function apply<T>(...schemas: ValidationSchema<T>[]): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FormModel } from '../../index';
|
|
2
|
+
import { ValidationSchema } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Тонкая identity-обёртка для типизации/discoverability (как `defineFormBehavior`). Возвращает схему как есть.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* export const step1 = defineValidationSchema<LoanForm>(({ model }) => {
|
|
9
|
+
* validate(model.$.loanAmount, [required(), min(50000)]);
|
|
10
|
+
* });
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function defineValidationSchema<T>(schema: ValidationSchema<T>): ValidationSchema<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Провалидировать модель ЛЮБОЙ схемой (шаг или вся форма). Открывает ambient-окно на время
|
|
16
|
+
* синхронного прогона `schema`, дожидается async-правил, разносит ошибки по нодам формы и гасит
|
|
17
|
+
* поля, ставшие валидными. Возвращает `true`, если нет блокирующих ошибок (`severity:'warning'` не блокирует).
|
|
18
|
+
*
|
|
19
|
+
* Гашение — на пару (model, schema): `validateModel(model, step2)` и `validateModel(model, form)` не мешают
|
|
20
|
+
* друг другу. Устаревший прогон (быстрый повторный вызов той же (model, schema)) отменяется через `AbortSignal`
|
|
21
|
+
* и возвращает `false` (**fail-closed** — отменённому результату нельзя доверять для submit).
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ `schema` должна быть СТАБИЛЬНОЙ ссылкой: отмена устаревших прогонов ключится по идентичности `schema`.
|
|
24
|
+
* Инлайн-стрелка (`validateModel(model, ({ model }) => …)`) каждый раз создаёт НОВЫЙ прогон без дедупликации —
|
|
25
|
+
* держите схемы в `const` / `defineValidationSchema`.
|
|
26
|
+
*
|
|
27
|
+
* `options.touch` (§6): по завершении помечает `touched` ИМЕННО поля, которые схема проверяла
|
|
28
|
+
* (не всё поддерево). Ошибки становятся видимыми (`shouldShowError = invalid && (touched||dirty)`)
|
|
29
|
+
* без ручного `form.markAsTouched()`; при пошаговой валидации следующий шаг не показывается
|
|
30
|
+
* «тронутым» до ввода. Валидные проверенные поля тоже метятся — это безвредно (ошибка не покажется).
|
|
31
|
+
*
|
|
32
|
+
* @param model - Модель данных формы.
|
|
33
|
+
* @param schema - Схема валидации (стабильная ссылка).
|
|
34
|
+
* @param options - `{ touch }` — пометить провалидированные поля `touched` для показа ошибок.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const ok = await validateModel(model, step2Validation); // один шаг
|
|
39
|
+
* const all = await validateModel(model, formValidation); // вся форма
|
|
40
|
+
* const stepOk = await validateModel(model, step2Validation, { touch: true }); // + показать ошибки шага
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateModel<T>(model: FormModel<T>, schema: ValidationSchema<T>, options?: {
|
|
44
|
+
touch?: boolean;
|
|
45
|
+
}): Promise<boolean>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReadonlySignal } from '@preact/signals-core';
|
|
2
|
-
import { FormModel } from '
|
|
3
|
-
import { ValidationSchema } from './
|
|
2
|
+
import { FormModel } from '../../index';
|
|
3
|
+
import { ValidationSchema } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Когда запускается schema-валидация:
|
|
6
6
|
* - `submit` (по умолчанию) — только явным вызовом `validate()` (на отправке);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FormModel, ValidationError } from '../../index';
|
|
2
|
+
/**
|
|
3
|
+
* Внутренний callable-вид правила: value-only фабрики игнорируют 2-й/3-й аргумент.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export type CallableRule = (value: unknown, model: unknown, root: unknown) => ValidationError | null;
|
|
8
|
+
/**
|
|
9
|
+
* Синхронное правило поля типа `TField`. Проверяется ТОЛЬКО значение (`value`).
|
|
10
|
+
*
|
|
11
|
+
* Позиционные `scope`/`root` помечены `never` намеренно: так встроенные value-only фабрики
|
|
12
|
+
* (`required()`/`min()`/… — они `(value, model, root) => …`) и inline-правила `(value) => …`
|
|
13
|
+
* ОБА присваиваются в `Rule<TField>[]` без `any`, при этом сохраняется проверка типа поля
|
|
14
|
+
* (`validate(model.$.age, [email()])` подсветится — `email` ждёт `string`, поле `number`).
|
|
15
|
+
* На вызове раннер приводит правило к callable и передаёт `(value, model, model)`.
|
|
16
|
+
*/
|
|
17
|
+
export type Rule<TField> = (value: TField, scope: never, root: never) => ValidationError | null;
|
|
18
|
+
/**
|
|
19
|
+
* Асинхронное правило поля. Получает `AbortSignal` для отмены устаревших ответов (быстрый повторный
|
|
20
|
+
* прогон той же схемы отменяет предыдущий). Сетевой сбой не должен блокировать — ловите и возвращайте `null`.
|
|
21
|
+
*/
|
|
22
|
+
export type AsyncRule<TField> = (value: TField, ctx: {
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
}) => Promise<ValidationError | null>;
|
|
25
|
+
/** Схема валидации — обычная функция над (под)моделью. First-class значение (можно `apply`/тестировать/переиспользовать). */
|
|
26
|
+
export type ValidationSchema<T> = (ctx: {
|
|
27
|
+
model: FormModel<T>;
|
|
28
|
+
}) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Validator, ValidateOptions } from '
|
|
1
|
+
import { Validator, ValidateOptions } from '../types/validation-schema';
|
|
2
2
|
/**
|
|
3
3
|
* Фабрика валидатора формата email.
|
|
4
4
|
*
|
|
@@ -10,14 +10,13 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
|
|
|
10
10
|
*
|
|
11
11
|
* @example Проверка формата email
|
|
12
12
|
* ```typescript
|
|
13
|
+
* import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
13
14
|
* import { required, email } from '@reformer/core/validators';
|
|
14
15
|
*
|
|
15
|
-
* //
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* validators: [required(), email({ message: 'Введите корректный email' })],
|
|
20
|
-
* },
|
|
16
|
+
* // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
|
|
17
|
+
* const validation = defineValidationSchema<MyForm>(({ model }) => {
|
|
18
|
+
* validate(model.$.email, [required(), email({ message: 'Введите корректный email' })]);
|
|
19
|
+
* });
|
|
21
20
|
* ```
|
|
22
21
|
*/
|
|
23
22
|
export declare function email<TForm = unknown, TField extends string | null | undefined = string>(options?: ValidateOptions): Validator<TForm, TField>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Validator, ValidateOptions } from '
|
|
1
|
+
import { Validator, ValidateOptions } from '../types/validation-schema';
|
|
2
2
|
/**
|
|
3
3
|
* Фабрика валидатора типа файла.
|
|
4
4
|
*
|
|
@@ -18,14 +18,13 @@ import { Validator, ValidateOptions } from '../../types/validation-schema';
|
|
|
18
18
|
*
|
|
19
19
|
* @example Только изображения и PDF
|
|
20
20
|
* ```typescript
|
|
21
|
+
* import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
21
22
|
* import { fileType } from '@reformer/core/validators';
|
|
22
23
|
*
|
|
23
|
-
* //
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* validators: [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })],
|
|
28
|
-
* },
|
|
24
|
+
* // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
|
|
25
|
+
* const validation = defineValidationSchema<MyForm>(({ model }) => {
|
|
26
|
+
* validate(model.$.documents, [fileType('image/*,.pdf', { message: 'Только изображения или PDF' })]);
|
|
27
|
+
* });
|
|
29
28
|
* ```
|
|
30
29
|
*/
|
|
31
30
|
export declare function fileType<TForm = unknown, TField = unknown>(accept: string, options?: ValidateOptions): Validator<TForm, TField>;
|