@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,298 @@
|
|
|
1
|
+
# i18n — локаль, загрузка словарей и провайдер
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*` без React:
|
|
6
|
+
объект локали, загрузка словарей, перевод и форматирование, тексты замечаний валидации. React-часть
|
|
7
|
+
— провайдер `I18nProvider` и хуки `useI18n`, `useMessages`, `useIssueMessage` — отдаёт
|
|
8
|
+
`@reformer/core/react`. Язык задаётся провайдером, подгружается динамически и переключается на лету;
|
|
9
|
+
узлы формы при этом не пересоздаются.
|
|
10
|
+
|
|
11
|
+
Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
|
|
12
|
+
|
|
13
|
+
> Импорт локали и перевода — из `@reformer/core/i18n`, провайдера и хуков — из
|
|
14
|
+
> `@reformer/core/react`. Из главной бочки `@reformer/core` ни то, ни другое не реэкспортируется:
|
|
15
|
+
> React-контекст локализации обязан существовать в одном экземпляре.
|
|
16
|
+
|
|
17
|
+
## Локаль — это данные
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
interface FormLocale {
|
|
21
|
+
code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural; не BCP 47 — по en
|
|
22
|
+
messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
|
|
23
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
24
|
+
dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
|
|
25
|
+
dateFormat?: string; // необязательно: токены date-fns для DatePicker
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Функций в локали нет, поэтому её можно положить JSON-файлом на сервер и получить по сети:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{ "code": "ru", "weekStartsOn": 1, "messages": { "profile.title": "Профиль" } }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`extendLocale(base, patch)` возвращает новую локаль: словари сливаются (ключи из `patch` важнее),
|
|
36
|
+
остальные поля заменяются.
|
|
37
|
+
|
|
38
|
+
## Формат сообщений — подмножество ICU
|
|
39
|
+
|
|
40
|
+
| Конструкция | Пример |
|
|
41
|
+
| ------------------- | ------------------------------------------------------------------------- |
|
|
42
|
+
| подстановка | `Открыт файл {name}` |
|
|
43
|
+
| множественные формы | `{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}` |
|
|
44
|
+
| точное значение | `{count, plural, =0{файлов нет} other{# файлов}}` |
|
|
45
|
+
| выбор | `{gender, select, male{он} female{она} other{оно}}` |
|
|
46
|
+
| экранирование | `'{'` → `{`, `'}'` → `}`, `''` → `'` |
|
|
47
|
+
|
|
48
|
+
- Ветка `other` обязательна. `#` внутри `plural` — число, отформатированное по языку.
|
|
49
|
+
- Типы `number`, `date`, `time`, `selectordinal` не поддерживаются: числа и даты форматируйте
|
|
50
|
+
отдельно (`useI18n().number`, `.date`) и передавайте готовой строкой.
|
|
51
|
+
- Категории (`one`/`few`/`many`) берутся из `Intl.PluralRules` языка локали.
|
|
52
|
+
|
|
53
|
+
## Загрузка локалей
|
|
54
|
+
|
|
55
|
+
Откуда брать словарь, решает приложение: источник — функция `(code) => локаль | словарь`.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
|
|
59
|
+
import { loadCoreLocale } from '@reformer/core/locale';
|
|
60
|
+
|
|
61
|
+
export const loadLocale = createLocaleLoader([
|
|
62
|
+
loadCoreLocale, // встроенные словари ядра — чанк нужного языка
|
|
63
|
+
fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
|
|
64
|
+
]);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Источники опрашиваются параллельно, сливаются по порядку: поздний перекрывает ранний.
|
|
68
|
+
- Источник отдаёт локаль целиком (`{ code, messages, … }`) или голый словарь (`{ "a.b": "…" }`);
|
|
69
|
+
`null` означает «для этого языка у меня ничего нет» и ошибкой не считается.
|
|
70
|
+
- Результат кэшируется по коду языка, идущий запрос не дублируется.
|
|
71
|
+
- Отказ любого источника — отказ загрузки. Источник, которому можно промахнуться, оборачивайте:
|
|
72
|
+
`(code) => source(code).catch(() => null)`.
|
|
73
|
+
- `loadLocale.peek(code)` — уже загруженная локаль или `undefined`; `loadLocale.preload(code)` — то
|
|
74
|
+
же, что вызов загрузчика.
|
|
75
|
+
|
|
76
|
+
Ядро отдаёт свои локали в трёх видах:
|
|
77
|
+
|
|
78
|
+
| Вид | Импорт | Для чего |
|
|
79
|
+
| ----------------- | ------------------------------------------- | ----------------------------------------------- |
|
|
80
|
+
| загрузчик | `loadCoreLocale` из `@reformer/core/locale` | основной путь: чанк языка по запросу |
|
|
81
|
+
| JSON-файл | `@reformer/core/locale/ru.json` | выложить на свой сервер, править без пересборки |
|
|
82
|
+
| синхронный модуль | `ru` из `@reformer/core/locale/ru` | SSR и тесты |
|
|
83
|
+
|
|
84
|
+
Встроенные языки ядра — `en` и `ru`. Словарь ядра несёт тексты ошибок валидации
|
|
85
|
+
(`validation.<code>`) и единицы размера файла (`format.fileSize.b|kb|mb|gb|tb`).
|
|
86
|
+
|
|
87
|
+
Пакеты выше отдают локаль накопительно и в тех же трёх видах, поэтому источник встроенных словарей
|
|
88
|
+
в загрузчике всегда один — самого верхнего из используемых пакетов:
|
|
89
|
+
|
|
90
|
+
| Пакеты приложения | Источник | Что в локали |
|
|
91
|
+
| ------------------------------- | -------------------------------------------- | --------------------------------- |
|
|
92
|
+
| только ядро | `loadCoreLocale` из `@reformer/core/locale` | ошибки валидации, размер файла |
|
|
93
|
+
| ядро + `@reformer/cdk` | `loadCdkLocale` из `@reformer/cdk/locale` | то же плюс строки cdk (`cdk.*`) |
|
|
94
|
+
| ядро + cdk + `@reformer/ui-kit` | `loadKitLocale` из `@reformer/ui-kit/locale` | то же плюс подписи кита (`kit.*`) |
|
|
95
|
+
|
|
96
|
+
## Провайдер
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { I18nProvider } from '@reformer/core/react';
|
|
100
|
+
|
|
101
|
+
function App() {
|
|
102
|
+
const [lang, setLang] = useState('ru');
|
|
103
|
+
return (
|
|
104
|
+
<I18nProvider lang={lang} load={loadLocale}>
|
|
105
|
+
<LangSwitch value={lang} onChange={setLang} />
|
|
106
|
+
<CreditForm />
|
|
107
|
+
</I18nProvider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Проп | Назначение |
|
|
113
|
+
| --------------- | ------------------------------------------------------------------------ |
|
|
114
|
+
| `lang` + `load` | провайдер грузит локаль сам; `load` — стабильная ссылка (константа модуля) |
|
|
115
|
+
| `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
|
|
116
|
+
| `onError` | отказ загрузки `(error, lang)` |
|
|
117
|
+
| `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
|
|
118
|
+
|
|
119
|
+
Поведение при `lang` + `load`:
|
|
120
|
+
|
|
121
|
+
- при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
|
|
122
|
+
`useI18n().pending` — для индикатора;
|
|
123
|
+
- при быстром переключении применяется только последний запрос;
|
|
124
|
+
- ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`; повторить запрос
|
|
125
|
+
того же языка — `useI18n().retry()`;
|
|
126
|
+
- упала самая первая загрузка — дети показываются на встроенном английском (а не `fallback`),
|
|
127
|
+
ошибка — в `useI18n().error`;
|
|
128
|
+
- новая ссылка `load` — новое хранилище: язык запрашивается у нового загрузчика, на экране тем
|
|
129
|
+
временем прежняя локаль;
|
|
130
|
+
- если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
|
|
131
|
+
синхронный и `fallback` не показывается.
|
|
132
|
+
|
|
133
|
+
SSR: эффекты на сервере не выполняются, поэтому там передавайте готовый объект —
|
|
134
|
+
`<I18nProvider locale={ru}>` — либо заранее вызовите `preload`.
|
|
135
|
+
|
|
136
|
+
Вложенный провайдер заменяет локаль для своего поддерева целиком.
|
|
137
|
+
|
|
138
|
+
## Хуки
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { useI18n, useMessages, useIssueMessage } from '@reformer/core/react';
|
|
142
|
+
|
|
143
|
+
const { code, locale, pending, error, retry, t, has, number, date, fileSize } = useI18n();
|
|
144
|
+
|
|
145
|
+
t('profile.title'); // ключ приложения
|
|
146
|
+
t('cart.items', { count: 3 }); // с подстановками
|
|
147
|
+
t('cart.empty', undefined, 'Your cart is empty'); // английский текст на случай промаха
|
|
148
|
+
number(1234.5); // '1 234,5' в ru
|
|
149
|
+
date(new Date(), { dateStyle: 'long' }); // '15 января 2024 г.' в ru
|
|
150
|
+
fileSize(1536); // '1,5 КБ' в ru, '1.5 KB' без провайдера
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`useMessages(builtinEn)` — для авторов пакетов и китов: ищет ключ в локали провайдера, а при
|
|
154
|
+
промахе берёт встроенную английскую таблицу пакета. Тип ключа выводится из таблицы.
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import en from './locale/en.json';
|
|
158
|
+
|
|
159
|
+
function ClearButton() {
|
|
160
|
+
const t = useMessages(en);
|
|
161
|
+
return <button aria-label={t('kit.select.clear')} />;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Тексты замечаний валидации
|
|
166
|
+
|
|
167
|
+
Замечание валидации — `ValidationIssue = { code, params?, severity? }`: текста у него нет. Правила из
|
|
168
|
+
`@reformer/core/validators` несут только `code` и `params`, свой код задаёт опция `code`. Строку
|
|
169
|
+
даёт резолвер при отображении:
|
|
170
|
+
|
|
171
|
+
1. словарь формы (`defineIssueMessages`) — запись по коду замечания;
|
|
172
|
+
2. `validation.<code>` из словаря локали;
|
|
173
|
+
3. `validation.<code>` из встроенной английской таблицы ядра;
|
|
174
|
+
4. сам код — для своих кодов, которых нет ни в одном словаре.
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { defineIssueMessages } from '@reformer/core/i18n';
|
|
178
|
+
|
|
179
|
+
validate(model.$.email, [
|
|
180
|
+
required(), // «Обязательное поле» в ru, английская фраза без провайдера
|
|
181
|
+
minLength(5, { code: 'email.minLength' }), // свой код: текст поля — в словаре формы
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Словарь формы: шаблон с параметрами либо функция (числа с разрядами — функцией)
|
|
185
|
+
export const formMessages = defineIssueMessages({
|
|
186
|
+
'email.minLength': 'Не меньше {minLength} символов',
|
|
187
|
+
'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
|
|
192
|
+
ошибки без повторной валидации.
|
|
193
|
+
- `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит замечание сам; словарь формы подключает
|
|
194
|
+
`<ValidationMessagesProvider messages={formMessages}>` из `@reformer/cdk`. В своём компоненте —
|
|
195
|
+
`useIssueMessage(formMessages)` из `@reformer/core/react`; вне React —
|
|
196
|
+
`resolveIssueMessage(issue, createI18n(locale), formMessages)`.
|
|
197
|
+
- Свой код локализуется записью словаря формы либо ключом `validation.<свой_код>` в словаре
|
|
198
|
+
приложения — второй вариант переключается вместе с языком.
|
|
199
|
+
- Встроенный текст меняется тем же ключом в словаре приложения:
|
|
200
|
+
`extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
|
|
201
|
+
- Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
|
|
202
|
+
размеры в байтах (`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) —
|
|
203
|
+
строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
function FieldIssues({ control }: { control: FormControl<string> }) {
|
|
207
|
+
const message = useIssueMessage(formMessages);
|
|
208
|
+
const { issues, shouldShowIssues } = useFormControl(control);
|
|
209
|
+
return shouldShowIssues ? <span role="alert">{message(issues[0])}</span> : null;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`ValidationMessagesProvider` из `@reformer/cdk` с `messages` накладывает словарь формы поверх
|
|
214
|
+
локали (вложенные провайдеры накладываются друг на друга); с `resolver` — заменяет резолвер
|
|
215
|
+
целиком, и локаль для замечаний его поддерева не участвует. Подробно — коды, встроенные параметры,
|
|
216
|
+
шаблоны и функции словаря — [36-issue-messages.md](./36-issue-messages.md).
|
|
217
|
+
|
|
218
|
+
## Что показывается, когда ключа нет
|
|
219
|
+
|
|
220
|
+
| Ключ | Нет в локали | Сообщение не разбирается |
|
|
221
|
+
| -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
|
|
222
|
+
| пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
|
|
223
|
+
| приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
|
|
224
|
+
|
|
225
|
+
Пропущенная подстановка выводится как `{name}`, значение, которое нельзя привести к строке
|
|
226
|
+
(`Object.create(null)`), — пустой строкой. Код языка не BCP 47 (пришёл из адресной строки) — числа,
|
|
227
|
+
даты и формы слов форматируются по `en`, в dev — предупреждение. Форма не падает ни в одном из
|
|
228
|
+
случаев: неполный словарь, пришедший по сети, — законный сценарий.
|
|
229
|
+
|
|
230
|
+
## Проверка словаря в тестах
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { validateLocale } from '@reformer/core/i18n';
|
|
234
|
+
|
|
235
|
+
expect(validateLocale(ruJson, enJson)).toEqual([]);
|
|
236
|
+
// находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка);
|
|
237
|
+
// битое сообщение образца — тоже syntax («в образце: …»), проверка остальных ключей продолжается
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Описатели сообщений (низкий уровень)
|
|
241
|
+
|
|
242
|
+
`msg(key, values?, defaultMessage?)` создаёт описатель сообщения, `defineMessages(appEn)` — `msg` с
|
|
243
|
+
типизированными ключами, `resolveLocalized(value, i18n)` раскрывает описатели в объекте пропсов.
|
|
244
|
+
|
|
245
|
+
> ⚠️ Рендереры и `FormField` описатели сами пока НЕ раскрывают. Не кладите `msg(...)` в
|
|
246
|
+
> `componentProps` схемы: компонент получит объект вместо строки. До появления раскрытия в
|
|
247
|
+
> рендерерах вызывайте `resolveLocalized` вручную или используйте `useI18n().t`.
|
|
248
|
+
|
|
249
|
+
## Anti-patterns
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// ❌ Импорт из главной бочки — React-части локализации там нет
|
|
253
|
+
import { I18nProvider } from '@reformer/core';
|
|
254
|
+
|
|
255
|
+
// ✅ Провайдер и хуки — из @reformer/core/react, локаль и перевод — из @reformer/core/i18n
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль запрашивается заново на каждый рендер
|
|
260
|
+
<I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
|
|
261
|
+
|
|
262
|
+
// ✅ Загрузчик — константа модуля: один кэш на приложение
|
|
263
|
+
const loadLocale = createLocaleLoader([loadCoreLocale]);
|
|
264
|
+
<I18nProvider lang={lang} load={loadLocale}>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// ❌ Число или дата типами ICU внутри сообщения — не поддерживается, сообщение не разберётся
|
|
269
|
+
const message = 'Итого: {total, number}';
|
|
270
|
+
|
|
271
|
+
// ✅ Отформатировать отдельно и передать готовой строкой
|
|
272
|
+
t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
// ❌ Текст в правиле: у RuleOptions и ValidationIssue нет message — не скомпилируется
|
|
277
|
+
required({ message: 'Укажите телефон' });
|
|
278
|
+
|
|
279
|
+
// ✅ Свой код в правиле, текст — в словаре формы (константа модуля), на экран — через резолвер
|
|
280
|
+
required({ code: 'phone.required' });
|
|
281
|
+
const phoneMessages = defineIssueMessages({ 'phone.required': 'Укажите телефон' }); // в модуле
|
|
282
|
+
const message = useIssueMessage(phoneMessages); // в компоненте
|
|
283
|
+
<span>{message(issues[0])}</span>;
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// ❌ Настройки локали без словаря: объект без поля messages принимается за словарь сообщений
|
|
288
|
+
const source = () => ({ weekStartsOn: 1 });
|
|
289
|
+
|
|
290
|
+
// ✅ Часть локали узнаётся по полю messages — отдавайте его хотя бы пустым
|
|
291
|
+
const source = () => ({ weekStartsOn: 1, messages: {} });
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## See also
|
|
295
|
+
|
|
296
|
+
- [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний: коды, словарь формы, резолвер
|
|
297
|
+
- [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
|
|
298
|
+
- [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Единый контракт формы — одна схема, одна сборка, одно поведение
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Форма описывается одинаково для всех способов реализации — разметка в JSX, рендерер по TS-схеме,
|
|
6
|
+
рендерер по JSON. Схема одна (дерево узлов), привязка одна (ручка `model.$.…`), сборка одна
|
|
7
|
+
(`createForm`), поведение одно (`defineFormBehavior`). Способы отличаются только видом схемы
|
|
8
|
+
(билдер или документ) и тем, кто рисует результат.
|
|
9
|
+
|
|
10
|
+
Прежние записи узла — ключи `value` / `array` и запись «имя поля → узел» — удалены: тип узла
|
|
11
|
+
закрыт, сборка их не читает. Удалены и оператор `each`, свободный `cross`, фабрики слоёв
|
|
12
|
+
(`createCoreForm`, `createReactForm`, `createJsonForm`) и отдельное поведение рендера
|
|
13
|
+
(`renderBehavior`): их заменяют `applyEach` со схемой строки, `cross` из аргумента схемы, `createForm` и
|
|
14
|
+
одно поведение формы.
|
|
15
|
+
|
|
16
|
+
| Слой | поле | массив под-форм | подформа |
|
|
17
|
+
| --------- | --------------------------------- | ----------------------------------------- | ------------------------------------- |
|
|
18
|
+
| схема | `{ model: model.$.x, component }` | `{ model: model.$.items, item }` | `{ model: model.$.group, part }` |
|
|
19
|
+
| валидация | `validate(model.$.x, rules)` | `applyEach(model.$.items, itemRules)` | `apply(model.$.group, groupRules)` |
|
|
20
|
+
| поведение | `compute(model.$.x, …)` | `applyEach(model.$.items, itemBehavior)` | `apply(model.$.group, groupBehavior)` |
|
|
21
|
+
|
|
22
|
+
Правило привязки: `model.$.…` — привязка (схема, правило, оператор). Без `$` — чтение значения
|
|
23
|
+
(`model.loanType === 'mortgage'`), мутация (`model.items.push()`) и под-модель как область.
|
|
24
|
+
|
|
25
|
+
## Модель — начальные значения и шаблоны элементов
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { arrayOf, createModel } from '@reformer/core';
|
|
29
|
+
|
|
30
|
+
const blankPhone = (): Phone => ({ number: '' });
|
|
31
|
+
const blankCoBorrower = (): CoBorrower => ({ name: '', phones: arrayOf(blankPhone) });
|
|
32
|
+
|
|
33
|
+
export const createCreditModel = () =>
|
|
34
|
+
createModel<CreditForm>({
|
|
35
|
+
loanType: 'consumer',
|
|
36
|
+
registrationAddress: { city: '', street: '' },
|
|
37
|
+
coBorrowers: arrayOf(blankCoBorrower), // пустой массив + шаблон нового элемента
|
|
38
|
+
tags: [], // массив как одно значение поля — шаблон не нужен
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`arrayOf(blank, items?)` возвращает обычный массив `U[]` и запоминает фабрику нового элемента.
|
|
43
|
+
После этого `push()` и `insertAt(i)` без значения кладут элемент по шаблону — в модели, в ноде
|
|
44
|
+
массива формы и в кнопке «Добавить» секции массива.
|
|
45
|
+
|
|
46
|
+
- Шаблон переживает `set` / `patch` / `reset` модели.
|
|
47
|
+
- Вложенный массив объявляется внутри шаблона элемента; элементы, пришедшие обычными данными
|
|
48
|
+
(загрузка с сервера), получают его шаблон из шаблона родителя.
|
|
49
|
+
- У массива без шаблона `push()` без значения бросает ошибку с подсказкой.
|
|
50
|
+
- Шаблон привязан к самому массиву: до `createModel` его нельзя копировать. `structuredClone`,
|
|
51
|
+
`JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона. Начальные значения с
|
|
52
|
+
`arrayOf` держат в фабрике, а не в константе, которую клонируют.
|
|
53
|
+
|
|
54
|
+
## Схема — одно дерево
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import type { FormModel, FormSchemaNode } from '@reformer/core';
|
|
58
|
+
|
|
59
|
+
// подформа: объявлена один раз, получает под-модель
|
|
60
|
+
const address = (model: FormModel<Address>): FormSchemaNode => ({
|
|
61
|
+
component: Box,
|
|
62
|
+
children: [
|
|
63
|
+
{ model: model.$.city, component: Input, componentProps: { label: 'Город' } },
|
|
64
|
+
{ model: model.$.street, component: Input, componentProps: { label: 'Улица' } },
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// строка массива — такая же функция от под-модели
|
|
69
|
+
const coBorrower = (model: FormModel<CoBorrower>): FormSchemaNode => ({
|
|
70
|
+
selector: 'co-borrower',
|
|
71
|
+
component: Box,
|
|
72
|
+
children: [{ model: model.$.name, component: Input, componentProps: { label: 'ФИО' } }],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const creditSchema = (model: FormModel<CreditForm>): FormSchemaNode => ({
|
|
76
|
+
component: Box,
|
|
77
|
+
children: [
|
|
78
|
+
{ model: model.$.loanType, component: SelectAsync, componentProps: { label: 'Тип' } }, // поле
|
|
79
|
+
{ model: model.$.tags, component: SelectMulti }, // массив как одно значение
|
|
80
|
+
{
|
|
81
|
+
selector: 'registration',
|
|
82
|
+
component: Section,
|
|
83
|
+
componentProps: { title: 'Адрес регистрации' },
|
|
84
|
+
children: [{ model: model.$.registrationAddress, part: address }], // подформа
|
|
85
|
+
},
|
|
86
|
+
{ model: model.$.coBorrowers, component: FieldArray, item: coBorrower }, // массив под-форм
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Чем узел является, решают ручка и соседние ключи:
|
|
92
|
+
|
|
93
|
+
| Узел | Как отличить |
|
|
94
|
+
| --------------- | --------------------------------------------- |
|
|
95
|
+
| поле | `model` — лист или массив; нет `item`, `part` |
|
|
96
|
+
| массив под-форм | `model` — массив, `item` — функция |
|
|
97
|
+
| подформа | `model` — группа, `part` — функция |
|
|
98
|
+
| контейнер | есть `children`, нет `model` |
|
|
99
|
+
|
|
100
|
+
- Тип узла `FormSchemaNode` закрыт: опечатка в ключе, группа без `part`, `item` не на массиве и
|
|
101
|
+
привязка value-фасадом (`model: model.email`) не компилируются.
|
|
102
|
+
- Вложенные узлы читаются только из `children` и из поддерева `part`. В `componentProps` и под
|
|
103
|
+
произвольными ключами сборка не заглядывает; в dev она предупреждает о ключах узла, которые не
|
|
104
|
+
читает.
|
|
105
|
+
- Вид ноды формы определяет узел модели, а не имя: поле данных может называться `value`, `schema`,
|
|
106
|
+
`form`, `model` или `children`.
|
|
107
|
+
- Поля подформы принадлежат той же форме: `form.registrationAddress.city`.
|
|
108
|
+
- Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одну и ту
|
|
109
|
+
же компиляцию поддерева: `content` подформы и `row(rowModel)` массива.
|
|
110
|
+
- `initialValue` узла-массива — запасной шаблон нового элемента для форм, чья модель создаётся из
|
|
111
|
+
данных без кода. Шаблон модели (`arrayOf`) главнее.
|
|
112
|
+
|
|
113
|
+
## Валидация — подформа и массив подключаются привязкой
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import {
|
|
117
|
+
apply,
|
|
118
|
+
applyEach,
|
|
119
|
+
defineValidation,
|
|
120
|
+
validate,
|
|
121
|
+
validateWhen,
|
|
122
|
+
} from '@reformer/core/validation';
|
|
123
|
+
|
|
124
|
+
const addressRules = defineValidation<Address>(({ model, cross }) => {
|
|
125
|
+
validate(model.$.city, [required()]);
|
|
126
|
+
// cross — из аргумента схемы: снимок области (Address) выведен, приведения не нужны
|
|
127
|
+
cross(model.$.street, (address) =>
|
|
128
|
+
address.city !== '' && address.street === '' ? streetRequired : null
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const coBorrowerRules = defineValidation<CoBorrower>(({ model }) => {
|
|
133
|
+
validate(model.$.name, [required()]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const contactsRules = defineValidation<CreditForm>(({ model }) => {
|
|
137
|
+
apply(model.$.registrationAddress, addressRules);
|
|
138
|
+
validateWhen(
|
|
139
|
+
() => !model.sameAsRegistration,
|
|
140
|
+
() => apply(model.$.residenceAddress, addressRules)
|
|
141
|
+
);
|
|
142
|
+
applyEach(model.$.coBorrowers, coBorrowerRules);
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
|
|
147
|
+
`cross` из аргумента схемы получает её снимок. Захватывать снимок в замыкание не нужно.
|
|
148
|
+
- Одна и та же схема подключается и к группе, и к элементам массива.
|
|
149
|
+
- `apply(model.$.a, rules)` принимает и массив ручек: `apply([model.$.a, model.$.b], rules)`.
|
|
150
|
+
- `apply(schemaA, schemaB)` — композиция схем над той же моделью — остаётся.
|
|
151
|
+
- Правило значения — `Rule<T> = (value: T) => ValidationIssue | null`: один аргумент. Тип значения
|
|
152
|
+
сверяется с типом поля; набор правил объявляют типом поля (`Rule<LoanType>[]`,
|
|
153
|
+
`Rule<number | null>[]`).
|
|
154
|
+
- Текста у замечания нет: `ValidationIssue = { code, params?, severity? }`. Правило несёт код
|
|
155
|
+
(встроенный либо свой — `required({ code: 'street.required' })`), строку по коду даёт словарь
|
|
156
|
+
формы (`defineIssueMessages`) поверх словаря локали — см. `36-issue-messages.md`.
|
|
157
|
+
- `cross` берётся только из аргумента схемы: свободного оператора с таким именем в
|
|
158
|
+
`@reformer/core/validation` нет. Нет и `each` — правила строки подключает `applyEach`.
|
|
159
|
+
|
|
160
|
+
### Результат прогона
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const result = await credit.validate(); // либо credit.validateStep('loan'), runValidation(model, rules)
|
|
164
|
+
// result.status: 'valid' | 'invalid' | 'error' | 'cancelled'
|
|
165
|
+
// result.issues: Map<ручка поля, замечания>; result.failures: правила, не вернувшие результат
|
|
166
|
+
|
|
167
|
+
const outcome = await credit.submission.submit(sendApplication); // SubmitResult
|
|
168
|
+
// outcome.status: 'submitted' | 'invalid' | 'error' | 'cancelled' | 'failed'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
- `validateModel()` (раннер без сборки) отвечает `boolean`: `true` только для `valid`.
|
|
172
|
+
- **Сбой async-правила блокирует**: статус `error`, на поле — замечание `{ code: 'ruleFailed' }`.
|
|
173
|
+
- Отменённый прогон (`cancelled`) по нодам не разносится.
|
|
174
|
+
- Пока идут async-правила поля, его нода в `pending`; `credit.validating` — идёт ли проверка.
|
|
175
|
+
- У нод методов `validate()` / `submit()` нет: проверка и отправка — уровень формы, см.
|
|
176
|
+
`38-form-flow.md`.
|
|
177
|
+
|
|
178
|
+
## Поведение — модель, форма и схема
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import {
|
|
182
|
+
apply,
|
|
183
|
+
applyEach,
|
|
184
|
+
defineFormBehavior,
|
|
185
|
+
enableWhen,
|
|
186
|
+
hideWhen,
|
|
187
|
+
onComponentEvent,
|
|
188
|
+
onMount,
|
|
189
|
+
renderEffect,
|
|
190
|
+
} from '@reformer/core/behaviors';
|
|
191
|
+
|
|
192
|
+
const addressBehavior = defineFormBehavior<Address>(({ model, schema }) => {
|
|
193
|
+
hideWhen(schema.node('street'), () => model.city === ''); // узел ищется внутри части
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema, flow }) => {
|
|
197
|
+
const isMortgage = () => model.loanType === 'mortgage';
|
|
198
|
+
|
|
199
|
+
enableWhen(model.$.propertyValue, isMortgage, { resetOnDisable: true });
|
|
200
|
+
hideWhen(schema.node('mortgage'), () => !isMortgage());
|
|
201
|
+
// визард в рендерере зовёт onSubmit через bundle.submission.submit — только после успешной
|
|
202
|
+
// проверки; values — модель целиком, с выключенными полями
|
|
203
|
+
onComponentEvent(schema.node('wizard'), 'onSubmit', (values: CreditForm) =>
|
|
204
|
+
sendApplication(values)
|
|
205
|
+
);
|
|
206
|
+
onMount(schema.node('data-boundary'), () => void loadApplication(model));
|
|
207
|
+
renderEffect(schema, () => {
|
|
208
|
+
if (isMortgage()) flow?.goTo('loan'); // поток шагов сборки; без flow — undefined
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
|
|
212
|
+
applyEach(model.$.coBorrowers, coBorrowerBehavior);
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Операторы узлов схемы:
|
|
217
|
+
|
|
218
|
+
| Оператор | Что записывает |
|
|
219
|
+
| ----------------------------------------- | ---------------------------------------------------------- |
|
|
220
|
+
| `hideWhen(node, condition)` | узел скрыт, пока условие истинно |
|
|
221
|
+
| `onComponentEvent(node, event, handler)` | обработчик пропа-события компонента |
|
|
222
|
+
| `onMount(node, fn)` / `onUnmount(node, fn)` | хуки монтирования узла |
|
|
223
|
+
| `onInit(node, fn)` | `fn` вызывается сразу, до первого рендера |
|
|
224
|
+
| `renderEffect(schema, fn)` | реактивный эффект, пока схема области смонтирована |
|
|
225
|
+
| `schema.node(sel).setHidden / patchProps` | принудительная видимость (поля не выключает) и пропсы узла |
|
|
226
|
+
| `schema.node(sel).getRef()` | ref на компонент узла — объект `{ current }` |
|
|
227
|
+
|
|
228
|
+
- Область поведения — `{ model, form, schema, flow }`: `flow` — поток шагов сборки, один на всю
|
|
229
|
+
сборку (его же получают поведения подформ и строк).
|
|
230
|
+
- **Области изолированы.** `schema.node(selector)` ищет узел только в своей области: корневое
|
|
231
|
+
поведение — в корневом дереве, поведение подформы и строки массива — в своём поддереве. До узла
|
|
232
|
+
строки или части корень дотягивается через `applyEach` / `apply`. Два монтирования одной части
|
|
233
|
+
не конфликтуют.
|
|
234
|
+
- **Исключение — ref по пути модели**: `schema.node('phones.0.number').getRef()` ищется от корня.
|
|
235
|
+
- **Операторы узлов только записывают правило — исполняет его рендерер.** При разметке в JSX они
|
|
236
|
+
ничего не делают: видимость, отправка и загрузка данных остаются в JSX.
|
|
237
|
+
- Правило на селектор, которого нет в дереве, ничего не делает; в dev — предупреждение.
|
|
238
|
+
- **Несколько поведений на один узел.** Условия `hideWhen` на один селектор объединяются по «или»:
|
|
239
|
+
узел скрыт, пока истинно любое. Обработчики событий и хуки монтирования — стеком: действует
|
|
240
|
+
последний, его снятие возвращает прежний. Снятие поведения убирает только его правила.
|
|
241
|
+
- **`setHidden` / `patchProps` поведения откатываются при его снятии** — вместе с записанным в
|
|
242
|
+
его `onDispose`. Значение, которое с тех пор переписал кто-то другой, остаётся.
|
|
243
|
+
|
|
244
|
+
## Сборка — `createForm`
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { createForm } from '@reformer/core';
|
|
248
|
+
import { useFormBundle } from '@reformer/core/react';
|
|
249
|
+
|
|
250
|
+
const credit = useFormBundle(() =>
|
|
251
|
+
createForm<CreditForm>({
|
|
252
|
+
model: createCreditModel(), // либо initial: { … }
|
|
253
|
+
schema: creditSchema, // (model) => узел; для JSON — документ + registry
|
|
254
|
+
behavior: creditBehavior,
|
|
255
|
+
validation: { extras: crossRules }, // правила вне шагов; голая схема = { rules }
|
|
256
|
+
flow: { steps: [{ id: 'loan', rules: loanRules }, { id: 'contacts', rules: contactsRules }] },
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
// credit = { model, form, render, flow, validating, submission, validate, validateStep, dispose }
|
|
260
|
+
|
|
261
|
+
<FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />; // рендерер
|
|
262
|
+
<FormField control={credit.form.loanType} />; // разметка в JSX — та же сборка
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- Порядок: модель → `seed` → дерево схемы → компиляция → уровень формы (правила, поток шагов) →
|
|
266
|
+
ноды формы → поведение → `setup`. Дерево строится и компилируется один раз. Ноды строятся по
|
|
267
|
+
виду узла модели: лист и массив-значение — `FormControl`, группа — `FormGroup`, массив
|
|
268
|
+
под-форм — `FormArray`.
|
|
269
|
+
- Повтор `selector` в одной области — исключение при сборке. В dev правило поведения на узел,
|
|
270
|
+
которого нет, и шаг `flow.steps` без узла с таким `selector` дают предупреждение.
|
|
271
|
+
- Уровень формы: `validate()` и `validateStep(id)` → `ValidationResult`, отправка — отдельный
|
|
272
|
+
объект `submission`: `submission.submit(handler)` → `SubmitResult`, `submission.submitting`;
|
|
273
|
+
`flow` — поток шагов (`next` / `previous` / `goTo`). См. `38-form-flow.md`.
|
|
274
|
+
- `dispose()` снимает сборку: поведение формы, подписки нод, живую проверку; идущие проверки
|
|
275
|
+
получают `cancelled`, значения модели остаются. Вне React владелец сборки зовёт его сам;
|
|
276
|
+
`useFormBundle` сборку при размонтировании не снимает.
|
|
277
|
+
- `render` — `{ tree, controller, node(selector) }`: скомпилированная схема (`CompiledSchema`, см.
|
|
278
|
+
`37-compiled-schema.md`) и схема-контроллер сборки.
|
|
279
|
+
- JSON: `createForm({ model, schema: document, registry })` — дерево собирает
|
|
280
|
+
`registry.resolveSchema`. Ядро документ не разбирает.
|
|
281
|
+
- Внутри рендерера бандл доступен компонентам через `useFormBundleContext()` — так визард берёт
|
|
282
|
+
форму и поток шагов сам и отправляет через `bundle.submission.submit`.
|
|
283
|
+
|
|
284
|
+
### Низкоуровневая фабрика
|
|
285
|
+
|
|
286
|
+
`createFormFromModel({ model, schema })` — форма из модели и готового дерева, без бандла.
|
|
287
|
+
|
|
288
|
+
Формы без модели нет: значения принадлежат модели. `createForm` без `model` и `initial` либо с
|
|
289
|
+
готовым деревом вместо билдера бросает ошибку с подсказкой.
|
|
290
|
+
|
|
291
|
+
## Частые ошибки
|
|
292
|
+
|
|
293
|
+
| ❌ Ошибка | Как правильно |
|
|
294
|
+
| -------------------------------------------------- | ------------------------------------------------------- |
|
|
295
|
+
| `model: model.address` (под-модель у поля) | `model: model.$.address.city` — привязка всегда с `$` |
|
|
296
|
+
| `{ model: model.$.address, component }` без `part` | группа полем не бывает — нужен `part` |
|
|
297
|
+
| `createForm({ model, schema: дерево })` | `schema` — билдер `(model) => узел` |
|
|
298
|
+
| `hideWhen(schema.node('row-узел'), …)` из корня | внутри `applyEach(model.$.items, itemBehavior)` |
|
|
299
|
+
| `push({})` для новой строки | `arrayOf(blank)` в модели и `push()` без значения |
|
|
300
|
+
| `createModel(structuredClone(INITIAL))` с `arrayOf` | фабрика начальных значений — клон теряет шаблон массива |
|
|
301
|
+
| `cross` в подформе читает корень | `apply(model.$.group, rules)` — область подформы |
|
|
302
|
+
| `{ value: model.$.x }`, `{ array: model.items }` | `{ model: model.$.x }`, `{ model: model.$.items, item }` |
|
|
303
|
+
| узлы записью `{ email: { model, component } }` | `{ children: [{ model, component }] }` |
|
|
304
|
+
| `Rule<unknown>[]` и `(form: Root)` с приведением | `Rule<ТипПоля>[]`, `cross` из аргумента схемы |
|
|
305
|
+
| `try { … } catch { return null }` в async-правиле «на всякий случай» | сбой правила блокирует сам; `catch` — только если проверка необязательна |
|
|
306
|
+
| `validation: { steps: { loan: rules } }` | `flow: { steps: [{ id: 'loan', rules }] }` |
|
|
307
|
+
| `required({ message: '…' })` | `required({ code: '<поле>.required' })` + `defineIssueMessages` |
|
|
308
|
+
| два узла с одним `selector` в одной области | разные селекторы — повтор бросает исключение |
|
|
309
|
+
| отправка `api.send(model.get())` в обход проверки | `bundle.submission.submit(api.send)` |
|