@reformer/core 11.0.0-beta.1 → 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 -24
- 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 -85
- 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 +25 -17
- 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 +17 -5
- package/dist/index.js +942 -2601
- 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 +58 -36
- 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 +1 -2
- 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 -1
- package/dist/validators.js +86 -108
- package/docs/llms/01-api-reference.md +60 -34
- package/docs/llms/02-quick-start.md +112 -74
- package/docs/llms/03-api-signatures.md +369 -148
- 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 +132 -84
- package/docs/llms/10-arrays.md +141 -61
- 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 +224 -70
- package/docs/llms/16-ui-components.md +86 -46
- package/docs/llms/17-nonexistent-api.md +217 -32
- package/docs/llms/18-conditional-fields.md +71 -32
- package/docs/llms/19-reading-values.md +45 -4
- package/docs/llms/20-compute-vs-watch.md +25 -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 +278 -128
- package/docs/llms/29-async-preload.md +41 -31
- 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 +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 +10690 -3089
- package/package.json +48 -9
- 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 -121
- package/dist/form/nodes/array-node.d.ts +0 -276
- package/dist/form/nodes/field-node.d.ts +0 -294
- 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/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/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 -11
- 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
|
@@ -2,91 +2,120 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Канонический submit-флоу
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(
|
|
11
|
-
`model.
|
|
5
|
+
Канонический submit-флоу — один вызов отправки сборки: `bundle.submission.submit(handler)`.
|
|
6
|
+
Отправка — отдельный объект сборки (`FormSubmission`): она прогоняет все правила один раз,
|
|
7
|
+
разносит замечания по нодам и зовёт обработчик со значениями модели (`model.get()`) только при
|
|
8
|
+
статусе `valid`. Результат — объект `SubmitResult`, по которому отличают отправку (`submitted`),
|
|
9
|
+
замечания в форме (`invalid`), сбой проверки (`error`), отмену (`cancelled`) и исключение
|
|
10
|
+
обработчика (`failed`). После успеха — `model.reset()` (к initial-снимку) либо
|
|
11
|
+
`model.captureInitial()` (сохранённые значения — новая точка отсчёта).
|
|
12
|
+
|
|
13
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`, схема через `defineValidation`): она
|
|
14
|
+
НЕ входит в схему формы и уходит в сборку полем `validation` (шаги — `flow`). Без сборки тот же
|
|
15
|
+
результат даёт раннер `validateModel(model, schema)`.
|
|
12
16
|
|
|
13
17
|
## API
|
|
14
18
|
|
|
15
19
|
```typescript
|
|
20
|
+
// Уровень формы — сборка createForm (см. 38-form-flow.md); отправка — объект bundle.submission
|
|
21
|
+
bundle.submission.submit<R>(handler: (values: T) => R | Promise<R>): Promise<SubmitResult<R>>;
|
|
22
|
+
type SubmitResult<R> =
|
|
23
|
+
| { status: 'submitted'; result: R } // обработчик отработал
|
|
24
|
+
| { status: 'invalid' | 'error' | 'cancelled'; validation: ValidationResult } // не вызывался
|
|
25
|
+
| { status: 'failed'; error: unknown }; // обработчик бросил исключение
|
|
26
|
+
bundle.submission.submitting: ReadonlySignal<boolean>; // работает обработчик
|
|
27
|
+
bundle.validate(): Promise<ValidationResult>; // проверить всю форму без отправки
|
|
28
|
+
bundle.validating: ReadonlySignal<boolean>; // идёт проверка
|
|
29
|
+
bundle.dispose(): void; // снять сборку: поведение, подписки нод, живую проверку; модель остаётся
|
|
30
|
+
|
|
31
|
+
// Отправка без сборки (визард в разметке без createForm) — без проверки
|
|
32
|
+
createSubmission<T>({ check: undefined, values: () => T }): FormSubmission<T>;
|
|
33
|
+
|
|
16
34
|
// Модель (источник истины значений):
|
|
17
35
|
interface ModelApi<T> {
|
|
18
|
-
get(): T; // снимок значений —
|
|
19
|
-
set(value: T): void; // полная установка (все ключи T)
|
|
20
|
-
patch(value:
|
|
21
|
-
|
|
22
|
-
|
|
36
|
+
get(): T; // снимок значений — его получает обработчик submit
|
|
37
|
+
set(value: T): void; // полная установка (все ключи T); при нехватке ключей — dev-предупреждение
|
|
38
|
+
patch(value: DeepPartial<T>): void; // частичное слияние: вложенные группы тоже частично, массив — целиком;
|
|
39
|
+
// ключ со значением undefined не трогается
|
|
40
|
+
isDirty(): boolean; // отличаются ли значения от initial-снимка (цели compute не в счёт)
|
|
41
|
+
reset(): void; // вернуть к initial-снимку (цели compute не пишет — их пересчитает поведение)
|
|
23
42
|
captureInitial(): void; // зафиксировать текущие как новый initial
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
//
|
|
27
|
-
// отменяет устаревшие прогоны той же (model, schema)
|
|
28
|
-
validateModel<T>(model: FormModel<T>, schema:
|
|
29
|
-
// true =
|
|
30
|
-
// false =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
form.
|
|
40
|
-
form.clearErrors(); // снять ошибки со всех нод
|
|
41
|
-
form.<field>.setErrors([{ code, message }]); // серверная/бизнес-ошибка в конкретное поле
|
|
42
|
-
form.<field>.clearErrors();
|
|
45
|
+
// Раннер без сборки — headless: находит ноды по сигналам модели, разносит по ним замечания,
|
|
46
|
+
// отменяет устаревшие прогоны той же (model, schema):
|
|
47
|
+
validateModel<T>(model: FormModel<T>, schema: ValidationDefinition<T>, options?: { touch?: boolean }): Promise<boolean>
|
|
48
|
+
// true = статус 'valid': блокирующих замечаний нет (severity:'warning' не блокирует, но показывается).
|
|
49
|
+
// false = 'invalid', 'error' (правило не удалось проверить — на поле замечание ruleFailed) либо 'cancelled'.
|
|
50
|
+
runValidation<T>(model, schema): Promise<ValidationResult> // только сбор
|
|
51
|
+
applyValidationResult(result, options?: { touch?: boolean }): void // разнос по нодам
|
|
52
|
+
|
|
53
|
+
// Нода поля/формы (точечная работа с UI-состоянием):
|
|
54
|
+
form.markAsTouched(); // тронуть все поля (показать замечания)
|
|
55
|
+
form.clearIssues(); // снять замечания со всех нод
|
|
56
|
+
form.<field>.setIssues([{ code, params }]); // серверная/бизнес-ошибка в конкретное поле
|
|
57
|
+
form.<field>.clearIssues();
|
|
58
|
+
form.reset(); // к точке отсчёта модели + чистые ноды (замечания, touched, dirty)
|
|
43
59
|
```
|
|
44
60
|
|
|
61
|
+
### Что отправка делает с полями
|
|
62
|
+
|
|
63
|
+
| Итог отправки | Касание (`touched`) |
|
|
64
|
+
| --------------------- | --------------------------------------------------------------------- |
|
|
65
|
+
| `invalid`, `error` | вся форма — видны замечания и полей, которых правила не касались |
|
|
66
|
+
| `cancelled` | форма тронутой не помечается: прогон устарел, замечаний итог не несёт |
|
|
67
|
+
| `submitted`, `failed` | только поля, которые проверили правила |
|
|
68
|
+
|
|
69
|
+
Отправка — попытка отправки: стратегия `afterFirstSubmit` после неё переходит в живую фазу
|
|
70
|
+
(`bundle.validate()` фазу не трогает, см. `33-validation-strategy.md`).
|
|
71
|
+
|
|
45
72
|
## Examples
|
|
46
73
|
|
|
47
74
|
### Базовый submit-handler
|
|
48
75
|
|
|
49
76
|
```tsx
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
77
|
+
import { createForm, type FormModel, type FormSchemaNode } from '@reformer/core';
|
|
78
|
+
import { useFormBundle } from '@reformer/core/react';
|
|
79
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
53
80
|
import { required, email, minLength } from '@reformer/core/validators';
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
interface RegistrationFormData {
|
|
83
|
+
username: string;
|
|
84
|
+
email: string;
|
|
85
|
+
password: string;
|
|
86
|
+
}
|
|
56
87
|
|
|
57
|
-
// Валидация — отдельная схема над
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
validate(model.$.
|
|
61
|
-
validate(model.$.
|
|
62
|
-
validate(model.$.password, [required({ message: 'Пароль обязателен' }), minLength(8)]);
|
|
88
|
+
// Валидация — отдельная схема над моделью; стабильная module-level `const`-ссылка.
|
|
89
|
+
const registrationRules = defineValidation<RegistrationFormData>(({ model }) => {
|
|
90
|
+
validate(model.$.username, [required({ code: 'username.required' }), minLength(3)]);
|
|
91
|
+
validate(model.$.email, [required({ code: 'email.required' }), email()]);
|
|
92
|
+
validate(model.$.password, [required({ code: 'password.required' }), minLength(8)]);
|
|
63
93
|
});
|
|
64
94
|
|
|
95
|
+
const createRegistration = () =>
|
|
96
|
+
createForm<RegistrationFormData>({
|
|
97
|
+
initial: { username: '', email: '', password: '' },
|
|
98
|
+
schema: registrationSchema, // (model) => дерево узлов, без правил
|
|
99
|
+
validation: registrationRules,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
async function register(values: RegistrationFormData): Promise<Response> {
|
|
103
|
+
return fetch('/api/v1/auth/register', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify(values),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
function RegistrationForm() {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
form.markAsTouched(); // показать ошибки на всех полях
|
|
75
|
-
|
|
76
|
-
// Шаг 1: прогон валидации данных. Ошибки автоматически проставятся в ноды → UI подсветит.
|
|
77
|
-
const valid = await validateModel(model, registrationSchema);
|
|
78
|
-
if (!valid) return;
|
|
79
|
-
|
|
80
|
-
// Шаг 2: достать чистые данные и отправить
|
|
81
|
-
const payload = model.get();
|
|
82
|
-
const response = await fetch('/api/v1/auth/register', {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: { 'Content-Type': 'application/json' },
|
|
85
|
-
body: JSON.stringify(payload),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Шаг 3: чистый старт после успеха
|
|
89
|
-
if (response.ok) model.reset();
|
|
111
|
+
const registration = useFormBundle(createRegistration);
|
|
112
|
+
const { form, model } = registration;
|
|
113
|
+
|
|
114
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
// Один прогон правил: при замечаниях обработчик не вызывается, вся форма помечена тронутой.
|
|
117
|
+
const outcome = await registration.submission.submit(register);
|
|
118
|
+
if (outcome.status === 'submitted' && outcome.result.ok) model.reset(); // чистый старт
|
|
90
119
|
};
|
|
91
120
|
|
|
92
121
|
return (
|
|
@@ -100,32 +129,57 @@ function RegistrationForm() {
|
|
|
100
129
|
}
|
|
101
130
|
```
|
|
102
131
|
|
|
103
|
-
### Submit с
|
|
132
|
+
### Submit с разбором результата и серверными замечаниями
|
|
104
133
|
|
|
105
134
|
```tsx
|
|
106
|
-
async function handleSubmit(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const response = await api.register(model.get());
|
|
115
|
-
if (response.success) {
|
|
116
|
-
model.reset();
|
|
117
|
-
navigate('/welcome');
|
|
118
|
-
} else {
|
|
119
|
-
// Серверная бизнес-ошибка — НЕ сбрасываем форму, кладём ошибку в поле
|
|
120
|
-
form.username.setErrors([{ code: 'taken', message: response.message }]);
|
|
135
|
+
async function handleSubmit(event: React.FormEvent) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
|
|
138
|
+
const outcome = await registration.submission.submit(async (values) => {
|
|
139
|
+
const response = await api.register(values);
|
|
140
|
+
if (!response.success) {
|
|
141
|
+
// Серверная бизнес-ошибка — НЕ сбрасываем форму, кладём замечание в поле
|
|
142
|
+
registration.form.username.setIssues([{ code: 'username.taken' }]);
|
|
121
143
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
return response;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
switch (outcome.status) {
|
|
148
|
+
case 'submitted':
|
|
149
|
+
if (outcome.result.success) {
|
|
150
|
+
registration.model.reset();
|
|
151
|
+
navigate('/welcome');
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 'invalid': // замечания уже на полях
|
|
155
|
+
break;
|
|
156
|
+
case 'error': // async-правило не ответило — outcome.validation.failures
|
|
157
|
+
showToast('Не удалось проверить форму, повторите попытку');
|
|
158
|
+
break;
|
|
159
|
+
case 'cancelled': // форму правили, пока шла проверка, либо отправку сменила следующая
|
|
160
|
+
break; // замечаний нет — пользователь отправит ещё раз
|
|
161
|
+
case 'failed': // обработчик бросил исключение (сеть) — значения остаются
|
|
162
|
+
showToast(`Ошибка сети: ${String(outcome.error)}`);
|
|
163
|
+
break;
|
|
125
164
|
}
|
|
126
165
|
}
|
|
127
166
|
```
|
|
128
167
|
|
|
168
|
+
Текст кода `username.taken` — запись в словаре формы (`defineIssueMessages`), см.
|
|
169
|
+
`36-issue-messages.md`.
|
|
170
|
+
|
|
171
|
+
### Значения для сервера
|
|
172
|
+
|
|
173
|
+
Обработчик получает `model.get()` — значения модели целиком: вычисляемые поля и поля, выключенные
|
|
174
|
+
`enableWhen` (их правила не считаются, но данные принадлежат модели). Поля, которых сервер не
|
|
175
|
+
ждёт, отрезает сам обработчик: удобно держать ввод и расчёт разными типами
|
|
176
|
+
(`CreditApplication` и `CreditApplicationDerived`, форма — их пересечение) и функцию
|
|
177
|
+
`toPayload(values): CreditApplication`, убирающую вычисляемые ключи.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const outcome = await credit.submission.submit((values) => api.send(toApplicationPayload(values)));
|
|
181
|
+
```
|
|
182
|
+
|
|
129
183
|
### Reset с подтверждением
|
|
130
184
|
|
|
131
185
|
```tsx
|
|
@@ -150,61 +204,103 @@ function ActionButtons({ model }: { model: FormModel<RegistrationFormData> }) {
|
|
|
150
204
|
}
|
|
151
205
|
```
|
|
152
206
|
|
|
153
|
-
###
|
|
207
|
+
### Без сборки — раннер (server action / Node)
|
|
154
208
|
|
|
155
209
|
Раннер headless — работает без UI-компонентов, только с моделью и схемой:
|
|
156
210
|
|
|
157
211
|
```typescript
|
|
158
212
|
import { createModel } from '@reformer/core';
|
|
159
|
-
import {
|
|
213
|
+
import { runValidation } from '@reformer/core/validation';
|
|
160
214
|
|
|
161
215
|
async function processFormPayload(rawData: Partial<MyForm>) {
|
|
162
216
|
const model = createModel<MyForm>(initialValues);
|
|
163
217
|
model.patch(rawData); // частичный load данных
|
|
164
218
|
|
|
165
|
-
//
|
|
166
|
-
const
|
|
167
|
-
if (
|
|
219
|
+
// Полный результат: статус и замечания по ручкам полей — без нод формы
|
|
220
|
+
const result = await runValidation(model, myFormRules);
|
|
221
|
+
if (result.status !== 'valid') return { ok: false as const, status: result.status };
|
|
168
222
|
return { ok: true as const, data: model.get() };
|
|
169
223
|
}
|
|
170
224
|
```
|
|
171
225
|
|
|
226
|
+
### Снятие сборки — `bundle.dispose()`
|
|
227
|
+
|
|
228
|
+
Владелец сборки — тот, кто вызвал `createForm`. Вне React (тесты, сервер, своя оболочка) сборку
|
|
229
|
+
снимают, когда форма больше не нужна: `bundle.dispose()` снимает поведение формы, подписки нод и
|
|
230
|
+
живую проверку, а идущие проверки — полная, шага, отправки — получают `cancelled`, и их замечания
|
|
231
|
+
на ноды не ложатся. Значения модели остаются. Вызов идемпотентен.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const registration = createRegistration();
|
|
235
|
+
try {
|
|
236
|
+
const outcome = await registration.submission.submit(register);
|
|
237
|
+
expect(outcome.status).toBe('invalid');
|
|
238
|
+
} finally {
|
|
239
|
+
registration.dispose();
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- `level.dispose()` у `FormLevel` (уровень без сборки, `createFormLevel`) снимает только уровень —
|
|
244
|
+
живую проверку и идущие проверки; форму не трогает.
|
|
245
|
+
- Форму из `createFormFromModel` снимает `disposeForm(form)` — поведение и подписки нод.
|
|
246
|
+
- `useFormBundle` сборку при размонтировании не снимает.
|
|
247
|
+
|
|
172
248
|
## Anti-patterns
|
|
173
249
|
|
|
174
250
|
```typescript
|
|
175
|
-
// ❌
|
|
176
|
-
|
|
177
|
-
|
|
251
|
+
// ❌ Отправка в обход проверки — правила не прогнаны, замечания не показаны
|
|
252
|
+
await api.send(bundle.model.get());
|
|
253
|
+
|
|
254
|
+
// ✅ Проверка и отправка одним вызовом
|
|
255
|
+
await bundle.submission.submit(api.send);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// ❌ Результат как boolean — объект всегда truthy
|
|
260
|
+
if (await bundle.submission.submit(api.send)) navigate('/done');
|
|
261
|
+
|
|
262
|
+
// ✅ Статус результата
|
|
263
|
+
const outcome = await bundle.submission.submit(api.send);
|
|
264
|
+
if (outcome.status === 'submitted') navigate('/done');
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// ❌ Методы проверки и отправки на ноде — удалены
|
|
269
|
+
await form.validate();
|
|
270
|
+
await form.submit(api.send);
|
|
178
271
|
|
|
179
|
-
// ✅
|
|
180
|
-
|
|
181
|
-
|
|
272
|
+
// ✅ Проверка — у сборки, отправка — её объект submission (см. 17-nonexistent-api.md)
|
|
273
|
+
await bundle.validate();
|
|
274
|
+
await bundle.submission.submit(api.send);
|
|
182
275
|
```
|
|
183
276
|
|
|
184
277
|
```typescript
|
|
185
|
-
// ❌ Чтение результата
|
|
186
|
-
const handleSubmit = (
|
|
187
|
-
|
|
188
|
-
validateModel(model, schema); // Promise проигнорирован, async
|
|
189
|
-
|
|
278
|
+
// ❌ Чтение результата без await — сабмитим по неполному вердикту
|
|
279
|
+
const handleSubmit = (event) => {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
validateModel(model, schema); // Promise проигнорирован, async-правила не дождались
|
|
282
|
+
send(model.get());
|
|
190
283
|
};
|
|
191
284
|
|
|
192
|
-
// ✅
|
|
193
|
-
const handleSubmit = async (
|
|
194
|
-
|
|
195
|
-
|
|
285
|
+
// ✅ bundle.submission.submit дожидается async-правил сам
|
|
286
|
+
const handleSubmit = async (event) => {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
await bundle.submission.submit(send);
|
|
196
289
|
};
|
|
197
290
|
```
|
|
198
291
|
|
|
199
292
|
```typescript
|
|
200
|
-
// ❌
|
|
201
|
-
createForm({
|
|
293
|
+
// ❌ Правила в узле схемы формы — ключа validators у узла нет, схема формы правил не несёт
|
|
294
|
+
createForm({
|
|
295
|
+
initial,
|
|
296
|
+
schema: (model) => ({ children: [{ model: model.$.username, component: Input, validators: [required()] }] }),
|
|
297
|
+
});
|
|
202
298
|
|
|
203
|
-
// ✅ Валидация — отдельная схема,
|
|
204
|
-
const
|
|
299
|
+
// ✅ Валидация — отдельная схема, уходит в сборку полем validation
|
|
300
|
+
const rules = defineValidation<T>(({ model }) => {
|
|
205
301
|
validate(model.$.username, [required()]);
|
|
206
302
|
});
|
|
207
|
-
|
|
303
|
+
createForm({ initial, schema, validation: rules });
|
|
208
304
|
```
|
|
209
305
|
|
|
210
306
|
```typescript
|
|
@@ -212,11 +308,20 @@ await validateModel(model, schema);
|
|
|
212
308
|
await api.send(model.get());
|
|
213
309
|
model.reset();
|
|
214
310
|
|
|
215
|
-
// ✅ Reset только после успеха
|
|
216
|
-
|
|
217
|
-
await api.send(
|
|
218
|
-
model.reset();
|
|
219
|
-
}
|
|
311
|
+
// ✅ Reset только после успеха — внутри обработчика либо по статусу 'submitted'
|
|
312
|
+
const outcome = await bundle.submission.submit(async (values) => {
|
|
313
|
+
await api.send(values);
|
|
314
|
+
bundle.model.reset();
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// ❌ Сборка в тесте или на сервере брошена — поведение и живая проверка продолжают работать
|
|
320
|
+
const bundle = createForm<RegistrationFormData>({ initial, schema, validation: rules });
|
|
321
|
+
await bundle.submission.submit(register);
|
|
322
|
+
|
|
323
|
+
// ✅ Владелец сборки снимает её сам; модель при этом остаётся
|
|
324
|
+
bundle.dispose();
|
|
220
325
|
```
|
|
221
326
|
|
|
222
327
|
```typescript
|
|
@@ -231,29 +336,74 @@ model.reset();
|
|
|
231
336
|
|
|
232
337
|
## Troubleshooting
|
|
233
338
|
|
|
234
|
-
**Q: После `reset()` в UI остались старые
|
|
235
|
-
A: `model.reset()` меняет значения;
|
|
236
|
-
`
|
|
237
|
-
`form.
|
|
238
|
-
|
|
239
|
-
**Q: `
|
|
240
|
-
A: Это правило с `severity: 'warning'` — оно показывается, но
|
|
241
|
-
|
|
242
|
-
|
|
339
|
+
**Q: После `reset()` в UI остались старые замечания.**
|
|
340
|
+
A: `model.reset()` меняет значения; замечания в нодах чистит валидация. Перезапусти
|
|
341
|
+
`bundle.validate()` после reset (валидные поля погаснут сами), очисти напрямую —
|
|
342
|
+
`form.clearIssues()` / `form.<field>.clearIssues()`, либо сбрось ноды — `form.reset()`.
|
|
343
|
+
|
|
344
|
+
**Q: `submit` вызвал обработчик, но в поле висит замечание.**
|
|
345
|
+
A: Это правило с `severity: 'warning'` — оно показывается, но отправку не блокирует. Блокируют только
|
|
346
|
+
замечания без `severity` (по умолчанию `'error'`). Чтобы не пускать submit при warning — проверь
|
|
347
|
+
`(await bundle.validate()).issues` сам до отправки.
|
|
348
|
+
|
|
349
|
+
**Q: `submit` вернул `status: 'error'`, а замечаний правил в форме нет — на поле «Не удалось проверить поле».**
|
|
350
|
+
A: Async-правило не вернуло результат (сеть, исключение). Сбой блокирует: на поле — замечание
|
|
351
|
+
`{ code: 'ruleFailed' }`. Причина — в `outcome.validation.failures`. Повторная отправка после
|
|
352
|
+
восстановления сети снимет замечание.
|
|
353
|
+
|
|
354
|
+
**Q: `submit` вернул `status: 'cancelled'`, обработчик не вызван, замечаний нет.**
|
|
355
|
+
A: Проверку отправки сменила следующая (вторая отправка, живой прогон стратегии `change`) либо
|
|
356
|
+
модель изменилась, пока проверка шла. Пока отвечает async-правило, форма не заперта
|
|
357
|
+
(`submission.submitting` ещё `false`): пользователь поправил поле, пришла загрузка, поведение записало
|
|
358
|
+
значение после таймера или запроса — правила этих значений не видели, и отправлять их нельзя.
|
|
359
|
+
Форма тронутой не помечается. Отправку повторяют: следующий прогон проверит уже новые значения.
|
|
360
|
+
Отменой кончается и отправка, идущая в момент `bundle.dispose()`. Чтобы правок не было — запри
|
|
361
|
+
форму на время проверки по `bundle.validating`. Вызов в одном тике со сборкой формы или правкой
|
|
362
|
+
поля отправку не отменяет: проверка сначала дожидается отложенных записей поведения (колбэк
|
|
363
|
+
`onChange`, сброс `resetOnDisable`), и правила, и обработчик видят значения после них.
|
|
364
|
+
|
|
365
|
+
**Q: В тесте запрос async-правила не уходит сразу после `validate()` / `submit()`.**
|
|
366
|
+
A: Перед вызовом в том же тике правили модель, и поведение ещё не дописало свои отложенные
|
|
367
|
+
записи — проверка ждёт их, правила стартуют микротаском позже (`validating` поднят сразу).
|
|
368
|
+
Дождись записей поведения до вызова (`await Promise.resolve()` / свой `settle()`) либо читай
|
|
369
|
+
запрос правила после `await`. Без отложенных записей правила исполняются синхронно в момент
|
|
370
|
+
вызова.
|
|
243
371
|
|
|
244
372
|
**Q: `reset()` не возвращает данные, загруженные с сервера.**
|
|
245
|
-
A: `reset()` возвращает к initial-снимку (значения на момент `createModel`).
|
|
246
|
-
|
|
247
|
-
|
|
373
|
+
A: `reset()` возвращает к initial-снимку (значения на момент `createModel`). У модели, которую
|
|
374
|
+
`createForm` создал из `initial`, снимок — собранная форма: в него входят правки `seed` и первые
|
|
375
|
+
записи поведения (копия `syncFields`, нормализация `transformValue`) — сборка выполняет их до
|
|
376
|
+
возврата, поэтому пара `syncFields` сбрасывается согласованной. `set/patch` НЕ меняют initial. Чтобы сделать
|
|
377
|
+
загруженные данные новой «точкой отсчёта» — вызови `model.captureInitial()` после загрузки.
|
|
378
|
+
|
|
379
|
+
**Q: Что сброс и `isDirty()` делают с вычисляемыми полями?**
|
|
380
|
+
A: Ничего. Цели `compute` / `computeFrom` и поля с `markDerived` (например, номер заявки от
|
|
381
|
+
сервера) `model.reset()` и `form.reset()` не пишут — так же, как `set` и `patch`: значение
|
|
382
|
+
пересчитает поведение, когда сброс вернёт его источники. В `isDirty()` такие поля не входят,
|
|
383
|
+
поэтому форма без единой правки не считается изменённой, даже если поведение уже посчитало ставку.
|
|
384
|
+
Явная запись — `model.rate = 0` или `form.rate.reset(0)` — пишет как обычно. Исключение —
|
|
385
|
+
`enableWhen(…, { resetOnDisable: true })`: выключенное поле возвращается к значению создания, даже
|
|
386
|
+
если оно цель `compute` (её `compute` обязан нести то же условие `when`).
|
|
387
|
+
|
|
388
|
+
**Q: Чем `form.reset()` отличается от `model.reset()`?**
|
|
389
|
+
A: Точка отсчёта у них одна — initial-снимок модели либо последний `model.captureInitial()`
|
|
390
|
+
(загруженные данные). `form.reset()` — сброс для пользователя (кнопка «Сбросить»): значения к точке
|
|
391
|
+
отсчёта и чистые ноды — замечания, touched, dirty; после него и форма, и модель не изменены.
|
|
392
|
+
`model.reset()` — сброс значений модели: ноды не трогает, замечания погаснут при следующей
|
|
393
|
+
проверке. Исключение — `enableWhen({ resetOnDisable: true })`: выключенное поле очищается к
|
|
394
|
+
значению, с которым нода создана, а не к загруженному.
|
|
248
395
|
|
|
249
396
|
**Q: Disabled-поля участвуют в submit-данных?**
|
|
250
|
-
A: `model.get()`
|
|
251
|
-
|
|
397
|
+
A: Да. Обработчик получает `model.get()` — модель целиком, с выключенными полями: их правила не
|
|
398
|
+
считаются, но данные принадлежат модели. Фильтруй сам либо используй
|
|
399
|
+
`enableWhen({ resetOnDisable: true })`, чтобы при disable поле возвращалось к initial. Замечания
|
|
400
|
+
отключённых полей сняты, в проверку они не входят.
|
|
252
401
|
|
|
253
402
|
## See also
|
|
254
403
|
|
|
404
|
+
- [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `validateStep`, `submission`, поток шагов
|
|
255
405
|
- [29-async-preload.md](./29-async-preload.md) — initial values и preload через `set`/`patch`
|
|
256
|
-
- [13-multi-step.md](./13-multi-step.md) — пошаговая
|
|
406
|
+
- [13-multi-step.md](./13-multi-step.md) — пошаговая форма: поток `flow`, визард
|
|
257
407
|
- [27-revalidate-when.md](./27-revalidate-when.md) — мост «поведение → валидация»: `revalidateWhen([deps], () => void validateModel(model, schema))`
|
|
258
|
-
- [
|
|
408
|
+
- [36-issue-messages.md](./36-issue-messages.md) — тексты замечаний по коду
|
|
259
409
|
- [05-common-mistakes.md](./05-common-mistakes.md) — типичные ошибки
|