@reformer/core 10.2.0 → 11.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -68
- package/dist/behaviors-value-D05z3my6.js +109 -0
- package/dist/behaviors.d.ts +1 -1
- package/dist/behaviors.js +376 -211
- package/dist/create-model-DPSG3I0O.js +30 -0
- package/dist/date-utils-90-qZbse.js +43 -0
- package/dist/en-HP8XEych.js +44 -0
- package/dist/form/aggregate-signals.d.ts +21 -63
- package/dist/form/behaviors/collections.d.ts +84 -0
- package/dist/form/behaviors/context.d.ts +56 -0
- package/dist/form/behaviors/index.d.ts +30 -0
- package/dist/form/behaviors/internals.d.ts +41 -0
- package/dist/form/{behaviors-node.d.ts → behaviors/node.d.ts} +28 -5
- package/dist/form/behaviors/operators.d.ts +59 -0
- package/dist/form/behaviors/schema-operators.d.ts +83 -0
- package/dist/form/behaviors/types.d.ts +37 -0
- package/dist/form/create-form.d.ts +59 -70
- package/dist/form/flow/flow.d.ts +87 -0
- package/dist/form/form-bundle.d.ts +153 -0
- package/dist/form/form-level.d.ts +121 -0
- package/dist/form/form-proxy-builder.d.ts +6 -22
- package/dist/form/index.d.ts +23 -27
- package/dist/form/nodes/form-array-node.d.ts +100 -0
- package/dist/form/nodes/form-control-node.d.ts +73 -0
- package/dist/form/nodes/form-group-node.d.ts +66 -0
- package/dist/form/nodes/node-kind.d.ts +29 -0
- package/dist/form/nodes/state/availability.d.ts +6 -0
- package/dist/form/nodes/state/component.d.ts +6 -0
- package/dist/form/nodes/state/index.d.ts +12 -0
- package/dist/form/nodes/state/interaction.d.ts +13 -0
- package/dist/form/nodes/state/issues.d.ts +9 -0
- package/dist/form/nodes/state/pending.d.ts +6 -0
- package/dist/form/schema/compile.d.ts +59 -0
- package/dist/form/schema/compiled.d.ts +143 -0
- package/dist/form/schema/row-key.d.ts +11 -0
- package/dist/form/schema-controller.d.ts +159 -0
- package/dist/form/schema-subtree.d.ts +28 -0
- package/dist/form/signal-node-registry.d.ts +27 -22
- package/dist/form/submission.d.ts +93 -0
- package/dist/form/type-guards.d.ts +14 -73
- package/dist/form/types/contracts.d.ts +21 -91
- package/dist/form/types/field-config.d.ts +27 -0
- package/dist/form/types/index.d.ts +5 -91
- package/dist/form/types/nodes.d.ts +159 -0
- package/dist/form/types/schema-node.d.ts +148 -50
- package/dist/form/types/validation-schema.d.ts +24 -41
- package/dist/form/validation/context.d.ts +67 -0
- package/dist/form/validation/index.d.ts +24 -28
- package/dist/form/validation/operators.d.ts +101 -0
- package/dist/form/validation/run.d.ts +120 -0
- package/dist/form/validation/strategy.d.ts +89 -0
- package/dist/form/validation/types.d.ts +83 -0
- package/dist/form/validators/date-utils.d.ts +42 -0
- package/dist/form/validators/email.d.ts +23 -0
- package/dist/form/{validation/validators → validators}/file-type.d.ts +10 -10
- package/dist/form/{validation/validators → validators}/file-utils.d.ts +3 -2
- package/dist/form/validators/future-date.d.ts +23 -0
- package/dist/form/{validation/validators → validators}/index.d.ts +7 -4
- package/dist/form/validators/integer.d.ts +22 -0
- package/dist/form/validators/is-date.d.ts +24 -0
- package/dist/form/validators/is-number.d.ts +25 -0
- package/dist/form/validators/max-age.d.ts +28 -0
- package/dist/form/validators/max-date.d.ts +25 -0
- package/dist/form/{validation/validators → validators}/max-file-size.d.ts +10 -10
- package/dist/form/validators/max-files.d.ts +26 -0
- package/dist/form/validators/max-length.d.ts +33 -0
- package/dist/form/validators/max-total-file-size.d.ts +26 -0
- package/dist/form/validators/max.d.ts +26 -0
- package/dist/form/validators/min-age.d.ts +27 -0
- package/dist/form/validators/min-date.d.ts +25 -0
- package/dist/form/{validation/validators → validators}/min-file-size.d.ts +10 -10
- package/dist/form/validators/min-files.d.ts +27 -0
- package/dist/form/validators/min-length.d.ts +33 -0
- package/dist/form/validators/min.d.ts +26 -0
- package/dist/form/validators/multiple-of.d.ts +25 -0
- package/dist/form/validators/non-negative.d.ts +22 -0
- package/dist/form/validators/non-zero.d.ts +22 -0
- package/dist/form/validators/past-date.d.ts +23 -0
- package/dist/form/validators/pattern.d.ts +26 -0
- package/dist/form/{validation/validators → validators}/phone.d.ts +11 -11
- package/dist/form/validators/required.d.ts +29 -0
- package/dist/form/validators/url.d.ts +45 -0
- package/dist/form/validators/validation-issue.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +54 -0
- package/dist/i18n/en.json.d.ts +49 -0
- package/dist/i18n/loader.d.ts +71 -0
- package/dist/i18n/locale-store.d.ts +46 -0
- package/dist/i18n/locale.d.ts +90 -0
- package/dist/i18n/message-format.d.ts +136 -0
- package/dist/i18n/resolve-localized.d.ts +17 -0
- package/dist/i18n/ru.json.d.ts +49 -0
- package/dist/i18n/translator.d.ts +61 -0
- package/dist/i18n/validation-message.d.ts +53 -0
- package/dist/i18n.d.ts +26 -0
- package/dist/i18n.js +86 -0
- package/dist/index.d.ts +18 -6
- package/dist/index.js +932 -2779
- package/dist/loader-BWkigpVe.js +52 -0
- package/dist/locale/en.d.ts +2 -0
- package/dist/locale/en.js +5 -0
- package/dist/locale/en.json +45 -0
- package/dist/locale/index.d.ts +18 -0
- package/dist/locale/ru.d.ts +2 -0
- package/dist/locale/ru.js +44 -0
- package/dist/locale/ru.json +45 -0
- package/dist/locale-CG54AU2Q.js +219 -0
- package/dist/locale.d.ts +2 -0
- package/dist/locale.js +9 -0
- package/dist/{state → model}/behaviors-value.d.ts +24 -6
- package/dist/{state/form-model.d.ts → model/create-model.d.ts} +18 -4
- package/dist/{state → model}/derived-registry.d.ts +1 -1
- package/dist/model/index.d.ts +42 -0
- package/dist/model/model-nodes.d.ts +190 -0
- package/dist/model/model-signals-proxy.d.ts +63 -0
- package/dist/model/model-value-proxy.d.ts +84 -0
- package/dist/{state → model}/safe-effect.d.ts +50 -2
- package/dist/model/types.d.ts +239 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +27 -0
- package/dist/node-kind-C-r6ZMdo.js +15 -0
- package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
- package/dist/platforms/react/field-binding/field-handle.d.ts +53 -0
- package/dist/platforms/react/form-bundle-context.d.ts +27 -0
- package/dist/{form → platforms/react}/hooks/types.d.ts +60 -38
- package/dist/platforms/react/hooks/use-form-bundle.d.ts +41 -0
- package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
- package/dist/{form → platforms/react}/hooks/use-form-validation.d.ts +13 -7
- package/dist/{form → platforms/react}/hooks/useArrayLength.d.ts +6 -6
- package/dist/{form → platforms/react}/hooks/useFormControl.d.ts +7 -9
- package/dist/{form → platforms/react}/hooks/useFormControlValue.d.ts +18 -18
- package/dist/{form → platforms/react}/hooks/useSignalSubscription.d.ts +9 -1
- package/dist/platforms/react/i18n/context.d.ts +18 -0
- package/dist/platforms/react/i18n/hooks.d.ts +65 -0
- package/dist/platforms/react/i18n/provider.d.ts +59 -0
- package/dist/platforms/react/index.d.ts +30 -0
- package/dist/react.d.ts +22 -0
- package/dist/react.js +505 -0
- package/dist/runtime-token.d.ts +22 -0
- package/dist/safe-effect-BazLOv1g.js +669 -0
- package/dist/schema-controller-CpiO2JoK.js +143 -0
- package/dist/signals.d.ts +15 -9
- package/dist/signals.js +11 -6
- package/dist/strategy-DzvnsWLh.js +375 -0
- package/dist/type-guards-D-N_5Ddh.js +19 -0
- package/dist/validation-issue-B9mjI8jz.js +7 -0
- package/dist/validation-message-Ced9MGxs.js +194 -0
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +11 -165
- package/dist/validators/email.d.ts +1 -1
- package/dist/validators/email.js +5 -8
- package/dist/validators/future-date.d.ts +1 -1
- package/dist/validators/future-date.js +10 -13
- package/dist/validators/integer.d.ts +1 -1
- package/dist/validators/integer.js +4 -7
- package/dist/validators/is-date.d.ts +1 -1
- package/dist/validators/is-date.js +5 -8
- package/dist/validators/is-number.d.ts +1 -1
- package/dist/validators/is-number.js +4 -7
- package/dist/validators/max-age.d.ts +1 -1
- package/dist/validators/max-age.js +8 -12
- package/dist/validators/max-date.d.ts +1 -1
- package/dist/validators/max-date.js +10 -13
- package/dist/validators/max-length.d.ts +1 -1
- package/dist/validators/max-length.js +7 -10
- package/dist/validators/max.d.ts +1 -1
- package/dist/validators/max.js +4 -7
- package/dist/validators/min-age.d.ts +1 -1
- package/dist/validators/min-age.js +10 -11
- package/dist/validators/min-date.d.ts +1 -1
- package/dist/validators/min-date.js +8 -11
- package/dist/validators/min-length.d.ts +1 -1
- package/dist/validators/min-length.js +4 -7
- package/dist/validators/min.d.ts +1 -1
- package/dist/validators/min.js +4 -7
- package/dist/validators/multiple-of.d.ts +1 -1
- package/dist/validators/multiple-of.js +11 -10
- package/dist/validators/non-negative.d.ts +1 -1
- package/dist/validators/non-negative.js +2 -5
- package/dist/validators/non-zero.d.ts +1 -1
- package/dist/validators/non-zero.js +4 -7
- package/dist/validators/past-date.d.ts +1 -1
- package/dist/validators/past-date.js +10 -13
- package/dist/validators/pattern.d.ts +1 -1
- package/dist/validators/pattern.js +5 -8
- package/dist/validators/phone.d.ts +1 -1
- package/dist/validators/phone.js +10 -11
- package/dist/validators/required.d.ts +1 -1
- package/dist/validators/required.js +4 -7
- package/dist/validators/url.d.ts +1 -1
- package/dist/validators/url.js +48 -17
- package/dist/validators.d.ts +1 -5
- package/dist/validators.js +98 -154
- package/docs/llms/01-api-reference.md +89 -0
- package/docs/llms/02-quick-start.md +181 -0
- package/docs/llms/03-api-signatures.md +494 -0
- package/docs/llms/04-common-patterns.md +204 -0
- package/docs/llms/05-common-mistakes.md +192 -0
- package/docs/llms/06-troubleshooting.md +16 -0
- package/docs/llms/07-complete-import.md +130 -0
- package/docs/llms/08-form-types.md +54 -0
- package/docs/llms/09-formschema.md +191 -0
- package/docs/llms/10-arrays.md +241 -0
- package/docs/llms/11-async-watchfield.md +53 -0
- package/docs/llms/12-array-cleanup.md +31 -0
- package/docs/llms/13-multi-step.md +162 -0
- package/docs/llms/14-extended-mistakes.md +101 -0
- package/docs/llms/15-project-structure.md +279 -0
- package/docs/llms/16-ui-components.md +229 -0
- package/docs/llms/17-nonexistent-api.md +278 -0
- package/docs/llms/18-conditional-fields.md +222 -0
- package/docs/llms/19-reading-values.md +100 -0
- package/docs/llms/20-compute-vs-watch.md +153 -0
- package/docs/llms/21-array-operations.md +163 -0
- package/docs/llms/22-cycle-detection.md +86 -0
- package/docs/llms/23-copy-from.md +141 -0
- package/docs/llms/24-sync-fields.md +115 -0
- package/docs/llms/25-reset-when.md +122 -0
- package/docs/llms/26-transform-value.md +136 -0
- package/docs/llms/27-revalidate-when.md +133 -0
- package/docs/llms/28-submit-and-reset.md +409 -0
- package/docs/llms/29-async-preload.md +203 -0
- package/docs/llms/30-type-safety-recipes.md +213 -0
- package/docs/llms/31-async-validator-debounce.md +178 -0
- package/docs/llms/32-async-options-loading.md +108 -0
- package/docs/llms/33-validation-strategy.md +305 -0
- package/docs/llms/34-i18n.md +298 -0
- package/docs/llms/35-unified-contract.md +309 -0
- package/docs/llms/36-issue-messages.md +284 -0
- package/docs/llms/37-compiled-schema.md +332 -0
- package/docs/llms/38-form-flow.md +396 -0
- package/llms-index.json +1 -0
- package/llms.txt +10014 -3826
- package/package.json +77 -15
- package/dist/behaviors-node-DGEFMTQu.js +0 -18
- package/dist/behaviors-value-C7dSEhCc.js +0 -134
- package/dist/date-utils-dTT_x_Be.js +0 -31
- package/dist/derived-registry-CluZCQ7U.js +0 -16
- package/dist/form/behaviors.d.ts +0 -133
- package/dist/form/error-handler.d.ts +0 -166
- package/dist/form/factories/index.d.ts +0 -6
- package/dist/form/factories/node-factory.d.ts +0 -188
- package/dist/form/form-submitter.d.ts +0 -125
- package/dist/form/nodes/array-node.d.ts +0 -360
- package/dist/form/nodes/field-node.d.ts +0 -294
- package/dist/form/nodes/form-node.d.ts +0 -340
- package/dist/form/nodes/group-node.d.ts +0 -288
- package/dist/form/nodes/model-array-node.d.ts +0 -121
- package/dist/form/status-machine.d.ts +0 -153
- package/dist/form/types/deep-schema.d.ts +0 -116
- package/dist/form/types/form-proxy.d.ts +0 -141
- package/dist/form/unique-id.d.ts +0 -53
- package/dist/form/validation/validators/date-utils.d.ts +0 -30
- package/dist/form/validation/validators/email.d.ts +0 -23
- package/dist/form/validation/validators/future-date.d.ts +0 -23
- package/dist/form/validation/validators/integer.d.ts +0 -22
- package/dist/form/validation/validators/is-date.d.ts +0 -23
- package/dist/form/validation/validators/is-number.d.ts +0 -24
- package/dist/form/validation/validators/max-age.d.ts +0 -25
- package/dist/form/validation/validators/max-date.d.ts +0 -25
- package/dist/form/validation/validators/max-files.d.ts +0 -26
- package/dist/form/validation/validators/max-length.d.ts +0 -27
- package/dist/form/validation/validators/max-total-file-size.d.ts +0 -26
- package/dist/form/validation/validators/max.d.ts +0 -25
- package/dist/form/validation/validators/min-age.d.ts +0 -25
- package/dist/form/validation/validators/min-date.d.ts +0 -25
- package/dist/form/validation/validators/min-files.d.ts +0 -26
- package/dist/form/validation/validators/min-length.d.ts +0 -27
- package/dist/form/validation/validators/min.d.ts +0 -25
- package/dist/form/validation/validators/multiple-of.d.ts +0 -24
- package/dist/form/validation/validators/non-negative.d.ts +0 -22
- package/dist/form/validation/validators/non-zero.d.ts +0 -22
- package/dist/form/validation/validators/past-date.d.ts +0 -23
- package/dist/form/validation/validators/pattern.d.ts +0 -33
- package/dist/form/validation/validators/required.d.ts +0 -32
- package/dist/form/validation/validators/url.d.ts +0 -43
- package/dist/form/validation-schema.d.ts +0 -106
- package/dist/form/validation-strategy.d.ts +0 -55
- package/dist/form/validators.d.ts +0 -6
- package/dist/form-model-DYCzGGKF.js +0 -286
- package/dist/state/index.d.ts +0 -31
- package/dist/state/subscription-manager.d.ts +0 -180
- package/dist/state/types.d.ts +0 -144
- package/dist/state.d.ts +0 -2
- package/dist/state.js +0 -21
- package/dist/subscription-manager-705bls97.js +0 -171
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
## 32. ASYNC OPTIONS LOADING
|
|
2
|
+
|
|
3
|
+
Для динамической подгрузки опций dropdown'а по значению другого поля (`region` → `city options`,
|
|
4
|
+
`carBrand` → `carModel options`) — используй `onChange` из `@reformer/core/behaviors` +
|
|
5
|
+
`form.field.updateComponentProps({ options })`. `onChange` даёт debounce и AbortSignal из коробки.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
9
|
+
|
|
10
|
+
type CityOption = { value: string; label: string };
|
|
11
|
+
|
|
12
|
+
async function fetchCitiesByRegion(region: string, opts?: { signal?: AbortSignal }): Promise<CityOption[]> {
|
|
13
|
+
const res = await fetch(`/api/cities?region=${encodeURIComponent(region)}`, { signal: opts?.signal });
|
|
14
|
+
return res.json();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
18
|
+
onChange(
|
|
19
|
+
model.$.registrationAddress.region,
|
|
20
|
+
async (region, { signal }) => {
|
|
21
|
+
// сбросить зависимое поле при смене source
|
|
22
|
+
model.registrationAddress.city = '';
|
|
23
|
+
|
|
24
|
+
if (!region) {
|
|
25
|
+
form.registrationAddress.city.updateComponentProps({ options: [] });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// (опционально) loading-состояние
|
|
30
|
+
form.registrationAddress.city.updateComponentProps({ loading: true, options: [] });
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const options = await fetchCitiesByRegion(region, { signal }); // отмена устаревших
|
|
34
|
+
form.registrationAddress.city.updateComponentProps({ loading: false, options });
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if ((e as Error).name === 'AbortError') return; // устаревший запрос — молча выходим
|
|
37
|
+
form.registrationAddress.city.updateComponentProps({ loading: false, options: [] });
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{ debounce: 300 }
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Lifecycle
|
|
46
|
+
|
|
47
|
+
1. `onChange` подписан на изменения `model.$.region`.
|
|
48
|
+
2. Debounce 300 мс (не fetch на каждое нажатие клавиши).
|
|
49
|
+
3. Async `fetchCitiesByRegion(region, { signal })` — `signal` отменяет устаревший запрос при
|
|
50
|
+
следующей смене значения (защита от race).
|
|
51
|
+
4. Результат — `updateComponentProps({ options })` на target-поле.
|
|
52
|
+
5. UI (Select / Combobox) обновляется через сигнал `componentProps`.
|
|
53
|
+
|
|
54
|
+
### Переиспользуемый оператор
|
|
55
|
+
|
|
56
|
+
Как в монорепо — собираешь свой оператор поверх `onChange`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { onChange } from '@reformer/core/behaviors';
|
|
60
|
+
import type { ReadonlySignal } from '@reformer/core/behaviors';
|
|
61
|
+
|
|
62
|
+
function loadOptionsOn<TValue, TOption>(
|
|
63
|
+
source: ReadonlySignal<TValue>,
|
|
64
|
+
target: { updateComponentProps(p: Record<string, unknown>): void; reset?: () => void },
|
|
65
|
+
fetcher: (value: TValue, opts?: { signal?: AbortSignal }) => Promise<TOption[]>,
|
|
66
|
+
options: { debounce?: number; resetTarget?: boolean } = {}
|
|
67
|
+
): void {
|
|
68
|
+
const { debounce = 300, resetTarget = false } = options;
|
|
69
|
+
onChange(
|
|
70
|
+
source,
|
|
71
|
+
async (value, { signal }) => {
|
|
72
|
+
if (resetTarget) target.reset?.();
|
|
73
|
+
if (!value) { target.updateComponentProps({ options: [] }); return; }
|
|
74
|
+
try {
|
|
75
|
+
const data = await fetcher(value, { signal });
|
|
76
|
+
target.updateComponentProps({ options: data });
|
|
77
|
+
} catch {
|
|
78
|
+
target.updateComponentProps({ options: [] });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{ debounce }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Usage:
|
|
86
|
+
loadOptionsOn(model.$.carBrand, form.carModel, fetchCarModels, { resetTarget: true });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Common patterns
|
|
90
|
+
|
|
91
|
+
- **Debounce 300–500 мс** — баланс UX и rate-limit.
|
|
92
|
+
- **Reset target value** — при смене source очисти зависимое поле (`model.city = ''`), чтобы не
|
|
93
|
+
остался устаревший выбор.
|
|
94
|
+
- **Loading state** — `updateComponentProps({ loading: true })` пока идёт fetch (компонент должен поддержать).
|
|
95
|
+
- **Cancellation** — `onChange` даёт `{ signal }` (AbortSignal); передавай его в `fetch` и
|
|
96
|
+
игнорируй `AbortError`.
|
|
97
|
+
|
|
98
|
+
### Initial load (preload at form mount)
|
|
99
|
+
|
|
100
|
+
Для загрузки опций при инициализации формы — external hook + `model.set` + `updateComponentProps`
|
|
101
|
+
(см. [29-async-preload.md](29-async-preload.md)). `onChange` не срабатывает на init (по умолчанию
|
|
102
|
+
`immediate: false`); для запуска сразу передай `{ immediate: true }`.
|
|
103
|
+
|
|
104
|
+
### See also
|
|
105
|
+
|
|
106
|
+
- [11-async-watchfield.md](11-async-watchfield.md) — общий паттерн `onChange`
|
|
107
|
+
- [29-async-preload.md](29-async-preload.md) — preload данных при инициализации
|
|
108
|
+
- [31-async-validator-debounce.md](31-async-validator-debounce.md) — async **валидация** (не options)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# useFormValidation — Единый выбор стратегии валидации
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Одна декларативная точка выбора «когда прогонять схему валидации» вместо ручной разводки
|
|
6
|
+
`markAsTouched` + `validateModel` + `revalidateWhen` + `useState(pending)`. Аддитивный слой над
|
|
7
|
+
функциональной схемой (`@reformer/core/validation`): переиспользует раннер схемы как
|
|
8
|
+
ЕДИНСТВЕННЫЙ движок (разнос ошибок по нодам, отмена устаревших прогонов, дедуп по `(model, schema)`),
|
|
9
|
+
второго движка не заводит. Других триггеров валидации нет: у ноды поля собственных валидаторов,
|
|
10
|
+
`updateOn` и `debounce` не бывает — когда запускать проверку, решает только стратегия.
|
|
11
|
+
|
|
12
|
+
Стратегия определяет только момент запуска и раскрытие ошибок — сами правила остаются в отдельной
|
|
13
|
+
`defineValidation<T>(({ model }) => …)`.
|
|
14
|
+
|
|
15
|
+
## API
|
|
16
|
+
|
|
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 потребителей.
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
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)
|
|
42
|
+
function useFormValidation<T>(args: {
|
|
43
|
+
model: FormModel<T>;
|
|
44
|
+
schema: ValidationDefinition<T>; // СТАБИЛЬНАЯ ссылка (module-level const / useMemo)
|
|
45
|
+
strategy?: ValidationStrategyKind; // default 'submit'
|
|
46
|
+
debounce?: number; // мс, для live-фаз (change / live-часть afterFirstSubmit)
|
|
47
|
+
liveAfterSubmit?: 'change' | 'blur'; // default 'change'
|
|
48
|
+
}): {
|
|
49
|
+
submit: () => Promise<boolean>; // попытка отправки: при отказе раскрыта вся форма, afterFirstSubmit → живая фаза
|
|
50
|
+
validate: () => Promise<boolean>; // проверка без отправки: раскрыты проверенные поля, фаза не меняется
|
|
51
|
+
isValidating: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// headless (@reformer/core/validation)
|
|
55
|
+
type ValidationStrategyKind = 'submit' | 'blur' | 'change' | 'afterFirstSubmit';
|
|
56
|
+
interface ValidationStrategyOptions {
|
|
57
|
+
strategy?: ValidationStrategyKind;
|
|
58
|
+
debounce?: number;
|
|
59
|
+
liveAfterSubmit?: 'change' | 'blur';
|
|
60
|
+
}
|
|
61
|
+
interface FormValidationRunOptions {
|
|
62
|
+
touch?: true | 'all'; // true (default) — проверенные поля; 'all' — при отказе ещё и вся форма
|
|
63
|
+
submit?: boolean; // прогон — попытка отправки: afterFirstSubmit → живая фаза
|
|
64
|
+
}
|
|
65
|
+
interface FormValidationController {
|
|
66
|
+
validate(): Promise<boolean>; // нейтральная проверка: touch:true, фазу afterFirstSubmit не трогает; true — только valid
|
|
67
|
+
run(options?: FormValidationRunOptions): Promise<ValidationResult>; // valid | invalid | error | cancelled
|
|
68
|
+
start(): () => void; // армит реактивные подписки (ТОЛЬКО на клиенте) → dispose
|
|
69
|
+
dispose(): void; // снять подписки и таймеры, отменить идущий прогон (он получит cancelled)
|
|
70
|
+
readonly isValidating: boolean;
|
|
71
|
+
readonly validating: ReadonlySignal<boolean>;
|
|
72
|
+
}
|
|
73
|
+
function createFormValidation<T>(
|
|
74
|
+
model: FormModel<T>,
|
|
75
|
+
schema: ValidationDefinition<T>,
|
|
76
|
+
options?: ValidationStrategyOptions,
|
|
77
|
+
): FormValidationController;
|
|
78
|
+
```
|
|
79
|
+
|
|
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`.
|
|
84
|
+
- `createFormValidation` ЧИСТА до `start()` (никаких подписок) → SSR-safe; `start()` возвращает
|
|
85
|
+
`dispose` и арминует триггеры только на клиенте.
|
|
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
|
+
|
|
102
|
+
### Стратегии
|
|
103
|
+
|
|
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 |
|
|
110
|
+
|
|
111
|
+
## Examples
|
|
112
|
+
|
|
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
|
|
139
|
+
|
|
140
|
+
Тихо до первой отправки, `submit()` раскрывает все замечания и переводит форму в живую фазу
|
|
141
|
+
(дальше проверка на ввод с задержкой 400 мс). `submit()` сам метит `touched` (при отказе — всю
|
|
142
|
+
форму) — ручной `markAsTouched` не нужен. `validate()` — кнопка «Проверить»: раскрывает замечания
|
|
143
|
+
проверенных полей, а форма остаётся в тихой фазе.
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
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';
|
|
150
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
151
|
+
|
|
152
|
+
type RegistrationData = { username: string; email: string; password: string };
|
|
153
|
+
|
|
154
|
+
// Схема — стабильный module-level const (иначе ломается дедуп раннера).
|
|
155
|
+
const registrationValidation = defineValidation<RegistrationData>(({ model }) => {
|
|
156
|
+
validate(model.$.username, [required(), minLength(3)]);
|
|
157
|
+
validate(model.$.email, [required(), email()]);
|
|
158
|
+
validate(model.$.password, [required(), minLength(8)]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function RegistrationForm() {
|
|
162
|
+
const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
|
|
163
|
+
const form = useMemo(() => createFormFromModel({ model }), [model]);
|
|
164
|
+
|
|
165
|
+
const { submit, validate, isValidating } = useFormValidation({
|
|
166
|
+
model,
|
|
167
|
+
schema: registrationValidation,
|
|
168
|
+
strategy: 'afterFirstSubmit',
|
|
169
|
+
debounce: 400,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const onSubmit = async (event: React.FormEvent) => {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
if (await submit()) await api.save(model.get()); // true = нет блокирующих ошибок
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<form onSubmit={onSubmit}>
|
|
179
|
+
<FormField control={form.username} />
|
|
180
|
+
<FormField control={form.email} />
|
|
181
|
+
<FormField control={form.password} />
|
|
182
|
+
<button type="button" onClick={() => void validate()}>
|
|
183
|
+
Проверить
|
|
184
|
+
</button>
|
|
185
|
+
<button type="submit" disabled={isValidating}>
|
|
186
|
+
Отправить
|
|
187
|
+
</button>
|
|
188
|
+
</form>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Визард — стратегия формы и проверка шага потоком
|
|
194
|
+
|
|
195
|
+
Живая стратегия — свойство формы, а не шага: она задаётся полем `validation` сборки и армируется
|
|
196
|
+
`useFormBundle`. Проверку шага делает поток: «Далее» визарда — `flow.next()`, он прогоняет правила
|
|
197
|
+
текущего шага и не пускает дальше при замечаниях. Отправка — `bundle.submission.submit` (все
|
|
198
|
+
шаги + `extras`). Живую фазу `afterFirstSubmit` включает только она: `flow.next()` и
|
|
199
|
+
`validateStep` фазу не переключают.
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
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
|
+
});
|
|
215
|
+
|
|
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
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Живая фаза прогоняет **полный** набор правил (все шаги + `extras`), а показ замечаний гейтится
|
|
230
|
+
`touched` / `dirty` полей: поля следующих шагов пользователь ещё не трогал — их замечания не видны.
|
|
231
|
+
Прежний per-step механизм (`defineSteps(…, { strategy })` + `useWizardStepValidation`) удалён.
|
|
232
|
+
|
|
233
|
+
### Headless / SSR / тесты — createFormValidation
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import { createFormValidation } from '@reformer/core/validation';
|
|
237
|
+
|
|
238
|
+
const controller = createFormValidation(model, registrationValidation, {
|
|
239
|
+
strategy: 'afterFirstSubmit',
|
|
240
|
+
debounce: 300,
|
|
241
|
+
});
|
|
242
|
+
const dispose = controller.start(); // ТОЛЬКО на клиенте (в React — из useEffect)
|
|
243
|
+
const checked = await controller.validate(); // проверка: замечания проверенных полей, фаза тихая
|
|
244
|
+
const sent = await controller.run({ touch: 'all', submit: true }); // попытка отправки → живая фаза
|
|
245
|
+
dispose();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Anti-patterns
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// ❌ schema пересоздаётся каждый рендер → контроллер и дедуп раннера ломаются
|
|
252
|
+
function MyForm() {
|
|
253
|
+
const schema = defineValidation<Form>(({ model }) => { … }); // новая ссылка на каждый рендер
|
|
254
|
+
useFormValidation({ model, schema, strategy: 'change' });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ✅ Стабильная ссылка: module-level const (или useMemo с пустыми deps)
|
|
258
|
+
const formValidation = defineValidation<Form>(({ model }) => { … });
|
|
259
|
+
function MyForm() {
|
|
260
|
+
useFormValidation({ model, schema: formValidation, strategy: 'change' });
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// ❌ updateOn / debounce / validators в узле схемы — таких ключей у узла нет, сборка их не читает
|
|
266
|
+
// { model: model.$.email, component: Input, updateOn: 'blur', validators: [required()] }
|
|
267
|
+
|
|
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' } }));
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// ❌ start() на сервере — арминует реактивные подписки при SSR
|
|
283
|
+
const controller = createFormValidation(model, schema, { strategy: 'change' });
|
|
284
|
+
controller.start(); // на сервере подписок быть не должно
|
|
285
|
+
|
|
286
|
+
// ✅ Фабрика чиста до start(); армить только на клиенте (useFormValidation делает это в useEffect)
|
|
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
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## See also
|
|
300
|
+
|
|
301
|
+
- [28-submit-and-reset.md](./28-submit-and-reset.md) — submit-флоу и `validateModel` (движок под стратегией)
|
|
302
|
+
- [27-revalidate-when.md](./27-revalidate-when.md) — ручные триггеры перевалидации (низкоуровневый примитив)
|
|
303
|
+
- [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
|
|
304
|
+
- [13-multi-step.md](./13-multi-step.md) — многошаговая форма: поток `flow`, `bundle.validateStep(id)`
|
|
305
|
+
- [38-form-flow.md](./38-form-flow.md) — уровень формы: `validate`, `submission`, поток шагов
|