@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,42 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
One form is one module folder. **Default layout — flat minimalist:** every concern is a single
|
|
4
4
|
file at the module root, and the whole form (entry + all wizard steps inline) lives in one
|
|
5
|
-
`index.tsx`.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
`index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
|
|
6
|
+
`form.schema.*` (layout), `form.behavior.ts` (form behavior: model operators AND schema-node rules
|
|
7
|
+
such as `hideWhen` / `onComponentEvent`), `form.validation.ts` (validation rules), and the optional
|
|
8
|
+
`form.flow.ts` (the steps of a wizard) and `form.messages.ts` (the issue-message dictionary); every
|
|
9
|
+
other file has no prefix. There is one behavior per form:
|
|
10
|
+
the former render-layer file `form.render.ts` (and `renderer.behavior.ts`) is retired — its node
|
|
11
|
+
rules move into `form.behavior.ts`. The old `renderer.schema.*` / `validation.ts` names, and the
|
|
12
|
+
retired `form.render.ts` / `wizard.tsx` / `renderer.wizard.tsx`, are accepted with a warning. No
|
|
13
|
+
folders until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts` and
|
|
14
|
+
rendered with `FieldArray` / `FieldArraySection` — no per-step component files.
|
|
11
15
|
|
|
12
16
|
```
|
|
13
17
|
src/
|
|
14
|
-
├── components/ui/ # App-wide reusable UI (FormField,
|
|
18
|
+
├── components/ui/ # App-wide reusable UI (FormField, FieldArraySection, ...)
|
|
15
19
|
│
|
|
16
20
|
├── forms/
|
|
17
21
|
│ └── [form-name]/ # Form module — flat, one file per concern
|
|
18
|
-
│ ├── index.tsx # entry + whole form:
|
|
19
|
-
│ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries
|
|
20
|
-
│ ├── model.ts # createModel + initial values +
|
|
21
|
-
│ ├── form.schema.ts #
|
|
22
|
-
│ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen /
|
|
23
|
-
│ ├── validation.ts
|
|
22
|
+
│ ├── index.tsx # entry + whole form: ONE createForm call → <FormWizard flow={bundle.flow}> with all steps inline
|
|
23
|
+
│ ├── types.ts # form type (interface or type) + enums + { value, label } option type + constant dictionaries
|
|
24
|
+
│ ├── model.ts # createModel + initial values + arrayOf(blank) templates of array rows
|
|
25
|
+
│ ├── form.schema.ts # schema builder (model) => node tree: { model: model.$.x, component, componentProps }
|
|
26
|
+
│ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / copyFrom / onChange + hideWhen / onComponentEvent
|
|
27
|
+
│ ├── form.validation.ts # rules of each step (exported) + extras
|
|
28
|
+
│ ├── form.flow.ts # optional, wizard: flow { steps: [{ id, rules }] }
|
|
29
|
+
│ ├── form.messages.ts # optional: defineIssueMessages dictionary (issue texts by code)
|
|
24
30
|
│ ├── data-sources.ts # options + async loaders (dataSources)
|
|
25
31
|
│ └── api.ts # submit + prefill/load
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
Optionally a wizard may keep its steps in folders: `steps/index.ts` (step order) and one
|
|
35
|
+
`steps/<slug>/` folder per step (`<slug>` — kebab-case of the step title, no number) with the same
|
|
36
|
+
file names — `form.schema.ts`, `form.validation.ts`, `form.behavior.ts` — for the step only;
|
|
37
|
+
everything that crosses steps stays in the root files.
|
|
38
|
+
|
|
28
39
|
Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
|
|
29
|
-
`index.tsx`; validation
|
|
40
|
+
`index.tsx`; validation rules are a single `form.validation.ts`, the steps — `form.flow.ts`.** This is the working default for
|
|
30
41
|
almost every form — reach for folders only when a file stops fitting on a screen.
|
|
31
42
|
|
|
32
43
|
### Key Files
|
|
33
44
|
|
|
34
45
|
```typescript
|
|
35
|
-
// forms/credit-application/types.ts
|
|
36
|
-
export
|
|
46
|
+
// forms/credit-application/types.ts — `interface` and `type` both work
|
|
47
|
+
export interface CreditApplication {
|
|
37
48
|
loanType: LoanType;
|
|
38
|
-
loanAmount: number | null;
|
|
49
|
+
loanAmount: number | null; // an empty number input is `null`
|
|
39
50
|
// ...
|
|
40
|
-
}
|
|
51
|
+
}
|
|
52
|
+
// computed fields (targets of `compute`) — a separate type; the form is the intersection
|
|
53
|
+
export interface CreditApplicationDerived {
|
|
54
|
+
monthlyPayment: number;
|
|
55
|
+
// ...
|
|
56
|
+
}
|
|
57
|
+
export type CreditApplicationForm = CreditApplication & CreditApplicationDerived;
|
|
41
58
|
|
|
42
59
|
// forms/credit-application/model.ts
|
|
43
60
|
import { createModel, type FormModel } from '@reformer/core';
|
|
@@ -45,81 +62,218 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
|
|
|
45
62
|
createModel<CreditApplicationForm>(createInitialCreditApplication());
|
|
46
63
|
|
|
47
64
|
// forms/credit-application/form.schema.ts
|
|
48
|
-
import type { FormModel } from '@reformer/core';
|
|
49
|
-
export const creditApplicationSchema = (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
import type { FormModel, FormSchemaNode } from '@reformer/core';
|
|
66
|
+
export const creditApplicationSchema = (
|
|
67
|
+
model: FormModel<CreditApplicationForm>
|
|
68
|
+
): FormSchemaNode => ({
|
|
69
|
+
// Nested nodes live in `children` only — not in componentProps, not under arbitrary keys.
|
|
70
|
+
children: [
|
|
71
|
+
{ model: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } }, // field
|
|
72
|
+
{ model: model.$.registrationAddress, part: address }, // sub-form: group + part
|
|
73
|
+
{ model: model.$.properties, component: FieldArray, item: propertyRow }, // array of sub-forms
|
|
74
|
+
],
|
|
53
75
|
});
|
|
54
76
|
|
|
55
|
-
// forms/credit-application/form.behavior.ts
|
|
56
|
-
import { defineFormBehavior, compute, enableWhen } from '@reformer/core/behaviors';
|
|
57
|
-
export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
// forms/credit-application/form.behavior.ts — ONE behavior: model operators and node rules
|
|
78
|
+
import { defineFormBehavior, compute, enableWhen, hideWhen } from '@reformer/core/behaviors';
|
|
79
|
+
export const creditApplicationBehavior = defineFormBehavior<CreditApplicationForm>(
|
|
80
|
+
({ model, schema }) => {
|
|
81
|
+
compute(model.$.monthlyPayment, () => computeMonthlyPayment(model));
|
|
82
|
+
enableWhen([model.$.propertyValue], () => model.loanType === 'mortgage', { resetOnDisable: true });
|
|
83
|
+
hideWhen(schema.node('mortgage'), () => model.loanType !== 'mortgage'); // executed by the renderer
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// forms/credit-application/form.validation.ts — step rules (exported) and the extras
|
|
88
|
+
import type { FormValidation } from '@reformer/core';
|
|
89
|
+
import { defineValidation } from '@reformer/core/validation';
|
|
90
|
+
export const loanRules = defineValidation<CreditApplicationForm>(({ model }) => { /* ... */ });
|
|
91
|
+
export const contactsRules = defineValidation<CreditApplicationForm>(({ model }) => { /* ... */ });
|
|
92
|
+
export const creditApplicationValidation: FormValidation<CreditApplicationForm> = {
|
|
93
|
+
extras: crossStepRules, // checked only as a whole (submit)
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// forms/credit-application/form.flow.ts — the steps of the wizard
|
|
97
|
+
import type { FormFlowConfig } from '@reformer/core';
|
|
98
|
+
import { loanRules, contactsRules } from './form.validation';
|
|
99
|
+
export const creditApplicationFlow: FormFlowConfig<CreditApplicationForm> = {
|
|
100
|
+
steps: [
|
|
101
|
+
{ id: 'loan', rules: loanRules }, // id = selector of the step node in form.schema.ts
|
|
102
|
+
{ id: 'contacts', rules: contactsRules },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// forms/credit-application/form.messages.ts — the issue texts by code
|
|
107
|
+
import { defineIssueMessages } from '@reformer/core/i18n';
|
|
108
|
+
export const creditApplicationMessages = defineIssueMessages({
|
|
109
|
+
'loanAmount.required': 'Укажите сумму кредита',
|
|
60
110
|
});
|
|
61
111
|
|
|
62
112
|
// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
|
|
63
113
|
import { createForm } from '@reformer/core';
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
// ONE call: model + form + behavior + validation + flow. In the page: useFormBundle(createCreditApplicationForm).
|
|
115
|
+
export const createCreditApplicationForm = () =>
|
|
116
|
+
createForm<CreditApplicationForm>({
|
|
117
|
+
model: createCreditApplicationModel(),
|
|
118
|
+
schema: creditApplicationSchema, // builder (model) => tree
|
|
119
|
+
behavior: creditApplicationBehavior,
|
|
120
|
+
validation: creditApplicationValidation, // { extras } — rules outside the steps
|
|
121
|
+
flow: creditApplicationFlow, // steps → bundle.flow, bundle.validateStep(id)
|
|
122
|
+
});
|
|
123
|
+
// The page: <ValidationMessagesProvider messages={creditApplicationMessages}> around the form,
|
|
124
|
+
// <FormWizard form={bundle.form} flow={bundle.flow} … onSubmit={() => bundle.submission.submit(api.send)} />.
|
|
68
125
|
```
|
|
69
126
|
|
|
70
127
|
### Scaling up: folders (large forms)
|
|
71
128
|
|
|
72
|
-
When the flat module gets unwieldy —
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
129
|
+
When the flat module gets unwieldy — a multi-step wizard, sub-forms reused across steps, rules
|
|
130
|
+
that outgrow one file — split it by **concern**, one folder per layer. Each layer answers one
|
|
131
|
+
question about the form:
|
|
132
|
+
|
|
133
|
+
| Folder | Question it answers |
|
|
134
|
+
| -------------- | ------------------------------------------------------- |
|
|
135
|
+
| `model/` | What data does the form hold, and what does it start as |
|
|
136
|
+
| `schema/` | How is the form laid out |
|
|
137
|
+
| `validation/` | What is a valid value |
|
|
138
|
+
| `behavior/` | How do values and field state react to each other |
|
|
139
|
+
| `flow/` | Which steps does the user walk through, in what order |
|
|
140
|
+
| `application/` | How is the form assembled, loaded and submitted |
|
|
77
141
|
|
|
78
142
|
```
|
|
79
143
|
forms/
|
|
80
|
-
└── [form-name]/
|
|
81
|
-
├── [FormName]Form.tsx
|
|
82
|
-
|
|
144
|
+
└── [form-name]/
|
|
145
|
+
├── [FormName]Form.tsx # Entry: builds the form, renders FormWizard + step components
|
|
146
|
+
│
|
|
147
|
+
├── model/
|
|
148
|
+
│ ├── model.ts # create[FormName]Model — the reactive model
|
|
149
|
+
│ ├── initial-value.ts # initial values; arrays via arrayOf(blank)
|
|
150
|
+
│ ├── predicates.ts # conditions shared by behavior, validation and JSX
|
|
151
|
+
│ └── factories/ # blank values of sub-forms and array rows
|
|
152
|
+
│ ├── address.ts
|
|
153
|
+
│ └── property.ts
|
|
154
|
+
│
|
|
155
|
+
├── schema/
|
|
156
|
+
│ ├── form.ts # the tree: loading → wizard → steps taken from the flow
|
|
157
|
+
│ ├── steps/ # content of each step (an array of nodes)
|
|
158
|
+
│ │ ├── loan.ts
|
|
159
|
+
│ │ └── contacts.ts
|
|
160
|
+
│ └── sections/ # sections, sub-forms (part) and array rows (item)
|
|
161
|
+
│ ├── address.ts
|
|
162
|
+
│ └── property.ts
|
|
83
163
|
│
|
|
84
|
-
├──
|
|
85
|
-
│ ├──
|
|
86
|
-
│ ├──
|
|
87
|
-
│ ├──
|
|
88
|
-
│ ├──
|
|
89
|
-
│ └──
|
|
164
|
+
├── validation/
|
|
165
|
+
│ ├── form.ts # FormValidation: { extras } — rules outside the steps
|
|
166
|
+
│ ├── messages.ts # defineIssueMessages: issue code → text
|
|
167
|
+
│ ├── common/ # shared rule sets, rules of sub-forms and array rows
|
|
168
|
+
│ │ ├── rules.ts
|
|
169
|
+
│ │ └── address.ts
|
|
170
|
+
│ ├── loan.ts # rules of one step — one file per step
|
|
171
|
+
│ ├── contacts.ts
|
|
172
|
+
│ └── cross-step.ts # rules of the whole form (checked on submit)
|
|
90
173
|
│
|
|
91
|
-
├──
|
|
92
|
-
│ ├──
|
|
93
|
-
│ ├──
|
|
94
|
-
│ ├──
|
|
95
|
-
│ ├──
|
|
96
|
-
│
|
|
174
|
+
├── behavior/
|
|
175
|
+
│ ├── index.ts # the form behavior, composed of the parts below
|
|
176
|
+
│ ├── derived.ts # computed fields
|
|
177
|
+
│ ├── conditions.ts # enabling fields and hiding sections
|
|
178
|
+
│ ├── synchronization.ts # copies, clearing lists
|
|
179
|
+
│ ├── dynamic-options.ts # options and limits that depend on values
|
|
180
|
+
│ └── operators.ts # custom operators built on the built-in ones
|
|
97
181
|
│
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
182
|
+
├── flow/
|
|
183
|
+
│ └── [form-name]-flow.ts # the list of steps: selector, title, icon, content, rules
|
|
184
|
+
│ # + FormFlowConfig { steps: [{ id, rules }] } for createForm
|
|
185
|
+
│
|
|
186
|
+
├── application/
|
|
187
|
+
│ ├── create.ts # ONE createForm call shared by every way of rendering
|
|
188
|
+
│ ├── load.ts # network: fetch the entity and dictionaries
|
|
189
|
+
│ ├── mapping.ts # server response → model (model.patch + captureInitial)
|
|
190
|
+
│ ├── payload.ts # form values → request body: drops the computed fields
|
|
191
|
+
│ ├── submit.ts # sending and reporting the outcome (handler of bundle.submission.submit)
|
|
192
|
+
│ └── renderer.ts # node rules for the renderer: loading, submit, navigation (setup)
|
|
193
|
+
│
|
|
194
|
+
├── components/ # React layout: steps/, nested-forms/, ui/
|
|
195
|
+
├── api/ # HTTP calls
|
|
196
|
+
└── types/ constants/ utils/ # form type, option dictionaries, pure calculations
|
|
102
197
|
```
|
|
103
198
|
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
the
|
|
199
|
+
Dependencies point one way: `application → flow → schema/steps, validation/<step>`;
|
|
200
|
+
`application → behavior → model`. Nothing below `application/` knows the entity id, the network or
|
|
201
|
+
the wizard — the behavior is static and reusable.
|
|
202
|
+
|
|
203
|
+
**The flow is the single place a step is declared.** One entry carries the step's `selector`,
|
|
204
|
+
title, icon, content and rules, so the step nodes of the schema, the `flow` of the assembly and
|
|
205
|
+
the wizard steps of a hand-written React page are all derived from the same list — there is
|
|
206
|
+
nowhere to pair the layout of one step with the rules of another:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// flow/credit-application-flow.ts
|
|
210
|
+
export const creditApplicationFlow = [
|
|
211
|
+
{ selector: 'loan', title: 'Loan', icon: '💰', content: loanStep, rules: loanRules },
|
|
212
|
+
{ selector: 'contacts', title: 'Contacts', icon: '📞', content: contactsStep, rules: contactsRules },
|
|
213
|
+
] as const satisfies readonly FlowStep[];
|
|
214
|
+
|
|
215
|
+
// the same list → the flow of createForm; the step id is the step selector
|
|
216
|
+
export const creditApplicationFlowSteps: FormFlowConfig<CreditApplicationForm> = {
|
|
217
|
+
steps: creditApplicationFlow.map((step) => ({ id: step.selector, rules: step.rules })),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// schema/form.ts — step nodes come from the flow
|
|
221
|
+
children: creditApplicationFlow.map((step) => ({
|
|
222
|
+
selector: step.selector,
|
|
223
|
+
component: Step,
|
|
224
|
+
componentProps: { title: step.title, icon: step.icon },
|
|
225
|
+
children: step.content(model),
|
|
226
|
+
})),
|
|
227
|
+
|
|
228
|
+
// validation/form.ts — only the rules outside the steps
|
|
229
|
+
export const creditApplicationValidation: FormValidation<CreditApplicationForm> = {
|
|
230
|
+
extras: crossStepRules,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// application/create.ts
|
|
234
|
+
createForm<CreditApplicationForm>({
|
|
235
|
+
model: createCreditApplicationModel(),
|
|
236
|
+
schema: creditApplicationSchema,
|
|
237
|
+
behavior: creditApplicationBehavior,
|
|
238
|
+
validation: creditApplicationValidation,
|
|
239
|
+
flow: creditApplicationFlowSteps,
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Texts of the issues live in `validation/messages.ts`.** Rules carry codes
|
|
244
|
+
(`required({ code: 'loanAmount.required' })`), the dictionary maps a code to its text, and every
|
|
245
|
+
way of rendering wraps the form in `<ValidationMessagesProvider messages={…}>` from
|
|
246
|
+
`@reformer/cdk` — the texts are the same in hand-written React, renderer-react and renderer-json.
|
|
247
|
+
|
|
248
|
+
**Conditions live in `model/predicates.ts`** as pure functions over values
|
|
249
|
+
(`isMortgage(loanType)`). Behavior (`enableWhen`, `hideWhen`), validation (`validateWhen`) and the
|
|
250
|
+
JSX of a step call the same function — a condition is written once.
|
|
251
|
+
|
|
252
|
+
**Loading and submitting are not behavior.** They belong to `application/`: the page (or, for the
|
|
253
|
+
renderer, `application/renderer.ts` passed as `setup`) calls `load`, hands the response to
|
|
254
|
+
`mapping`, and sends through `bundle.submission.submit(handler)` — one run of all the rules, the
|
|
255
|
+
handler is called only when the form is valid. Written into the model with `model.patch` followed
|
|
256
|
+
by `model.captureInitial()`, the loaded values become the form's starting point: the form is not
|
|
257
|
+
dirty, and `model.reset()` returns to them. The handler receives `model.get()` — the whole model,
|
|
258
|
+
disabled fields included; `application/payload.ts` drops the fields the server does not expect
|
|
259
|
+
(computed ones) before the request.
|
|
260
|
+
|
|
261
|
+
Rule of thumb for this layout: **one folder per question; a step is declared once, in the flow;
|
|
262
|
+
the root holds only the entry component.**
|
|
109
263
|
|
|
110
264
|
### Scaling
|
|
111
265
|
|
|
112
266
|
| Complexity | Structure |
|
|
113
267
|
| ---------- | --------------------------------------------------------------------------------------- |
|
|
114
268
|
| Simple | Single file: `index.tsx` (model + schema + behavior + component) |
|
|
115
|
-
| **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
|
|
116
|
-
| Folders (`
|
|
269
|
+
| **Minimalist (flat)** | **Default.** One file per concern at the module root (`types` / `model` / `form.schema` / `form.behavior` / `form.validation` / `data-sources` / `api`) + `index.tsx` with all steps inline |
|
|
270
|
+
| Folders (`model/` + `schema/` + `validation/` + `behavior/` + `flow/` + `application/`) | Large forms: one folder per layer, a step declared once in the flow, reusable sections and rules |
|
|
117
271
|
|
|
118
|
-
> The leading layout is configurable: set `REFORMER_FORM_LAYOUT`
|
|
119
|
-
> when registering the MCP server
|
|
120
|
-
> is `minimalist`.
|
|
272
|
+
> The leading layout of the MCP generators is configurable: set `REFORMER_FORM_LAYOUT`
|
|
273
|
+
> (`minimalist` | `folders`) when registering the MCP server. Default is `minimalist`.
|
|
121
274
|
|
|
122
|
-
>
|
|
123
|
-
>
|
|
124
|
-
>
|
|
125
|
-
>
|
|
275
|
+
> The MCP layout check (`validate_form` with `kind: "layout"`) verifies the flat layout file names
|
|
276
|
+
> (and the optional `steps/<slug>/` folders); the folders layout is recognized by its top-level
|
|
277
|
+
> folders only. Cross-target variants (renderer-react: the same files; renderer-json: plus
|
|
278
|
+
> `registry.ts`, and `form.schema.ts` holds the JSON-DSL document) are described in the
|
|
279
|
+
> **form-directory-layout** guide (`find_recipe directory-layout`).
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> **Default rule (read first)**: для UI используй `FormField` из
|
|
4
4
|
> [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
|
|
5
|
-
> строкой `<FormField control={form.x} />`. Свои
|
|
5
|
+
> строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
|
|
6
6
|
> ui-kit не подходит (другая design system, особый low-level input).
|
|
7
7
|
>
|
|
8
8
|
> Канонический schema-driven подход:
|
|
9
9
|
>
|
|
10
|
-
> - **компонент** объявляется в схеме как `component: Input` (или `
|
|
10
|
+
> - **компонент** объявляется в схеме как `component: Input` (или `SelectAsync`, `CheckboxWithLabel`, …) —
|
|
11
|
+
> сам компонент кита, отдельных «field-версий» нет: связывает его с полем обёртка (`FormField`)
|
|
11
12
|
> - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
|
|
12
13
|
> - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
|
|
13
14
|
>
|
|
@@ -17,44 +18,48 @@
|
|
|
17
18
|
### Default — FormField из ui-kit (canonical)
|
|
18
19
|
|
|
19
20
|
```tsx
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { FormField, Input,
|
|
21
|
+
import { createForm, type FormModel, type FormSchemaNode } from '@reformer/core';
|
|
22
|
+
import { useFormBundle } from '@reformer/core/react';
|
|
23
|
+
import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
interface RegistrationForm {
|
|
25
26
|
email: string;
|
|
26
27
|
country: string;
|
|
27
28
|
agree: boolean;
|
|
28
|
-
}
|
|
29
|
+
}
|
|
29
30
|
|
|
30
31
|
function RegistrationPage() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
email:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
32
|
+
// useFormBundle вызывает фабрику ровно один раз и держит сборку стабильной между рендерами
|
|
33
|
+
const { form } = useFormBundle(() =>
|
|
34
|
+
createForm<RegistrationForm>({
|
|
35
|
+
initial: { email: '', country: 'ru', agree: false },
|
|
36
|
+
schema: (model: FormModel<RegistrationForm>): FormSchemaNode => ({
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
model: model.$.email,
|
|
40
|
+
component: Input,
|
|
41
|
+
componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
model: model.$.country,
|
|
45
|
+
component: SelectAsync,
|
|
46
|
+
componentProps: {
|
|
47
|
+
label: 'Country',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'ru', label: 'Россия' },
|
|
50
|
+
{ value: 'by', label: 'Беларусь' },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
model: model.$.agree,
|
|
56
|
+
component: CheckboxWithLabel,
|
|
57
|
+
componentProps: { label: 'I agree to terms' },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
);
|
|
58
63
|
|
|
59
64
|
return (
|
|
60
65
|
<form>
|
|
@@ -69,7 +74,8 @@ function RegistrationPage() {
|
|
|
69
74
|
|
|
70
75
|
`FormField` сам читает `componentProps.label`, `componentProps.placeholder`,
|
|
71
76
|
`componentProps.options` через `useFormControl(...).componentProps` и применяет
|
|
72
|
-
их к нужному `<input>`/`<select>`/etc.
|
|
77
|
+
их к нужному `<input>`/`<select>`/etc. Текст замечания (по коду — словарь формы из
|
|
78
|
+
`ValidationMessagesProvider messages`, затем локаль), `pending` для async-валидаций,
|
|
73
79
|
`data-testid` для e2e — всё из коробки.
|
|
74
80
|
|
|
75
81
|
### Anti-patterns (не делай так)
|
|
@@ -92,7 +98,7 @@ function RegistrationPage() {
|
|
|
92
98
|
✅ Всё это в схеме:
|
|
93
99
|
|
|
94
100
|
```ts
|
|
95
|
-
{
|
|
101
|
+
{ model: model.$.email, component: Input, componentProps: { label: 'Email' } }
|
|
96
102
|
```
|
|
97
103
|
|
|
98
104
|
```tsx
|
|
@@ -113,7 +119,8 @@ import { InputMask } from 'react-input-mask';
|
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
`children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
|
|
116
|
-
props (`value`, `onChange`, `onBlur`, `aria-invalid`)
|
|
122
|
+
props (`value`, `onChange`, `onBlur`, `aria-invalid`) — в диалекте ребёнка, если тот объявил
|
|
123
|
+
статику `reformerAdapter` (`defineFieldControl` из `@reformer/ui-kit/fields`), иначе value-based.
|
|
117
124
|
|
|
118
125
|
### Advanced — write your own from scratch (rare)
|
|
119
126
|
|
|
@@ -122,13 +129,15 @@ props (`value`, `onChange`, `onBlur`, `aria-invalid`).
|
|
|
122
129
|
из `componentProps`, не из JSX-props.
|
|
123
130
|
|
|
124
131
|
```tsx
|
|
125
|
-
import type {
|
|
126
|
-
import { useFormControl } from '@reformer/core';
|
|
132
|
+
import type { FormControl } from '@reformer/core';
|
|
133
|
+
import { useFormControl, useIssueMessage } from '@reformer/core/react';
|
|
134
|
+
import { formMessages } from './form.validation'; // defineIssueMessages — константа модуля
|
|
127
135
|
|
|
128
|
-
type MyFormFieldProps<T> = { control:
|
|
136
|
+
type MyFormFieldProps<T> = { control: FormControl<T> }; // ← ОДИН prop
|
|
129
137
|
|
|
130
138
|
function MyFormField<T>({ control }: MyFormFieldProps<T>) {
|
|
131
|
-
const { value,
|
|
139
|
+
const { value, issues, disabled, shouldShowIssues, componentProps } = useFormControl(control);
|
|
140
|
+
const message = useIssueMessage(formMessages); // текст замечания по коду
|
|
132
141
|
// componentProps = { label, placeholder, type, options, ... } — из СХЕМЫ
|
|
133
142
|
const cp = (componentProps ?? {}) as Record<string, unknown>;
|
|
134
143
|
|
|
@@ -143,24 +152,54 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
|
|
|
143
152
|
onChange={(e) => (control.setValue as (v: unknown) => void)(e.target.value)}
|
|
144
153
|
onBlur={() => control.markAsTouched()}
|
|
145
154
|
/>
|
|
146
|
-
{shouldShowError
|
|
155
|
+
{/* shouldShowIssues — ошибки и предупреждения; shouldShowError — для aria-invalid */}
|
|
156
|
+
{shouldShowIssues && issues[0] && <span>{message(issues[0])}</span>}
|
|
147
157
|
</label>
|
|
148
158
|
);
|
|
149
159
|
}
|
|
150
160
|
```
|
|
151
161
|
|
|
162
|
+
Внутри дерева с `ValidationMessagesProvider` (cdk) вместо `useIssueMessage(formMessages)` берут
|
|
163
|
+
`useIssueMessageResolver()` из `@reformer/cdk` — он учитывает словари всех провайдеров выше. См.
|
|
164
|
+
`36-issue-messages.md`.
|
|
165
|
+
|
|
152
166
|
Использование — как у `FormField`:
|
|
153
167
|
|
|
154
168
|
```tsx
|
|
155
169
|
<MyFormField control={form.email} /> // ← без label-prop
|
|
156
170
|
```
|
|
157
171
|
|
|
172
|
+
### Связывание поля с контролом — `FieldAdapter` (`@reformer/core/react`)
|
|
173
|
+
|
|
174
|
+
Контролы говорят на разных диалектах (`checked` + `onCheckedChange`, `onValueChange`, DOM-событие
|
|
175
|
+
в `onChange`), форма — на value-based seam (`value` + `onChange(value)` + `onBlur`). Перевод
|
|
176
|
+
описывает `FieldAdapter` (`valueProp`, `changeProp`, `fromEmit`, `toValue`, `bindBlur`, `strip`,
|
|
177
|
+
`passControl` — все необязательны). Компонент объявляет его статикой `reformerAdapter`, а обёртка
|
|
178
|
+
поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`) читает статику
|
|
179
|
+
и связывает поле сама — отдельные «field-версии» компонентов не нужны.
|
|
180
|
+
|
|
181
|
+
| Символ | Назначение |
|
|
182
|
+
| ----------------------------- | --------------------------------------------------------------------------- |
|
|
183
|
+
| `FieldAdapter` | Тип адаптера: диалект контрола |
|
|
184
|
+
| `getFieldAdapter(component)` | Статика `component.reformerAdapter` либо `undefined` |
|
|
185
|
+
| `bindFieldProps(adapter, seam, props)` | Props контрола под адаптер (без адаптера — seam как есть) |
|
|
186
|
+
| `FieldHandle` | Императивный handle поля: `focus` / `blur` / `scrollIntoView` / `getElement` |
|
|
187
|
+
| `makeElementFieldHandle(ref)` | Базовый `FieldHandle` из DOM-узла |
|
|
188
|
+
| `useFieldHandleRef(outerRef)` | Ref для контрола + публикация handle потребителю (строит ОБЁРТКА поля) |
|
|
189
|
+
|
|
190
|
+
В приложении обычно достаточно `defineFieldControl(MyControl, { adapter })` из
|
|
191
|
+
`@reformer/ui-kit/fields` (плюс готовые пресеты `checkedAdapter`, `valueChangeAdapter`, …); для
|
|
192
|
+
компонентов чужой библиотеки без статики — `RendererSettings.resolveFieldAdapter`.
|
|
193
|
+
|
|
158
194
|
### Integration with UI libraries (shadcn etc.)
|
|
159
195
|
|
|
160
196
|
> **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
|
|
161
197
|
> `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
|
|
162
198
|
> компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
|
|
163
|
-
> контрола.
|
|
199
|
+
> контрола. Свой компонент можно подготовить и один раз — статикой
|
|
200
|
+
> `defineFieldControl(MyInput, { adapter })` из `@reformer/ui-kit/fields`: его поймут и рендерер,
|
|
201
|
+
> и `FormField.Control` из `@reformer/cdk`. Ручная обёртка на `useFormControl` (ниже) нужна для
|
|
202
|
+
> прямого JSX без `FormField` и рендерера.
|
|
164
203
|
|
|
165
204
|
Если есть существующая design system — оборачивай её компоненты в один
|
|
166
205
|
`MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
|
|
@@ -170,8 +209,9 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
|
|
|
170
209
|
import { Input } from '@/components/ui/input';
|
|
171
210
|
import { Label } from '@/components/ui/label';
|
|
172
211
|
|
|
173
|
-
function ShadcnFormField({ control }: { control:
|
|
174
|
-
const { value,
|
|
212
|
+
function ShadcnFormField({ control }: { control: FormControl<string> }) {
|
|
213
|
+
const { value, issues, disabled, shouldShowIssues, componentProps } = useFormControl(control);
|
|
214
|
+
const message = useIssueMessage(formMessages);
|
|
175
215
|
const cp = (componentProps ?? {}) as Record<string, unknown>;
|
|
176
216
|
|
|
177
217
|
return (
|
|
@@ -182,7 +222,7 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
|
|
|
182
222
|
onChange={(e) => control.setValue(e.target.value)}
|
|
183
223
|
disabled={disabled}
|
|
184
224
|
/>
|
|
185
|
-
{
|
|
225
|
+
{shouldShowIssues && issues[0] && <p className="text-red-500">{message(issues[0])}</p>}
|
|
186
226
|
</div>
|
|
187
227
|
);
|
|
188
228
|
}
|