@reformer/core 10.2.0 → 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 -27
- 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 -91
- package/dist/form/types/field-config.d.ts +27 -0
- package/dist/form/types/index.d.ts +5 -91
- package/dist/form/types/nodes.d.ts +159 -0
- package/dist/form/types/schema-node.d.ts +148 -50
- package/dist/form/types/validation-schema.d.ts +24 -41
- 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 +18 -6
- package/dist/index.js +932 -2779
- 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 +60 -38
- 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 +22 -0
- 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 -5
- package/dist/validators.js +98 -154
- package/docs/llms/01-api-reference.md +89 -0
- package/docs/llms/02-quick-start.md +181 -0
- package/docs/llms/03-api-signatures.md +494 -0
- package/docs/llms/04-common-patterns.md +204 -0
- package/docs/llms/05-common-mistakes.md +192 -0
- package/docs/llms/06-troubleshooting.md +16 -0
- package/docs/llms/07-complete-import.md +130 -0
- package/docs/llms/08-form-types.md +54 -0
- package/docs/llms/09-formschema.md +191 -0
- package/docs/llms/10-arrays.md +241 -0
- package/docs/llms/11-async-watchfield.md +53 -0
- package/docs/llms/12-array-cleanup.md +31 -0
- package/docs/llms/13-multi-step.md +162 -0
- package/docs/llms/14-extended-mistakes.md +101 -0
- package/docs/llms/15-project-structure.md +279 -0
- package/docs/llms/16-ui-components.md +229 -0
- package/docs/llms/17-nonexistent-api.md +278 -0
- package/docs/llms/18-conditional-fields.md +222 -0
- package/docs/llms/19-reading-values.md +100 -0
- package/docs/llms/20-compute-vs-watch.md +153 -0
- package/docs/llms/21-array-operations.md +163 -0
- package/docs/llms/22-cycle-detection.md +86 -0
- package/docs/llms/23-copy-from.md +141 -0
- package/docs/llms/24-sync-fields.md +115 -0
- package/docs/llms/25-reset-when.md +122 -0
- package/docs/llms/26-transform-value.md +136 -0
- package/docs/llms/27-revalidate-when.md +133 -0
- package/docs/llms/28-submit-and-reset.md +409 -0
- package/docs/llms/29-async-preload.md +203 -0
- package/docs/llms/30-type-safety-recipes.md +213 -0
- package/docs/llms/31-async-validator-debounce.md +178 -0
- package/docs/llms/32-async-options-loading.md +108 -0
- package/docs/llms/33-validation-strategy.md +305 -0
- 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 +10014 -3826
- package/package.json +77 -15
- 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 -125
- package/dist/form/nodes/array-node.d.ts +0 -360
- package/dist/form/nodes/field-node.d.ts +0 -294
- package/dist/form/nodes/form-node.d.ts +0 -340
- package/dist/form/nodes/group-node.d.ts +0 -288
- package/dist/form/nodes/model-array-node.d.ts +0 -121
- package/dist/form/status-machine.d.ts +0 -153
- package/dist/form/types/deep-schema.d.ts +0 -116
- package/dist/form/types/form-proxy.d.ts +0 -141
- package/dist/form/unique-id.d.ts +0 -53
- 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 -6
- 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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
## 3. COMMON PATTERNS
|
|
2
|
+
|
|
3
|
+
Значения живут в модели (`model.$.field`), поведение (`defineFormBehavior`) работает на живых
|
|
4
|
+
сигналах, валидация — отдельная схема (`defineValidation`, прогон по требованию через
|
|
5
|
+
раннер). Слои раздельны: схема формы правил НЕ несёт.
|
|
6
|
+
|
|
7
|
+
### Conditional Fields with Auto-Reset
|
|
8
|
+
|
|
9
|
+
`enableWhen` — оператор поведения: поле включается по условию, при выключении сбрасывается к
|
|
10
|
+
initial. Вызывается внутри `defineFormBehavior`:
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
import { defineFormBehavior, enableWhen } from '@reformer/core/behaviors';
|
|
14
|
+
|
|
15
|
+
const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
16
|
+
enableWhen(
|
|
17
|
+
[model.$.propertyValue, model.$.initialPayment],
|
|
18
|
+
() => model.loanType === 'mortgage',
|
|
19
|
+
{ resetOnDisable: true }
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Computed Field (same or cross level)
|
|
25
|
+
|
|
26
|
+
`compute`/`computeFrom` пишут в целевой сигнал при изменении источников. Цель не входит
|
|
27
|
+
в источники → цикла нет. Кросс-уровневые вычисления работают так же — источники берутся
|
|
28
|
+
по сигналам из любого места модели.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { defineFormBehavior, compute, computeFrom } from '@reformer/core/behaviors';
|
|
32
|
+
|
|
33
|
+
const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
34
|
+
// compute: auto-tracking — читаем что нужно прямо из value-модели
|
|
35
|
+
compute(model.$.total, () => (model.price ?? 0) * (model.quantity ?? 0));
|
|
36
|
+
|
|
37
|
+
// computeFrom: явный список источников
|
|
38
|
+
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
|
|
39
|
+
|
|
40
|
+
// кросс-уровневое: fullName из вложенной группы
|
|
41
|
+
compute(model.$.fullName, () =>
|
|
42
|
+
[model.personalData.firstName, model.personalData.lastName].filter(Boolean).join(' ')
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Async reaction / dynamic options — `onChange`
|
|
48
|
+
|
|
49
|
+
Для async-реакции на изменение поля (загрузка справочников, зависимые селекты) используй
|
|
50
|
+
`onChange` из DSL. Колбэк выполняется вне effect-контекста (можно писать сигналы/ноды), а
|
|
51
|
+
2-й аргумент — `{ signal }` (AbortSignal) для отмены устаревших запросов.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
55
|
+
|
|
56
|
+
const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
57
|
+
onChange(
|
|
58
|
+
model.$.region,
|
|
59
|
+
async (region, { signal }) => {
|
|
60
|
+
if (!region) {
|
|
61
|
+
form.city.updateComponentProps({ options: [] });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const cities = await fetchCities(region, { signal });
|
|
65
|
+
form.city.updateComponentProps({ options: cities });
|
|
66
|
+
},
|
|
67
|
+
{ debounce: 300 }
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core/model` (без debounce/AbortSignal;
|
|
73
|
+
> для сети предпочтителен `onChange`). См. `20-compute-vs-watch.md`, `32-async-options-loading.md`.
|
|
74
|
+
|
|
75
|
+
### Reading values
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// В React-компоненте — хуки (@reformer/core/react)
|
|
79
|
+
const { value, issues, disabled } = useFormControl(form.email);
|
|
80
|
+
const loanType = useFormControlValue(form.loanType); // значение напрямую
|
|
81
|
+
|
|
82
|
+
// Вне React — из модели
|
|
83
|
+
model.email; // value-доступ (реактивно внутри effect/computed)
|
|
84
|
+
model.$.email.value; // через сигнал
|
|
85
|
+
model.$.email.peek(); // нереактивный снимок
|
|
86
|
+
model.get(); // весь объект-снимок
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Submit + validation
|
|
90
|
+
|
|
91
|
+
Валидация — отдельная `ValidationDefinition<T>` (`defineValidation`), а не часть layout; в сборку
|
|
92
|
+
она уходит полем `validation`. `bundle.submission.submit(handler)` прогоняет все правила один раз,
|
|
93
|
+
разносит замечания по нодам и зовёт обработчик только при статусе `valid` (`severity: 'warning'` не
|
|
94
|
+
блокирует). Результат — `SubmitResult`.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
async function handleSubmit(event: React.FormEvent) {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
const outcome = await bundle.submission.submit(async (values) => {
|
|
100
|
+
await api.send(values);
|
|
101
|
+
bundle.model.reset(); // к initial-снимку — только после успеха
|
|
102
|
+
});
|
|
103
|
+
if (outcome.status === 'invalid') showHint('Проверьте форму'); // поля уже подсвечены
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Без сборки — раннер `validateModel(model, schema)`: сам разносит замечания по нодам и отвечает
|
|
108
|
+
`Promise<boolean>`.
|
|
109
|
+
|
|
110
|
+
Multi-step: шаги и их правила — поток сборки `createForm({ flow: { steps: [{ id, rules }] } })`
|
|
111
|
+
(`id` — `selector` шага), правила вне шагов — `validation: { extras }`. Сборка отдаёт
|
|
112
|
+
`bundle.validateStep(id)`, `bundle.validate()` и поток `bundle.flow` (`next` / `previous` / `goTo`);
|
|
113
|
+
визард принимает поток пропом `flow`. См. `13-multi-step.md`, `28-submit-and-reset.md`,
|
|
114
|
+
`38-form-flow.md`.
|
|
115
|
+
|
|
116
|
+
### Cross-field validation — через `cross`
|
|
117
|
+
|
|
118
|
+
Cross-field правило — обычная функция над **снимком** модели. Оператор `cross` берётся из
|
|
119
|
+
аргумента схемы и вешает правило на поле-носитель ошибки; тип снимка выведен из схемы:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
123
|
+
import { required, min } from '@reformer/core/validators';
|
|
124
|
+
import type { ValidationIssue } from '@reformer/core';
|
|
125
|
+
|
|
126
|
+
// снимок формы читается напрямую — без каста, соседние поля доступны как поля объекта
|
|
127
|
+
const initialPaymentVsProperty = (form: MyForm): ValidationIssue | null =>
|
|
128
|
+
form.initialPayment && form.propertyValue && form.initialPayment > form.propertyValue
|
|
129
|
+
? { code: 'initialPaymentTooHigh' } // текст кода — в словаре формы (defineIssueMessages)
|
|
130
|
+
: null;
|
|
131
|
+
|
|
132
|
+
const schema = defineValidation<MyForm>(({ model, cross }) => {
|
|
133
|
+
validate(model.$.initialPayment, [required(), min(0)]);
|
|
134
|
+
cross(model.$.initialPayment, initialPaymentVsProperty); // ошибка сядет на initialPayment
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
> Правила строки массива и подформы — отдельная схема над их типом, подключённая через
|
|
139
|
+
> `applyEach(model.$.items, rowRules)` / `apply(model.$.group, groupRules)`. У такой схемы своя
|
|
140
|
+
> область: её `cross` получает снимок строки или группы.
|
|
141
|
+
|
|
142
|
+
### Conditional validation — `validateWhen`
|
|
143
|
+
|
|
144
|
+
Условная валидация — `validateWhen(() => cond, () => { … })`: правила внутри активны, только пока
|
|
145
|
+
условие истинно; при `false` ранее тронутые поля гасятся. Это не `enable` (то — поведение), а
|
|
146
|
+
включение/выключение самих проверок:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { defineValidation, validateWhen, validate } from '@reformer/core/validation';
|
|
150
|
+
|
|
151
|
+
const loanRules = defineValidation<MyForm>(({ model, cross }) => {
|
|
152
|
+
validateWhen(
|
|
153
|
+
() => model.loanType === 'mortgage',
|
|
154
|
+
() => {
|
|
155
|
+
validate(model.$.propertyValue, [required(), min(1000000)]);
|
|
156
|
+
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Перезапуск прогона — `revalidateWhen` (мост поведение → валидация)
|
|
163
|
+
|
|
164
|
+
Схема прогоняется по требованию (submit/шаг) либо живой стратегией сборки
|
|
165
|
+
(`validation: { strategy: 'change' }`, см. `33-validation-strategy.md`). Чтобы перезапустить
|
|
166
|
+
отдельную схему при изменении зависимости — `revalidateWhen` из **поведения** (мост
|
|
167
|
+
поведение → валидация):
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
171
|
+
import { validateModel } from '@reformer/core/validation';
|
|
172
|
+
|
|
173
|
+
const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
174
|
+
revalidateWhen([model.$.propertyValue], () => void validateModel(model, schema));
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Extracting named rules
|
|
179
|
+
|
|
180
|
+
Когда тело правила растёт — выноси в именованную функцию. Field-правило типизируй `Rule<T>`
|
|
181
|
+
(`(value) => ValidationIssue | null`), cross-field — обычной `(f: Root) => ValidationIssue | null`.
|
|
182
|
+
Схема остаётся плоской и читается как оглавление:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
186
|
+
import { required } from '@reformer/core/validators';
|
|
187
|
+
import type { Rule } from '@reformer/core/validation';
|
|
188
|
+
|
|
189
|
+
// field-правило: (value) => error
|
|
190
|
+
const validateAdultAge: Rule<string> = (value) => {
|
|
191
|
+
if (!value) return null;
|
|
192
|
+
const age = new Date().getFullYear() - new Date(value).getFullYear();
|
|
193
|
+
return age < 18 ? { code: 'tooYoung' } : null;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const schema = defineValidation<MyForm>(({ model }) => {
|
|
197
|
+
validate(model.$.birthDate, [required(), validateAdultAge]);
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Naming convention** (camelCase, семантика, не эхо оператора):
|
|
202
|
+
|
|
203
|
+
- Cross-field правило → инвариант: `initialPaymentVsPropertyValue`, `paymentToIncomeUnderHalf`.
|
|
204
|
+
- Field-правило → проверка: `validateAdultAge`, `passwordsMatch`.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
## 4. COMMON MISTAKES
|
|
2
|
+
|
|
3
|
+
### Imports rule (#1 cause of cascading errors — read first)
|
|
4
|
+
|
|
5
|
+
- Модель/форма/типы (`createForm`, `FormModel`, `FormSchemaNode`, `ValidationIssue`, `FormFlow`) — из `@reformer/core`.
|
|
6
|
+
- Хуки (`useFormBundle`, `useFormControl`, `useFormFlow`, `useIssueMessage`, …) и `I18nProvider` — из `@reformer/core/react`.
|
|
7
|
+
- Схема валидации (`defineValidation` + операторы `validate`/`validateAsync`/`validateWhen`/`apply`/`applyEach`) и раннер `validateModel` — из `@reformer/core/validation`. `cross` берётся из аргумента схемы.
|
|
8
|
+
- Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.
|
|
9
|
+
- Словарь текстов замечаний (`defineIssueMessages`, `resolveIssueMessage`) — из `@reformer/core/i18n`.
|
|
10
|
+
- Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// ✅ CORRECT
|
|
14
|
+
import {
|
|
15
|
+
createModel,
|
|
16
|
+
createForm,
|
|
17
|
+
type FormGroup,
|
|
18
|
+
type FormSchemaNode,
|
|
19
|
+
type FormModel,
|
|
20
|
+
type ValidationIssue,
|
|
21
|
+
} from '@reformer/core';
|
|
22
|
+
import { useFormBundle, useFormControl, useFormControlValue } from '@reformer/core/react';
|
|
23
|
+
import { defineIssueMessages } from '@reformer/core/i18n';
|
|
24
|
+
// схема валидации + раннер — отдельный слой:
|
|
25
|
+
import {
|
|
26
|
+
defineValidation,
|
|
27
|
+
validate,
|
|
28
|
+
validateAsync,
|
|
29
|
+
validateWhen,
|
|
30
|
+
apply,
|
|
31
|
+
applyEach,
|
|
32
|
+
validateModel,
|
|
33
|
+
type Rule,
|
|
34
|
+
type AsyncRule,
|
|
35
|
+
type ValidationDefinition,
|
|
36
|
+
} from '@reformer/core/validation';
|
|
37
|
+
import { required, min, max, email } from '@reformer/core/validators';
|
|
38
|
+
// поведение формы — декларативный DSL:
|
|
39
|
+
import { defineFormBehavior, compute, enableWhen, copyFrom, onChange } from '@reformer/core/behaviors';
|
|
40
|
+
// примитивы над сигналами (вне формы, возвращают cleanup) — отдельный сабпат:
|
|
41
|
+
import { computeFrom, watchField } from '@reformer/core/model';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> **В корне `@reformer/core` операторов поведения нет.** `computeFrom`, `copyFrom`, `watchField`,
|
|
45
|
+
> `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` — примитивы сабпата
|
|
46
|
+
> `@reformer/core/model`; `enableWhen` / `disableWhen` — только операторы `@reformer/core/behaviors`.
|
|
47
|
+
> В DSL для реакции на изменения используется `onChange`.
|
|
48
|
+
|
|
49
|
+
### Значения — в модели, не в форме
|
|
50
|
+
|
|
51
|
+
Источник истины значений — модель. Форма (ноды) отражает их; `form.x.setValue(v)` пишет в ту же
|
|
52
|
+
модель.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// ✅ читаем/пишем значение
|
|
56
|
+
model.email = 'a@b.c'; // value-доступ
|
|
57
|
+
model.$.email.value; // сигнал
|
|
58
|
+
model.get(); // весь снимок для submit
|
|
59
|
+
|
|
60
|
+
// В компоненте — реактивно через хуки
|
|
61
|
+
const email = useFormControlValue(form.email);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### useFormControlValue (CRITICAL)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// WRONG - useFormControlValue returns T directly, NOT { value: T }
|
|
68
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
69
|
+
// Result: loanType is ALWAYS undefined!
|
|
70
|
+
|
|
71
|
+
// CORRECT
|
|
72
|
+
const loanType = useFormControlValue(control.loanType);
|
|
73
|
+
|
|
74
|
+
// ALSO CORRECT - useFormControl returns object
|
|
75
|
+
const { value, issues } = useFormControl(control.loanType);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Behaviors принимают СИГНАЛЫ, а не пути/форму
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// ❌ WRONG - строковые пути и (form) => ... это старый API, удалён
|
|
82
|
+
enableWhen(path.city, (form) => Boolean(form.country));
|
|
83
|
+
computeFrom(['price', 'quantity'], 'total', (values) => values.price * values.quantity);
|
|
84
|
+
|
|
85
|
+
// ✅ CORRECT - сигналы модели, условие читает model напрямую
|
|
86
|
+
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true });
|
|
87
|
+
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Валидаторы — оператор `validate`, а не узел схемы
|
|
91
|
+
|
|
92
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`), а не ключ узла. Схема формы
|
|
93
|
+
(билдер `createForm` / JSON-DSL) правил **не несёт**: они живут в `ValidationDefinition` и уходят в
|
|
94
|
+
сборку полем `validation` (шаги — `flow`). Запуск — уровнем формы:
|
|
95
|
+
`bundle.submission.submit(handler)`, `bundle.validate()`, `bundle.validateStep(id)`; без сборки —
|
|
96
|
+
`validateModel(model, schema)`. У нод методов `validate()` / `submit()` нет.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// ❌ WRONG - ключей value и validators у узла нет, как и записи «имя → узел»
|
|
100
|
+
const schema = {
|
|
101
|
+
email: { value: model.$.email, component: Input, validators: [required(), email()] },
|
|
102
|
+
};
|
|
103
|
+
validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
|
|
104
|
+
required({ message: 'Email is required' }); // опции message нет — текст живёт в словаре формы
|
|
105
|
+
|
|
106
|
+
// ✅ CORRECT - операторы validate/validateAsync внутри defineValidation; у правила — код
|
|
107
|
+
const emailSchema = defineValidation<MyForm>(({ model }) => {
|
|
108
|
+
validate(model.$.email, [required({ code: 'email.required' }), email()]);
|
|
109
|
+
validateAsync(model.$.email, [
|
|
110
|
+
async (value, { signal }) => {
|
|
111
|
+
const res = await fetch(`/api/free?email=${value}`, { signal });
|
|
112
|
+
return (await res.json()).free ? null : { code: 'email.taken' };
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
// тексты кодов — словарь формы
|
|
117
|
+
export const formMessages = defineIssueMessages({
|
|
118
|
+
'email.required': 'Укажите email',
|
|
119
|
+
'email.taken': 'Email уже занят',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// прогон — уровнем формы: замечания сами доезжают до нод, warnings не блокируют.
|
|
123
|
+
// Отклонённое async-правило — сбой: статус `error`, на поле замечание `ruleFailed`.
|
|
124
|
+
const outcome = await bundle.submission.submit(api.send); // createForm({ …, validation: emailSchema })
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Cross-field — оператор `cross`, а не `ctx.form`
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// ❌ WRONG - ctx.form / ctx.setFieldValue / value.value — старый API; ModelValidator(value,scope,root) удалён
|
|
131
|
+
watchField(path.amount, (amount, ctx) => {
|
|
132
|
+
const rate = ctx.form.rate.value.value;
|
|
133
|
+
ctx.setFieldValue('total', amount * rate);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross
|
|
137
|
+
compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));
|
|
138
|
+
|
|
139
|
+
// cross-field правило — обычная функция над снимком модели; `cross` берётся из аргумента схемы:
|
|
140
|
+
const amountVsMax = (form: MyForm): ValidationIssue | null =>
|
|
141
|
+
form.amount != null && form.maxAmount != null && form.amount > form.maxAmount
|
|
142
|
+
? { code: 'amount.tooBig', params: { max: form.maxAmount } }
|
|
143
|
+
: null;
|
|
144
|
+
|
|
145
|
+
const amountSchema = defineValidation<MyForm>(({ model, cross }) => {
|
|
146
|
+
cross(model.$.amount, amountVsMax); // check получает снимок модели, ошибка — на model.$.amount
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Form-shape types — `interface` или `type`
|
|
151
|
+
|
|
152
|
+
Форму данных можно объявлять и `interface`, и `type` — оба работают с `createModel<T>`,
|
|
153
|
+
`createForm<T>`, `FormGroup<T>` и `FormArray<T>`. Группу от поля `FormGroup` отличает по виду
|
|
154
|
+
значения (обычный объект, а не массив, дата, файл или функция), а не по index signature. Эталонная
|
|
155
|
+
форма (`CreditApplication`) объявлена через `interface`.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// ✅ interface
|
|
159
|
+
export interface PropertyItem {
|
|
160
|
+
type: PropertyType;
|
|
161
|
+
description: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ✅ type alias — то же самое
|
|
165
|
+
export type PropertyItem = {
|
|
166
|
+
type: PropertyType;
|
|
167
|
+
description: string;
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Единственная оговорка — свои обобщённые хелперы: `interface` не присваивается
|
|
172
|
+
`Record<string, unknown>` (у него нет неявной index signature). Ограничение пиши через
|
|
173
|
+
`T extends object`, а не `T extends Record<string, unknown>`.
|
|
174
|
+
|
|
175
|
+
`number | null` — конвенциональный тип для очищенного поля; встроенные валидаторы
|
|
176
|
+
(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают
|
|
177
|
+
пустые значения внутри.
|
|
178
|
+
|
|
179
|
+
### Вид ноды — гардами, а не instanceof
|
|
180
|
+
|
|
181
|
+
`FormControl`, `FormGroup`, `FormArray` — интерфейсы: классов нод в публичном API нет, проверка
|
|
182
|
+
`instanceof` не скомпилируется. Вид ноды различают гарды по метке вида:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// ❌ FormControl — тип, а не класс
|
|
186
|
+
if (node instanceof FormControl) { ... }
|
|
187
|
+
|
|
188
|
+
// ✅ type guards
|
|
189
|
+
import { isFormNode, isFormControl, isFormGroup, isFormArray } from '@reformer/core';
|
|
190
|
+
if (isFormControl(node)) node.updateComponentProps({ placeholder: 'Email' });
|
|
191
|
+
if (isFormArray(node)) node.push();
|
|
192
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## 5. TROUBLESHOOTING
|
|
2
|
+
|
|
3
|
+
| Error | Cause | Solution |
|
|
4
|
+
| -------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
5
|
+
| `'message' does not exist in type 'RuleOptions'` | Текст передан в правило — у правил текста нет | `required({ code: 'phone.required' })` + запись `'phone.required'` в `defineIssueMessages`; см. 36-issue-messages.md |
|
|
6
|
+
| `'string' is not assignable to 'RuleOptions'` | Валидатору передали строку вместо options | `required({ code: 'field.required' })` |
|
|
7
|
+
| `Module has no exported member` | Неверный источник импорта | хуки и `I18nProvider` — из `@reformer/core/react`; `watchField`/примитивы — из `@reformer/core/model`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators`; удалённое — 17-nonexistent-api.md |
|
|
8
|
+
| `undefined` из `useFormControlValue` | Деструктурировали хук | `const v = useFormControlValue(...)` — без деструктуризации |
|
|
9
|
+
| `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
|
|
10
|
+
| `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
|
|
11
|
+
| Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` / `form.field.setValue(...)` |
|
|
12
|
+
| Замечания не появляются после submit | Отправка в обход проверки (`api.send(model.get())`) | `await bundle.submission.submit(handler)` — один прогон правил, при отказе вся форма помечается тронутой; без сборки — `await validateModel(model, schema)` |
|
|
13
|
+
| На поле текст вида `loanAmount.required` | Свой код правила без записи в словаре формы | Добавь код в `defineIssueMessages` и подключи `ValidationMessagesProvider messages` (cdk) |
|
|
14
|
+
| `compileSchema: в области «#» селектор «X» повторяется` | Два узла с одним `selector` в одной области схемы | Разные селекторы; см. 37-compiled-schema.md |
|
|
15
|
+
| `createForm: шага «X» нет в потоке` / `FormFlow: шага «X» нет` | `validateStep(id)` / `flow.goTo(id)` с неизвестным шагом | `id` — из `flow.steps`; см. 38-form-flow.md |
|
|
16
|
+
| `flow.goTo(id)` вернул `reason: 'locked'` | Прыжок вперёд через непройденный шаг | Вперёд — `flow.next()`; `goTo` — назад и на открытые шаги |
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
## Import Patterns
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
// Модель, форма, уровень формы, типы — из @reformer/core. React здесь нет: корень работает и на сервере
|
|
5
|
+
import {
|
|
6
|
+
createModel,
|
|
7
|
+
createForm,
|
|
8
|
+
createFormFromModel,
|
|
9
|
+
disposeForm, // снять форму из createFormFromModel; сборку снимает bundle.dispose()
|
|
10
|
+
createSubmission, // отправка без сборки (визард в разметке без createForm)
|
|
11
|
+
arrayOf, // массив модели с шаблоном новой строки
|
|
12
|
+
compileSchema, // авторское дерево → CompiledSchema (обычно не нужен: createForm компилирует сам)
|
|
13
|
+
createFormFlow, // поток шагов без сборки
|
|
14
|
+
isFormControl,
|
|
15
|
+
isFormGroup,
|
|
16
|
+
isFormArray,
|
|
17
|
+
} from '@reformer/core';
|
|
18
|
+
|
|
19
|
+
// Привязка к React — хуки, контекст сборки, связка поля с контролом, провайдер локали — из /react
|
|
20
|
+
import {
|
|
21
|
+
useFormBundle,
|
|
22
|
+
useFormControl,
|
|
23
|
+
useFormControlValue,
|
|
24
|
+
useArrayLength,
|
|
25
|
+
useFormFlow, // снимок потока шагов для рендера
|
|
26
|
+
useFormValidation, // стратегия без сборки: submit() — попытка отправки, validate() — проверка
|
|
27
|
+
useIssueMessage, // текст замечания: словарь формы → локаль → английский → код
|
|
28
|
+
I18nProvider,
|
|
29
|
+
} from '@reformer/core/react';
|
|
30
|
+
import type { FieldControlState } from '@reformer/core/react';
|
|
31
|
+
|
|
32
|
+
// Типы
|
|
33
|
+
import type {
|
|
34
|
+
FormModel, // реактивная модель данных
|
|
35
|
+
FormBundle, // результат createForm: { model, form, render, flow, validate, validateStep, submission, dispose, … }
|
|
36
|
+
FormNode, // общий контракт ноды
|
|
37
|
+
FormControl, // нода одного поля
|
|
38
|
+
FormGroup, // нода группы — тип формы для props компонентов
|
|
39
|
+
FormArray, // нода массива под-форм
|
|
40
|
+
ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
|
|
41
|
+
ModelSignals, // дерево ручек ($)
|
|
42
|
+
PathAwareSignal, // ручка, знающая свой путь
|
|
43
|
+
ValidationIssue, // { code, params?, severity? } — без текста
|
|
44
|
+
RuleOptions, // { code?, params? } — опции фабрик правил
|
|
45
|
+
FormSchemaNode, // узел схемы: поле | массив под-форм | подформа | контейнер
|
|
46
|
+
CompiledSchema, // скомпилированная схема: bundle.render.tree
|
|
47
|
+
FormValidation, // правила формы как данные: { rules?, extras?, strategy?, debounce? }
|
|
48
|
+
FormFlowConfig, // поток шагов в конфиге: { steps: [{ id, rules? }] }
|
|
49
|
+
FormFlow, // поток шагов сборки: current, next(), previous(), goTo(id)
|
|
50
|
+
FlowTransition, // итог перехода: moved | stayed
|
|
51
|
+
FormSubmission, // отправка сборки (bundle.submission): submit(handler), submitting
|
|
52
|
+
SubmitResult, // итог bundle.submission.submit: submitted | invalid | error | cancelled | failed
|
|
53
|
+
} from '@reformer/core';
|
|
54
|
+
|
|
55
|
+
// Схема валидации: операторы + раннер — из /validation
|
|
56
|
+
import {
|
|
57
|
+
defineValidation,
|
|
58
|
+
validate,
|
|
59
|
+
validateAsync,
|
|
60
|
+
validateWhen,
|
|
61
|
+
apply, // правила подформы: apply(model.$.group, rules); композиция: apply(rulesA, rulesB)
|
|
62
|
+
applyEach, // правила строк массива: applyEach(model.$.items, rules)
|
|
63
|
+
validateModel, // раннер без сборки: сбор + разнос, Promise<boolean>
|
|
64
|
+
runValidation, // только сбор: Promise<ValidationResult>
|
|
65
|
+
applyValidationResult, // разнос собранного результата по нодам
|
|
66
|
+
} from '@reformer/core/validation';
|
|
67
|
+
import type {
|
|
68
|
+
Rule, // (value) => ValidationIssue | null
|
|
69
|
+
AsyncRule, // (value, { signal }) => Promise<ValidationIssue | null>
|
|
70
|
+
ValidationDefinition, // ({ model, cross }) => void — cross берётся из аргумента схемы
|
|
71
|
+
ValidationResult, // { status, issues, failures }
|
|
72
|
+
} from '@reformer/core/validation';
|
|
73
|
+
|
|
74
|
+
// Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))
|
|
75
|
+
import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';
|
|
76
|
+
|
|
77
|
+
// Тексты замечаний и локаль без React — из /i18n
|
|
78
|
+
import { defineIssueMessages, resolveIssueMessage, createI18n } from '@reformer/core/i18n';
|
|
79
|
+
|
|
80
|
+
// Декларативный DSL behaviors — из /behaviors
|
|
81
|
+
import {
|
|
82
|
+
defineFormBehavior,
|
|
83
|
+
compute,
|
|
84
|
+
computeFrom,
|
|
85
|
+
copyFrom,
|
|
86
|
+
onChange,
|
|
87
|
+
enableWhen,
|
|
88
|
+
disableWhen,
|
|
89
|
+
transformValue,
|
|
90
|
+
resetWhen,
|
|
91
|
+
syncFields,
|
|
92
|
+
revalidateWhen,
|
|
93
|
+
apply,
|
|
94
|
+
applyEach,
|
|
95
|
+
aggregateInto,
|
|
96
|
+
exclusiveFlag,
|
|
97
|
+
hideWhen, // операторы узлов схемы — исполняет рендерер
|
|
98
|
+
renderEffect,
|
|
99
|
+
} from '@reformer/core/behaviors';
|
|
100
|
+
|
|
101
|
+
// Примитивы над сигналами (принимают сигналы, возвращают cleanup) — из /model, в корне их нет
|
|
102
|
+
import { computeFrom, copyFrom, watchField } from '@reformer/core/model';
|
|
103
|
+
// Утилиты значений модели — тоже из /model
|
|
104
|
+
import {
|
|
105
|
+
deepEqual, // сравнение значений модели (на нём isDirty)
|
|
106
|
+
deferredWritesSettled, // дождаться отложенных записей поведения тика; undefined — ждать нечего
|
|
107
|
+
} from '@reformer/core/model';
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Форма данных — `interface` или `type`
|
|
111
|
+
|
|
112
|
+
Оба варианта работают одинаково: `createModel<T>`, `createForm<T>`, `FormGroup<T>` и `FormArray<T>`
|
|
113
|
+
принимают и `interface`, и `type`-alias — для корня, вложенных групп и элементов массивов. Группу
|
|
114
|
+
от поля сборка отличает по виду значения (обычный объект — группа; массив объектов — массив
|
|
115
|
+
под-форм; примитив, массив примитивов, дата, файл — поле), а не по index signature.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
export interface AddressForm {
|
|
119
|
+
street: string;
|
|
120
|
+
city: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CoBorrower {
|
|
124
|
+
fullName: string;
|
|
125
|
+
phone: string;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Оговорка для своих обобщённых хелперов: `interface` не присваивается `Record<string, unknown>`, поэтому
|
|
130
|
+
ограничение пиши `T extends object`. См. `30-type-safety-recipes.md`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
## 7. FORM TYPE DEFINITION
|
|
2
|
+
|
|
3
|
+
Форму данных объявляй через `interface` или `type`-alias — оба варианта работают одинаково (см.
|
|
4
|
+
`30-type-safety-recipes.md`). Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без
|
|
5
|
+
узлов/сигналов.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// CORRECT form type definition
|
|
9
|
+
interface MyForm {
|
|
10
|
+
// Required fields
|
|
11
|
+
name: string;
|
|
12
|
+
email: string;
|
|
13
|
+
|
|
14
|
+
// Optional fields — конвенция для форм: null («пользователь очистил»)
|
|
15
|
+
phone: string | null;
|
|
16
|
+
age: number | null;
|
|
17
|
+
|
|
18
|
+
// Enum/union types
|
|
19
|
+
status: 'active' | 'inactive';
|
|
20
|
+
|
|
21
|
+
// Nested objects
|
|
22
|
+
address: {
|
|
23
|
+
street: string;
|
|
24
|
+
city: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Arrays of objects — model-owned (см. 10-arrays.md)
|
|
28
|
+
items: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Модель создаётся из initial-значений этого типа
|
|
35
|
+
const model = createModel<MyForm>({
|
|
36
|
+
name: '',
|
|
37
|
+
email: '',
|
|
38
|
+
phone: null,
|
|
39
|
+
age: null,
|
|
40
|
+
status: 'active',
|
|
41
|
+
address: { street: '', city: '' },
|
|
42
|
+
items: [],
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`number | null` / `string | null` работают со встроенными валидаторами напрямую —
|
|
47
|
+
`min`/`max`/`minLength`/`minDate`/`minAge` пропускают пустые значения внутри, guard `if (v != null)`
|
|
48
|
+
не нужен.
|
|
49
|
+
|
|
50
|
+
Какой ноде формы соответствует поле, решает вид значения: объект — `FormGroup`
|
|
51
|
+
(`form.address.city`), массив объектов — `FormArray` (`form.items.at(0)`), примитив, массив
|
|
52
|
+
примитивов, `Date`, `File` — `FormControl`. Вычисляемые поля (цели `compute`) удобно держать
|
|
53
|
+
отдельным типом и объявлять форму пересечением: `type MyFormFull = MyForm & MyFormDerived` — тогда
|
|
54
|
+
на сервер уходит только ввод (`MyForm`), а вычисляемые ключи отрезаются перед отправкой.
|