@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,284 @@
|
|
|
1
|
+
# Тексты замечаний валидации — коды, словарь формы и резолвер
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Замечание валидации текста не несёт. Правило сообщает **что** нарушено (`code`) и **с какими
|
|
6
|
+
числами** (`params`), а отображаемую строку по коду собирает резолвер в момент показа:
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
interface ValidationIssue {
|
|
10
|
+
readonly code: string; // встроенный ('required', 'minLength') либо свой ('loanAmount.required')
|
|
11
|
+
readonly params?: Readonly<Record<string, unknown>>; // порог правила, фактическое значение
|
|
12
|
+
readonly severity?: 'error' | 'warning'; // 'warning' показывается, но отправку не блокирует
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Отсюда два следствия:
|
|
17
|
+
|
|
18
|
+
- смена языка переводит уже показанные замечания — повторная валидация не нужна;
|
|
19
|
+
- одно и то же правило получает разный текст у разных полей: правилу дают свой код, а текст кода
|
|
20
|
+
кладут в словарь формы.
|
|
21
|
+
|
|
22
|
+
Опций `message` / `messageKey` у правил нет, у замечания нет поля `message` — см.
|
|
23
|
+
[17-nonexistent-api.md](./17-nonexistent-api.md).
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// @reformer/core — опции фабрик @reformer/core/validators (второй или последний аргумент)
|
|
29
|
+
interface RuleOptions {
|
|
30
|
+
code?: string; // свой код вместо встроенного; принято `<поле>.<правило>`
|
|
31
|
+
params?: Readonly<Record<string, unknown>>; // доп. параметры — поверх параметров правила
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// @reformer/core/i18n — без React
|
|
35
|
+
type IssueMessage =
|
|
36
|
+
| string // шаблон: 'Не меньше {minLength} символов', plural, select
|
|
37
|
+
| ((params: Readonly<Record<string, unknown>>, i18n: I18nHandle) => string);
|
|
38
|
+
type IssueMessages = Readonly<Record<string, IssueMessage>>; // код → текст
|
|
39
|
+
|
|
40
|
+
function defineIssueMessages<const M extends IssueMessages>(messages: M): M; // возвращает аргумент
|
|
41
|
+
function resolveIssueMessage(
|
|
42
|
+
issue: ValidationIssue,
|
|
43
|
+
i18n: I18nHandle, // createI18n(locale)
|
|
44
|
+
messages?: IssueMessages // словарь формы
|
|
45
|
+
): string;
|
|
46
|
+
|
|
47
|
+
// @reformer/core/react
|
|
48
|
+
function useIssueMessage(messages?: IssueMessages): (issue: ValidationIssue) => string;
|
|
49
|
+
|
|
50
|
+
// @reformer/cdk — словарь формы для поддерева; FormField читает его сам
|
|
51
|
+
function ValidationMessagesProvider(props: {
|
|
52
|
+
messages?: IssueMessages;
|
|
53
|
+
resolver?: (issue: ValidationIssue) => string; // полное переопределение
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
}): ReactNode;
|
|
56
|
+
function useIssueMessageResolver(): (issue: ValidationIssue) => string;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Порядок поиска текста
|
|
60
|
+
|
|
61
|
+
| Шаг | Источник | Когда срабатывает |
|
|
62
|
+
| --- | ---------------------------------------------------------- | --------------------------------------------------- |
|
|
63
|
+
| 1 | словарь формы (`defineIssueMessages`) — запись по `code` | у поля свой текст |
|
|
64
|
+
| 2 | `validation.<code>` из словаря активной локали | встроенный код либо свой код в словаре приложения |
|
|
65
|
+
| 3 | `validation.<code>` из встроенной английской таблицы ядра | встроенный код без провайдера локали |
|
|
66
|
+
| 4 | сам `code` | свой код, которого нет ни в одном словаре |
|
|
67
|
+
|
|
68
|
+
Свой код **не** падает обратно на текст встроенного правила: `required({ code: 'phone.required' })`
|
|
69
|
+
без записи `'phone.required'` в словаре покажет строку `phone.required`. Каждый свой код получает
|
|
70
|
+
запись в словаре формы (или ключ `validation.<код>` в словаре приложения).
|
|
71
|
+
|
|
72
|
+
### Встроенные коды и параметры
|
|
73
|
+
|
|
74
|
+
| Фабрика | `code` | `params` |
|
|
75
|
+
| ------------------------------- | ------------------ | ------------------------------------------ |
|
|
76
|
+
| `required()` | `required` | — |
|
|
77
|
+
| `min(n)` / `max(n)` | `min` / `max` | `min` / `max`, `actual` |
|
|
78
|
+
| `minLength(n)` / `maxLength(n)` | `minLength` / `maxLength`; у массива — `minItems` / `maxItems` | `minLength` / `maxLength`, `actualLength` |
|
|
79
|
+
| `email()` | `email` | — |
|
|
80
|
+
| `pattern(re)` | `pattern` | `pattern` (исходник регулярки) |
|
|
81
|
+
| `url()` | `url`, `url_protocol` | `allowedProtocols` (у `url_protocol`) |
|
|
82
|
+
| `phone()` | `phone` | `format` |
|
|
83
|
+
| `isNumber()` / `integer()` | `isNumber` / `integer` | — |
|
|
84
|
+
| `multipleOf(n)` | `multipleOf` | `multipleOf` |
|
|
85
|
+
| `nonNegative()` / `nonZero()` | `nonNegative` / `nonZero` | — |
|
|
86
|
+
| `isDate()` | `date_invalid` | — |
|
|
87
|
+
| `minDate(d)` / `maxDate(d)` | `date_min` / `date_max` | `minDate` / `maxDate` |
|
|
88
|
+
| `pastDate()` / `futureDate()` | `date_future` / `date_past` | — (код называет нарушение) |
|
|
89
|
+
| `minAge(n)` / `maxAge(n)` | `date_min_age` / `date_max_age` | `minAge` / `maxAge`, `currentAge` |
|
|
90
|
+
| `minAge(n)`, рождение в будущем | `date_future` | `minAge` |
|
|
91
|
+
| `fileType(accept)` | `fileType` | `accept`, `fileName` |
|
|
92
|
+
| `maxFileSize(b)` / `minFileSize(b)` | `maxFileSize` / `minFileSize` | `maxFileSize` / `minFileSize`, `fileName`, `actualSize` |
|
|
93
|
+
| `maxFiles(n)` / `minFiles(n)` | `maxFiles` / `minFiles` | `maxFiles` / `minFiles`, `actualCount` |
|
|
94
|
+
| `maxTotalFileSize(b)` | `maxTotalFileSize` | `maxTotalFileSize`, `actualTotal` |
|
|
95
|
+
| отклонённое async-правило | `ruleFailed` | — |
|
|
96
|
+
|
|
97
|
+
Свой код (`{ code }`) заменяет только код: параметры правила остаются, `params` из опций
|
|
98
|
+
накладываются поверх них.
|
|
99
|
+
|
|
100
|
+
### Шаблон и функция
|
|
101
|
+
|
|
102
|
+
- Шаблон — тот же формат, что в словарях локали (подмножество ICU): подстановка `{minLength}`,
|
|
103
|
+
`{count, plural, one{…} other{…}}`, `{kind, select, …}`.
|
|
104
|
+
- Параметры готовятся до подстановки: `Date` — по языку, массив — перечнем, размеры в байтах
|
|
105
|
+
(`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) — строкой с
|
|
106
|
+
единицей. Числа остаются числами (иначе не сработал бы `plural`) и печатаются **без разрядов**.
|
|
107
|
+
- Число с разрядами, валюта, вычисление — функция `(params, i18n) => string`; `i18n` — ручка
|
|
108
|
+
активной локали (`i18n.number`, `i18n.date`, `i18n.t`).
|
|
109
|
+
- Битый шаблон в словаре формы текст поля не прячет: резолвер идёт дальше по порядку поиска.
|
|
110
|
+
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
113
|
+
### Свой код у правила и словарь формы
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// validation/loan.ts — правила несут коды
|
|
117
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
118
|
+
import { max, min, minLength, required } from '@reformer/core/validators';
|
|
119
|
+
|
|
120
|
+
export const loanRules = defineValidation<CreditForm>(({ model }) => {
|
|
121
|
+
validate(model.$.loanAmount, [
|
|
122
|
+
required({ code: 'loanAmount.required' }),
|
|
123
|
+
min(50_000, { code: 'loanAmount.min' }),
|
|
124
|
+
]);
|
|
125
|
+
validate(model.$.loanPurpose, [minLength(10, { code: 'loanPurpose.minLength' })]);
|
|
126
|
+
validate(model.$.carYear, [max(new Date().getFullYear(), { code: 'carYear.max' })]);
|
|
127
|
+
validate(model.$.email, [required()]); // встроенный код: текст даёт локаль
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// validation/messages.ts — тексты по кодам; константа модуля (стабильная ссылка)
|
|
133
|
+
import { defineIssueMessages } from '@reformer/core/i18n';
|
|
134
|
+
|
|
135
|
+
export const creditMessages = defineIssueMessages({
|
|
136
|
+
'loanAmount.required': 'Укажите сумму кредита',
|
|
137
|
+
'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
|
|
138
|
+
'loanPurpose.minLength': 'Минимум {minLength} символов',
|
|
139
|
+
'carYear.max': 'Не позднее {max}',
|
|
140
|
+
// встроенный код тоже можно переопределить — для всей формы
|
|
141
|
+
required: 'Заполните поле',
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Подключение словаря — провайдер cdk
|
|
146
|
+
|
|
147
|
+
`FormField` из `@reformer/cdk` и `@reformer/ui-kit` резолвит замечания сам; словарь формы
|
|
148
|
+
подключается один раз на корне формы:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { ValidationMessagesProvider } from '@reformer/cdk';
|
|
152
|
+
import { creditMessages } from './validation/messages';
|
|
153
|
+
|
|
154
|
+
<ValidationMessagesProvider messages={creditMessages}>
|
|
155
|
+
<FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />
|
|
156
|
+
</ValidationMessagesProvider>;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- Вложенные провайдеры с `messages` накладываются друг на друга: ближний важнее, код, которого в
|
|
160
|
+
ближнем словаре нет, ищется во внешнем, затем в локали.
|
|
161
|
+
- `resolver` заменяет резолвер поддерева целиком: локаль и внешние провайдеры для его замечаний не
|
|
162
|
+
участвуют (кроме `messages` этого же провайдера).
|
|
163
|
+
- Язык задаёт `I18nProvider` из `@reformer/core/react`; без него тексты английские — см.
|
|
164
|
+
[34-i18n.md](./34-i18n.md).
|
|
165
|
+
|
|
166
|
+
### Свой компонент замечаний — `useIssueMessage`
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import type { FormControl } from '@reformer/core';
|
|
170
|
+
import { useFormControl, useIssueMessage } from '@reformer/core/react';
|
|
171
|
+
import { creditMessages } from './validation/messages';
|
|
172
|
+
|
|
173
|
+
function FieldIssues({ control }: { control: FormControl<string> }) {
|
|
174
|
+
const message = useIssueMessage(creditMessages); // словарь формы → локаль → английский → код
|
|
175
|
+
// shouldShowIssues — и у поля с одним предупреждением; shouldShowError — только у невалидного
|
|
176
|
+
const { issues, shouldShowIssues } = useFormControl(control);
|
|
177
|
+
if (!shouldShowIssues) return null;
|
|
178
|
+
return (
|
|
179
|
+
<ul role="alert">
|
|
180
|
+
{issues.map((issue) => (
|
|
181
|
+
<li key={issue.code}>{message(issue)}</li>
|
|
182
|
+
))}
|
|
183
|
+
</ul>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Внутри дерева с `ValidationMessagesProvider` — `useIssueMessageResolver()` из `@reformer/cdk`: он
|
|
189
|
+
учитывает словари всех провайдеров выше.
|
|
190
|
+
|
|
191
|
+
### Вне React
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { createI18n, resolveIssueMessage } from '@reformer/core/i18n';
|
|
195
|
+
import { ru } from '@reformer/core/locale/ru';
|
|
196
|
+
|
|
197
|
+
const i18n = createI18n(ru);
|
|
198
|
+
resolveIssueMessage({ code: 'minLength', params: { minLength: 8 } }, i18n); // 'Не меньше 8 символов'
|
|
199
|
+
resolveIssueMessage({ code: 'loanAmount.required' }, i18n, creditMessages); // 'Укажите сумму кредита'
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Замечание из своего правила и с сервера
|
|
203
|
+
|
|
204
|
+
Своё правило, `cross` и ответ сервера возвращают тот же `ValidationIssue` — код и параметры; текст
|
|
205
|
+
кода — в словаре формы:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const initialPaymentTooHigh = (loan: CreditForm): ValidationIssue | null =>
|
|
209
|
+
loan.initialPayment !== null &&
|
|
210
|
+
loan.propertyValue !== null &&
|
|
211
|
+
loan.initialPayment > loan.propertyValue
|
|
212
|
+
? { code: 'initialPaymentTooHigh', params: { max: loan.propertyValue } }
|
|
213
|
+
: null;
|
|
214
|
+
|
|
215
|
+
// ответ сервера — в ноду поля
|
|
216
|
+
form.email.setIssues([{ code: 'email.taken' }]);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Несколько языков в словаре формы
|
|
220
|
+
|
|
221
|
+
Запись словаря — функция, получающая ручку локали, поэтому текст поля можно взять из словаря
|
|
222
|
+
приложения и переключать вместе с языком:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
export const creditMessages = defineIssueMessages({
|
|
226
|
+
'loanAmount.required': (_params, i18n) => i18n.t('credit.loanAmount.required'),
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Другой путь — ключ `validation.loanAmount.required` в словаре приложения (шаг 2 порядка поиска).
|
|
231
|
+
|
|
232
|
+
## Anti-patterns
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// ❌ Текст в правиле: у RuleOptions нет message — не скомпилируется
|
|
236
|
+
required({ message: 'Укажите телефон' });
|
|
237
|
+
|
|
238
|
+
// ✅ Свой код в правиле, текст — в словаре формы
|
|
239
|
+
required({ code: 'phone.required' });
|
|
240
|
+
const messages = defineIssueMessages({ 'phone.required': 'Укажите телефон' });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
// ❌ Словарь, собранный в рендере: новая ссылка — резолвер пересоздаётся на каждый рендер
|
|
245
|
+
const message = useIssueMessage(defineIssueMessages({ 'phone.required': 'Укажите телефон' }));
|
|
246
|
+
|
|
247
|
+
// ✅ Словарь — константа модуля
|
|
248
|
+
const messages = defineIssueMessages({ 'phone.required': 'Укажите телефон' });
|
|
249
|
+
function PhoneIssues() {
|
|
250
|
+
const message = useIssueMessage(messages);
|
|
251
|
+
/* … */
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
// ❌ Показ замечания как строки: текста у замечания нет
|
|
257
|
+
<span>{issues[0].message}</span>
|
|
258
|
+
|
|
259
|
+
// ✅ Текст — через резолвер
|
|
260
|
+
<span>{message(issues[0])}</span>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// ❌ Свой код без записи в словаре — на экране сам код «loanAmount.required»
|
|
265
|
+
validate(model.$.loanAmount, [required({ code: 'loanAmount.required' })]);
|
|
266
|
+
|
|
267
|
+
// ✅ Каждому своему коду — запись в словаре формы (или validation.<код> в локали приложения)
|
|
268
|
+
defineIssueMessages({ 'loanAmount.required': 'Укажите сумму кредита' });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// ❌ Число с разрядами шаблоном — шаблон печатает 50000 без пробелов
|
|
273
|
+
'loanAmount.min': 'Минимум {min} ₽',
|
|
274
|
+
|
|
275
|
+
// ✅ Функция форматирует число сама
|
|
276
|
+
'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## See also
|
|
280
|
+
|
|
281
|
+
- [34-i18n.md](./34-i18n.md) — локаль, загрузка словарей, `I18nProvider`
|
|
282
|
+
- [03-api-signatures.md](./03-api-signatures.md) — фабрики правил и `RuleOptions`
|
|
283
|
+
- [38-form-flow.md](./38-form-flow.md) — `bundle.submission.submit` и проверка шагов
|
|
284
|
+
- [17-nonexistent-api.md](./17-nonexistent-api.md) — удалённые `message`, `messageKey`, `ValidationError`
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Скомпилированная схема — compileSchema, виды узлов и области
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Авторская схема (`FormSchemaNode`) описывает вид узла ключами: `model`, `item`, `part`,
|
|
6
|
+
`children`. Разбирать их по-своему раньше приходилось каждому, кто обходил дерево. Теперь дерево
|
|
7
|
+
разбирает один компилятор — `compileSchema(tree, model)` — и все читают его итог,
|
|
8
|
+
`CompiledSchema`: у каждого узла явный вид (`kind`), нормализованная привязка к модели,
|
|
9
|
+
идентификатор и область.
|
|
10
|
+
|
|
11
|
+
`createForm` компилирует дерево **один раз**: ноды формы и рендерер читают одну и ту же
|
|
12
|
+
компиляцию, она лежит в `bundle.render.tree`. Сам `compileSchema` нужен редко — своему
|
|
13
|
+
рендереру, инструментам (инспектор дерева, проверка селекторов в тестах) и фабрикам, которые
|
|
14
|
+
собирают форму без `createForm`.
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import {
|
|
20
|
+
compileSchema,
|
|
21
|
+
isCompiledNode,
|
|
22
|
+
type CompiledSchema,
|
|
23
|
+
type CompiledNode,
|
|
24
|
+
type CompiledChild,
|
|
25
|
+
type SchemaNodeId,
|
|
26
|
+
type SchemaScopeId,
|
|
27
|
+
} from '@reformer/core';
|
|
28
|
+
|
|
29
|
+
function compileSchema(
|
|
30
|
+
tree: FormSchemaNode,
|
|
31
|
+
model?: object, // модель области: корень сборки, под-модель строки или группы
|
|
32
|
+
options?: { scope?: SchemaScopeId } // по умолчанию корень '#'
|
|
33
|
+
): CompiledSchema;
|
|
34
|
+
|
|
35
|
+
function isCompiledNode(value: unknown): value is CompiledNode; // узел компилятора, а не автора
|
|
36
|
+
|
|
37
|
+
interface CompiledSchema {
|
|
38
|
+
readonly scope: SchemaScopeId; // '#', '#/address', '#/phones[3]'
|
|
39
|
+
readonly model: object | undefined; // модель области
|
|
40
|
+
readonly root: CompiledNode; // корневой узел области
|
|
41
|
+
readonly nodes: ReadonlyMap<SchemaNodeId, CompiledNode>; // узлы ЭТОЙ области по id
|
|
42
|
+
readonly fields: ReadonlyMap<object, CompiledFieldNode>; // поля по ручке — области и её подформ
|
|
43
|
+
readonly arrays: ReadonlyMap<object, CompiledArrayNode>; // массивы под-форм по ручке
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Виды узлов — `kind`
|
|
48
|
+
|
|
49
|
+
| `kind` | Узел автора | Что добавляет компилятор |
|
|
50
|
+
| ----------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
51
|
+
| `field` | `{ model: model.$.<лист или массив целиком>, component }` | `handle` (ручка значения), `disabled` |
|
|
52
|
+
| `array` | `{ model: model.$.<массив>, item: (row) => узел }` | `handle`, `control` (фасад массива модели), `initialValue`, `row(rowModel)` |
|
|
53
|
+
| `part` | `{ model: model.$.<группа>, part: (sub) => узел }` | `handle`, `model` (под-модель группы), `content` — схема части |
|
|
54
|
+
| `container` | всё остальное: `children`, `component` | `children: CompiledChild[]` — узлы и текст по порядку |
|
|
55
|
+
| `text` | подряд идущие строки, числа, сигналы среди `children` | `parts` — склеенные части; только ребёнок контейнера |
|
|
56
|
+
|
|
57
|
+
Общие поля узла (кроме `text`): `id`, `path` (позиционный путь `#/0/2`), `selector`, `scope`,
|
|
58
|
+
`component` (как есть — ядро его не интерпретирует), `props` (`componentProps` автора, без них —
|
|
59
|
+
пустой объект), `source` (авторский узел).
|
|
60
|
+
|
|
61
|
+
Порядок распознавания важен: ручка массива — тоже ручка значения, поэтому сначала проверяется
|
|
62
|
+
`item` (массив под-форм), затем `part` (подформа), затем привязка-значение (поле), остальное —
|
|
63
|
+
контейнер. Контейнер без `component` рендерер рисует как одних детей.
|
|
64
|
+
|
|
65
|
+
### Идентификатор узла — `id`
|
|
66
|
+
|
|
67
|
+
- Узел с `selector` — `id` равен `selector`.
|
|
68
|
+
- Узел без `selector` — позиционный путь от корня области: `#`, `#/0`, `#/0/2`.
|
|
69
|
+
- Текст — позиционный путь первой части.
|
|
70
|
+
- `SchemaNodeId` — строка с брендом: в `compiled.nodes.get(...)` ключ приводят
|
|
71
|
+
(`'email' as SchemaNodeId`).
|
|
72
|
+
|
|
73
|
+
### Области — `scope`
|
|
74
|
+
|
|
75
|
+
| Область | Идентификатор | Что в ней |
|
|
76
|
+
| ---------------- | ---------------------------- | --------------------------------------- |
|
|
77
|
+
| корень сборки | `#` | дерево `schema(model)` |
|
|
78
|
+
| подформа (`part`) | `#/registrationAddress` | содержимое части; путь группы от модели области |
|
|
79
|
+
| строка массива | `#/phones[<ключ>]` | разметка строки из `item(rowModel)` |
|
|
80
|
+
| вложенные | `#/coBorrowers[3]/phones[7]` | область внутри области |
|
|
81
|
+
|
|
82
|
+
- В скобках строки — **стабильный ключ** строки, а не индекс: он держится за элемент модели и не
|
|
83
|
+
меняется при `move` / `swap`. Тот же ключ отдаёт `FormArray.keyAt(index)`.
|
|
84
|
+
- **`selector` уникален в своей области.** Повтор в одной области — исключение при сборке с путями
|
|
85
|
+
обоих узлов. Селекторы строк массива и подформ живут в своих областях и с корнем не
|
|
86
|
+
пересекаются: одна часть, подключённая дважды, даёт две области без конфликта.
|
|
87
|
+
- По `selector` узел адресуют поведение (`schema.node(selector)` — в своей области) и шаги потока
|
|
88
|
+
(`flow.steps[].id` — `selector` узла шага в корневой области).
|
|
89
|
+
|
|
90
|
+
### Подформы и строки
|
|
91
|
+
|
|
92
|
+
- **Подформа** компилируется сразу; её содержимое — `content` (своя `CompiledSchema`). Поля и
|
|
93
|
+
массивы части попадают и в `fields` / `arrays` родителя: у части нет отдельной формы, её поля
|
|
94
|
+
принадлежат той же сборке (`form.registrationAddress.city`). Подформа на самой модели области
|
|
95
|
+
(`model: model.# Скомпилированная схема — compileSchema, виды узлов и области
|
|
96
|
+
|
|
97
|
+
## Purpose
|
|
98
|
+
|
|
99
|
+
Авторская схема (`FormSchemaNode`) описывает вид узла ключами: `model`, `item`, `part`,
|
|
100
|
+
`children`. Разбирать их по-своему раньше приходилось каждому, кто обходил дерево. Теперь дерево
|
|
101
|
+
разбирает один компилятор — `compileSchema(tree, model)` — и все читают его итог,
|
|
102
|
+
`CompiledSchema`: у каждого узла явный вид (`kind`), нормализованная привязка к модели,
|
|
103
|
+
идентификатор и область.
|
|
104
|
+
|
|
105
|
+
`createForm` компилирует дерево **один раз**: ноды формы и рендерер читают одну и ту же
|
|
106
|
+
компиляцию, она лежит в `bundle.render.tree`. Сам `compileSchema` нужен редко — своему
|
|
107
|
+
рендереру, инструментам (инспектор дерева, проверка селекторов в тестах) и фабрикам, которые
|
|
108
|
+
собирают форму без `createForm`.
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import {
|
|
114
|
+
compileSchema,
|
|
115
|
+
isCompiledNode,
|
|
116
|
+
type CompiledSchema,
|
|
117
|
+
type CompiledNode,
|
|
118
|
+
type CompiledChild,
|
|
119
|
+
type SchemaNodeId,
|
|
120
|
+
type SchemaScopeId,
|
|
121
|
+
} from '@reformer/core';
|
|
122
|
+
|
|
123
|
+
function compileSchema(
|
|
124
|
+
tree: FormSchemaNode,
|
|
125
|
+
model?: object, // модель области: корень сборки, под-модель строки или группы
|
|
126
|
+
options?: { scope?: SchemaScopeId } // по умолчанию корень '#'
|
|
127
|
+
): CompiledSchema;
|
|
128
|
+
|
|
129
|
+
function isCompiledNode(value: unknown): value is CompiledNode; // узел компилятора, а не автора
|
|
130
|
+
|
|
131
|
+
interface CompiledSchema {
|
|
132
|
+
readonly scope: SchemaScopeId; // '#', '#/address', '#/phones[3]'
|
|
133
|
+
readonly model: object | undefined; // модель области
|
|
134
|
+
readonly root: CompiledNode; // корневой узел области
|
|
135
|
+
readonly nodes: ReadonlyMap<SchemaNodeId, CompiledNode>; // узлы ЭТОЙ области по id
|
|
136
|
+
readonly fields: ReadonlyMap<object, CompiledFieldNode>; // поля по ручке — области и её подформ
|
|
137
|
+
readonly arrays: ReadonlyMap<object, CompiledArrayNode>; // массивы под-форм по ручке
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Виды узлов — `kind`
|
|
142
|
+
|
|
143
|
+
| `kind` | Узел автора | Что добавляет компилятор |
|
|
144
|
+
| ----------- | -------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
145
|
+
| `field` | `{ model: model.$.<лист или массив целиком>, component }` | `handle` (ручка значения), `disabled` |
|
|
146
|
+
| `array` | `{ model: model.$.<массив>, item: (row) => узел }` | `handle`, `control` (фасад массива модели), `initialValue`, `row(rowModel)` |
|
|
147
|
+
| `part` | `{ model: model.$.<группа>, part: (sub) => узел }` | `handle`, `model` (под-модель группы), `content` — схема части |
|
|
148
|
+
| `container` | всё остальное: `children`, `component` | `children: CompiledChild[]` — узлы и текст по порядку |
|
|
149
|
+
| `text` | подряд идущие строки, числа, сигналы среди `children` | `parts` — склеенные части; только ребёнок контейнера |
|
|
150
|
+
|
|
151
|
+
Общие поля узла (кроме `text`): `id`, `path` (позиционный путь `#/0/2`), `selector`, `scope`,
|
|
152
|
+
`component` (как есть — ядро его не интерпретирует), `props` (`componentProps` автора, без них —
|
|
153
|
+
пустой объект), `source` (авторский узел).
|
|
154
|
+
|
|
155
|
+
Порядок распознавания важен: ручка массива — тоже ручка значения, поэтому сначала проверяется
|
|
156
|
+
`item` (массив под-форм), затем `part` (подформа), затем привязка-значение (поле), остальное —
|
|
157
|
+
контейнер. Контейнер без `component` рендерер рисует как одних детей.
|
|
158
|
+
|
|
159
|
+
### Идентификатор узла — `id`
|
|
160
|
+
|
|
161
|
+
- Узел с `selector` — `id` равен `selector`.
|
|
162
|
+
- Узел без `selector` — позиционный путь от корня области: `#`, `#/0`, `#/0/2`.
|
|
163
|
+
- Текст — позиционный путь первой части.
|
|
164
|
+
- `SchemaNodeId` — строка с брендом: в `compiled.nodes.get(...)` ключ приводят
|
|
165
|
+
(`'email' as SchemaNodeId`).
|
|
166
|
+
|
|
167
|
+
### Области — `scope`
|
|
168
|
+
|
|
169
|
+
| Область | Идентификатор | Что в ней |
|
|
170
|
+
| ---------------- | ---------------------------- | --------------------------------------- |
|
|
171
|
+
| корень сборки | `#` | дерево `schema(model)` |
|
|
172
|
+
| подформа (`part`) | `#/registrationAddress` | содержимое части; путь группы от модели области |
|
|
173
|
+
| строка массива | `#/phones[<ключ>]` | разметка строки из `item(rowModel)` |
|
|
174
|
+
| вложенные | `#/coBorrowers[3]/phones[7]` | область внутри области |
|
|
175
|
+
|
|
176
|
+
- В скобках строки — **стабильный ключ** строки, а не индекс: он держится за элемент модели и не
|
|
177
|
+
меняется при `move` / `swap`. Тот же ключ отдаёт `FormArray.keyAt(index)`.
|
|
178
|
+
- **`selector` уникален в своей области.** Повтор в одной области — исключение при сборке с путями
|
|
179
|
+
обоих узлов. Селекторы строк массива и подформ живут в своих областях и с корнем не
|
|
180
|
+
пересекаются: одна часть, подключённая дважды, даёт две области без конфликта.
|
|
181
|
+
- По `selector` узел адресуют поведение (`schema.node(selector)` — в своей области) и шаги потока
|
|
182
|
+
(`flow.steps[].id` — `selector` узла шага в корневой области).
|
|
183
|
+
|
|
184
|
+
### Подформы и строки
|
|
185
|
+
|
|
186
|
+
) делит с ней область: правило к её узлу из поведения области работает.
|
|
187
|
+
- **Строка массива** компилируется при первом обращении: `arrayNode.row(rowModel)` — одна
|
|
188
|
+
компиляция на под-модель строки; форма строки и рендерер получают один и тот же объект. Узлы
|
|
189
|
+
строк в `nodes` корня не лежат.
|
|
190
|
+
|
|
191
|
+
### Что компилятор проверяет
|
|
192
|
+
|
|
193
|
+
| Ситуация | Итог |
|
|
194
|
+
| --------------------------------------------------------- | ------------------------------------------------------ |
|
|
195
|
+
| повтор `selector` в одной области | исключение с путями обоих узлов |
|
|
196
|
+
| узел содержит сам себя | исключение — дерево зациклено |
|
|
197
|
+
| ключ узла, который сборка не читает (`value`, `array`, …) | dev-предупреждение с подсказкой замены (у строк массива — одно на шаблон) |
|
|
198
|
+
| узел без `component` и без `children` | dev-предупреждение: ничего не рисует; пустая секция — `children: []` |
|
|
199
|
+
| одна ручка модели в двух узлах | dev-предупреждение; нода берёт `component`, `componentProps` и `disabled` последней привязки |
|
|
200
|
+
| `item` на привязке не-массиве, `part` на не-группе | dev-предупреждение, узел становится контейнером |
|
|
201
|
+
| нераспознанная привязка (`model: model.email` без `$`) | dev-предупреждение, узел становится контейнером |
|
|
202
|
+
| ребёнок — группа модели или под-модель вместо узла | dev-предупреждение, ребёнок пропущен |
|
|
203
|
+
| `null` / `undefined` / `boolean` среди `children` | пропускается молча — условная вставка `cond && узел` |
|
|
204
|
+
|
|
205
|
+
Вложенные узлы читаются только из `children` и из поддерева `part`; в `componentProps` и под
|
|
206
|
+
произвольными ключами компилятор не заглядывает.
|
|
207
|
+
|
|
208
|
+
## Examples
|
|
209
|
+
|
|
210
|
+
### Компиляция и поиск узлов
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { compileSchema, createModel, type SchemaNodeId } from '@reformer/core';
|
|
214
|
+
|
|
215
|
+
const model = createModel({ email: '', phones: [{ number: '' }] });
|
|
216
|
+
|
|
217
|
+
const compiled = compileSchema(
|
|
218
|
+
{
|
|
219
|
+
children: [
|
|
220
|
+
{ selector: 'email', model: model.$.email, component: Input },
|
|
221
|
+
{ model: model.$.phones, item: (row) => ({ model: row.$.number, component: Input }) },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
model
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
compiled.nodes.get('email' as SchemaNodeId)?.kind; // 'field'
|
|
228
|
+
compiled.fields.get(model.$.email)?.component; // Input
|
|
229
|
+
compiled.arrays.get(model.$.phones)?.row(model.phones.at(0)!).scope; // '#/phones[<ключ строки>]'
|
|
230
|
+
compiled.nodes.get('#/1' as SchemaNodeId)?.kind; // 'array' — узел без selector: позиционный id
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Схема сборки — `bundle.render.tree`
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const bundle = createForm<CreditForm>({ model: createCreditModel(), schema: creditSchema });
|
|
237
|
+
|
|
238
|
+
bundle.render.tree; // CompiledSchema корневой области; без schema — undefined
|
|
239
|
+
bundle.render.node('wizard'); // управление узлом корневой области по selector
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Своему рендереру и инструментам нужна именно эта компиляция: повторный `compileSchema` того же
|
|
243
|
+
дерева даёт другие объекты и другой кэш строк.
|
|
244
|
+
|
|
245
|
+
### Обход дерева — список селекторов
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import type { CompiledNode, CompiledSchema } from '@reformer/core';
|
|
249
|
+
|
|
250
|
+
function selectorsOf(schema: CompiledSchema): string[] {
|
|
251
|
+
const out: string[] = [];
|
|
252
|
+
const walk = (node: CompiledNode): void => {
|
|
253
|
+
if (node.selector !== undefined) out.push(`${node.scope} ${node.selector}`);
|
|
254
|
+
if (node.kind === 'container') {
|
|
255
|
+
for (const child of node.children) if (child.kind !== 'text') walk(child);
|
|
256
|
+
}
|
|
257
|
+
if (node.kind === 'part') walk(node.content.root); // часть — своя область
|
|
258
|
+
// строки массива — node.row(rowModel) для каждой под-модели строки
|
|
259
|
+
};
|
|
260
|
+
walk(schema.root);
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Свой рендерер — разбор по `kind`
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
function renderNode(node: CompiledChild): ReactNode {
|
|
269
|
+
switch (node.kind) {
|
|
270
|
+
case 'text':
|
|
271
|
+
return node.parts.map((part) => (typeof part === 'object' ? part.value : part)).join('');
|
|
272
|
+
case 'field':
|
|
273
|
+
return <Field key={node.id} handle={node.handle} component={node.component} props={node.props} />;
|
|
274
|
+
case 'array': // у каждой строки своя область: node.row(под-модель строки)
|
|
275
|
+
return Array.from({ length: node.control.length }, (_, i) =>
|
|
276
|
+
renderNode(node.row(node.control.at(i) as object).root)
|
|
277
|
+
);
|
|
278
|
+
case 'part':
|
|
279
|
+
return renderNode(node.content.root);
|
|
280
|
+
case 'container':
|
|
281
|
+
return node.children.map(renderNode);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Anti-patterns
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// ❌ Два узла с одним selector в корне — исключение при сборке
|
|
290
|
+
children: [
|
|
291
|
+
{ selector: 'address', model: model.$.registrationAddress, part: address },
|
|
292
|
+
{ selector: 'address', model: model.$.residenceAddress, part: address },
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
// ✅ Разные селекторы; внутри части селекторы свои — повтор в двух частях не конфликтует
|
|
296
|
+
children: [
|
|
297
|
+
{ selector: 'registration', model: model.$.registrationAddress, part: address },
|
|
298
|
+
{ selector: 'residence', model: model.$.residenceAddress, part: address },
|
|
299
|
+
];
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// ❌ Индекс строки в идентификаторе области: после move/swap он указывает на чужую строку
|
|
304
|
+
const scope = `#/phones[${index}]`;
|
|
305
|
+
|
|
306
|
+
// ✅ Ключ строки — FormArray.keyAt(index); он же стоит в скобках области строки
|
|
307
|
+
const scope = `#/phones[${form.phones.keyAt(index)}]`;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// ❌ Узел строки массива ищется в nodes корня — его там нет
|
|
312
|
+
bundle.render.tree?.nodes.get('co-borrower' as SchemaNodeId); // undefined
|
|
313
|
+
|
|
314
|
+
// ✅ Узел строки — в схеме строки; в поведении — applyEach + schema.node(...) строки
|
|
315
|
+
const coBorrowers = bundle.render.tree?.arrays.get(model.$.coBorrowers);
|
|
316
|
+
coBorrowers?.row(model.coBorrowers.at(0)!).nodes.get('co-borrower' as SchemaNodeId);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// ❌ Узлы в componentProps — компилятор туда не заглядывает, поле не появится
|
|
321
|
+
{ component: Section, componentProps: { content: { model: model.$.email, component: Input } } }
|
|
322
|
+
|
|
323
|
+
// ✅ Вложенные узлы — только в children
|
|
324
|
+
{ component: Section, children: [{ model: model.$.email, component: Input }] }
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## See also
|
|
328
|
+
|
|
329
|
+
- [09-formschema.md](./09-formschema.md) — авторский формат узлов
|
|
330
|
+
- [35-unified-contract.md](./35-unified-contract.md) — одна схема, одна сборка, одно поведение
|
|
331
|
+
- [38-form-flow.md](./38-form-flow.md) — шаги потока адресуются `selector` узлов шагов
|
|
332
|
+
- [10-arrays.md](./10-arrays.md) — массивы под-форм и `keyAt`
|