@reformer/core 11.0.0-beta.7 → 11.0.0-beta.9
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-HA_Q03hc.js +108 -0
- package/dist/behaviors.js +382 -217
- package/dist/create-model-BvcbNPgL.js +30 -0
- package/dist/date-utils-90-qZbse.js +43 -0
- package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
- package/dist/form/aggregate-signals.d.ts +21 -63
- package/dist/form/behaviors/collections.d.ts +42 -8
- package/dist/form/behaviors/context.d.ts +37 -4
- package/dist/form/behaviors/index.d.ts +6 -4
- package/dist/form/behaviors/internals.d.ts +9 -3
- package/dist/form/behaviors/node.d.ts +25 -2
- package/dist/form/behaviors/operators.d.ts +17 -9
- package/dist/form/behaviors/schema-operators.d.ts +78 -0
- package/dist/form/behaviors/types.d.ts +25 -5
- package/dist/form/create-form.d.ts +57 -70
- package/dist/form/flow/flow.d.ts +87 -0
- package/dist/form/form-bundle.d.ts +153 -0
- package/dist/form/form-config-base.d.ts +46 -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 +20 -20
- 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 +153 -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 +19 -92
- package/dist/form/types/field-config.d.ts +27 -0
- package/dist/form/types/index.d.ts +5 -57
- package/dist/form/types/nodes.d.ts +159 -0
- package/dist/form/types/schema-node.d.ts +147 -46
- package/dist/form/types/validation-schema.d.ts +23 -27
- package/dist/form/validation/context.d.ts +46 -6
- package/dist/form/validation/index.d.ts +9 -8
- package/dist/form/validation/operators.d.ts +71 -17
- package/dist/form/validation/run.d.ts +93 -18
- package/dist/form/validation/strategy.d.ts +41 -13
- package/dist/form/validation/types.d.ts +74 -19
- package/dist/form/validators/date-utils.d.ts +13 -1
- package/dist/form/validators/email.d.ts +8 -7
- package/dist/form/validators/file-type.d.ts +8 -7
- package/dist/form/validators/file-utils.d.ts +2 -1
- package/dist/form/validators/future-date.d.ts +8 -7
- package/dist/form/validators/index.d.ts +1 -1
- package/dist/form/validators/integer.d.ts +8 -7
- package/dist/form/validators/is-date.d.ts +11 -9
- package/dist/form/validators/is-number.d.ts +11 -9
- package/dist/form/validators/max-age.d.ts +13 -9
- package/dist/form/validators/max-date.d.ts +8 -7
- package/dist/form/validators/max-file-size.d.ts +8 -7
- package/dist/form/validators/max-files.d.ts +8 -7
- package/dist/form/validators/max-length.d.ts +14 -7
- package/dist/form/validators/max-total-file-size.d.ts +8 -7
- package/dist/form/validators/max.d.ts +10 -8
- package/dist/form/validators/min-age.d.ts +12 -9
- package/dist/form/validators/min-date.d.ts +8 -7
- package/dist/form/validators/min-file-size.d.ts +8 -7
- package/dist/form/validators/min-files.d.ts +11 -9
- package/dist/form/validators/min-length.d.ts +14 -7
- package/dist/form/validators/min.d.ts +10 -8
- package/dist/form/validators/multiple-of.d.ts +10 -8
- package/dist/form/validators/non-negative.d.ts +8 -7
- package/dist/form/validators/non-zero.d.ts +8 -7
- package/dist/form/validators/past-date.d.ts +8 -7
- package/dist/form/validators/pattern.d.ts +9 -8
- package/dist/form/validators/phone.d.ts +8 -7
- package/dist/form/validators/required.d.ts +11 -9
- package/dist/form/validators/url.d.ts +20 -13
- package/dist/form/validators/validation-issue.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +1 -1
- package/dist/i18n/en.json.d.ts +5 -1
- package/dist/i18n/locale-store.d.ts +7 -0
- package/dist/i18n/message-format.d.ts +24 -4
- package/dist/i18n/ru.json.d.ts +5 -1
- package/dist/i18n/translator.d.ts +11 -0
- package/dist/i18n/validation-message.d.ts +42 -7
- package/dist/i18n.d.ts +7 -12
- package/dist/i18n.js +66 -283
- package/dist/index.d.ts +16 -7
- package/dist/index.js +958 -2798
- package/dist/loader-BWkigpVe.js +52 -0
- package/dist/locale/en.js +1 -1
- package/dist/locale/en.json +4 -1
- package/dist/locale/ru.js +4 -1
- package/dist/locale/ru.json +4 -1
- package/dist/locale-CG54AU2Q.js +219 -0
- package/dist/locale.js +1 -1
- package/dist/model/behaviors-value.d.ts +24 -6
- package/dist/model/create-model.d.ts +2 -2
- package/dist/model/derived-registry.d.ts +1 -1
- package/dist/model/index.d.ts +16 -5
- package/dist/model/model-nodes.d.ts +138 -8
- package/dist/model/model-signals-proxy.d.ts +11 -5
- package/dist/model/model-value-proxy.d.ts +84 -3
- package/dist/model/safe-effect.d.ts +33 -1
- package/dist/model/types.d.ts +70 -17
- package/dist/model.js +21 -17
- package/dist/node-kind-C-r6ZMdo.js +15 -0
- package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
- package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
- package/dist/platforms/react/form-bundle-context.d.ts +27 -0
- package/dist/platforms/react/hooks/types.d.ts +57 -38
- package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
- package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
- package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
- package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
- package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
- package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
- package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
- package/dist/platforms/react/i18n/context.d.ts +6 -1
- package/dist/platforms/react/i18n/hooks.d.ts +16 -10
- package/dist/platforms/react/i18n/provider.d.ts +9 -2
- package/dist/platforms/react/index.d.ts +7 -4
- package/dist/react.d.ts +22 -0
- package/dist/react.js +505 -0
- package/dist/runtime-token.d.ts +1 -2
- package/dist/safe-effect-BbtX3KuN.js +643 -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-CoFSemuP.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.js +9 -8
- package/dist/validators/email.js +5 -5
- package/dist/validators/future-date.js +8 -8
- package/dist/validators/integer.js +3 -3
- package/dist/validators/is-date.js +4 -4
- package/dist/validators/is-number.js +4 -4
- package/dist/validators/max-age.js +8 -9
- package/dist/validators/max-date.js +8 -8
- package/dist/validators/max-length.js +7 -7
- package/dist/validators/max.js +4 -4
- package/dist/validators/min-age.js +10 -8
- package/dist/validators/min-date.js +3 -3
- package/dist/validators/min-length.js +5 -5
- package/dist/validators/min.js +4 -4
- package/dist/validators/multiple-of.js +11 -7
- package/dist/validators/non-negative.js +3 -3
- package/dist/validators/non-zero.js +3 -3
- package/dist/validators/past-date.js +8 -8
- package/dist/validators/pattern.js +5 -5
- package/dist/validators/phone.js +11 -9
- package/dist/validators/required.js +2 -2
- package/dist/validators/url.js +48 -12
- package/dist/validators.js +17 -17
- package/docs/llms/01-api-reference.md +60 -34
- package/docs/llms/02-quick-start.md +111 -80
- package/docs/llms/03-api-signatures.md +368 -153
- package/docs/llms/04-common-patterns.md +58 -56
- package/docs/llms/05-common-mistakes.md +69 -44
- package/docs/llms/06-troubleshooting.md +14 -9
- package/docs/llms/07-complete-import.md +74 -43
- package/docs/llms/08-form-types.md +11 -4
- package/docs/llms/09-formschema.md +131 -83
- package/docs/llms/10-arrays.md +119 -69
- package/docs/llms/11-async-watchfield.md +2 -2
- package/docs/llms/13-multi-step.md +134 -63
- package/docs/llms/14-extended-mistakes.md +20 -17
- package/docs/llms/15-project-structure.md +221 -74
- package/docs/llms/16-ui-components.md +55 -42
- package/docs/llms/17-nonexistent-api.md +199 -32
- package/docs/llms/18-conditional-fields.md +66 -32
- package/docs/llms/19-reading-values.md +15 -3
- package/docs/llms/20-compute-vs-watch.md +16 -10
- package/docs/llms/21-array-operations.md +76 -26
- package/docs/llms/22-cycle-detection.md +1 -1
- package/docs/llms/23-copy-from.md +2 -2
- package/docs/llms/24-sync-fields.md +6 -5
- package/docs/llms/25-reset-when.md +14 -7
- package/docs/llms/26-transform-value.md +4 -4
- package/docs/llms/27-revalidate-when.md +16 -15
- package/docs/llms/28-submit-and-reset.md +276 -128
- package/docs/llms/29-async-preload.md +40 -30
- package/docs/llms/30-type-safety-recipes.md +105 -51
- package/docs/llms/31-async-validator-debounce.md +96 -53
- package/docs/llms/33-validation-strategy.md +178 -66
- package/docs/llms/34-i18n.md +69 -43
- package/docs/llms/35-unified-contract.md +307 -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 +397 -0
- package/llms-index.json +1 -1
- package/llms.txt +6968 -3278
- package/package.json +23 -4
- package/dist/behaviors-value-C7dSEhCc.js +0 -134
- package/dist/create-model-B8U-COv-.js +0 -103
- package/dist/date-utils-dTT_x_Be.js +0 -31
- package/dist/form/create-core-form.d.ts +0 -70
- package/dist/form/factories/index.d.ts +0 -6
- package/dist/form/factories/node-factory.d.ts +0 -189
- package/dist/form/form-submitter.d.ts +0 -121
- package/dist/form/nodes/array-node.d.ts +0 -276
- package/dist/form/nodes/field-node.d.ts +0 -297
- package/dist/form/nodes/form-node.d.ts +0 -326
- package/dist/form/nodes/group-node.d.ts +0 -284
- package/dist/form/nodes/model-array-node.d.ts +0 -121
- package/dist/form/nodes/subscription-manager.d.ts +0 -180
- package/dist/form/status-machine.d.ts +0 -115
- package/dist/form/types/deep-schema.d.ts +0 -107
- package/dist/form/types/form-proxy.d.ts +0 -141
- package/dist/form/unique-id.d.ts +0 -45
- package/dist/form/validation/config.d.ts +0 -73
- package/dist/form/validation/error-handler.d.ts +0 -166
- package/dist/form/validators/validation-error.d.ts +0 -8
- package/dist/index-BrNkQj1y.js +0 -105
- package/dist/loader-Dv_Hl7JC.js +0 -216
- package/dist/model-signals-proxy-2vqS243J.js +0 -250
- package/dist/node-DGEFMTQu.js +0 -18
- package/dist/strategy-BXS_gBfz.js +0 -170
- package/dist/validation-error-B-U3RsV0.js +0 -11
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Единый контракт формы — одна схема, одна сборка, одно поведение
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Форма описывается одинаково для всех способов реализации — разметка в JSX, рендерер по TS-схеме,
|
|
6
|
+
рендерер по JSON. Схема одна (дерево узлов), привязка одна (ручка `model.$.…`), сборка одна
|
|
7
|
+
(`createForm`), поведение одно (`defineFormBehavior`). Способы отличаются только видом схемы
|
|
8
|
+
(билдер или документ) и тем, кто рисует результат.
|
|
9
|
+
|
|
10
|
+
Прежние записи узла — ключи `value` / `array` и запись «имя поля → узел» — удалены: тип узла
|
|
11
|
+
закрыт, сборка их не читает. Удалены и оператор `each`, свободный `cross` и имя `createCoreForm`:
|
|
12
|
+
их заменяют `applyEach` со схемой строки, `cross` из аргумента схемы и `createForm`.
|
|
13
|
+
|
|
14
|
+
| Слой | поле | массив под-форм | подформа |
|
|
15
|
+
| --------- | --------------------------------- | ----------------------------------------- | ------------------------------------- |
|
|
16
|
+
| схема | `{ model: model.$.x, component }` | `{ model: model.$.items, item }` | `{ model: model.$.group, part }` |
|
|
17
|
+
| валидация | `validate(model.$.x, rules)` | `applyEach(model.$.items, itemRules)` | `apply(model.$.group, groupRules)` |
|
|
18
|
+
| поведение | `compute(model.$.x, …)` | `applyEach(model.$.items, itemBehavior)` | `apply(model.$.group, groupBehavior)` |
|
|
19
|
+
|
|
20
|
+
Правило привязки: `model.$.…` — привязка (схема, правило, оператор). Без `$` — чтение значения
|
|
21
|
+
(`model.loanType === 'mortgage'`), мутация (`model.items.push()`) и под-модель как область.
|
|
22
|
+
|
|
23
|
+
## Модель — начальные значения и шаблоны элементов
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { arrayOf, createModel } from '@reformer/core';
|
|
27
|
+
|
|
28
|
+
const blankPhone = (): Phone => ({ number: '' });
|
|
29
|
+
const blankCoBorrower = (): CoBorrower => ({ name: '', phones: arrayOf(blankPhone) });
|
|
30
|
+
|
|
31
|
+
export const createCreditModel = () =>
|
|
32
|
+
createModel<CreditForm>({
|
|
33
|
+
loanType: 'consumer',
|
|
34
|
+
registrationAddress: { city: '', street: '' },
|
|
35
|
+
coBorrowers: arrayOf(blankCoBorrower), // пустой массив + шаблон нового элемента
|
|
36
|
+
tags: [], // массив как одно значение поля — шаблон не нужен
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`arrayOf(blank, items?)` возвращает обычный массив `U[]` и запоминает фабрику нового элемента.
|
|
41
|
+
После этого `push()` и `insertAt(i)` без значения кладут элемент по шаблону — в модели, в ноде
|
|
42
|
+
массива формы и в кнопке «Добавить» секции массива.
|
|
43
|
+
|
|
44
|
+
- Шаблон переживает `set` / `patch` / `reset` модели.
|
|
45
|
+
- Вложенный массив объявляется внутри шаблона элемента; элементы, пришедшие обычными данными
|
|
46
|
+
(загрузка с сервера), получают его шаблон из шаблона родителя.
|
|
47
|
+
- У массива без шаблона `push()` без значения бросает ошибку с подсказкой.
|
|
48
|
+
- Шаблон привязан к самому массиву: до `createModel` его нельзя копировать. `structuredClone`,
|
|
49
|
+
`JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона. Начальные значения с
|
|
50
|
+
`arrayOf` держат в фабрике, а не в константе, которую клонируют.
|
|
51
|
+
|
|
52
|
+
## Схема — одно дерево
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import type { FormModel, FormSchemaNode } from '@reformer/core';
|
|
56
|
+
|
|
57
|
+
// подформа: объявлена один раз, получает под-модель
|
|
58
|
+
const address = (model: FormModel<Address>): FormSchemaNode => ({
|
|
59
|
+
component: Box,
|
|
60
|
+
children: [
|
|
61
|
+
{ model: model.$.city, component: Input, componentProps: { label: 'Город' } },
|
|
62
|
+
{ model: model.$.street, component: Input, componentProps: { label: 'Улица' } },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// строка массива — такая же функция от под-модели
|
|
67
|
+
const coBorrower = (model: FormModel<CoBorrower>): FormSchemaNode => ({
|
|
68
|
+
selector: 'co-borrower',
|
|
69
|
+
component: Box,
|
|
70
|
+
children: [{ model: model.$.name, component: Input, componentProps: { label: 'ФИО' } }],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const creditSchema = (model: FormModel<CreditForm>): FormSchemaNode => ({
|
|
74
|
+
component: Box,
|
|
75
|
+
children: [
|
|
76
|
+
{ model: model.$.loanType, component: SelectAsync, componentProps: { label: 'Тип' } }, // поле
|
|
77
|
+
{ model: model.$.tags, component: SelectMulti }, // массив как одно значение
|
|
78
|
+
{
|
|
79
|
+
selector: 'registration',
|
|
80
|
+
component: Section,
|
|
81
|
+
componentProps: { title: 'Адрес регистрации' },
|
|
82
|
+
children: [{ model: model.$.registrationAddress, part: address }], // подформа
|
|
83
|
+
},
|
|
84
|
+
{ model: model.$.coBorrowers, component: FieldArray, item: coBorrower }, // массив под-форм
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Чем узел является, решают ручка и соседние ключи:
|
|
90
|
+
|
|
91
|
+
| Узел | Как отличить |
|
|
92
|
+
| --------------- | --------------------------------------------- |
|
|
93
|
+
| поле | `model` — лист или массив; нет `item`, `part` |
|
|
94
|
+
| массив под-форм | `model` — массив, `item` — функция |
|
|
95
|
+
| подформа | `model` — группа, `part` — функция |
|
|
96
|
+
| контейнер | есть `children`, нет `model` |
|
|
97
|
+
|
|
98
|
+
- Тип узла `FormSchemaNode` закрыт: опечатка в ключе, группа без `part`, `item` не на массиве и
|
|
99
|
+
привязка value-фасадом (`model: model.email`) не компилируются.
|
|
100
|
+
- Вложенные узлы читаются только из `children` и из поддерева `part`. В `componentProps` и под
|
|
101
|
+
произвольными ключами сборка не заглядывает; в dev она предупреждает о ключах узла, которые не
|
|
102
|
+
читает.
|
|
103
|
+
- Вид ноды формы определяет узел модели, а не имя: поле данных может называться `value`, `schema`,
|
|
104
|
+
`form`, `model` или `children`.
|
|
105
|
+
- Поля подформы принадлежат той же форме: `form.registrationAddress.city`.
|
|
106
|
+
- Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одну и ту
|
|
107
|
+
же компиляцию поддерева: `content` подформы и `row(rowModel)` массива.
|
|
108
|
+
- `initialValue` узла-массива — запасной шаблон нового элемента для форм, чья модель создаётся из
|
|
109
|
+
данных без кода. Шаблон модели (`arrayOf`) главнее.
|
|
110
|
+
|
|
111
|
+
## Валидация — подформа и массив подключаются привязкой
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import {
|
|
115
|
+
apply,
|
|
116
|
+
applyEach,
|
|
117
|
+
defineValidation,
|
|
118
|
+
validate,
|
|
119
|
+
validateWhen,
|
|
120
|
+
} from '@reformer/core/validation';
|
|
121
|
+
|
|
122
|
+
const addressRules = defineValidation<Address>(({ model, cross }) => {
|
|
123
|
+
validate(model.$.city, [required()]);
|
|
124
|
+
// cross — из аргумента схемы: снимок области (Address) выведен, приведения не нужны
|
|
125
|
+
cross(model.$.street, (address) =>
|
|
126
|
+
address.city !== '' && address.street === '' ? streetRequired : null
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const coBorrowerRules = defineValidation<CoBorrower>(({ model }) => {
|
|
131
|
+
validate(model.$.name, [required()]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const contactsRules = defineValidation<CreditForm>(({ model }) => {
|
|
135
|
+
apply(model.$.registrationAddress, addressRules);
|
|
136
|
+
validateWhen(
|
|
137
|
+
() => !model.sameAsRegistration,
|
|
138
|
+
() => apply(model.$.residenceAddress, addressRules)
|
|
139
|
+
);
|
|
140
|
+
applyEach(model.$.coBorrowers, coBorrowerRules);
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
|
|
145
|
+
`cross` из аргумента схемы получает её снимок. Захватывать снимок в замыкание не нужно.
|
|
146
|
+
- Одна и та же схема подключается и к группе, и к элементам массива.
|
|
147
|
+
- `apply(model.$.a, rules)` принимает и массив ручек: `apply([model.$.a, model.$.b], rules)`.
|
|
148
|
+
- `apply(schemaA, schemaB)` — композиция схем над той же моделью — остаётся.
|
|
149
|
+
- Правило значения — `Rule<T> = (value: T) => ValidationIssue | null`: один аргумент. Тип значения
|
|
150
|
+
сверяется с типом поля; набор правил объявляют типом поля (`Rule<LoanType>[]`,
|
|
151
|
+
`Rule<number | null>[]`).
|
|
152
|
+
- Текста у замечания нет: `ValidationIssue = { code, params?, severity? }`. Правило несёт код
|
|
153
|
+
(встроенный либо свой — `required({ code: 'street.required' })`), строку по коду даёт словарь
|
|
154
|
+
формы (`defineIssueMessages`) поверх словаря локали — см. `36-issue-messages.md`.
|
|
155
|
+
- `cross` берётся только из аргумента схемы: свободного оператора с таким именем в
|
|
156
|
+
`@reformer/core/validation` нет. Нет и `each` — правила строки подключает `applyEach`.
|
|
157
|
+
|
|
158
|
+
### Результат прогона
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const result = await credit.validate(); // либо credit.validateStep('loan'), runValidation(model, rules)
|
|
162
|
+
// result.status: 'valid' | 'invalid' | 'error' | 'cancelled'
|
|
163
|
+
// result.issues: Map<ручка поля, замечания>; result.failures: правила, не вернувшие результат
|
|
164
|
+
|
|
165
|
+
const outcome = await credit.submission.submit(sendApplication); // SubmitResult
|
|
166
|
+
// outcome.status: 'submitted' | 'invalid' | 'error' | 'cancelled' | 'failed'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `validateModel()` (раннер без сборки) отвечает `boolean`: `true` только для `valid`.
|
|
170
|
+
- **Сбой async-правила блокирует**: статус `error`, на поле — замечание `{ code: 'ruleFailed' }`.
|
|
171
|
+
- Отменённый прогон (`cancelled`) по нодам не разносится.
|
|
172
|
+
- Пока идут async-правила поля, его нода в `pending`; `credit.validating` — идёт ли проверка.
|
|
173
|
+
- У нод методов `validate()` / `submit()` нет: проверка и отправка — уровень формы, см.
|
|
174
|
+
`38-form-flow.md`.
|
|
175
|
+
|
|
176
|
+
## Поведение — модель, форма и схема
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import {
|
|
180
|
+
apply,
|
|
181
|
+
applyEach,
|
|
182
|
+
defineFormBehavior,
|
|
183
|
+
enableWhen,
|
|
184
|
+
hideWhen,
|
|
185
|
+
onComponentEvent,
|
|
186
|
+
onMount,
|
|
187
|
+
renderEffect,
|
|
188
|
+
} from '@reformer/core/behaviors';
|
|
189
|
+
|
|
190
|
+
const addressBehavior = defineFormBehavior<Address>(({ model, schema }) => {
|
|
191
|
+
hideWhen(schema.node('street'), () => model.city === ''); // узел ищется внутри части
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema, flow }) => {
|
|
195
|
+
const isMortgage = () => model.loanType === 'mortgage';
|
|
196
|
+
|
|
197
|
+
enableWhen(model.$.propertyValue, isMortgage, { resetOnDisable: true });
|
|
198
|
+
hideWhen(schema.node('mortgage'), () => !isMortgage());
|
|
199
|
+
// визард в рендерере зовёт onSubmit через bundle.submission.submit — только после успешной
|
|
200
|
+
// проверки; values — модель целиком, с выключенными полями
|
|
201
|
+
onComponentEvent(schema.node('wizard'), 'onSubmit', (values: CreditForm) =>
|
|
202
|
+
sendApplication(values)
|
|
203
|
+
);
|
|
204
|
+
onMount(schema.node('data-boundary'), () => void loadApplication(model));
|
|
205
|
+
renderEffect(schema, () => {
|
|
206
|
+
if (isMortgage()) flow?.goTo('loan'); // поток шагов сборки; без flow — undefined
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
|
|
210
|
+
applyEach(model.$.coBorrowers, coBorrowerBehavior);
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Операторы узлов схемы:
|
|
215
|
+
|
|
216
|
+
| Оператор | Что записывает |
|
|
217
|
+
| ----------------------------------------- | ---------------------------------------------------------- |
|
|
218
|
+
| `hideWhen(node, condition)` | узел скрыт, пока условие истинно |
|
|
219
|
+
| `onComponentEvent(node, event, handler)` | обработчик пропа-события компонента |
|
|
220
|
+
| `onMount(node, fn)` / `onUnmount(node, fn)` | хуки монтирования узла |
|
|
221
|
+
| `onInit(node, fn)` | `fn` вызывается сразу, до первого рендера |
|
|
222
|
+
| `renderEffect(schema, fn)` | реактивный эффект, пока схема области смонтирована |
|
|
223
|
+
| `schema.node(sel).setHidden / patchProps` | принудительная видимость и пропсы узла |
|
|
224
|
+
| `schema.node(sel).getRef()` | ref на компонент узла — объект `{ current }` |
|
|
225
|
+
|
|
226
|
+
- Область поведения — `{ model, form, schema, flow }`: `flow` — поток шагов сборки, один на всю
|
|
227
|
+
сборку (его же получают поведения подформ и строк).
|
|
228
|
+
- **Области изолированы.** `schema.node(selector)` ищет узел только в своей области: корневое
|
|
229
|
+
поведение — в корневом дереве, поведение подформы и строки массива — в своём поддереве. До узла
|
|
230
|
+
строки или части корень дотягивается через `applyEach` / `apply`. Два монтирования одной части
|
|
231
|
+
не конфликтуют.
|
|
232
|
+
- **Исключение — ref по пути модели**: `schema.node('phones.0.number').getRef()` ищется от корня.
|
|
233
|
+
- **Операторы узлов только записывают правило — исполняет его рендерер.** При разметке в JSX они
|
|
234
|
+
ничего не делают: видимость, отправка и загрузка данных остаются в JSX.
|
|
235
|
+
- Правило на селектор, которого нет в дереве, ничего не делает; в dev — предупреждение.
|
|
236
|
+
- **Несколько поведений на один узел.** Условия `hideWhen` на один селектор объединяются по «или»:
|
|
237
|
+
узел скрыт, пока истинно любое. Обработчики событий и хуки монтирования — стеком: действует
|
|
238
|
+
последний, его снятие возвращает прежний. Снятие поведения убирает только его правила.
|
|
239
|
+
- **`setHidden` / `patchProps` поведения откатываются при его снятии** — вместе с записанным в
|
|
240
|
+
его `onDispose`. Значение, которое с тех пор переписал кто-то другой, остаётся.
|
|
241
|
+
|
|
242
|
+
## Сборка — `createForm`
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { createForm } from '@reformer/core';
|
|
246
|
+
import { useFormBundle } from '@reformer/core/react';
|
|
247
|
+
|
|
248
|
+
const credit = useFormBundle(() =>
|
|
249
|
+
createForm<CreditForm>({
|
|
250
|
+
model: createCreditModel(), // либо initial: { … }
|
|
251
|
+
schema: creditSchema, // (model) => узел; для JSON — документ + registry
|
|
252
|
+
behavior: creditBehavior,
|
|
253
|
+
validation: { extras: crossRules }, // правила вне шагов; голая схема = { rules }
|
|
254
|
+
flow: { steps: [{ id: 'loan', rules: loanRules }, { id: 'contacts', rules: contactsRules }] },
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
// credit = { model, form, render, flow, validating, submission, validate, validateStep, dispose }
|
|
258
|
+
|
|
259
|
+
<FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />; // рендерер
|
|
260
|
+
<FormField control={credit.form.loanType} />; // разметка в JSX — та же сборка
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- Порядок: модель → `seed` → дерево схемы → компиляция → уровень формы (правила, поток шагов) →
|
|
264
|
+
ноды формы → поведение → `setup`. Дерево строится и компилируется один раз. Ноды строятся по
|
|
265
|
+
виду узла модели: лист и массив-значение — `FormControl`, группа — `FormGroup`, массив
|
|
266
|
+
под-форм — `FormArray`.
|
|
267
|
+
- Повтор `selector` в одной области — исключение при сборке. В dev правило поведения на узел,
|
|
268
|
+
которого нет, и шаг `flow.steps` без узла с таким `selector` дают предупреждение.
|
|
269
|
+
- Уровень формы: `validate()` и `validateStep(id)` → `ValidationResult`, отправка — отдельный
|
|
270
|
+
объект `submission`: `submission.submit(handler)` → `SubmitResult`, `submission.submitting`;
|
|
271
|
+
`flow` — поток шагов (`next` / `previous` / `goTo`). См. `38-form-flow.md`.
|
|
272
|
+
- `dispose()` снимает сборку: поведение формы, подписки нод, живую проверку; идущие проверки
|
|
273
|
+
получают `cancelled`, значения модели остаются. Вне React владелец сборки зовёт его сам;
|
|
274
|
+
`useFormBundle` сборку при размонтировании не снимает.
|
|
275
|
+
- `render` — `{ tree, controller, node(selector) }`: скомпилированная схема (`CompiledSchema`, см.
|
|
276
|
+
`37-compiled-schema.md`) и схема-контроллер сборки.
|
|
277
|
+
- JSON: `createForm({ model, schema: document, registry })` — дерево собирает
|
|
278
|
+
`registry.resolveSchema`. Ядро документ не разбирает.
|
|
279
|
+
- Внутри рендерера бандл доступен компонентам через `useFormBundleContext()` — так визард берёт
|
|
280
|
+
форму и поток шагов сам и отправляет через `bundle.submission.submit`.
|
|
281
|
+
|
|
282
|
+
### Низкоуровневая фабрика
|
|
283
|
+
|
|
284
|
+
`createFormFromModel({ model, schema })` — форма из модели и готового дерева, без бандла.
|
|
285
|
+
|
|
286
|
+
Формы без модели нет: значения принадлежат модели. `createForm` без `model` и `initial` либо с
|
|
287
|
+
готовым деревом вместо билдера бросает ошибку с подсказкой.
|
|
288
|
+
|
|
289
|
+
## Частые ошибки
|
|
290
|
+
|
|
291
|
+
| ❌ Ошибка | Как правильно |
|
|
292
|
+
| -------------------------------------------------- | ------------------------------------------------------- |
|
|
293
|
+
| `model: model.address` (под-модель у поля) | `model: model.$.address.city` — привязка всегда с `$` |
|
|
294
|
+
| `{ model: model.$.address, component }` без `part` | группа полем не бывает — нужен `part` |
|
|
295
|
+
| `createForm({ model, schema: дерево })` | `schema` — билдер `(model) => узел` |
|
|
296
|
+
| `hideWhen(schema.node('row-узел'), …)` из корня | внутри `applyEach(model.$.items, itemBehavior)` |
|
|
297
|
+
| `push({})` для новой строки | `arrayOf(blank)` в модели и `push()` без значения |
|
|
298
|
+
| `createModel(structuredClone(INITIAL))` с `arrayOf` | фабрика начальных значений — клон теряет шаблон массива |
|
|
299
|
+
| `cross` в подформе читает корень | `apply(model.$.group, rules)` — область подформы |
|
|
300
|
+
| `{ value: model.$.x }`, `{ array: model.items }` | `{ model: model.$.x }`, `{ model: model.$.items, item }` |
|
|
301
|
+
| узлы записью `{ email: { model, component } }` | `{ children: [{ model, component }] }` |
|
|
302
|
+
| `Rule<unknown>[]` и `(form: Root)` с приведением | `Rule<ТипПоля>[]`, `cross` из аргумента схемы |
|
|
303
|
+
| `try { … } catch { return null }` в async-правиле «на всякий случай» | сбой правила блокирует сам; `catch` — только если проверка необязательна |
|
|
304
|
+
| `validation: { steps: { loan: rules } }` | `flow: { steps: [{ id: 'loan', rules }] }` |
|
|
305
|
+
| `required({ message: '…' })` | `required({ code: '<поле>.required' })` + `defineIssueMessages` |
|
|
306
|
+
| два узла с одним `selector` в одной области | разные селекторы — повтор бросает исключение |
|
|
307
|
+
| отправка `api.send(model.get())` в обход проверки | `bundle.submission.submit(api.send)` |
|
|
@@ -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`
|