@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
|
@@ -7,175 +7,363 @@
|
|
|
7
7
|
createModel<T extends object>(initial: T): FormModel<T>
|
|
8
8
|
// model.get() / model.set(full) / model.patch(partial) / model.isDirty()
|
|
9
9
|
// model.reset() / model.captureInitial() / model.signalAt(path)
|
|
10
|
-
// model.$.field → PathAwareSignal<FieldType> (
|
|
11
|
-
// model.
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
10
|
+
// model.$.field → PathAwareSignal<FieldType> (ручка: привязка в схеме, правиле, операторе)
|
|
11
|
+
// model.$.group.value → снимок группы/массива; в dev заморожен — правка на месте бросает
|
|
12
|
+
// model.arrayField → ModelArray<Item> (push/insertAt/removeAt/move/swap/clear/at/map/length)
|
|
13
|
+
// итерируем (for…of, spread); Object.keys — индексы; insertAt/move за концом — RangeError
|
|
14
|
+
|
|
15
|
+
// Сборка одним вызовом: модель, форма, уровень формы и дерево для рендера
|
|
16
|
+
createForm<T extends object>({
|
|
17
|
+
initial | model,
|
|
18
|
+
schema?: (model) => tree, // билдер; JSON — документ + registry
|
|
19
|
+
behavior?, // defineFormBehavior(...)
|
|
20
|
+
validation?, // ValidationDefinition<T> | { rules?, extras?, strategy?, debounce? }
|
|
21
|
+
flow?, // { steps: [{ id, rules? }] } — id = selector узла шага
|
|
22
|
+
seed?, setup?,
|
|
23
|
+
}): FormBundle<T>
|
|
24
|
+
// bundle = { model, form, render, flow, validating, submission, validate, validateStep, dispose }
|
|
25
|
+
// (+ служебное validation: { rules, controller } | undefined — его армирует useFormBundle)
|
|
26
|
+
// await bundle.validate() → ValidationResult (вся форма; замечания показываются)
|
|
27
|
+
// await bundle.validateStep('loan') → ValidationResult (правила шага; неизвестный шаг — исключение)
|
|
28
|
+
// await bundle.submission.submit(handler) → SubmitResult<R> (один прогон; обработчик — только при valid)
|
|
29
|
+
// bundle.submission.submitting → ReadonlySignal<boolean> (работает обработчик)
|
|
30
|
+
// bundle.flow?.next() / previous() / goTo(id) → FlowTransition
|
|
31
|
+
// bundle.render.tree → CompiledSchema (см. 37-compiled-schema.md)
|
|
32
|
+
// bundle.dispose() → снять сборку: поведение, подписки нод, живую проверку;
|
|
33
|
+
// идущие проверки — cancelled; модель остаётся; идемпотентно
|
|
34
|
+
// useFormBundle(() => createForm(…)) снимает сборку сам — при размонтировании компонента
|
|
35
|
+
|
|
36
|
+
// Отправка — отдельный объект сборки (`bundle.submission`)
|
|
37
|
+
interface FormSubmission<T> {
|
|
38
|
+
readonly submitting: ReadonlySignal<boolean>; // работает обработчик; пока идёт проверка — false
|
|
39
|
+
submit<R>(handler: (values: T) => R | Promise<R>): Promise<SubmitResult<R>>;
|
|
40
|
+
}
|
|
41
|
+
// Без сборки (визард в разметке без createForm): check: undefined — отправка без проверки
|
|
42
|
+
createSubmission<T>(dependencies: {
|
|
43
|
+
check: (() => Promise<ActionCheck<T>>) | undefined; // ActionCheck = { result, values, stale }
|
|
44
|
+
values: () => T;
|
|
45
|
+
}): FormSubmission<T>
|
|
46
|
+
|
|
47
|
+
// Низкоуровневая фабрика: форма из готовых модели и дерева, без уровня формы
|
|
48
|
+
createFormFromModel<T>({ model, schema?, behavior? }): FormGroup<T>
|
|
49
|
+
disposeForm<T>(form: FormGroup<T>): void // снять такую форму: поведение и подписки нод; модель остаётся
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Form nodes
|
|
53
|
+
|
|
54
|
+
Ноды строит сборка; прикладной код работает с ними через интерфейсы. Классов нод в публичном
|
|
55
|
+
API нет — вид ноды узнают гардами `isFormControl` / `isFormGroup` / `isFormArray`.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
interface FormNode<T> {
|
|
59
|
+
readonly value: ReadonlySignal<T>;
|
|
60
|
+
readonly valid: ReadonlySignal<boolean>; // нет блокирующих замечаний, не disabled, не pending (у поля, группы и массива одинаково)
|
|
61
|
+
readonly invalid: ReadonlySignal<boolean>;
|
|
62
|
+
readonly pending: ReadonlySignal<boolean>; // идёт async-правило
|
|
63
|
+
readonly disabled: ReadonlySignal<boolean>;
|
|
64
|
+
readonly touched: ReadonlySignal<boolean>;
|
|
65
|
+
readonly dirty: ReadonlySignal<boolean>;
|
|
66
|
+
readonly issues: ReadonlySignal<readonly ValidationIssue[]>; // у группы и массива — свои и детей
|
|
67
|
+
readonly ownIssues: ReadonlySignal<readonly ValidationIssue[]>; // только свои: правила на ручке группы/массива
|
|
68
|
+
getValue(): T;
|
|
69
|
+
setValue(value: T): void; // пишет в модель
|
|
70
|
+
reset(value?: T): void; // значение и состояние — к начальным
|
|
71
|
+
markAsTouched(): void; // у группы — каскадно
|
|
72
|
+
markAsUntouched(): void;
|
|
73
|
+
enable(): void;
|
|
74
|
+
disable(): void;
|
|
75
|
+
setIssues(issues: readonly ValidationIssue[]): void; // ответ сервера, своя проверка
|
|
76
|
+
clearIssues(): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface FormControl<T> extends FormNode<T> {
|
|
80
|
+
readonly component: ComponentRef | undefined;
|
|
81
|
+
readonly componentProps: ReadonlySignal<Readonly<Record<string, unknown>>>;
|
|
82
|
+
// invalid && (touched || dirty) — состояние ошибки: aria-invalid, рамка
|
|
83
|
+
readonly shouldShowError: ReadonlySignal<boolean>;
|
|
84
|
+
// есть замечания (ошибки ИЛИ предупреждения) && (touched || dirty) — показ текстов замечаний
|
|
85
|
+
readonly shouldShowIssues: ReadonlySignal<boolean>;
|
|
86
|
+
updateComponentProps(patch: Record<string, unknown>): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface FormArray<T extends object> extends FormNode<T[]> {
|
|
90
|
+
readonly length: number; // число; чтение в эффекте/computed подписывает
|
|
91
|
+
at(index: number): FormGroup<T> | undefined;
|
|
92
|
+
map<R>(fn: (row: FormGroup<T>, index: number) => R): R[];
|
|
93
|
+
forEach(fn: (row: FormGroup<T>, index: number) => void): void;
|
|
94
|
+
keyAt(index: number): string; // стабильный ключ строки — React `key`
|
|
95
|
+
push(item?: T): void; // без значения — по шаблону arrayOf(blank)
|
|
96
|
+
insert(index: number, item?: T): void;
|
|
97
|
+
removeAt(index: number): void;
|
|
98
|
+
move(from: number, to: number): void;
|
|
99
|
+
swap(first: number, second: number): void;
|
|
100
|
+
clear(): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Группа: члены FormNode + ноды полей по именам + пространство `$` со всеми полями
|
|
104
|
+
type FormGroup<T> = FormNode<T> & /* поля по именам */ & { readonly $: FormFields<T> };
|
|
105
|
+
// form.email — FormControl<string>
|
|
106
|
+
// form.address.city — FormControl<string> (вложенная группа — FormGroup)
|
|
107
|
+
// form.items — FormArray<Item> (массив объектов); form.tags — FormControl<string[]>
|
|
108
|
+
// form.$.value — поле, затенённое членом FormNode (value, issues, reset, …)
|
|
26
109
|
```
|
|
27
110
|
|
|
111
|
+
Поле затеняют только члены `FormNode`: поле данных `value` берётся через `form.$.value`. Имена
|
|
112
|
+
`id`, `status`, `fields`, `submit` — обычные поля (`form.status`).
|
|
113
|
+
|
|
28
114
|
### Validators
|
|
29
115
|
|
|
30
116
|
Валидаторы — **чистые фабрики** из `@reformer/core/validators`: возвращают правило `Rule<T>` =
|
|
31
|
-
`(value) =>
|
|
32
|
-
|
|
117
|
+
`(value: T) => ValidationIssue | null`. Передаются оператору `validate(sig, [required(), min(50000)])`
|
|
118
|
+
внутри схемы валидации, а **не** в схему формы.
|
|
33
119
|
|
|
34
120
|
```typescript
|
|
35
|
-
required(options?:
|
|
36
|
-
min(value: number, options?:
|
|
37
|
-
max(value: number, options?:
|
|
38
|
-
minLength(length: number, options?:
|
|
39
|
-
maxLength(length: number, options?:
|
|
40
|
-
email(options?:
|
|
41
|
-
pattern(regex: RegExp, options?:
|
|
42
|
-
url(options?: {
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
121
|
+
required(options?: RuleOptions) // Rule<unknown>; пусто: null, undefined, '', [], NaN, false
|
|
122
|
+
min(value: number, options?: RuleOptions) // Rule<number | null | undefined>
|
|
123
|
+
max(value: number, options?: RuleOptions)
|
|
124
|
+
minLength(length: number, options?: RuleOptions) // Rule<unknown>: строка либо массив
|
|
125
|
+
maxLength(length: number, options?: RuleOptions)
|
|
126
|
+
email(options?: RuleOptions) // Rule<string | null | undefined>
|
|
127
|
+
pattern(regex: RegExp, options?: RuleOptions)
|
|
128
|
+
url(options?: RuleOptions & { requireProtocol?: boolean; allowedProtocols?: string[] })
|
|
129
|
+
// url: разбор new URL; localhost, IP, порт, кириллический домен — да; пробелы и userinfo — нет;
|
|
130
|
+
// протоколы по умолчанию http/https (без учёта регистра), чужой — код url_protocol
|
|
131
|
+
phone(options?: RuleOptions & { format?: PhoneFormat })
|
|
132
|
+
// Number validator factories — Rule<number | null | undefined>
|
|
133
|
+
isNumber(options?: RuleOptions)
|
|
134
|
+
integer(options?: RuleOptions)
|
|
135
|
+
multipleOf(divisor: number, options?: RuleOptions) // делитель 0 / не конечный — RangeError при создании
|
|
136
|
+
nonNegative(options?: RuleOptions) // value >= 0
|
|
137
|
+
nonZero(options?: RuleOptions) // value !== 0
|
|
138
|
+
// Date validator factories — Rule<string | Date | null | undefined>
|
|
139
|
+
// строгий формат: Date либо строка YYYY-MM-DD; другие строки (дата-время, ДД.ММ.ГГГГ) — не даты:
|
|
140
|
+
// isDate даёт date_invalid, остальные правила их пропускают. Рождённый 29 февраля в невисокосный
|
|
141
|
+
// год взрослеет 28 февраля (ГК РФ, ст. 192); дата рождения в будущем — minAge даёт date_future
|
|
142
|
+
isDate(options?: RuleOptions)
|
|
143
|
+
minDate(date: Date, options?: RuleOptions)
|
|
144
|
+
maxDate(date: Date, options?: RuleOptions)
|
|
145
|
+
pastDate(options?: RuleOptions)
|
|
146
|
+
futureDate(options?: RuleOptions)
|
|
147
|
+
minAge(years: number, options?: RuleOptions)
|
|
148
|
+
maxAge(years: number, options?: RuleOptions)
|
|
149
|
+
// File validator factories — Rule<unknown>: файл либо массив файлов
|
|
150
|
+
fileType(accept: string, options?: RuleOptions) // синтаксис accept; «звезда/звезда» — любой файл
|
|
151
|
+
maxFileSize(bytes: number, options?: RuleOptions)
|
|
152
|
+
minFileSize(bytes: number, options?: RuleOptions)
|
|
153
|
+
maxFiles(count: number, options?: RuleOptions)
|
|
154
|
+
minFiles(count: number, options?: RuleOptions) // [] при count > 0 — замечание, как у minLength
|
|
155
|
+
maxTotalFileSize(bytes: number, options?: RuleOptions)
|
|
58
156
|
```
|
|
59
157
|
|
|
60
|
-
|
|
61
|
-
|
|
158
|
+
Тип значения правила сверяется с типом поля: `validate(model.$.age, [email()])` для числового поля
|
|
159
|
+
не компилируется. Набор правил для поля объявляют типом поля — `Rule<LoanType>[]`,
|
|
160
|
+
`Rule<number | null>[]`: фабрики с более широким типом значения в него присваиваются.
|
|
161
|
+
|
|
162
|
+
`RuleOptions` — `{ code?: string; params?: Record<string, unknown> }`. Текста у правила нет:
|
|
163
|
+
замечание несёт только код и параметры (`ValidationIssue = { code, params?, severity? }`), а строку
|
|
164
|
+
по коду при отображении даёт словарь — словарь формы (`defineIssueMessages`), затем словарь локали
|
|
165
|
+
(`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без провайдера.
|
|
166
|
+
Текст конкретного поля — свой код (`required({ code: 'phone.required' })`) и запись в словаре
|
|
167
|
+
формы. Подробно — [36-issue-messages.md](./36-issue-messages.md).
|
|
168
|
+
|
|
169
|
+
Использование: схема формы привязывает поля к ручкам модели, правила живут отдельной
|
|
170
|
+
`ValidationDefinition` и уходят в сборку полем `validation`.
|
|
62
171
|
|
|
63
172
|
```typescript
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
173
|
+
import { createForm, type FormModel, type FormSchemaNode } from '@reformer/core';
|
|
174
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
66
175
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
67
176
|
|
|
68
|
-
|
|
69
|
-
|
|
177
|
+
interface Loan {
|
|
178
|
+
email: string;
|
|
179
|
+
age: number | null;
|
|
180
|
+
amount: number | null;
|
|
181
|
+
}
|
|
70
182
|
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
183
|
+
// схема формы правил не несёт — только привязка поля к ручке модели + компонент
|
|
184
|
+
const loanSchema = (model: FormModel<Loan>): FormSchemaNode => ({
|
|
185
|
+
children: [
|
|
186
|
+
{ model: model.$.email, component: Input },
|
|
187
|
+
{ model: model.$.age, component: InputNumber },
|
|
188
|
+
{ model: model.$.amount, component: InputNumber },
|
|
189
|
+
],
|
|
79
190
|
});
|
|
80
191
|
|
|
81
|
-
// правила — в ОТДЕЛЬНОЙ схеме валидации
|
|
82
|
-
const loanValidation =
|
|
83
|
-
validate(model.$.email,
|
|
84
|
-
validate(model.$.age,
|
|
192
|
+
// правила — в ОТДЕЛЬНОЙ схеме валидации
|
|
193
|
+
const loanValidation = defineValidation<Loan>(({ model }) => {
|
|
194
|
+
validate(model.$.email, [required(), email()]);
|
|
195
|
+
validate(model.$.age, [required(), min(18)]);
|
|
85
196
|
validate(model.$.amount, [min(0), max(1000)]);
|
|
86
197
|
});
|
|
87
198
|
|
|
88
|
-
const
|
|
199
|
+
const loan = createForm<Loan>({
|
|
200
|
+
initial: { email: '', age: null, amount: null },
|
|
201
|
+
schema: loanSchema,
|
|
202
|
+
validation: loanValidation,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const result = await loan.validate(); // ValidationResult; замечания сами доедут до loan.form.<field>.issues
|
|
206
|
+
if (result.status === 'valid') send(loan.model.get());
|
|
89
207
|
```
|
|
90
208
|
|
|
91
209
|
### Custom & cross-field validators
|
|
92
210
|
|
|
93
|
-
Контракт валидации — `@reformer/core/validation`. Схема (`
|
|
94
|
-
(под)моделью; внутри вызываются
|
|
95
|
-
|
|
211
|
+
Контракт валидации — `@reformer/core/validation`. Схема (`ValidationDefinition<T>`) — обычная функция
|
|
212
|
+
над (под)моделью; внутри вызываются операторы, которые сами пишут замечания в сток текущего прогона —
|
|
213
|
+
автор коллектора не видит.
|
|
96
214
|
|
|
97
215
|
```typescript
|
|
98
|
-
type Rule<T> = (value: T
|
|
99
|
-
type AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<
|
|
100
|
-
|
|
216
|
+
type Rule<T> = (value: T) => ValidationIssue | null;
|
|
217
|
+
type AsyncRule<T> = (value: T, ctx: { signal: AbortSignal }) => Promise<ValidationIssue | null>;
|
|
218
|
+
|
|
219
|
+
interface ValidationScope<T> {
|
|
220
|
+
readonly model: FormModel<T>;
|
|
221
|
+
// правило над несколькими полями: check получает снимок модели области
|
|
222
|
+
cross(handle: PathAwareSignal<unknown>, check: (snapshot: T) => ValidationIssue | null): void;
|
|
223
|
+
}
|
|
224
|
+
type ValidationDefinition<T> = (scope: ValidationScope<T>) => void;
|
|
101
225
|
```
|
|
102
226
|
|
|
103
|
-
Операторы (
|
|
227
|
+
Операторы (валидны только внутри прогона схемы):
|
|
104
228
|
|
|
105
229
|
| Оператор | Назначение |
|
|
106
230
|
|---|---|
|
|
107
231
|
| `validate(sig, rules: Rule<T>[])` | синхронные правила поля |
|
|
108
232
|
| `validateAsync(sig, rules: AsyncRule<T>[])` | асинхронные правила (раннер дожидается, прокидывает `AbortSignal`) |
|
|
109
233
|
| `validateWhen(cond: () => boolean, cb: () => void)` | условная ветка: правила внутри активны/гасятся по `cond` (не трогает enable — это поведение) |
|
|
110
|
-
| `cross(sig,
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
234
|
+
| `cross(sig, check)` из аргумента схемы | правило над несколькими полями; `check` получает снимок модели области, тип снимка выведен |
|
|
235
|
+
| `apply(model.$.group, schema)` | правила подформы: схема над типом группы подключается к ручке группы |
|
|
236
|
+
| `applyEach(model.$.items, schema)` | правила строк массива: схема над типом строки |
|
|
237
|
+
| `apply(...schemas: ValidationDefinition<T>[])` | композиция схем над той же моделью |
|
|
238
|
+
| `defineValidation<T>(fn): ValidationDefinition<T>` | тонкая identity-обёртка (типизация/discoverability) |
|
|
239
|
+
|
|
240
|
+
Прогон. В сборке — уровень формы (`bundle.validate()`, `bundle.validateStep(id)`,
|
|
241
|
+
`bundle.submission.submit(handler)`, см. [38-form-flow.md](./38-form-flow.md)). Без сборки — раннер
|
|
242
|
+
либо контроллер стратегии:
|
|
243
|
+
|
|
244
|
+
| Функция | Что делает |
|
|
245
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
246
|
+
| `validateModel(model, schema, { touch }?)` | сбор + разнос; `true` только при статусе `valid` |
|
|
247
|
+
| `runValidation(model, schema)` | только сбор: `Promise<ValidationResult>`, ноды не трогает |
|
|
248
|
+
| `applyValidationResult(result, { touch }?)` | разнос собранного результата по нодам формы |
|
|
249
|
+
| `createFormValidation(model, schema, options?)` | контроллер стратегии: `validate()`, `run(options?)`, `start()`, `dispose()` — см. [33-validation-strategy.md](./33-validation-strategy.md) |
|
|
115
250
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
251
|
+
```typescript
|
|
252
|
+
// Контроллер стратегии (@reformer/core/validation)
|
|
253
|
+
controller.validate(): Promise<boolean>; // нейтральная проверка: проверенные поля touched, фаза не меняется
|
|
254
|
+
controller.run(options?: FormValidationRunOptions): Promise<ValidationResult>;
|
|
255
|
+
interface FormValidationRunOptions {
|
|
256
|
+
touch?: true | 'all'; // 'all' — при отказе (invalid, error) тронуть всю форму
|
|
257
|
+
submit?: boolean; // попытка отправки: переводит afterFirstSubmit в живую фазу
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Хук без сборки (@reformer/core/react)
|
|
261
|
+
useFormValidation({ model, schema, strategy?, debounce?, liveAfterSubmit? }): {
|
|
262
|
+
submit: () => Promise<boolean>; // попытка отправки: при отказе раскрыта вся форма
|
|
263
|
+
validate: () => Promise<boolean>; // проверка без отправки: фаза afterFirstSubmit не меняется
|
|
264
|
+
isValidating: boolean;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
type ValidationStatus = 'valid' | 'invalid' | 'cancelled' | 'error';
|
|
270
|
+
|
|
271
|
+
interface ValidationResult {
|
|
272
|
+
readonly status: ValidationStatus;
|
|
273
|
+
/** Замечания по ручке поля `model.$.…`; путь — `handle.__path`. */
|
|
274
|
+
readonly issues: ReadonlyMap<PathAwareSignal<unknown>, readonly ValidationIssue[]>;
|
|
275
|
+
/** Правила, которые не вернули результат (сеть, исключение). */
|
|
276
|
+
readonly failures: readonly { handle: PathAwareSignal<unknown>; error: unknown }[];
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- `valid` — блокирующих замечаний нет (`severity: 'warning'` не блокирует);
|
|
281
|
+
- `invalid` — есть блокирующие замечания;
|
|
282
|
+
- `error` — async-правило не вернуло результат: сбой **блокирует**, на поле ставится замечание
|
|
283
|
+
`{ code: 'ruleFailed' }` («Не удалось проверить поле. Повторите попытку»), причина — в `failures`;
|
|
284
|
+
- `cancelled` — прогон вытеснен более новым по той же паре (модель, схема); такой результат не
|
|
285
|
+
разносится.
|
|
286
|
+
|
|
287
|
+
`validateModel` отвечает `false` на всё, кроме `valid`. Различить замечания, сбой и отмену —
|
|
288
|
+
`bundle.validate()` сборки либо `runValidation`:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const result = await bundle.validate();
|
|
292
|
+
if (result.status === 'error') showToast('Не удалось проверить форму, повторите попытку');
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Пока идут async-правила поля, его нода в состоянии `pending` (`form.email.pending.value`).
|
|
119
296
|
|
|
120
297
|
Кастомные правила, cross-field и async — в схеме:
|
|
121
298
|
|
|
122
299
|
```typescript
|
|
123
|
-
import { type
|
|
300
|
+
import { type ValidationIssue } from '@reformer/core';
|
|
124
301
|
import {
|
|
125
|
-
|
|
302
|
+
defineValidation, validate, validateAsync,
|
|
126
303
|
type Rule, type AsyncRule,
|
|
127
304
|
} from '@reformer/core/validation';
|
|
128
305
|
import { required } from '@reformer/core/validators';
|
|
129
306
|
|
|
130
|
-
|
|
307
|
+
interface Signup {
|
|
308
|
+
password: string;
|
|
309
|
+
confirm: string;
|
|
310
|
+
email: string;
|
|
311
|
+
}
|
|
131
312
|
|
|
132
|
-
//
|
|
313
|
+
// Правило значения (Rule<T>)
|
|
133
314
|
const strongPassword: Rule<string> = (value) =>
|
|
134
|
-
|
|
315
|
+
value.length < 8 ? { code: 'password.weak', params: { minLength: 8 } } : null;
|
|
135
316
|
|
|
136
|
-
//
|
|
137
|
-
const passwordsMatch = (
|
|
138
|
-
|
|
139
|
-
? { code: 'mismatch'
|
|
317
|
+
// Правило над несколькими полями: обычная функция над снимком модели
|
|
318
|
+
const passwordsMatch = (signup: Signup): ValidationIssue | null =>
|
|
319
|
+
signup.confirm && signup.password && signup.confirm !== signup.password
|
|
320
|
+
? { code: 'confirm.mismatch' }
|
|
140
321
|
: null;
|
|
141
322
|
|
|
142
|
-
// Async (AsyncRule<T>): получает { signal }
|
|
323
|
+
// Async (AsyncRule<T>): получает { signal }. Отклонённый промис — сбой правила: прогон получает
|
|
324
|
+
// статус `error`, поле — замечание `ruleFailed`. Если сетевой сбой не должен блокировать отправку,
|
|
325
|
+
// перехвати его в правиле и верни `null`.
|
|
143
326
|
const emailUnique: AsyncRule<string> = async (value, { signal }) => {
|
|
144
327
|
if (!value) return null;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
328
|
+
const response = await fetch(`/api/check-email?email=${encodeURIComponent(value)}`, { signal });
|
|
329
|
+
return (await response.json()).available
|
|
330
|
+
? null
|
|
331
|
+
: { code: 'email.taken' };
|
|
151
332
|
};
|
|
152
333
|
|
|
153
|
-
const signupValidation =
|
|
334
|
+
const signupValidation = defineValidation<Signup>(({ model, cross }) => {
|
|
154
335
|
validate(model.$.password, [required(), strongPassword]);
|
|
155
336
|
validate(model.$.confirm, [required()]);
|
|
156
|
-
cross(model.$.confirm, passwordsMatch);
|
|
337
|
+
cross(model.$.confirm, passwordsMatch); // замечание вешается на confirm
|
|
157
338
|
validateAsync(model.$.email, [emailUnique]);
|
|
158
339
|
});
|
|
340
|
+
// Тексты кодов 'password.weak', 'confirm.mismatch', 'email.taken' — в словаре формы
|
|
341
|
+
// (defineIssueMessages), см. 36-issue-messages.md
|
|
159
342
|
```
|
|
160
343
|
|
|
344
|
+
> `cross` берётся только из аргумента схемы: из `@reformer/core/validation` он не импортируется.
|
|
345
|
+
|
|
161
346
|
### Conditional & array validation
|
|
162
347
|
|
|
163
|
-
Условные
|
|
348
|
+
Условные ветки, подформы и массивы — операторы внутри схемы (не узлы дерева):
|
|
164
349
|
|
|
165
350
|
- **условная ветка** `validateWhen(() => cond, () => { validate(...) })` — правила внутри активны только
|
|
166
|
-
при истинном условии; при ложном
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
- **композиция** `apply(...schemas)` — объединяет
|
|
351
|
+
при истинном условии; при ложном замечания полей ветки гасятся. Enable/сброс поля — дело поведения;
|
|
352
|
+
- **подформа** `apply(model.$.group, groupRules)` — схема над типом группы; принимает и массив ручек;
|
|
353
|
+
- **массив** `applyEach(model.$.items, rowRules)` — схема над типом строки применяется к каждой строке;
|
|
354
|
+
- **композиция** `apply(...schemas)` — объединяет схемы над той же моделью.
|
|
355
|
+
|
|
356
|
+
У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
|
|
357
|
+
`cross` получает её снимок.
|
|
170
358
|
|
|
171
359
|
```typescript
|
|
172
360
|
import {
|
|
173
|
-
|
|
361
|
+
apply, applyEach, defineValidation, validate, validateWhen,
|
|
174
362
|
} from '@reformer/core/validation';
|
|
175
363
|
import { required, min } from '@reformer/core/validators';
|
|
176
364
|
|
|
177
365
|
// Условная валидация — ипотечная ветка активна только при loanType === 'mortgage'
|
|
178
|
-
const
|
|
366
|
+
const loanRules = defineValidation<LoanForm>(({ model }) => {
|
|
179
367
|
validate(model.$.loanType, [required()]);
|
|
180
368
|
validateWhen(
|
|
181
369
|
() => model.loanType === 'mortgage',
|
|
@@ -186,64 +374,46 @@ const step1 = defineValidationSchema<LoanForm>(({ model }) => {
|
|
|
186
374
|
);
|
|
187
375
|
});
|
|
188
376
|
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
cross(im.$.income, () =>
|
|
195
|
-
item.income < item.loanShare ? { code: 'tooLow', message: 'Доход ниже доли' } : null);
|
|
196
|
-
});
|
|
377
|
+
// Правила строки массива — отдельная схема над типом строки
|
|
378
|
+
const coBorrowerRules = defineValidation<CoBorrower>(({ model, cross }) => {
|
|
379
|
+
validate(model.$.income, [required(), min(10_000)]);
|
|
380
|
+
cross(model.$.income, (coBorrower) =>
|
|
381
|
+
coBorrower.income < coBorrower.loanShare ? { code: 'income.tooLow' } : null);
|
|
197
382
|
});
|
|
198
383
|
|
|
199
|
-
|
|
200
|
-
|
|
384
|
+
const additionalRules = defineValidation<LoanForm>(({ model }) => {
|
|
385
|
+
applyEach(model.$.coBorrowers, coBorrowerRules);
|
|
386
|
+
});
|
|
201
387
|
```
|
|
202
388
|
|
|
203
|
-
|
|
389
|
+
Пошаговая и полная валидация — поток шагов сборки: `flow.steps[].id` — `selector` шага в схеме,
|
|
390
|
+
`validation.extras` — правила вне шагов.
|
|
204
391
|
|
|
205
392
|
```typescript
|
|
206
|
-
const
|
|
393
|
+
const loan = createForm<LoanForm>({
|
|
394
|
+
initial,
|
|
395
|
+
schema: loanSchema,
|
|
396
|
+
validation: { extras: crossStepRules }, // правила всей формы — проверяются только целиком
|
|
397
|
+
flow: {
|
|
398
|
+
steps: [
|
|
399
|
+
{ id: 'loan', rules: loanRules }, // id = selector узла шага
|
|
400
|
+
{ id: 'additional', rules: additionalRules },
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
});
|
|
207
404
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
213
|
-
}
|
|
405
|
+
await loan.validateStep('loan'); // правила одного шага → ValidationResult
|
|
406
|
+
await loan.validate(); // все шаги + extras → ValidationResult
|
|
407
|
+
await loan.flow?.next(); // проверить текущий шаг и перейти → FlowTransition
|
|
408
|
+
loan.validating.value; // идёт ли проверка — полная либо шага
|
|
214
409
|
```
|
|
215
410
|
|
|
216
|
-
|
|
217
|
-
`@reformer/core/validation` — это публичный API, а не локальный сахар примера.
|
|
411
|
+
Подробно — [13-multi-step.md](./13-multi-step.md) и [38-form-flow.md](./38-form-flow.md).
|
|
218
412
|
|
|
219
413
|
### Behaviors
|
|
220
414
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
computeFrom(sources: ReadonlySignal[], target: Signal, fn: (...vals) => R, options?: { when?: (...vals) => boolean }): () => void
|
|
225
|
-
copyFrom(source: ReadonlySignal, target: Signal, options?: { when?: () => boolean; transform?: (v) => v }): () => void
|
|
226
|
-
watchField(source: ReadonlySignal, cb: (value) => void, options?: { immediate?: boolean }): () => void
|
|
227
|
-
enableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void
|
|
228
|
-
disableWhen(target: ReadonlySignal, condition: () => boolean, options?: { resetOnDisable?: boolean }): () => void
|
|
229
|
-
transformValue(target: Signal, transformer: (value) => value): () => void
|
|
230
|
-
resetWhen(target: Signal, condition: () => boolean, options?: { resetValue?: T }): () => void
|
|
231
|
-
syncFields(a: Signal, b: Signal, options?: { transform?: (v) => v }): () => void
|
|
232
|
-
revalidateWhen(deps: ReadonlySignal[], revalidate: () => void): () => void
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
import { computeFrom, enableWhen, copyFrom } from '@reformer/core';
|
|
237
|
-
|
|
238
|
-
const cleanups = [
|
|
239
|
-
computeFrom([model.$.price, model.$.quantity], model.$.total, (p, q) => p * q),
|
|
240
|
-
enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true }),
|
|
241
|
-
copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
|
|
242
|
-
];
|
|
243
|
-
// при teardown: cleanups.forEach((c) => c());
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Декларативный DSL из `@reformer/core/behaviors`** (регистрирует cleanup сам, передаётся в `createForm({ behavior })`):
|
|
415
|
+
**Поведение формы — декларативный DSL из `@reformer/core/behaviors`** (регистрирует cleanup сам,
|
|
416
|
+
передаётся в `createForm({ behavior })`):
|
|
247
417
|
|
|
248
418
|
```typescript
|
|
249
419
|
import { defineFormBehavior, compute, enableWhen, onChange } from '@reformer/core/behaviors';
|
|
@@ -256,15 +426,66 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
|
256
426
|
});
|
|
257
427
|
});
|
|
258
428
|
|
|
259
|
-
const form = createForm({
|
|
429
|
+
const { form } = createForm<MyForm>({ initial, schema, behavior });
|
|
260
430
|
```
|
|
261
431
|
|
|
432
|
+
Область поведения — `{ model, form, schema, flow }`: `schema` адресует узлы своей области
|
|
433
|
+
(`schema.node(selector)`), `flow` — поток шагов сборки (без `flow` в конфиге — `undefined`).
|
|
434
|
+
|
|
262
435
|
DSL-операторы: `compute` (auto-tracking, без явного списка источников), `computeFrom`, `copyFrom`,
|
|
263
436
|
`onChange` (реакция на изменение; `{ debounce, immediate }`, 2-й аргумент колбэка — `{ signal }` AbortSignal),
|
|
264
437
|
`enableWhen`/`disableWhen`, `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`,
|
|
265
|
-
`apply` (
|
|
438
|
+
`apply` (поведение подформы), `applyEach` (поведение строки массива), `exclusiveFlag`, `aggregateInto`;
|
|
439
|
+
операторы узлов схемы — `hideWhen`, `onComponentEvent`, `onMount`, `onUnmount`, `onInit`, `renderEffect`.
|
|
266
440
|
См. `20-compute-vs-watch.md`.
|
|
267
441
|
|
|
442
|
+
- `copyFrom` групп разной формы — как `set` группы модели: поля цели, которых нет в значении
|
|
443
|
+
источника, остаются прежними; в dev — одно предупреждение на оператор. Значение цели целиком
|
|
444
|
+
собирает `transform`.
|
|
445
|
+
- `exclusiveFlag`: выбор строки (флаг стал `true`) снимает флаг у остальных. Строка, пришедшая с
|
|
446
|
+
`true` (начальные данные, добавленная), прежний выбор не снимает — флаг сбрасывается у неё; из
|
|
447
|
+
нескольких пришедших сразу остаётся первая.
|
|
448
|
+
- Ручка не того вида — в dev ошибка с путём: `applyEach` / `exclusiveFlag` / `aggregateInto` ждут
|
|
449
|
+
массив, `apply` — группу. `enableWhen` / `disableWhen` на поле без ноды формы ничего не делают;
|
|
450
|
+
в dev — предупреждение.
|
|
451
|
+
|
|
452
|
+
**Примитивы над сигналами — `@reformer/core/model`** (принимают сигналы, возвращают cleanup; в корне
|
|
453
|
+
`@reformer/core` их нет). Нужны вне формы — там, где нет области поведения:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
computeFrom(sources: ReadonlySignal[], target: Signal, fn: (...vals) => R, options?: { when?: (...vals) => boolean }): () => void
|
|
457
|
+
copyFrom(source: ReadonlySignal, target: Signal, options?: { when?: () => boolean; transform?: (v) => v }): () => void
|
|
458
|
+
watchField(source: ReadonlySignal, cb: (value) => void, options?: { immediate?: boolean }): () => void
|
|
459
|
+
transformValue(target: Signal, transformer: (value) => value): () => void
|
|
460
|
+
resetWhen(target: Signal, condition: () => boolean, options?: { resetValue?: T }): () => void // resetValue обязателен, если null не в типе поля
|
|
461
|
+
syncFields(a: Signal, b: Signal, options?: { transform?: (v) => v }): () => void
|
|
462
|
+
revalidateWhen(deps: ReadonlySignal[], revalidate: () => void): () => void
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Утилиты значений там же, в `@reformer/core/model`:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// сравнение значений модели: объекты и массивы — по содержимому, даты — по времени, NaN равен себе;
|
|
469
|
+
// на нём стоят isDirty и сверка «модель изменилась, пока шла проверка» при отправке и переходе шага
|
|
470
|
+
deepEqual(first: unknown, second: unknown): boolean
|
|
471
|
+
// дождаться отложенных записей поведения текущего тика (onChange, resetOnDisable);
|
|
472
|
+
// undefined — ждать нечего
|
|
473
|
+
deferredWritesSettled(): Promise<void> | undefined
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
import { computeFrom, copyFrom } from '@reformer/core/model';
|
|
478
|
+
|
|
479
|
+
const cleanups = [
|
|
480
|
+
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, quantity) => price * quantity),
|
|
481
|
+
copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail === true }),
|
|
482
|
+
];
|
|
483
|
+
// при teardown: cleanups.forEach((cleanup) => cleanup());
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
`enableWhen` / `disableWhen` работают с состоянием ноды формы и существуют только как операторы
|
|
487
|
+
поведения (`@reformer/core/behaviors`).
|
|
488
|
+
|
|
268
489
|
Поведение **не владеет** валидацией — это отдельный слой. Мост «поведение инициирует валидацию» — через
|
|
269
490
|
`revalidateWhen`, который просто вызывает внешний раннер валидации:
|
|
270
491
|
|