@reformer/core 11.0.0-beta.8 → 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 +91 -73
- package/dist/behaviors-value-HA_Q03hc.js +108 -0
- package/dist/behaviors.js +370 -242
- package/dist/{create-model-BBRHhHwK.js → create-model-BvcbNPgL.js} +6 -6
- 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 +28 -7
- package/dist/form/behaviors/context.d.ts +19 -2
- package/dist/form/behaviors/index.d.ts +3 -3
- package/dist/form/behaviors/internals.d.ts +9 -2
- 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 +6 -4
- package/dist/form/behaviors/types.d.ts +12 -4
- package/dist/form/create-form.d.ts +47 -35
- package/dist/form/flow/flow.d.ts +87 -0
- package/dist/form/form-bundle.d.ts +45 -28
- 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 +16 -21
- 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 +30 -14
- package/dist/form/signal-node-registry.d.ts +6 -7
- 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 +139 -63
- package/dist/form/types/validation-schema.d.ts +23 -27
- package/dist/form/validation/context.d.ts +42 -7
- package/dist/form/validation/index.d.ts +9 -8
- package/dist/form/validation/operators.d.ts +22 -30
- 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 -20
- 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 +924 -2855
- 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 +11 -5
- package/dist/model/model-nodes.d.ts +79 -12
- package/dist/model/model-signals-proxy.d.ts +5 -6
- package/dist/model/model-value-proxy.d.ts +20 -15
- package/dist/model/safe-effect.d.ts +33 -1
- package/dist/model/types.d.ts +63 -13
- package/dist/model.js +20 -18
- 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 +2 -2
- 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 +6 -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 -9
- 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 +129 -81
- package/docs/llms/10-arrays.md +106 -75
- 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 +51 -27
- 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 +39 -29
- 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 +177 -65
- package/docs/llms/34-i18n.md +69 -43
- package/docs/llms/35-unified-contract.md +100 -39
- 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 +6084 -3420
- package/package.json +23 -4
- package/dist/behaviors-value-C7dSEhCc.js +0 -134
- package/dist/date-utils-dTT_x_Be.js +0 -31
- package/dist/form/create-core-form.d.ts +0 -72
- 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 -126
- 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-value-proxy-caTmAiUd.js +0 -419
- package/dist/node-BeV_lV5i.js +0 -100
- package/dist/strategy-DgzELWno.js +0 -204
- package/dist/validation-error-B-U3RsV0.js +0 -11
|
@@ -4,29 +4,52 @@
|
|
|
4
4
|
|
|
5
5
|
Одна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки
|
|
6
6
|
`markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над
|
|
7
|
-
функциональной схемой (`@reformer/core/validation`): переиспользует
|
|
8
|
-
ЕДИНСТВЕННЫЙ движок (
|
|
9
|
-
второго движка не заводит.
|
|
10
|
-
|
|
7
|
+
функциональной схемой (`@reformer/core/validation`): переиспользует раннер схемы как
|
|
8
|
+
ЕДИНСТВЕННЫЙ движок (разнос ошибок по нодам, отмена устаревших прогонов, дедуп по `(model, schema)`),
|
|
9
|
+
второго движка не заводит. Других триггеров валидации нет: у ноды поля собственных валидаторов,
|
|
10
|
+
`updateOn` и `debounce` не бывает — когда запускать проверку, решает только стратегия.
|
|
11
11
|
|
|
12
12
|
Стратегия определяет только момент запуска и раскрытие ошибок — сами правила остаются в отдельной
|
|
13
|
-
`
|
|
13
|
+
`defineValidation<T>(({ model }) => …)`.
|
|
14
14
|
|
|
15
15
|
## API
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Три точки входа к одному контроллеру:
|
|
18
|
+
|
|
19
|
+
- **сборка `createForm`** — `validation: { rules?, extras?, strategy, debounce, liveAfterSubmit }`.
|
|
20
|
+
Контроллер собирает уровень формы, армирует `useFormBundle` (в эффекте, не при SSR);
|
|
21
|
+
`bundle.validate()` и `bundle.submission.submit(handler)` — полный прогон с раскрытием замечаний.
|
|
22
|
+
Это путь по умолчанию для формы со сборкой;
|
|
23
|
+
- **React-хук `useFormValidation`** из `@reformer/core/react` — для формы без сборки
|
|
24
|
+
(`createFormFromModel` + своя схема);
|
|
25
|
+
- **headless-фабрика `createFormValidation`** из `@reformer/core/validation` — для SSR / тестов /
|
|
26
|
+
не-React потребителей.
|
|
20
27
|
|
|
21
28
|
```typescript
|
|
22
|
-
//
|
|
29
|
+
// Сборка (@reformer/core)
|
|
30
|
+
createForm<T>({
|
|
31
|
+
…,
|
|
32
|
+
validation: {
|
|
33
|
+
rules?: ValidationDefinition<T>; // полный набор; без него — правила шагов flow + extras
|
|
34
|
+
extras?: ValidationDefinition<T>;
|
|
35
|
+
strategy?: ValidationStrategyKind; // default 'submit'
|
|
36
|
+
debounce?: number;
|
|
37
|
+
liveAfterSubmit?: 'change' | 'blur';
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// React (@reformer/core/react)
|
|
23
42
|
function useFormValidation<T>(args: {
|
|
24
43
|
model: FormModel<T>;
|
|
25
|
-
schema:
|
|
44
|
+
schema: ValidationDefinition<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)
|
|
26
45
|
strategy?: ValidationStrategyKind; // default 'submit'
|
|
27
46
|
debounce?: number; // мс, для live-фаз (change / live-часть afterFirstSubmit)
|
|
28
47
|
liveAfterSubmit?: 'change' | 'blur'; // default 'change'
|
|
29
|
-
}): {
|
|
48
|
+
}): {
|
|
49
|
+
submit: () => Promise<boolean>; // попытка отправки: при отказе раскрыта вся форма, afterFirstSubmit → живая фаза
|
|
50
|
+
validate: () => Promise<boolean>; // проверка без отправки: раскрыты проверенные поля, фаза не меняется
|
|
51
|
+
isValidating: boolean;
|
|
52
|
+
};
|
|
30
53
|
|
|
31
54
|
// headless (@reformer/core/validation)
|
|
32
55
|
type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';
|
|
@@ -35,51 +58,101 @@ interface ValidationStrategyOptions {
|
|
|
35
58
|
debounce?: number;
|
|
36
59
|
liveAfterSubmit?: 'change' | 'blur';
|
|
37
60
|
}
|
|
61
|
+
interface FormValidationRunOptions {
|
|
62
|
+
touch?: true | 'all'; // true (default) — проверенные поля; 'all' — при отказе ещё и вся форма
|
|
63
|
+
submit?: boolean; // прогон — попытка отправки: afterFirstSubmit → живая фаза
|
|
64
|
+
}
|
|
38
65
|
interface FormValidationController {
|
|
39
|
-
validate(): Promise<boolean>; //
|
|
66
|
+
validate(): Promise<boolean>; // нейтральная проверка: touch:true, фазу afterFirstSubmit не трогает; true — только valid
|
|
67
|
+
run(options?: FormValidationRunOptions): Promise<ValidationResult>; // valid | invalid | error | cancelled
|
|
40
68
|
start(): () => void; // армит реактивные подписки (ТОЛЬКО на клиенте) → dispose
|
|
41
|
-
dispose(): void;
|
|
69
|
+
dispose(): void; // снять подписки и таймеры, отменить идущий прогон (он получит cancelled)
|
|
42
70
|
readonly isValidating: boolean;
|
|
43
71
|
readonly validating: ReadonlySignal<boolean>;
|
|
44
72
|
}
|
|
45
73
|
function createFormValidation<T>(
|
|
46
74
|
model: FormModel<T>,
|
|
47
|
-
schema:
|
|
75
|
+
schema: ValidationDefinition<T>,
|
|
48
76
|
options?: ValidationStrategyOptions,
|
|
49
77
|
): FormValidationController;
|
|
50
78
|
```
|
|
51
79
|
|
|
52
|
-
- `useFormValidation` мемоизирует контроллер по
|
|
53
|
-
|
|
80
|
+
- `useFormValidation` мемоизирует контроллер по
|
|
81
|
+
`[model, schema, strategy, debounce, liveAfterSubmit]` и армит стратегию в `useEffect`
|
|
82
|
+
(SSR-safe). `submit()` = `controller.run({ touch: 'all', submit: true })`, `validate()` =
|
|
83
|
+
`controller.validate()`; обе — стабильные ссылки, отвечают `true` только при `valid`.
|
|
54
84
|
- `createFormValidation` ЧИСТА до `start()` (никаких подписок) → SSR-safe; `start()` возвращает
|
|
55
85
|
`dispose` и арминует триггеры только на клиенте.
|
|
56
86
|
|
|
87
|
+
### Живая фаза `afterFirstSubmit` — одно правило
|
|
88
|
+
|
|
89
|
+
Живую фазу включает **только попытка отправки**:
|
|
90
|
+
|
|
91
|
+
| Вызов | Попытка отправки | Касание при отказе |
|
|
92
|
+
| --------------------------------------------------------------- | ---------------- | ------------------ |
|
|
93
|
+
| `bundle.submission.submit(handler)` | да | вся форма |
|
|
94
|
+
| `submit()` хука `useFormValidation` | да | вся форма |
|
|
95
|
+
| `controller.run({ touch: 'all', submit: true })` | да | вся форма |
|
|
96
|
+
| `bundle.validate()`, `validate()` хука, `controller.validate()` | нет | проверенные поля |
|
|
97
|
+
| `bundle.validateStep(id)`, `flow.next()` | нет | поля шага |
|
|
98
|
+
|
|
99
|
+
Кнопка «Проверить» — `validate()`: показывает замечания, но форма остаётся в тихой фазе. Отменённая
|
|
100
|
+
отправка (`cancelled`) форму тронутой не помечает.
|
|
101
|
+
|
|
57
102
|
### Стратегии
|
|
58
103
|
|
|
59
|
-
| strategy
|
|
60
|
-
|
|
|
61
|
-
| `submit` (default) | только `validate()` / `submit()` | `true`
|
|
62
|
-
| `blur`
|
|
63
|
-
| `change`
|
|
64
|
-
| `afterFirstSubmit` | тихо до
|
|
104
|
+
| strategy | когда прогон | touch | раскрытие замечаний |
|
|
105
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------- |
|
|
106
|
+
| `submit` (default) | только `validate()` / `submit()` (в сборке — `bundle.validate()` / `bundle.submission.submit()`) | `true` | всё на submit |
|
|
107
|
+
| `blur` | каждый уход из поля (потеря фокуса), и повторный тоже | `false` | только сблюренные (по `touched`) |
|
|
108
|
+
| `change` | на каждый ввод (+ `debounce`) | `false` | только редактированные (по `dirty`) |
|
|
109
|
+
| `afterFirstSubmit` | тихо до первой попытки отправки, затем live (`liveAfterSubmit: 'change'` \| `'blur'`, default `'change'`); `validate()` фазу не переключает | при отправке — вся форма (при отказе), дальше live — `false` | тихо → раскрыть всё на отправке → далее live |
|
|
65
110
|
|
|
66
111
|
## Examples
|
|
67
112
|
|
|
68
|
-
###
|
|
113
|
+
### Сборка — стратегия полем `validation`
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { createForm } from '@reformer/core';
|
|
117
|
+
import { useFormBundle } from '@reformer/core/react';
|
|
118
|
+
|
|
119
|
+
const createRegistration = () =>
|
|
120
|
+
createForm<RegistrationData>({
|
|
121
|
+
initial: { username: '', email: '', password: '' },
|
|
122
|
+
schema: registrationSchema,
|
|
123
|
+
validation: { rules: registrationValidation, strategy: 'afterFirstSubmit', debounce: 400 },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function RegistrationForm() {
|
|
127
|
+
const registration = useFormBundle(createRegistration); // армирует живую стратегию в эффекте
|
|
128
|
+
const onSubmit = async (event: React.FormEvent) => {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
// первая отправка раскрывает все замечания и переводит форму в живую фазу;
|
|
131
|
+
// registration.validate() фазу не переключает
|
|
132
|
+
await registration.submission.submit(api.save);
|
|
133
|
+
};
|
|
134
|
+
/* … */
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Без сборки — useFormValidation, afterFirstSubmit + debounce
|
|
69
139
|
|
|
70
|
-
Тихо до первой отправки, `submit()` раскрывает все
|
|
71
|
-
(дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` —
|
|
72
|
-
`markAsTouched` не нужен.
|
|
140
|
+
Тихо до первой отправки, `submit()` раскрывает все замечания и переводит форму в живую фазу
|
|
141
|
+
(дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` (при отказе — всю
|
|
142
|
+
форму) — ручной `markAsTouched` не нужен. `validate()` — кнопка «Проверить»: раскрывает замечания
|
|
143
|
+
проверенных полей, а форма остаётся в тихой фазе.
|
|
73
144
|
|
|
74
145
|
```tsx
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
146
|
+
import { useMemo } from 'react';
|
|
147
|
+
import { createModel, createFormFromModel } from '@reformer/core';
|
|
148
|
+
import { useFormValidation } from '@reformer/core/react';
|
|
149
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
77
150
|
import { required, email, minLength } from '@reformer/core/validators';
|
|
78
151
|
|
|
79
152
|
type RegistrationData = { username: string; email: string; password: string };
|
|
80
153
|
|
|
81
154
|
// Схема — стабильный module-level const (иначе ломается дедуп раннера).
|
|
82
|
-
const registrationValidation =
|
|
155
|
+
const registrationValidation = defineValidation<RegistrationData>(({ model }) => {
|
|
83
156
|
validate(model.$.username, [required(), minLength(3)]);
|
|
84
157
|
validate(model.$.email, [required(), email()]);
|
|
85
158
|
validate(model.$.password, [required(), minLength(8)]);
|
|
@@ -89,15 +162,15 @@ function RegistrationForm() {
|
|
|
89
162
|
const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
|
|
90
163
|
const form = useMemo(() => createFormFromModel({ model }), [model]);
|
|
91
164
|
|
|
92
|
-
const { submit, isValidating } = useFormValidation({
|
|
165
|
+
const { submit, validate, isValidating } = useFormValidation({
|
|
93
166
|
model,
|
|
94
167
|
schema: registrationValidation,
|
|
95
168
|
strategy: 'afterFirstSubmit',
|
|
96
169
|
debounce: 400,
|
|
97
170
|
});
|
|
98
171
|
|
|
99
|
-
const onSubmit = async (
|
|
100
|
-
|
|
172
|
+
const onSubmit = async (event: React.FormEvent) => {
|
|
173
|
+
event.preventDefault();
|
|
101
174
|
if (await submit()) await api.save(model.get()); // true = нет блокирующих ошибок
|
|
102
175
|
};
|
|
103
176
|
|
|
@@ -106,49 +179,69 @@ function RegistrationForm() {
|
|
|
106
179
|
<FormField control={form.username} />
|
|
107
180
|
<FormField control={form.email} />
|
|
108
181
|
<FormField control={form.password} />
|
|
109
|
-
<button type="
|
|
182
|
+
<button type="button" onClick={() => void validate()}>
|
|
183
|
+
Проверить
|
|
184
|
+
</button>
|
|
185
|
+
<button type="submit" disabled={isValidating}>
|
|
186
|
+
Отправить
|
|
187
|
+
</button>
|
|
110
188
|
</form>
|
|
111
189
|
);
|
|
112
190
|
}
|
|
113
191
|
```
|
|
114
192
|
|
|
115
|
-
###
|
|
193
|
+
### Визард — стратегия формы и проверка шага потоком
|
|
116
194
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
195
|
+
Живая стратегия — свойство формы, а не шага: она задаётся полем `validation` сборки и армируется
|
|
196
|
+
`useFormBundle`. Проверку шага делает поток: «Далее» визарда — `flow.next()`, он прогоняет правила
|
|
197
|
+
текущего шага и не пускает дальше при замечаниях. Отправка — `bundle.submission.submit` (все
|
|
198
|
+
шаги + `extras`). Живую фазу `afterFirstSubmit` включает только она: `flow.next()` и
|
|
199
|
+
`validateStep` фазу не переключают.
|
|
121
200
|
|
|
122
201
|
```tsx
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
const createCredit = () =>
|
|
203
|
+
createForm<CreditForm>({
|
|
204
|
+
model: createCreditModel(),
|
|
205
|
+
schema: creditSchema,
|
|
206
|
+
validation: { extras: crossFieldRules, strategy: 'blur' }, // живая проверка при потере фокуса
|
|
207
|
+
flow: {
|
|
208
|
+
steps: [
|
|
209
|
+
{ id: 'loan', rules: loanStep },
|
|
210
|
+
{ id: 'applicant', rules: applicantStep },
|
|
211
|
+
{ id: 'confirm' }, // без правил
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
});
|
|
130
215
|
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
return
|
|
216
|
+
function CreditWizard() {
|
|
217
|
+
const credit = useFormBundle(createCredit);
|
|
218
|
+
return (
|
|
219
|
+
<FormWizard
|
|
220
|
+
form={credit.form}
|
|
221
|
+
flow={credit.flow}
|
|
222
|
+
steps={STEPS}
|
|
223
|
+
onSubmit={() => void credit.submission.submit(api.save)}
|
|
224
|
+
/>
|
|
225
|
+
);
|
|
134
226
|
}
|
|
135
|
-
|
|
136
|
-
<FormWizard form={form} config={config}>
|
|
137
|
-
<LoanStepBody />
|
|
138
|
-
</FormWizard>;
|
|
139
227
|
```
|
|
140
228
|
|
|
229
|
+
Живая фаза прогоняет **полный** набор правил (все шаги + `extras`), а показ замечаний гейтится
|
|
230
|
+
`touched` / `dirty` полей: поля следующих шагов пользователь ещё не трогал — их замечания не видны.
|
|
231
|
+
Прежний per-step механизм (`defineSteps(…, { strategy })` + `useWizardStepValidation`) удалён.
|
|
232
|
+
|
|
141
233
|
### Headless / SSR / тесты — createFormValidation
|
|
142
234
|
|
|
143
235
|
```typescript
|
|
144
236
|
import { createFormValidation } from '@reformer/core/validation';
|
|
145
237
|
|
|
146
|
-
const
|
|
238
|
+
const controller = createFormValidation(model, registrationValidation, {
|
|
147
239
|
strategy: 'afterFirstSubmit',
|
|
148
240
|
debounce: 300,
|
|
149
241
|
});
|
|
150
|
-
const dispose =
|
|
151
|
-
const
|
|
242
|
+
const dispose = controller.start(); // ТОЛЬКО на клиенте (в React — из useEffect)
|
|
243
|
+
const checked = await controller.validate(); // проверка: замечания проверенных полей, фаза тихая
|
|
244
|
+
const sent = await controller.run({ touch: 'all', submit: true }); // попытка отправки → живая фаза
|
|
152
245
|
dispose();
|
|
153
246
|
```
|
|
154
247
|
|
|
@@ -157,32 +250,50 @@ dispose();
|
|
|
157
250
|
```typescript
|
|
158
251
|
// ❌ schema пересоздаётся каждый рендер → контроллер и дедуп раннера ломаются
|
|
159
252
|
function MyForm() {
|
|
160
|
-
const schema =
|
|
253
|
+
const schema = defineValidation<Form>(({ model }) => { … }); // новая ссылка на каждый рендер
|
|
161
254
|
useFormValidation({ model, schema, strategy: 'change' });
|
|
162
255
|
}
|
|
163
256
|
|
|
164
257
|
// ✅ Стабильная ссылка: module-level const (или useMemo с пустыми deps)
|
|
165
|
-
const formValidation =
|
|
258
|
+
const formValidation = defineValidation<Form>(({ model }) => { … });
|
|
166
259
|
function MyForm() {
|
|
167
260
|
useFormValidation({ model, schema: formValidation, strategy: 'change' });
|
|
168
261
|
}
|
|
169
262
|
```
|
|
170
263
|
|
|
171
264
|
```typescript
|
|
172
|
-
// ❌
|
|
173
|
-
//
|
|
265
|
+
// ❌ updateOn / debounce / validators в узле схемы — таких ключей у узла нет, сборка их не читает
|
|
266
|
+
// { model: model.$.email, component: Input, updateOn: 'blur', validators: [required()] }
|
|
174
267
|
|
|
175
|
-
// ✅
|
|
268
|
+
// ✅ Момент запуска — стратегия формы; правила — схема валидации
|
|
269
|
+
// createForm({ …, validation: { rules: formValidation, strategy: 'blur', debounce: 300 } })
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// ❌ Своя стратегия поверх сборки: второй контроллер на ту же модель и правила
|
|
274
|
+
const bundle = useFormBundle(() => createForm({ …, validation: rules }));
|
|
275
|
+
useFormValidation({ model: bundle.model, schema: rules, strategy: 'change' });
|
|
276
|
+
|
|
277
|
+
// ✅ Стратегия — полем validation сборки; армирует useFormBundle
|
|
278
|
+
const bundle = useFormBundle(() => createForm({ …, validation: { rules, strategy: 'change' } }));
|
|
176
279
|
```
|
|
177
280
|
|
|
178
281
|
```typescript
|
|
179
282
|
// ❌ start() на сервере — арминует реактивные подписки при SSR
|
|
180
|
-
const
|
|
181
|
-
|
|
283
|
+
const controller = createFormValidation(model, schema, { strategy: 'change' });
|
|
284
|
+
controller.start(); // на сервере подписок быть не должно
|
|
182
285
|
|
|
183
286
|
// ✅ Фабрика чиста до start(); армить только на клиенте (useFormValidation делает это в useEffect)
|
|
184
|
-
const
|
|
185
|
-
if (typeof window !== 'undefined')
|
|
287
|
+
const controller = createFormValidation(model, schema, { strategy: 'change' });
|
|
288
|
+
if (typeof window !== 'undefined') controller.start();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// ❌ afterFirstSubmit: ждать живой фазы после проверки — validate() фазу не переключает
|
|
293
|
+
await bundle.validate(); // форма в тихой фазе, ввод не перепроверяется
|
|
294
|
+
|
|
295
|
+
// ✅ Живую фазу включает попытка отправки
|
|
296
|
+
await bundle.submission.submit(api.save); // без сборки — submit() хука useFormValidation
|
|
186
297
|
```
|
|
187
298
|
|
|
188
299
|
## See also
|
|
@@ -190,4 +301,5 @@ if (typeof window !== 'undefined') ctrl.start();
|
|
|
190
301
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — submit-флоу и `validateModel` (движок под стратегией)
|
|
191
302
|
- [27-revalidate-when.md](./27-revalidate-when.md) — ручные триггеры перевалидации (низкоуровневый примитив)
|
|
192
303
|
- [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
|
|
193
|
-
- [13-multi-step.md](./13-multi-step.md) —
|
|
304
|
+
- [13-multi-step.md](./13-multi-step.md) — многошаговая форма: поток `flow`, `bundle.validateStep(id)`
|
|
305
|
+
- [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `submission`, поток шагов
|
package/docs/llms/34-i18n.md
CHANGED
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer
|
|
6
|
-
локали, загрузка словарей,
|
|
7
|
-
|
|
5
|
+
Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*` без React:
|
|
6
|
+
объект локали, загрузка словарей, перевод и форматирование, тексты замечаний валидации. React-часть
|
|
7
|
+
— провайдер `I18nProvider` и хуки `useI18n`, `useMessages`, `useIssueMessage` — отдаёт
|
|
8
|
+
`@reformer/core/react`. Язык задаётся провайдером, подгружается динамически и переключается на лету;
|
|
9
|
+
узлы формы при этом не пересоздаются.
|
|
8
10
|
|
|
9
11
|
Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
|
|
10
12
|
|
|
11
|
-
> Импорт
|
|
12
|
-
>
|
|
13
|
+
> Импорт локали и перевода — из `@reformer/core/i18n`, провайдера и хуков — из
|
|
14
|
+
> `@reformer/core/react`. Из главной бочки `@reformer/core` ни то, ни другое не реэкспортируется:
|
|
15
|
+
> React-контекст локализации обязан существовать в одном экземпляре.
|
|
13
16
|
|
|
14
17
|
## Локаль — это данные
|
|
15
18
|
|
|
16
19
|
```typescript
|
|
17
20
|
interface FormLocale {
|
|
18
|
-
code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural
|
|
21
|
+
code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural; не BCP 47 — по en
|
|
19
22
|
messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
|
|
20
23
|
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
21
24
|
dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
|
|
@@ -93,7 +96,7 @@ export const loadLocale = createLocaleLoader([
|
|
|
93
96
|
## Провайдер
|
|
94
97
|
|
|
95
98
|
```tsx
|
|
96
|
-
import { I18nProvider } from '@reformer/core/
|
|
99
|
+
import { I18nProvider } from '@reformer/core/react';
|
|
97
100
|
|
|
98
101
|
function App() {
|
|
99
102
|
const [lang, setLang] = useState('ru');
|
|
@@ -108,7 +111,7 @@ function App() {
|
|
|
108
111
|
|
|
109
112
|
| Проп | Назначение |
|
|
110
113
|
| --------------- | ------------------------------------------------------------------------ |
|
|
111
|
-
| `lang` + `load` | провайдер грузит локаль
|
|
114
|
+
| `lang` + `load` | провайдер грузит локаль сам; `load` — стабильная ссылка (константа модуля) |
|
|
112
115
|
| `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
|
|
113
116
|
| `onError` | отказ загрузки `(error, lang)` |
|
|
114
117
|
| `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
|
|
@@ -118,7 +121,12 @@ function App() {
|
|
|
118
121
|
- при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
|
|
119
122
|
`useI18n().pending` — для индикатора;
|
|
120
123
|
- при быстром переключении применяется только последний запрос;
|
|
121
|
-
- ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`;
|
|
124
|
+
- ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`; повторить запрос
|
|
125
|
+
того же языка — `useI18n().retry()`;
|
|
126
|
+
- упала самая первая загрузка — дети показываются на встроенном английском (а не `fallback`),
|
|
127
|
+
ошибка — в `useI18n().error`;
|
|
128
|
+
- новая ссылка `load` — новое хранилище: язык запрашивается у нового загрузчика, на экране тем
|
|
129
|
+
временем прежняя локаль;
|
|
122
130
|
- если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
|
|
123
131
|
синхронный и `fallback` не показывается.
|
|
124
132
|
|
|
@@ -130,7 +138,9 @@ SSR: эффекты на сервере не выполняются, поэто
|
|
|
130
138
|
## Хуки
|
|
131
139
|
|
|
132
140
|
```typescript
|
|
133
|
-
|
|
141
|
+
import { useI18n, useMessages, useIssueMessage } from '@reformer/core/react';
|
|
142
|
+
|
|
143
|
+
const { code, locale, pending, error, retry, t, has, number, date, fileSize } = useI18n();
|
|
134
144
|
|
|
135
145
|
t('profile.title'); // ключ приложения
|
|
136
146
|
t('cart.items', { count: 3 }); // с подстановками
|
|
@@ -152,31 +162,40 @@ function ClearButton() {
|
|
|
152
162
|
}
|
|
153
163
|
```
|
|
154
164
|
|
|
155
|
-
## Тексты
|
|
165
|
+
## Тексты замечаний валидации
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
|
|
167
|
+
Замечание валидации — `ValidationIssue = { code, params?, severity? }`: текста у него нет. Правила из
|
|
168
|
+
`@reformer/core/validators` несут только `code` и `params`, свой код задаёт опция `code`. Строку
|
|
169
|
+
даёт резолвер при отображении:
|
|
159
170
|
|
|
160
|
-
1. `
|
|
161
|
-
2.
|
|
162
|
-
3. `validation.<code>` из
|
|
163
|
-
4.
|
|
164
|
-
5. сам код — для своих кодов, которых нет ни в одном словаре.
|
|
171
|
+
1. словарь формы (`defineIssueMessages`) — запись по коду замечания;
|
|
172
|
+
2. `validation.<code>` из словаря локали;
|
|
173
|
+
3. `validation.<code>` из встроенной английской таблицы ядра;
|
|
174
|
+
4. сам код — для своих кодов, которых нет ни в одном словаре.
|
|
165
175
|
|
|
166
176
|
```typescript
|
|
177
|
+
import { defineIssueMessages } from '@reformer/core/i18n';
|
|
178
|
+
|
|
167
179
|
validate(model.$.email, [
|
|
168
180
|
required(), // «Обязательное поле» в ru, английская фраза без провайдера
|
|
169
|
-
minLength(5, {
|
|
170
|
-
email({ messageKey: 'profile.email.invalid' }), // ключ словаря приложения: переключается вместе с языком
|
|
181
|
+
minLength(5, { code: 'email.minLength' }), // свой код: текст поля — в словаре формы
|
|
171
182
|
]);
|
|
183
|
+
|
|
184
|
+
// Словарь формы: шаблон с параметрами либо функция (числа с разрядами — функцией)
|
|
185
|
+
export const formMessages = defineIssueMessages({
|
|
186
|
+
'email.minLength': 'Не меньше {minLength} символов',
|
|
187
|
+
'loanAmount.min': (params, i18n) => `Минимум ${i18n.number(Number(params.min))} ₽`,
|
|
188
|
+
});
|
|
172
189
|
```
|
|
173
190
|
|
|
174
191
|
- Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
|
|
175
192
|
ошибки без повторной валидации.
|
|
176
|
-
- `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
`
|
|
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
|
+
приложения — второй вариант переключается вместе с языком.
|
|
180
199
|
- Встроенный текст меняется тем же ключом в словаре приложения:
|
|
181
200
|
`extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
|
|
182
201
|
- Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
|
|
@@ -184,15 +203,17 @@ validate(model.$.email, [
|
|
|
184
203
|
строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
|
|
185
204
|
|
|
186
205
|
```tsx
|
|
187
|
-
function
|
|
188
|
-
const message =
|
|
189
|
-
const {
|
|
190
|
-
return
|
|
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;
|
|
191
210
|
}
|
|
192
211
|
```
|
|
193
212
|
|
|
194
|
-
|
|
195
|
-
|
|
213
|
+
`ValidationMessagesProvider` из `@reformer/cdk` с `messages` накладывает словарь формы поверх
|
|
214
|
+
локали (вложенные провайдеры накладываются друг на друга); с `resolver` — заменяет резолвер
|
|
215
|
+
целиком, и локаль для замечаний его поддерева не участвует. Подробно — коды, встроенные параметры,
|
|
216
|
+
шаблоны и функции словаря — [36-issue-messages.md](./36-issue-messages.md).
|
|
196
217
|
|
|
197
218
|
## Что показывается, когда ключа нет
|
|
198
219
|
|
|
@@ -201,8 +222,10 @@ function FieldErrors({ control }: { control: FieldNode<string> }) {
|
|
|
201
222
|
| пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
|
|
202
223
|
| приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
|
|
203
224
|
|
|
204
|
-
Пропущенная подстановка выводится как `{name}
|
|
205
|
-
|
|
225
|
+
Пропущенная подстановка выводится как `{name}`, значение, которое нельзя привести к строке
|
|
226
|
+
(`Object.create(null)`), — пустой строкой. Код языка не BCP 47 (пришёл из адресной строки) — числа,
|
|
227
|
+
даты и формы слов форматируются по `en`, в dev — предупреждение. Форма не падает ни в одном из
|
|
228
|
+
случаев: неполный словарь, пришедший по сети, — законный сценарий.
|
|
206
229
|
|
|
207
230
|
## Проверка словаря в тестах
|
|
208
231
|
|
|
@@ -210,7 +233,8 @@ function FieldErrors({ control }: { control: FieldNode<string> }) {
|
|
|
210
233
|
import { validateLocale } from '@reformer/core/i18n';
|
|
211
234
|
|
|
212
235
|
expect(validateLocale(ruJson, enJson)).toEqual([]);
|
|
213
|
-
// находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка)
|
|
236
|
+
// находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка);
|
|
237
|
+
// битое сообщение образца — тоже syntax («в образце: …»), проверка остальных ключей продолжается
|
|
214
238
|
```
|
|
215
239
|
|
|
216
240
|
## Описатели сообщений (низкий уровень)
|
|
@@ -225,15 +249,14 @@ expect(validateLocale(ruJson, enJson)).toEqual([]);
|
|
|
225
249
|
## Anti-patterns
|
|
226
250
|
|
|
227
251
|
```typescript
|
|
228
|
-
// ❌ Импорт из главной бочки —
|
|
252
|
+
// ❌ Импорт из главной бочки — React-части локализации там нет
|
|
229
253
|
import { I18nProvider } from '@reformer/core';
|
|
230
254
|
|
|
231
|
-
// ✅
|
|
232
|
-
import { I18nProvider } from '@reformer/core/i18n';
|
|
255
|
+
// ✅ Провайдер и хуки — из @reformer/core/react, локаль и перевод — из @reformer/core/i18n
|
|
233
256
|
```
|
|
234
257
|
|
|
235
258
|
```tsx
|
|
236
|
-
// ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль
|
|
259
|
+
// ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль запрашивается заново на каждый рендер
|
|
237
260
|
<I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
|
|
238
261
|
|
|
239
262
|
// ✅ Загрузчик — константа модуля: один кэш на приложение
|
|
@@ -250,12 +273,14 @@ t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) }
|
|
|
250
273
|
```
|
|
251
274
|
|
|
252
275
|
```tsx
|
|
253
|
-
// ❌
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// ✅
|
|
257
|
-
|
|
258
|
-
|
|
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>;
|
|
259
284
|
```
|
|
260
285
|
|
|
261
286
|
```typescript
|
|
@@ -268,5 +293,6 @@ const source = () => ({ weekStartsOn: 1, messages: {} });
|
|
|
268
293
|
|
|
269
294
|
## See also
|
|
270
295
|
|
|
296
|
+
- [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний: коды, словарь формы, резолвер
|
|
271
297
|
- [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
|
|
272
298
|
- [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация
|