@reformer/core 11.0.0-beta.1 → 11.0.0-beta.10
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 +90 -68
- package/dist/behaviors-value-D05z3my6.js +109 -0
- package/dist/behaviors.d.ts +1 -1
- package/dist/behaviors.js +376 -211
- package/dist/create-model-DPSG3I0O.js +30 -0
- package/dist/date-utils-90-qZbse.js +43 -0
- package/dist/en-HP8XEych.js +44 -0
- package/dist/form/aggregate-signals.d.ts +21 -63
- package/dist/form/behaviors/collections.d.ts +84 -0
- package/dist/form/behaviors/context.d.ts +56 -0
- package/dist/form/behaviors/index.d.ts +30 -0
- package/dist/form/behaviors/internals.d.ts +41 -0
- package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +28 -5
- package/dist/form/behaviors/operators.d.ts +59 -0
- package/dist/form/behaviors/schema-operators.d.ts +83 -0
- package/dist/form/behaviors/types.d.ts +37 -0
- package/dist/form/create-form.d.ts +59 -70
- package/dist/form/flow/flow.d.ts +87 -0
- package/dist/form/form-bundle.d.ts +153 -0
- package/dist/form/form-level.d.ts +121 -0
- package/dist/form/form-proxy-builder.d.ts +6 -22
- package/dist/form/index.d.ts +23 -24
- package/dist/form/nodes/form-array-node.d.ts +100 -0
- package/dist/form/nodes/form-control-node.d.ts +73 -0
- package/dist/form/nodes/form-group-node.d.ts +66 -0
- package/dist/form/nodes/node-kind.d.ts +29 -0
- package/dist/form/nodes/state/availability.d.ts +6 -0
- package/dist/form/nodes/state/component.d.ts +6 -0
- package/dist/form/nodes/state/index.d.ts +12 -0
- package/dist/form/nodes/state/interaction.d.ts +13 -0
- package/dist/form/nodes/state/issues.d.ts +9 -0
- package/dist/form/nodes/state/pending.d.ts +6 -0
- package/dist/form/schema/compile.d.ts +59 -0
- package/dist/form/schema/compiled.d.ts +143 -0
- package/dist/form/schema/row-key.d.ts +11 -0
- package/dist/form/schema-controller.d.ts +159 -0
- package/dist/form/schema-subtree.d.ts +28 -0
- package/dist/form/signal-node-registry.d.ts +27 -22
- package/dist/form/submission.d.ts +93 -0
- package/dist/form/type-guards.d.ts +14 -73
- package/dist/form/types/contracts.d.ts +21 -85
- package/dist/form/types/field-config.d.ts +27 -0
- package/dist/form/types/index.d.ts +5 -57
- package/dist/form/types/nodes.d.ts +159 -0
- package/dist/form/types/schema-node.d.ts +147 -46
- package/dist/form/types/validation-schema.d.ts +25 -17
- package/dist/form/validation/context.d.ts +67 -0
- package/dist/form/validation/index.d.ts +24 -28
- package/dist/form/validation/operators.d.ts +101 -0
- package/dist/form/validation/run.d.ts +120 -0
- package/dist/form/validation/strategy.d.ts +89 -0
- package/dist/form/validation/types.d.ts +83 -0
- package/dist/form/validators/date-utils.d.ts +42 -0
- package/dist/form/validators/email.d.ts +23 -0
- package/dist/form/{validation/validators → validators}/file-type.d.ts +10 -10
- package/dist/form/{validation/validators → validators}/file-utils.d.ts +3 -2
- package/dist/form/validators/future-date.d.ts +23 -0
- package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
- package/dist/form/validators/integer.d.ts +22 -0
- package/dist/form/validators/is-date.d.ts +24 -0
- package/dist/form/validators/is-number.d.ts +25 -0
- package/dist/form/validators/max-age.d.ts +28 -0
- package/dist/form/validators/max-date.d.ts +25 -0
- package/dist/form/{validation/validators → validators}/max-file-size.d.ts +10 -10
- package/dist/form/validators/max-files.d.ts +26 -0
- package/dist/form/validators/max-length.d.ts +33 -0
- package/dist/form/validators/max-total-file-size.d.ts +26 -0
- package/dist/form/validators/max.d.ts +26 -0
- package/dist/form/validators/min-age.d.ts +27 -0
- package/dist/form/validators/min-date.d.ts +25 -0
- package/dist/form/{validation/validators → validators}/min-file-size.d.ts +10 -10
- package/dist/form/validators/min-files.d.ts +27 -0
- package/dist/form/validators/min-length.d.ts +33 -0
- package/dist/form/validators/min.d.ts +26 -0
- package/dist/form/validators/multiple-of.d.ts +25 -0
- package/dist/form/validators/non-negative.d.ts +22 -0
- package/dist/form/validators/non-zero.d.ts +22 -0
- package/dist/form/validators/past-date.d.ts +23 -0
- package/dist/form/validators/pattern.d.ts +26 -0
- package/dist/form/{validation/validators → validators}/phone.d.ts +11 -11
- package/dist/form/validators/required.d.ts +29 -0
- package/dist/form/validators/url.d.ts +45 -0
- package/dist/form/validators/validation-issue.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +54 -0
- package/dist/i18n/en.json.d.ts +49 -0
- package/dist/i18n/loader.d.ts +71 -0
- package/dist/i18n/locale-store.d.ts +46 -0
- package/dist/i18n/locale.d.ts +90 -0
- package/dist/i18n/message-format.d.ts +136 -0
- package/dist/i18n/resolve-localized.d.ts +17 -0
- package/dist/i18n/ru.json.d.ts +49 -0
- package/dist/i18n/translator.d.ts +61 -0
- package/dist/i18n/validation-message.d.ts +53 -0
- package/dist/i18n.d.ts +26 -0
- package/dist/i18n.js +86 -0
- package/dist/index.d.ts +17 -5
- package/dist/index.js +942 -2601
- package/dist/loader-BWkigpVe.js +52 -0
- package/dist/locale/en.d.ts +2 -0
- package/dist/locale/en.js +5 -0
- package/dist/locale/en.json +45 -0
- package/dist/locale/index.d.ts +18 -0
- package/dist/locale/ru.d.ts +2 -0
- package/dist/locale/ru.js +44 -0
- package/dist/locale/ru.json +45 -0
- package/dist/locale-CG54AU2Q.js +219 -0
- package/dist/locale.d.ts +2 -0
- package/dist/locale.js +9 -0
- package/dist/{state → model}/behaviors-value.d.ts +24 -6
- package/dist/{state/form-model.d.ts → model/create-model.d.ts} +18 -4
- package/dist/{state → model}/derived-registry.d.ts +1 -1
- package/dist/model/index.d.ts +42 -0
- package/dist/model/model-nodes.d.ts +190 -0
- package/dist/model/model-signals-proxy.d.ts +63 -0
- package/dist/model/model-value-proxy.d.ts +84 -0
- package/dist/{state → model}/safe-effect.d.ts +50 -2
- package/dist/model/types.d.ts +239 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +27 -0
- package/dist/node-kind-C-r6ZMdo.js +15 -0
- package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
- package/dist/platforms/react/field-binding/field-handle.d.ts +53 -0
- package/dist/platforms/react/form-bundle-context.d.ts +27 -0
- package/dist/{form → platforms/react}/hooks/types.d.ts +58 -36
- package/dist/platforms/react/hooks/use-form-bundle.d.ts +41 -0
- package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
- package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +13 -7
- package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +6 -6
- package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +7 -9
- package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +18 -18
- package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +9 -1
- package/dist/platforms/react/i18n/context.d.ts +18 -0
- package/dist/platforms/react/i18n/hooks.d.ts +65 -0
- package/dist/platforms/react/i18n/provider.d.ts +59 -0
- package/dist/platforms/react/index.d.ts +30 -0
- package/dist/react.d.ts +22 -0
- package/dist/react.js +505 -0
- package/dist/runtime-token.d.ts +1 -2
- package/dist/safe-effect-BazLOv1g.js +669 -0
- package/dist/schema-controller-CpiO2JoK.js +143 -0
- package/dist/signals.d.ts +15 -9
- package/dist/signals.js +11 -6
- package/dist/strategy-DzvnsWLh.js +375 -0
- package/dist/type-guards-D-N_5Ddh.js +19 -0
- package/dist/validation-issue-B9mjI8jz.js +7 -0
- package/dist/validation-message-Ced9MGxs.js +194 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +11 -165
- package/dist/validators/email.d.ts +1 -1
- package/dist/validators/email.js +5 -8
- package/dist/validators/future-date.d.ts +1 -1
- package/dist/validators/future-date.js +10 -13
- package/dist/validators/integer.d.ts +1 -1
- package/dist/validators/integer.js +4 -7
- package/dist/validators/is-date.d.ts +1 -1
- package/dist/validators/is-date.js +5 -8
- package/dist/validators/is-number.d.ts +1 -1
- package/dist/validators/is-number.js +4 -7
- package/dist/validators/max-age.d.ts +1 -1
- package/dist/validators/max-age.js +8 -12
- package/dist/validators/max-date.d.ts +1 -1
- package/dist/validators/max-date.js +10 -13
- package/dist/validators/max-length.d.ts +1 -1
- package/dist/validators/max-length.js +7 -10
- package/dist/validators/max.d.ts +1 -1
- package/dist/validators/max.js +4 -7
- package/dist/validators/min-age.d.ts +1 -1
- package/dist/validators/min-age.js +10 -11
- package/dist/validators/min-date.d.ts +1 -1
- package/dist/validators/min-date.js +8 -11
- package/dist/validators/min-length.d.ts +1 -1
- package/dist/validators/min-length.js +4 -7
- package/dist/validators/min.d.ts +1 -1
- package/dist/validators/min.js +4 -7
- package/dist/validators/multiple-of.d.ts +1 -1
- package/dist/validators/multiple-of.js +11 -10
- package/dist/validators/non-negative.d.ts +1 -1
- package/dist/validators/non-negative.js +2 -5
- package/dist/validators/non-zero.d.ts +1 -1
- package/dist/validators/non-zero.js +4 -7
- package/dist/validators/past-date.d.ts +1 -1
- package/dist/validators/past-date.js +10 -13
- package/dist/validators/pattern.d.ts +1 -1
- package/dist/validators/pattern.js +5 -8
- package/dist/validators/phone.d.ts +1 -1
- package/dist/validators/phone.js +10 -11
- package/dist/validators/required.d.ts +1 -1
- package/dist/validators/required.js +4 -7
- package/dist/validators/url.d.ts +1 -1
- package/dist/validators/url.js +48 -17
- package/dist/validators.d.ts +1 -1
- package/dist/validators.js +86 -108
- package/docs/llms/01-api-reference.md +60 -34
- package/docs/llms/02-quick-start.md +112 -74
- package/docs/llms/03-api-signatures.md +369 -148
- package/docs/llms/04-common-patterns.md +58 -56
- package/docs/llms/05-common-mistakes.md +69 -44
- package/docs/llms/06-troubleshooting.md +14 -9
- package/docs/llms/07-complete-import.md +74 -43
- package/docs/llms/08-form-types.md +11 -4
- package/docs/llms/09-formschema.md +132 -84
- package/docs/llms/10-arrays.md +141 -61
- package/docs/llms/11-async-watchfield.md +2 -2
- package/docs/llms/13-multi-step.md +134 -63
- package/docs/llms/14-extended-mistakes.md +20 -17
- package/docs/llms/15-project-structure.md +224 -70
- package/docs/llms/16-ui-components.md +86 -46
- package/docs/llms/17-nonexistent-api.md +217 -32
- package/docs/llms/18-conditional-fields.md +71 -32
- package/docs/llms/19-reading-values.md +45 -4
- package/docs/llms/20-compute-vs-watch.md +25 -10
- package/docs/llms/21-array-operations.md +76 -26
- package/docs/llms/22-cycle-detection.md +1 -1
- package/docs/llms/23-copy-from.md +2 -2
- package/docs/llms/24-sync-fields.md +6 -5
- package/docs/llms/25-reset-when.md +14 -7
- package/docs/llms/26-transform-value.md +4 -4
- package/docs/llms/27-revalidate-when.md +16 -15
- package/docs/llms/28-submit-and-reset.md +278 -128
- package/docs/llms/29-async-preload.md +41 -31
- package/docs/llms/30-type-safety-recipes.md +105 -51
- package/docs/llms/31-async-validator-debounce.md +96 -53
- package/docs/llms/33-validation-strategy.md +178 -66
- package/docs/llms/34-i18n.md +298 -0
- package/docs/llms/35-unified-contract.md +309 -0
- package/docs/llms/36-issue-messages.md +284 -0
- package/docs/llms/37-compiled-schema.md +332 -0
- package/docs/llms/38-form-flow.md +396 -0
- package/llms-index.json +1 -0
- package/llms.txt +10690 -3089
- package/package.json +48 -9
- package/dist/behaviors-node-DGEFMTQu.js +0 -18
- package/dist/behaviors-value-C7dSEhCc.js +0 -134
- package/dist/date-utils-dTT_x_Be.js +0 -31
- package/dist/derived-registry-CluZCQ7U.js +0 -16
- package/dist/form/behaviors.d.ts +0 -133
- package/dist/form/error-handler.d.ts +0 -166
- package/dist/form/factories/index.d.ts +0 -6
- package/dist/form/factories/node-factory.d.ts +0 -188
- package/dist/form/form-submitter.d.ts +0 -121
- package/dist/form/nodes/array-node.d.ts +0 -276
- package/dist/form/nodes/field-node.d.ts +0 -294
- package/dist/form/nodes/form-node.d.ts +0 -326
- package/dist/form/nodes/group-node.d.ts +0 -284
- package/dist/form/nodes/model-array-node.d.ts +0 -121
- package/dist/form/status-machine.d.ts +0 -115
- package/dist/form/types/deep-schema.d.ts +0 -107
- package/dist/form/types/form-proxy.d.ts +0 -141
- package/dist/form/unique-id.d.ts +0 -45
- package/dist/form/validation/validators/date-utils.d.ts +0 -30
- package/dist/form/validation/validators/email.d.ts +0 -23
- package/dist/form/validation/validators/future-date.d.ts +0 -23
- package/dist/form/validation/validators/integer.d.ts +0 -22
- package/dist/form/validation/validators/is-date.d.ts +0 -23
- package/dist/form/validation/validators/is-number.d.ts +0 -24
- package/dist/form/validation/validators/max-age.d.ts +0 -25
- package/dist/form/validation/validators/max-date.d.ts +0 -25
- package/dist/form/validation/validators/max-files.d.ts +0 -26
- package/dist/form/validation/validators/max-length.d.ts +0 -27
- package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
- package/dist/form/validation/validators/max.d.ts +0 -25
- package/dist/form/validation/validators/min-age.d.ts +0 -25
- package/dist/form/validation/validators/min-date.d.ts +0 -25
- package/dist/form/validation/validators/min-files.d.ts +0 -26
- package/dist/form/validation/validators/min-length.d.ts +0 -27
- package/dist/form/validation/validators/min.d.ts +0 -25
- package/dist/form/validation/validators/multiple-of.d.ts +0 -24
- package/dist/form/validation/validators/non-negative.d.ts +0 -22
- package/dist/form/validation/validators/non-zero.d.ts +0 -22
- package/dist/form/validation/validators/past-date.d.ts +0 -23
- package/dist/form/validation/validators/pattern.d.ts +0 -33
- package/dist/form/validation/validators/required.d.ts +0 -32
- package/dist/form/validation/validators/url.d.ts +0 -43
- package/dist/form/validation-schema.d.ts +0 -106
- package/dist/form/validation-strategy.d.ts +0 -55
- package/dist/form/validators.d.ts +0 -11
- package/dist/form-model-DYCzGGKF.js +0 -286
- package/dist/state/index.d.ts +0 -31
- package/dist/state/subscription-manager.d.ts +0 -180
- package/dist/state/types.d.ts +0 -144
- package/dist/state.d.ts +0 -2
- package/dist/state.js +0 -21
- package/dist/subscription-manager-705bls97.js +0 -171
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Загрузка формы данными с
|
|
5
|
+
Загрузка формы данными с сервера: (1) **initial values** в `createModel(...)` / `createForm({ initial })`; (2)
|
|
6
6
|
**`model.set` / `model.patch`** для загрузки/обновления значений; (3) **external React-hook**
|
|
7
7
|
для full-blown async preload (параллельный fetch заявки + справочников, обработка ошибок,
|
|
8
8
|
race-guard через deps). Динамические `componentProps` (опции селектов) обновляются через
|
|
@@ -14,7 +14,8 @@ race-guard через deps). Динамические `componentProps` (опци
|
|
|
14
14
|
```typescript
|
|
15
15
|
// Модель:
|
|
16
16
|
model.set(value: T): void; // полная установка значений (load полного DTO). Не меняет initial.
|
|
17
|
-
|
|
17
|
+
// DTO без части ключей: поля остаются прежними, в dev — предупреждение.
|
|
18
|
+
model.patch(value: DeepPartial<T>): void; // частичное слияние (только переданные ключи, на любой глубине групп; undefined — «не трогать»). Не меняет initial.
|
|
18
19
|
model.reset(): void; // вернуть к initial-снимку
|
|
19
20
|
model.captureInitial(): void; // сделать текущие значения новым initial-снимком
|
|
20
21
|
|
|
@@ -30,28 +31,38 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
|
|
|
30
31
|
### Initial values в модели
|
|
31
32
|
|
|
32
33
|
```typescript
|
|
33
|
-
import { createModel,
|
|
34
|
-
import { Input,
|
|
34
|
+
import { createModel, createFormFromModel, type FormSchemaNode } from '@reformer/core';
|
|
35
|
+
import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
interface ProfileForm {
|
|
38
|
+
username: string;
|
|
39
|
+
language: 'ru' | 'en';
|
|
40
|
+
marketing: boolean;
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
|
|
39
|
-
const schema = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const schema: FormSchemaNode = {
|
|
45
|
+
children: [
|
|
46
|
+
{ model: model.$.username, component: Input, componentProps: { label: 'Username' } },
|
|
47
|
+
{
|
|
48
|
+
model: model.$.language,
|
|
49
|
+
component: SelectAsync,
|
|
50
|
+
componentProps: {
|
|
51
|
+
label: 'Язык',
|
|
52
|
+
options: [
|
|
53
|
+
{ value: 'ru', label: 'Русский' },
|
|
54
|
+
{ value: 'en', label: 'English' },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
model: model.$.marketing,
|
|
60
|
+
component: CheckboxWithLabel,
|
|
61
|
+
componentProps: { label: 'Рассылка' },
|
|
50
62
|
},
|
|
51
|
-
|
|
52
|
-
marketing: { value: model.$.marketing, component: Input, componentProps: { type: 'checkbox' } },
|
|
63
|
+
],
|
|
53
64
|
};
|
|
54
|
-
const form =
|
|
65
|
+
const form = createFormFromModel({ model, schema });
|
|
55
66
|
// model.get() === { username: '', language: 'ru', marketing: true }
|
|
56
67
|
// после правок: model.reset() возвращает к этим значениям
|
|
57
68
|
```
|
|
@@ -60,13 +71,13 @@ const form = createForm({ model, schema });
|
|
|
60
71
|
|
|
61
72
|
```tsx
|
|
62
73
|
import { useEffect, useState } from 'react';
|
|
63
|
-
import type { FormModel,
|
|
74
|
+
import type { FormModel, FormGroup } from '@reformer/core';
|
|
64
75
|
|
|
65
76
|
interface LoadingState { isLoading: boolean; error: string | null }
|
|
66
77
|
|
|
67
78
|
export function useLoadCreditApplication(
|
|
68
79
|
model: FormModel<CreditApplicationForm>,
|
|
69
|
-
form:
|
|
80
|
+
form: FormGroup<CreditApplicationForm>,
|
|
70
81
|
applicationId: string | null
|
|
71
82
|
): LoadingState {
|
|
72
83
|
const [state, setState] = useState<LoadingState>({ isLoading: !!applicationId, error: null });
|
|
@@ -102,7 +113,7 @@ export function useLoadCreditApplication(
|
|
|
102
113
|
})();
|
|
103
114
|
|
|
104
115
|
return () => { cancelled = true; };
|
|
105
|
-
}, [applicationId]); // model/form стабильны (
|
|
116
|
+
}, [applicationId]); // model/form стабильны (сборка через useFormBundle)
|
|
106
117
|
|
|
107
118
|
return state;
|
|
108
119
|
}
|
|
@@ -137,16 +148,13 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
|
|
|
137
148
|
```typescript
|
|
138
149
|
// ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
|
|
139
150
|
function MyForm() {
|
|
140
|
-
const
|
|
141
|
-
const form = createForm({ model, schema });
|
|
151
|
+
const bundle = createForm<T>({ initial, schema }); // КАЖДЫЙ рендер!
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
// ✅
|
|
154
|
+
// ✅ Стабильная сборка: useFormBundle вызывает фабрику ровно один раз (useMemo React вправе
|
|
155
|
+
// сбросить и пересобрать форму, потеряв введённое)
|
|
145
156
|
function MyForm() {
|
|
146
|
-
const { model, form } =
|
|
147
|
-
const m = createModel<T>(initial);
|
|
148
|
-
return { model: m, form: createForm({ model: m, schema: buildSchema(m) }) };
|
|
149
|
-
}, []);
|
|
157
|
+
const { model, form } = useFormBundle(() => createForm<T>({ initial, schema }));
|
|
150
158
|
}
|
|
151
159
|
```
|
|
152
160
|
|
|
@@ -174,8 +182,10 @@ useEffect(() => {
|
|
|
174
182
|
|
|
175
183
|
## Troubleshooting
|
|
176
184
|
|
|
177
|
-
**Q: После `set`/`patch` не показываются
|
|
178
|
-
A: `set`/`patch` не запускают
|
|
185
|
+
**Q: После `set`/`patch` не показываются замечания.**
|
|
186
|
+
A: `set`/`patch` не запускают валидацию (кроме живой стратегии `change`). После загрузки вызови
|
|
187
|
+
`await bundle.validate()`; без сборки — `await validateModel(model, schema)` из
|
|
188
|
+
`@reformer/core/validation`.
|
|
179
189
|
|
|
180
190
|
**Q: Опции в `<Select>` не появляются после `updateComponentProps`.**
|
|
181
191
|
A: (1) Оберни вызов в `queueMicrotask`; (2) убедись, что компонент подписан через `useFormControl`;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
## 30. TYPE-SAFETY RECIPES
|
|
2
2
|
|
|
3
|
-
Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as
|
|
3
|
+
Идиоматичные паттерны, которые держат сгенерированный код без `any` и `as`-кастов.
|
|
4
4
|
|
|
5
5
|
### Recipe 1 — Imports (root cause prevention)
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Модель/форма/типы — из `@reformer/core`; хуки — из `@reformer/core/react`; примитивы над
|
|
8
|
+
сигналами — из `@reformer/core/model`.
|
|
8
9
|
- Схема валидации (операторы + раннер `validateModel`) — из `@reformer/core/validation`.
|
|
9
10
|
- Чистые фабрики валидаторов — из `@reformer/core/validators`.
|
|
11
|
+
- Словарь текстов замечаний (`defineIssueMessages`) — из `@reformer/core/i18n`.
|
|
10
12
|
- Декларативный DSL (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
11
13
|
|
|
12
14
|
```typescript
|
|
@@ -14,13 +16,13 @@ import {
|
|
|
14
16
|
createModel,
|
|
15
17
|
createForm,
|
|
16
18
|
type FormModel,
|
|
17
|
-
type
|
|
18
|
-
type
|
|
19
|
+
type FormGroup,
|
|
20
|
+
type FormSchemaNode,
|
|
19
21
|
} from '@reformer/core';
|
|
22
|
+
import { useFormBundle, useFormControl } from '@reformer/core/react';
|
|
20
23
|
import {
|
|
21
|
-
|
|
24
|
+
defineValidation,
|
|
22
25
|
validate,
|
|
23
|
-
cross,
|
|
24
26
|
validateModel,
|
|
25
27
|
type Rule,
|
|
26
28
|
} from '@reformer/core/validation';
|
|
@@ -28,65 +30,96 @@ import { required, min, max, email } from '@reformer/core/validators';
|
|
|
28
30
|
import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
> **watchField — из `@reformer/core`** (примитив), НЕ из `@reformer/core/behaviors` (там `onChange`)
|
|
33
|
+
> **watchField — из `@reformer/core/model`** (примитив), НЕ из `@reformer/core/behaviors` (там `onChange`)
|
|
34
|
+
> и не из корня `@reformer/core`.
|
|
32
35
|
|
|
33
|
-
### Recipe 2 — Form-shape types
|
|
36
|
+
### Recipe 2 — Form-shape types: `interface` или `type`
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
Оба варианта работают одинаково. Ограничения `Record<string, …>` у `createModel<T>`,
|
|
39
|
+
`createForm<T>`, `FormGroup<T>` и `FormArray<T>` нет (`createModel` и `createForm` требуют только
|
|
40
|
+
`T extends object`). Вид ноды поля `FormFields<T>` выводит по виду значения: обычный объект —
|
|
41
|
+
`FormGroup`, массив объектов — `FormArray`, остальное (примитив, массив примитивов, `Date`, `File`)
|
|
42
|
+
— `FormControl`. Index signature здесь не участвует, поэтому `interface` распознаётся так же, как
|
|
43
|
+
`type`. Эталонная форма объявлена через `interface`.
|
|
38
44
|
|
|
39
45
|
```typescript
|
|
40
|
-
export
|
|
46
|
+
export interface PropertyItem {
|
|
41
47
|
type: 'apartment' | 'house' | 'car';
|
|
42
48
|
description: string;
|
|
43
49
|
estimatedValue: number;
|
|
44
|
-
}
|
|
50
|
+
}
|
|
45
51
|
|
|
46
|
-
export
|
|
52
|
+
export interface CreditApplication {
|
|
47
53
|
loanAmount: number | null;
|
|
48
|
-
properties: PropertyItem[];
|
|
54
|
+
properties: PropertyItem[]; // form.properties — FormArray<PropertyItem>
|
|
55
|
+
address: Address; // form.address — FormGroup<Address>
|
|
56
|
+
tags: string[]; // form.tags — FormControl<string[]>
|
|
49
57
|
// ...
|
|
50
|
-
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// вычисляемые поля — отдельным типом; форма — пересечение
|
|
61
|
+
export interface CreditApplicationDerived {
|
|
62
|
+
monthlyPayment: number;
|
|
63
|
+
}
|
|
64
|
+
export type CreditApplicationForm = CreditApplication & CreditApplicationDerived;
|
|
51
65
|
```
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
Оговорка только для своих обобщённых хелперов: `interface` не присваивается
|
|
68
|
+
`Record<string, unknown>` (неявной index signature у него нет). Ограничение пиши через
|
|
69
|
+
`T extends object`:
|
|
54
70
|
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
```typescript
|
|
72
|
+
// ❌ interface-форму этот хелпер не примет
|
|
73
|
+
function snapshot<T extends Record<string, unknown>>(form: FormGroup<T>): T { return form.getValue(); }
|
|
74
|
+
|
|
75
|
+
// ✅
|
|
76
|
+
function snapshot<T extends object>(form: FormGroup<T>): T { return form.getValue(); }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Recipe 3 — Схема привязана к ручкам модели
|
|
80
|
+
|
|
81
|
+
Привязка узла — ручка модели (`model: model.$.field`), а не литерал. Тип узла `FormSchemaNode`
|
|
82
|
+
закрыт: вид узла (поле, массив под-форм, подформа) сверяется с тем, чем является привязка, а
|
|
83
|
+
опечатка в ключе не компилируется. Правил в схеме нет — они живут в отдельной схеме валидации.
|
|
57
84
|
|
|
58
85
|
```typescript
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
86
|
+
const creditSchema = (model: FormModel<CreditApplicationForm>): FormSchemaNode => ({
|
|
87
|
+
children: [
|
|
88
|
+
{ model: model.$.loanAmount, component: InputNumber },
|
|
89
|
+
// вложенная группа — подформа: билдер получает под-модель FormModel<PersonalData>
|
|
90
|
+
{ model: model.$.personalData, part: personalData },
|
|
91
|
+
// массив под-форм — { model, item }: билдер получает под-модель строки
|
|
92
|
+
{ model: model.$.properties, component: FieldArray, item: propertyRow },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// правила — отдельно; тип правила сверяется с типом поля
|
|
97
|
+
const LOAN_AMOUNT_RULES: Rule<number | null>[] = [required(), min(50000)];
|
|
68
98
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
validate(model.$.loanAmount, [required(), min(50000)]);
|
|
99
|
+
const validation = defineValidation<CreditApplicationForm>(({ model }) => {
|
|
100
|
+
validate(model.$.loanAmount, LOAN_AMOUNT_RULES);
|
|
72
101
|
});
|
|
73
102
|
```
|
|
74
103
|
|
|
75
|
-
### Recipe 4 — Cross-field валидаторы: `cross` над типизированным
|
|
104
|
+
### Recipe 4 — Cross-field валидаторы: `cross` над типизированным снимком
|
|
76
105
|
|
|
77
|
-
Правило — обычная функция `(
|
|
78
|
-
|
|
106
|
+
Правило — обычная функция `(snapshot: T) => ValidationIssue | null`. Оператор `cross` берётся из
|
|
107
|
+
аргумента схемы: тип снимка выведен из схемы, соседние поля читаются без `as`, ошибка вешается на
|
|
108
|
+
поле-носитель:
|
|
79
109
|
|
|
80
110
|
```typescript
|
|
81
|
-
import type {
|
|
111
|
+
import type { ValidationIssue } from '@reformer/core';
|
|
82
112
|
|
|
83
|
-
const initialPaymentVsProperty = (
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
const initialPaymentVsProperty = (application: CreditApplicationForm): ValidationIssue | null =>
|
|
114
|
+
application.initialPayment &&
|
|
115
|
+
application.propertyValue &&
|
|
116
|
+
application.initialPayment > application.propertyValue
|
|
117
|
+
? { code: 'initialPaymentTooHigh' } // текст — в словаре формы (defineIssueMessages)
|
|
86
118
|
: null;
|
|
87
119
|
|
|
88
|
-
|
|
89
|
-
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
120
|
+
const loanRules = defineValidation<CreditApplicationForm>(({ model, cross }) => {
|
|
121
|
+
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
122
|
+
});
|
|
90
123
|
```
|
|
91
124
|
|
|
92
125
|
### Recipe 5 — `compute` читает модель напрямую (без аннотаций)
|
|
@@ -112,27 +145,32 @@ compute(model.$.fullName, () =>
|
|
|
112
145
|
пустые значения — guard `if (value != null)` не нужен.
|
|
113
146
|
|
|
114
147
|
```typescript
|
|
115
|
-
export
|
|
116
|
-
loanAmount: number | null; //
|
|
148
|
+
export interface CreditForm {
|
|
149
|
+
loanAmount: number | null; // validate(model.$.loanAmount, [min(50000)]) — ок
|
|
117
150
|
loanPurpose: string | null; // minLength — ок
|
|
118
151
|
birthDate: string | null; // minAge — ок
|
|
119
|
-
}
|
|
152
|
+
}
|
|
120
153
|
```
|
|
121
154
|
|
|
122
155
|
### Recipe 7 — Нода поля для кастомных компонентов
|
|
123
156
|
|
|
124
|
-
`useFormControl(control)` типизируется по `
|
|
157
|
+
`useFormControl(control)` типизируется по `FormControl<T>`. В props компонента используй `FormControl<T>`:
|
|
125
158
|
|
|
126
159
|
```typescript
|
|
127
|
-
import type {
|
|
160
|
+
import type { FormControl } from '@reformer/core';
|
|
161
|
+
import { useFormControl } from '@reformer/core/react';
|
|
128
162
|
|
|
129
|
-
type MyFieldProps<T> = { control:
|
|
163
|
+
type MyFieldProps<T> = { control: FormControl<T> };
|
|
130
164
|
function MyField<T>({ control }: MyFieldProps<T>) {
|
|
131
|
-
const { value,
|
|
165
|
+
const { value, issues, disabled } = useFormControl(control);
|
|
132
166
|
// ...
|
|
133
167
|
}
|
|
134
168
|
```
|
|
135
169
|
|
|
170
|
+
Для массива под-форм — `FormArray<Item>` (`useFormControl` отдаёт `ArrayControlState`), для группы —
|
|
171
|
+
`FormGroup<Group>`. Вид неизвестной ноды сужают гарды `isFormControl` / `isFormGroup` /
|
|
172
|
+
`isFormArray`.
|
|
173
|
+
|
|
136
174
|
### Recipe 8 — Вынос правил в именованные функции
|
|
137
175
|
|
|
138
176
|
Field-правила — именованные `Rule<T>`, validation-схема остаётся плоской:
|
|
@@ -143,17 +181,33 @@ import type { Rule } from '@reformer/core/validation';
|
|
|
143
181
|
const validateAdultAge: Rule<string | null> = (value) => {
|
|
144
182
|
if (!value) return null;
|
|
145
183
|
const age = new Date().getFullYear() - new Date(value).getFullYear();
|
|
146
|
-
return age < 18 ? { code: '
|
|
184
|
+
return age < 18 ? { code: 'birthDate.adult', params: { minAge: 18 } } : null;
|
|
147
185
|
};
|
|
148
186
|
|
|
149
|
-
// внутри
|
|
187
|
+
// внутри defineValidation<MyForm>(({ model }) => { ... }):
|
|
150
188
|
validate(model.$.birthDate, [validateAdultAge]);
|
|
151
189
|
```
|
|
152
190
|
|
|
191
|
+
### Recipe 9 — Результаты уровня формы — сужение по `status`
|
|
192
|
+
|
|
193
|
+
`bundle.validate()`, `bundle.submission.submit(handler)` и `flow.next()` отвечают
|
|
194
|
+
дискриминированными объединениями: поле результата доступно после проверки `status`, без `as`.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
const outcome = await bundle.submission.submit(api.send); // SubmitResult<Awaited<ReturnType<typeof api.send>>>
|
|
198
|
+
if (outcome.status === 'submitted') use(outcome.result);
|
|
199
|
+
else if (outcome.status === 'failed') report(outcome.error);
|
|
200
|
+
else showIssues(outcome.validation.issues); // 'invalid' | 'error' | 'cancelled'
|
|
201
|
+
|
|
202
|
+
const transition = await bundle.flow!.next(); // FlowTransition
|
|
203
|
+
if (transition.status === 'stayed') console.log(transition.reason); // 'invalid' | 'error' | …
|
|
204
|
+
```
|
|
205
|
+
|
|
153
206
|
### Anti-patterns to avoid
|
|
154
207
|
|
|
155
208
|
- `import { computeFrom } from '@reformer/core/behaviors'` для примитива, вызываемого вне
|
|
156
|
-
`defineFormBehavior` → примитив живёт в `@reformer/core` (возвращает cleanup).
|
|
157
|
-
- `
|
|
209
|
+
`defineFormBehavior` → примитив живёт в `@reformer/core/model` (возвращает cleanup).
|
|
210
|
+
- `T extends Record<string, unknown>` в своих хелперах формы → `T extends object` (Recipe 2).
|
|
158
211
|
- `as`-касты значений полей внутри `compute` → читай `model.field` напрямую (Recipe 5).
|
|
159
212
|
- строковые пути / `(form) => ...` в behaviors → это удалённый API, используй сигналы (`model.$.x`).
|
|
213
|
+
- `node instanceof …` → ноды — интерфейсы; гарды `isFormControl` / `isFormGroup` / `isFormArray`.
|
|
@@ -1,62 +1,110 @@
|
|
|
1
1
|
## 31. ASYNC VALIDATOR
|
|
2
2
|
|
|
3
3
|
Для проверок типа «уникальность email», «валидация ИНН через API», «проверка адреса» —
|
|
4
|
-
async-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<
|
|
4
|
+
async-правило это `AsyncRule<T>` = `(value, { signal }) => Promise<ValidationIssue | null>`.
|
|
5
5
|
Оно регистрируется оператором `validateAsync(sig, [asyncRules])` внутри схемы валидации
|
|
6
|
-
(`@reformer/core/validation`) и исполняется
|
|
7
|
-
|
|
6
|
+
(`@reformer/core/validation`) и исполняется прогоном — уровнем формы (`bundle.validate()`,
|
|
7
|
+
`bundle.submission.submit(handler)`, `flow.next()`) либо раннером `validateModel`. Async-правила
|
|
8
|
+
прогоняются параллельно через `Promise.all`, прогон их дожидается.
|
|
8
9
|
|
|
9
|
-
Слои разделены:
|
|
10
|
-
отдельной функции-схеме `
|
|
10
|
+
Слои разделены: схема формы (билдер `createForm` / JSON) правил НЕ несёт — они живут в
|
|
11
|
+
отдельной функции-схеме `defineValidation<T>(({ model }) => …)`.
|
|
12
|
+
|
|
13
|
+
**Сбой async-правила блокирует.** Если правило отклонило промис (сеть, исключение), прогон получает
|
|
14
|
+
статус `error`, а поле — замечание `{ code: 'ruleFailed' }` («Не удалось проверить поле. Повторите
|
|
15
|
+
попытку»): форму, которую не удалось проверить, прогон валидной не считает.
|
|
11
16
|
|
|
12
17
|
```ts
|
|
13
|
-
import { createModel } from '@reformer/core';
|
|
18
|
+
import { createForm, createModel } from '@reformer/core';
|
|
14
19
|
import {
|
|
15
20
|
validate,
|
|
16
21
|
validateAsync,
|
|
17
|
-
|
|
22
|
+
defineValidation,
|
|
18
23
|
validateModel,
|
|
19
24
|
type AsyncRule,
|
|
20
25
|
} from '@reformer/core/validation';
|
|
21
26
|
import { required, email } from '@reformer/core/validators';
|
|
22
27
|
|
|
23
|
-
// async-правило: (value, { signal }) => Promise<
|
|
28
|
+
// async-правило: (value, { signal }) => Promise<ValidationIssue | null>.
|
|
24
29
|
// `signal` — AbortSignal устаревшего прогона: прокинь его в fetch, чтобы отменить in-flight.
|
|
25
30
|
const checkEmailUnique: AsyncRule<string> = async (value, { signal }) => {
|
|
26
31
|
if (!value) return null; // пусто = валидно (sync `required` отдельно)
|
|
27
|
-
try
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
return null; // сетевой сбой/отмена НЕ блокирует submit — возвращаем null, а не ошибку
|
|
33
|
-
}
|
|
32
|
+
// try/catch не нужен: отклонённый запрос раннер запишет сбоем правила (статус `error`,
|
|
33
|
+
// ошибка `ruleFailed` на поле), а отмену устаревшего прогона отличит сам по `signal`.
|
|
34
|
+
const res = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
|
|
35
|
+
const { available } = (await res.json()) as { available: boolean };
|
|
36
|
+
return available ? null : { code: 'email.taken' }; // текст кода — в словаре формы
|
|
34
37
|
};
|
|
35
38
|
|
|
36
|
-
const model = createModel<{ email: string }>({ email: '' });
|
|
37
|
-
|
|
38
39
|
// Схема — обычная функция над моделью; sync и async — разными операторами на одном сигнале.
|
|
39
|
-
const schema =
|
|
40
|
+
const schema = defineValidation<{ email: string }>(({ model }) => {
|
|
40
41
|
validate(model.$.email, [required(), email()]); // sync-фабрики
|
|
41
42
|
validateAsync(model.$.email, [checkEmailUnique]); // async-правило
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
//
|
|
45
|
+
// В сборке — прогон уровнем формы:
|
|
46
|
+
const signup = createForm<{ email: string }>({ initial: { email: '' }, validation: schema });
|
|
47
|
+
const outcome = await signup.submission.submit(api.signup); // обработчик — только при valid
|
|
48
|
+
|
|
49
|
+
// Без сборки — раннер:
|
|
50
|
+
const model = createModel<{ email: string }>({ email: '' });
|
|
45
51
|
const ok = await validateModel(model, schema); // Promise<boolean>
|
|
46
52
|
```
|
|
47
53
|
|
|
48
54
|
### Как это исполняется
|
|
49
55
|
|
|
50
|
-
1. `validateModel(model, schema)` открывает ambient-окно и
|
|
56
|
+
1. Прогон (`bundle.validate()` / `validateModel(model, schema)`) открывает ambient-окно и
|
|
57
|
+
**синхронно** прогоняет схему:
|
|
51
58
|
`validate`/`validateAsync`/`cross` регистрируют правила своих полей.
|
|
52
59
|
2. Sync-правила выполняются сразу; async-правила из `validateAsync` собираются и после закрытия
|
|
53
60
|
ambient-окна дожидаются параллельно (`Promise.all`) с прокинутым `AbortSignal`.
|
|
54
|
-
3.
|
|
55
|
-
|
|
56
|
-
4.
|
|
57
|
-
|
|
61
|
+
3. Пока async-правила поля не завершились, его нода в состоянии `pending`
|
|
62
|
+
(`form.email.pending.value`, `useFormControl(form.email).pending`).
|
|
63
|
+
4. Замечания разносятся по нодам формы, UI подсвечивает поле; поля, ставшие валидными, гасятся.
|
|
64
|
+
Отклонённое правило даёт на своём поле замечание `ruleFailed`.
|
|
65
|
+
5. Итог — `ValidationResult` со статусом (`bundle.validate()`, `runValidation`) либо `boolean`
|
|
66
|
+
(`validateModel`: `true` только при `valid`; `severity:'warning'` не блокирует). Замечания
|
|
67
|
+
(`invalid`), сбой правила (`error`) и устаревший отменённый прогон (`cancelled`) — не `valid`.
|
|
58
68
|
|
|
59
|
-
Синхронного варианта у
|
|
69
|
+
Синхронного варианта у контракта нет: прогон всегда `async`.
|
|
70
|
+
|
|
71
|
+
### Результат прогона и сбой правила
|
|
72
|
+
|
|
73
|
+
Когда нужно отличить «в форме замечания» от «проверить не удалось», бери полный результат —
|
|
74
|
+
`bundle.validate()` / `bundle.validateStep(id)` сборки, а для отправки — статус `SubmitResult`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const outcome = await bundle.submission.submit(api.send);
|
|
78
|
+
if (outcome.status === 'error') {
|
|
79
|
+
// outcome.validation.failures: [{ handle, error }] — какое правило и почему не вернуло результат
|
|
80
|
+
showToast('Не удалось проверить форму. Повторите попытку');
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Без сборки — `runValidation(model, schema)` (только сбор) и `applyValidationResult`:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { applyValidationResult, runValidation } from '@reformer/core/validation';
|
|
88
|
+
|
|
89
|
+
const result = await runValidation(model, schema); // только сбор: ни замечаний, ни pending на нодах
|
|
90
|
+
if (result.status === 'cancelled') return; // вытеснен более новым прогоном — не разносится
|
|
91
|
+
applyValidationResult(result, { touch: true }); // замечания (и ruleFailed) — на поля
|
|
92
|
+
if (result.status === 'valid') await send(model.get());
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Если сбой сети НЕ должен блокировать отправку (проверка необязательна), перехвати его в самом
|
|
96
|
+
правиле и верни `null` — это явное решение автора правила, а не поведение по умолчанию:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const softCheck: AsyncRule<string> = async (value, { signal }) => {
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`/api/check?value=${encodeURIComponent(value)}`, { signal });
|
|
102
|
+
return (await res.json()).ok ? null : { code: 'rejected' };
|
|
103
|
+
} catch {
|
|
104
|
+
return null; // осознанно: недоступность сервиса проверку не проваливает
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
```
|
|
60
108
|
|
|
61
109
|
### Sync и async — два оператора
|
|
62
110
|
|
|
@@ -72,37 +120,32 @@ validateAsync(model.$.inn, [checkInnInRegistry]); // обращение к API
|
|
|
72
120
|
|
|
73
121
|
### UI integration
|
|
74
122
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`setErrors`), поэтому спиннер async-валидации — это собственный флаг (обычно form-level, как
|
|
78
|
-
`ui.pending` в submit-флоу, или локальный state компонента):
|
|
123
|
+
Пока идут async-правила поля, раннер держит его ноду в `pending` — индикатор проверки поля
|
|
124
|
+
берётся из состояния ноды, свой флаг не нужен:
|
|
79
125
|
|
|
80
126
|
```tsx
|
|
81
|
-
const
|
|
127
|
+
const { issues, pending, shouldShowIssues } = useFormControl(form.email);
|
|
128
|
+
const message = useIssueMessage(formMessages); // @reformer/core/react
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
setChecking(true);
|
|
85
|
-
try {
|
|
86
|
-
return await validateModel(model, schema);
|
|
87
|
-
} finally {
|
|
88
|
-
setChecking(false);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null;
|
|
130
|
+
return pending ? <Spinner /> : shouldShowIssues ? <Error text={message(issues[0])} /> : null;
|
|
93
131
|
```
|
|
94
132
|
|
|
133
|
+
Индикатор прогона всей формы — сигнал `bundle.validating` сборки (`createForm`): он истинен,
|
|
134
|
+
пока идёт полный прогон либо прогон шага; `bundle.submission.submitting` — пока работает обработчик
|
|
135
|
+
отправки.
|
|
136
|
+
|
|
95
137
|
### Debounce и отмена
|
|
96
138
|
|
|
97
|
-
- **Отмена устаревших —
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
- Валидация запускается on-demand (submit / шаг / через `revalidateWhen`)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
- **Отмена устаревших — бесплатно.** Быстрый повторный прогон той же пары `(model, schema)`
|
|
140
|
+
отменяет предыдущий in-flight прогон через `AbortController`; его `AbortSignal` прокинут в
|
|
141
|
+
`validateAsync`-правила, поэтому `fetch(url, { signal })` рвётся сам. Ручной debounce для
|
|
142
|
+
КОРРЕКТНОСТИ не нужен — держи схему стабильным `const` (`defineValidation`), иначе отмена не
|
|
143
|
+
сматчит прогоны по идентичности.
|
|
144
|
+
- Валидация запускается on-demand (submit / шаг / через `revalidateWhen`) либо живой стратегией
|
|
145
|
+
сборки — `validation: { rules, strategy: 'change', debounce: 300 }` (см. `33-validation-strategy.md`):
|
|
146
|
+
у живой фазы debounce встроен.
|
|
147
|
+
- Если нужно дебаунсить дорогой async-валидатор относительно частых изменений одного поля — триггерь
|
|
148
|
+
прогон из поведения через `onChange`, который даёт debounce из коробки:
|
|
106
149
|
|
|
107
150
|
```ts
|
|
108
151
|
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
@@ -114,16 +157,16 @@ return checking ? <Spinner /> : errors.length ? <Error errors={errors} /> : null
|
|
|
114
157
|
```
|
|
115
158
|
|
|
116
159
|
Либо оборачивай в свой debounce колбэк `revalidateWhen([...], () => void validateModel(...))`.
|
|
117
|
-
- **Async cross-field** — инлайн `validateAsync`-правило, замыкающее `model` и читающее
|
|
118
|
-
соседей `model.get()` до первого `await` (
|
|
119
|
-
`cross
|
|
160
|
+
- **Async cross-field** — инлайн `validateAsync`-правило, замыкающее `model` и читающее снимок
|
|
161
|
+
соседей `model.get()` до первого `await` (`AsyncRule` получает только `value` и `signal`;
|
|
162
|
+
`cross` — синхронный):
|
|
120
163
|
|
|
121
164
|
```ts
|
|
122
165
|
validateAsync(model.$.email, [
|
|
123
166
|
async (email, { signal }) => {
|
|
124
167
|
const { username } = model.get(); // снапшот соседних полей до await
|
|
125
168
|
const res = await fetch(`/api/check?email=${email}&user=${username}`, { signal });
|
|
126
|
-
return (await res.json()).ok ? null : { code: 'conflict'
|
|
169
|
+
return (await res.json()).ok ? null : { code: 'conflict' };
|
|
127
170
|
},
|
|
128
171
|
]);
|
|
129
172
|
```
|