@reformer/core 10.1.0 → 11.0.0-beta.1
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/dist/{behaviors-node-DUrqAqp1.js → behaviors-node-DGEFMTQu.js} +1 -1
- package/dist/{behaviors-value-lCpqicfa.js → behaviors-value-C7dSEhCc.js} +43 -57
- package/dist/behaviors.js +108 -107
- package/dist/derived-registry-CluZCQ7U.js +16 -0
- package/dist/form/form-submitter.d.ts +0 -4
- package/dist/form/hooks/types.d.ts +3 -3
- package/dist/form/hooks/use-form-validation.d.ts +48 -0
- package/dist/form/index.d.ts +2 -3
- package/dist/form/nodes/array-node.d.ts +0 -84
- package/dist/form/nodes/form-node.d.ts +0 -14
- package/dist/form/nodes/group-node.d.ts +0 -4
- package/dist/form/status-machine.d.ts +0 -38
- package/dist/form/types/contracts.d.ts +0 -6
- package/dist/form/types/deep-schema.d.ts +0 -9
- package/dist/form/types/index.d.ts +4 -38
- package/dist/form/types/schema-node.d.ts +13 -16
- package/dist/form/types/validation-schema.d.ts +0 -25
- package/dist/form/unique-id.d.ts +3 -11
- package/dist/form/validation-schema.d.ts +16 -3
- package/dist/form/validation-strategy.d.ts +55 -0
- package/dist/form/validators.d.ts +8 -3
- package/dist/form-model-DYCzGGKF.js +286 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +477 -648
- package/dist/runtime-token.d.ts +23 -0
- package/dist/state/form-model.d.ts +12 -1
- package/dist/state.js +17 -15
- package/dist/subscription-manager-705bls97.js +171 -0
- package/dist/validation.js +133 -81
- package/dist/validators.d.ts +0 -4
- package/dist/validators.js +88 -122
- package/docs/llms/01-api-reference.md +63 -0
- package/docs/llms/02-quick-start.md +143 -0
- package/docs/llms/03-api-signatures.md +273 -0
- package/docs/llms/04-common-patterns.md +202 -0
- package/docs/llms/05-common-mistakes.md +167 -0
- package/docs/llms/06-troubleshooting.md +11 -0
- package/docs/llms/07-complete-import.md +99 -0
- package/docs/llms/08-form-types.md +47 -0
- package/docs/llms/09-formschema.md +143 -0
- package/docs/llms/10-arrays.md +161 -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 +91 -0
- package/docs/llms/14-extended-mistakes.md +98 -0
- package/docs/llms/15-project-structure.md +125 -0
- package/docs/llms/16-ui-components.md +189 -0
- package/docs/llms/17-nonexistent-api.md +93 -0
- package/docs/llms/18-conditional-fields.md +183 -0
- package/docs/llms/19-reading-values.md +59 -0
- package/docs/llms/20-compute-vs-watch.md +138 -0
- package/docs/llms/21-array-operations.md +113 -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 +114 -0
- package/docs/llms/25-reset-when.md +115 -0
- package/docs/llms/26-transform-value.md +136 -0
- package/docs/llms/27-revalidate-when.md +132 -0
- package/docs/llms/28-submit-and-reset.md +259 -0
- package/docs/llms/29-async-preload.md +193 -0
- package/docs/llms/30-type-safety-recipes.md +159 -0
- package/docs/llms/31-async-validator-debounce.md +135 -0
- package/docs/llms/32-async-options-loading.md +108 -0
- package/docs/llms/33-validation-strategy.md +193 -0
- package/llms.txt +435 -1580
- package/package.json +33 -10
- package/dist/form-model-C8Fi83k4.js +0 -439
package/dist/validators.js
CHANGED
|
@@ -1,200 +1,166 @@
|
|
|
1
|
-
import { required as
|
|
2
|
-
import { min as
|
|
3
|
-
import { max as
|
|
4
|
-
import { minLength as
|
|
5
|
-
import { maxLength as
|
|
6
|
-
import { email as
|
|
7
|
-
import { pattern as
|
|
8
|
-
import { url as
|
|
9
|
-
import { phone as
|
|
10
|
-
import { isNumber as
|
|
11
|
-
import { integer as
|
|
12
|
-
import { multipleOf as
|
|
13
|
-
import { nonNegative as
|
|
14
|
-
import { nonZero as
|
|
15
|
-
import { isDate as
|
|
16
|
-
import { minDate as
|
|
17
|
-
import { maxDate as
|
|
18
|
-
import { pastDate as
|
|
19
|
-
import { futureDate as
|
|
20
|
-
import { minAge as
|
|
21
|
-
import { maxAge as
|
|
22
|
-
function
|
|
1
|
+
import { required as z } from "./validators/required.js";
|
|
2
|
+
import { min as y } from "./validators/min.js";
|
|
3
|
+
import { max as b } from "./validators/max.js";
|
|
4
|
+
import { minLength as L } from "./validators/min-length.js";
|
|
5
|
+
import { maxLength as D } from "./validators/max-length.js";
|
|
6
|
+
import { email as T } from "./validators/email.js";
|
|
7
|
+
import { pattern as w } from "./validators/pattern.js";
|
|
8
|
+
import { url as k } from "./validators/url.js";
|
|
9
|
+
import { phone as B } from "./validators/phone.js";
|
|
10
|
+
import { isNumber as Z } from "./validators/is-number.js";
|
|
11
|
+
import { integer as G } from "./validators/integer.js";
|
|
12
|
+
import { multipleOf as I } from "./validators/multiple-of.js";
|
|
13
|
+
import { nonNegative as K } from "./validators/non-negative.js";
|
|
14
|
+
import { nonZero as P } from "./validators/non-zero.js";
|
|
15
|
+
import { isDate as R } from "./validators/is-date.js";
|
|
16
|
+
import { minDate as V } from "./validators/min-date.js";
|
|
17
|
+
import { maxDate as Y } from "./validators/max-date.js";
|
|
18
|
+
import { pastDate as $ } from "./validators/past-date.js";
|
|
19
|
+
import { futureDate as ee } from "./validators/future-date.js";
|
|
20
|
+
import { minAge as ne } from "./validators/min-age.js";
|
|
21
|
+
import { maxAge as ie } from "./validators/max-age.js";
|
|
22
|
+
function s(r) {
|
|
23
23
|
if (typeof r != "object" || r === null) return !1;
|
|
24
|
-
const
|
|
25
|
-
return !(typeof
|
|
24
|
+
const t = r;
|
|
25
|
+
return !(typeof t.name != "string" || t.size !== void 0 && typeof t.size != "number" || t.type !== void 0 && typeof t.type != "string");
|
|
26
26
|
}
|
|
27
27
|
function o(r) {
|
|
28
|
-
return Array.isArray(r) ? r.filter(
|
|
28
|
+
return Array.isArray(r) ? r.filter(s) : s(r) ? [r] : null;
|
|
29
29
|
}
|
|
30
|
-
function
|
|
31
|
-
const e =
|
|
30
|
+
function u(r, t) {
|
|
31
|
+
const e = t.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean);
|
|
32
32
|
if (e.length === 0) return !0;
|
|
33
|
-
const n = r.name.toLowerCase(),
|
|
34
|
-
return e.some((l) => l.startsWith(".") ? n.endsWith(l) : l.endsWith("/*") ?
|
|
33
|
+
const n = r.name.toLowerCase(), i = (r.type ?? "").toLowerCase();
|
|
34
|
+
return e.some((l) => l.startsWith(".") ? n.endsWith(l) : l.endsWith("/*") ? i.startsWith(l.slice(0, -1)) : l.includes("/") ? i === l : !1);
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function m(r, t) {
|
|
37
37
|
return (e) => {
|
|
38
38
|
if (e == null || e === "")
|
|
39
39
|
return null;
|
|
40
40
|
const n = o(e);
|
|
41
41
|
if (n === null || n.length === 0) return null;
|
|
42
|
-
for (const
|
|
43
|
-
if (typeof
|
|
42
|
+
for (const i of n)
|
|
43
|
+
if (typeof i.size == "number" && i.size > r)
|
|
44
44
|
return {
|
|
45
45
|
code: "maxFileSize",
|
|
46
|
-
message:
|
|
46
|
+
message: t?.message ?? "invalid",
|
|
47
47
|
params: {
|
|
48
48
|
maxFileSize: r,
|
|
49
|
-
fileName:
|
|
50
|
-
actualSize:
|
|
51
|
-
...
|
|
49
|
+
fileName: i.name,
|
|
50
|
+
actualSize: i.size,
|
|
51
|
+
...t?.params
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
54
|
return null;
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
-
function
|
|
57
|
+
function a(r, t) {
|
|
58
58
|
return (e) => {
|
|
59
59
|
if (e == null || e === "")
|
|
60
60
|
return null;
|
|
61
61
|
const n = o(e);
|
|
62
62
|
if (n === null || n.length === 0) return null;
|
|
63
|
-
for (const
|
|
64
|
-
if (typeof
|
|
63
|
+
for (const i of n)
|
|
64
|
+
if (typeof i.size == "number" && i.size < r)
|
|
65
65
|
return {
|
|
66
66
|
code: "minFileSize",
|
|
67
|
-
message:
|
|
67
|
+
message: t?.message ?? "invalid",
|
|
68
68
|
params: {
|
|
69
69
|
minFileSize: r,
|
|
70
|
-
fileName:
|
|
71
|
-
actualSize:
|
|
72
|
-
...
|
|
70
|
+
fileName: i.name,
|
|
71
|
+
actualSize: i.size,
|
|
72
|
+
...t?.params
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
return null;
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
|
-
function
|
|
78
|
+
function p(r, t) {
|
|
79
79
|
return (e) => {
|
|
80
80
|
if (e == null || e === "")
|
|
81
81
|
return null;
|
|
82
82
|
const n = o(e);
|
|
83
83
|
if (n === null || n.length === 0) return null;
|
|
84
|
-
for (const
|
|
85
|
-
if (!
|
|
84
|
+
for (const i of n)
|
|
85
|
+
if (!u(i, r))
|
|
86
86
|
return {
|
|
87
87
|
code: "fileType",
|
|
88
|
-
message:
|
|
89
|
-
params: { accept: r, fileName:
|
|
88
|
+
message: t?.message ?? "invalid",
|
|
89
|
+
params: { accept: r, fileName: i.name, ...t?.params }
|
|
90
90
|
};
|
|
91
91
|
return null;
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
-
function
|
|
94
|
+
function c(r, t) {
|
|
95
95
|
return (e) => {
|
|
96
96
|
if (e == null || e === "")
|
|
97
97
|
return null;
|
|
98
98
|
const n = o(e);
|
|
99
99
|
return n === null || n.length === 0 ? null : n.length > r ? {
|
|
100
100
|
code: "maxFiles",
|
|
101
|
-
message:
|
|
102
|
-
params: { maxFiles: r, actualCount: n.length, ...
|
|
101
|
+
message: t?.message ?? "invalid",
|
|
102
|
+
params: { maxFiles: r, actualCount: n.length, ...t?.params }
|
|
103
103
|
} : null;
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
-
function
|
|
106
|
+
function x(r, t) {
|
|
107
107
|
return (e) => {
|
|
108
108
|
if (e == null || e === "")
|
|
109
109
|
return null;
|
|
110
110
|
const n = o(e);
|
|
111
111
|
return n === null || n.length === 0 ? null : n.length < r ? {
|
|
112
112
|
code: "minFiles",
|
|
113
|
-
message:
|
|
114
|
-
params: { minFiles: r, actualCount: n.length, ...
|
|
113
|
+
message: t?.message ?? "invalid",
|
|
114
|
+
params: { minFiles: r, actualCount: n.length, ...t?.params }
|
|
115
115
|
} : null;
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
-
function
|
|
118
|
+
function g(r, t) {
|
|
119
119
|
return (e) => {
|
|
120
120
|
if (e == null || e === "")
|
|
121
121
|
return null;
|
|
122
122
|
const n = o(e);
|
|
123
123
|
if (n === null || n.length === 0) return null;
|
|
124
|
-
const
|
|
124
|
+
const i = n.reduce(
|
|
125
125
|
(l, f) => l + (typeof f.size == "number" ? f.size : 0),
|
|
126
126
|
0
|
|
127
127
|
);
|
|
128
|
-
return
|
|
128
|
+
return i > r ? {
|
|
129
129
|
code: "maxTotalFileSize",
|
|
130
|
-
message:
|
|
131
|
-
params: { maxTotalFileSize: r, actualTotal:
|
|
130
|
+
message: t?.message ?? "invalid",
|
|
131
|
+
params: { maxTotalFileSize: r, actualTotal: i, ...t?.params }
|
|
132
132
|
} : null;
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
-
const te = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
136
|
-
__proto__: null,
|
|
137
|
-
email: d,
|
|
138
|
-
fileType: O,
|
|
139
|
-
futureDate: N,
|
|
140
|
-
integer: x,
|
|
141
|
-
isDate: L,
|
|
142
|
-
isFileLike: m,
|
|
143
|
-
isNumber: F,
|
|
144
|
-
matchesFileAccept: s,
|
|
145
|
-
max: p,
|
|
146
|
-
maxAge: _,
|
|
147
|
-
maxDate: C,
|
|
148
|
-
maxFileSize: j,
|
|
149
|
-
maxFiles: k,
|
|
150
|
-
maxLength: g,
|
|
151
|
-
maxTotalFileSize: B,
|
|
152
|
-
min: a,
|
|
153
|
-
minAge: W,
|
|
154
|
-
minDate: T,
|
|
155
|
-
minFileSize: w,
|
|
156
|
-
minFiles: q,
|
|
157
|
-
minLength: c,
|
|
158
|
-
multipleOf: S,
|
|
159
|
-
nonNegative: b,
|
|
160
|
-
nonZero: A,
|
|
161
|
-
pastDate: D,
|
|
162
|
-
pattern: z,
|
|
163
|
-
phone: y,
|
|
164
|
-
required: u,
|
|
165
|
-
toFileArray: o,
|
|
166
|
-
url: h
|
|
167
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
168
135
|
export {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
g as
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
u as required,
|
|
136
|
+
T as email,
|
|
137
|
+
p as fileType,
|
|
138
|
+
ee as futureDate,
|
|
139
|
+
G as integer,
|
|
140
|
+
R as isDate,
|
|
141
|
+
s as isFileLike,
|
|
142
|
+
Z as isNumber,
|
|
143
|
+
u as matchesFileAccept,
|
|
144
|
+
b as max,
|
|
145
|
+
ie as maxAge,
|
|
146
|
+
Y as maxDate,
|
|
147
|
+
m as maxFileSize,
|
|
148
|
+
c as maxFiles,
|
|
149
|
+
D as maxLength,
|
|
150
|
+
g as maxTotalFileSize,
|
|
151
|
+
y as min,
|
|
152
|
+
ne as minAge,
|
|
153
|
+
V as minDate,
|
|
154
|
+
a as minFileSize,
|
|
155
|
+
x as minFiles,
|
|
156
|
+
L as minLength,
|
|
157
|
+
I as multipleOf,
|
|
158
|
+
K as nonNegative,
|
|
159
|
+
P as nonZero,
|
|
160
|
+
$ as pastDate,
|
|
161
|
+
w as pattern,
|
|
162
|
+
B as phone,
|
|
163
|
+
z as required,
|
|
198
164
|
o as toFileArray,
|
|
199
|
-
|
|
165
|
+
k as url
|
|
200
166
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
## 1. API Reference
|
|
2
|
+
|
|
3
|
+
### Imports (CRITICALLY IMPORTANT)
|
|
4
|
+
|
|
5
|
+
Архитектура M1: значения живут в **модели** (`createModel`), а форма (`createForm`) строит ноды поверх сигналов модели. Behaviors работают на сигналах (`model.$.field`), а не на строковых путях.
|
|
6
|
+
|
|
7
|
+
| What | Where |
|
|
8
|
+
| ------------------------------------------------------------------------------------------------------ | --------------------------- |
|
|
9
|
+
| `createModel`, `createForm` | `@reformer/core` |
|
|
10
|
+
| `validateModel`, `defineValidationSchema`, `validate`, `validateAsync`, `validateWhen`, `cross`, `each`, `apply` | `@reformer/core/validation` |
|
|
11
|
+
| `Rule`, `AsyncRule`, `ValidationSchema` (типы) | `@reformer/core/validation` |
|
|
12
|
+
| `useFormControl`, `useFormControlValue`, `useArrayLength` | `@reformer/core` |
|
|
13
|
+
| `FormModel`, `FormProxy`, `FieldNode`, `GroupNode`, `ArrayNode`, `ModelArrayNode` | `@reformer/core` |
|
|
14
|
+
| `ModelSignals`, `ModelArray`, `ModelValue`, `ModelObject`, `PathAwareSignal` | `@reformer/core` |
|
|
15
|
+
| `ValidationError`, `FieldConfig`, `FormSchema`, `FieldControlState` | `@reformer/core` |
|
|
16
|
+
| `computeFrom`, `copyFrom`, `watchField`, `enableWhen`, `disableWhen` | `@reformer/core` (примитивы) |
|
|
17
|
+
| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` | `@reformer/core` (примитивы) |
|
|
18
|
+
| `required`, `min`, `max`, `minLength`, `maxLength`, `email`, `pattern`, `url`, `phone` | `@reformer/core/validators` |
|
|
19
|
+
| `isNumber`, `integer`, `multipleOf`, `nonNegative`, `nonZero` | `@reformer/core/validators` |
|
|
20
|
+
| `isDate`, `minDate`, `maxDate`, `pastDate`, `futureDate`, `minAge`, `maxAge` | `@reformer/core/validators` |
|
|
21
|
+
| `defineFormBehavior`, `compute`, `computeFrom`, `copyFrom`, `onChange`, `enableWhen`, `disableWhen` | `@reformer/core/behaviors` |
|
|
22
|
+
| `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen`, `apply`, `applyEach`, `aggregateInto` | `@reformer/core/behaviors` |
|
|
23
|
+
| `exclusiveFlag`, `onDispose`, `getScope`, `effect`, `defer` | `@reformer/core/behaviors` |
|
|
24
|
+
|
|
25
|
+
> **Два способа писать behaviors.** Низкоуровневые примитивы (`computeFrom`, `copyFrom`, `watchField`,
|
|
26
|
+
> `enableWhen`, …) экспортируются из `@reformer/core`, принимают **сигналы** (`model.$.x`), возвращают
|
|
27
|
+
> **cleanup-функцию** и вызываются императивно (например, в `useEffect`). Декларативный DSL
|
|
28
|
+
> (`defineFormBehavior` + операторы) экспортируется из `@reformer/core/behaviors`, регистрирует cleanup
|
|
29
|
+
> сам и передаётся в `createForm({ behavior })`. См. `20-compute-vs-watch.md`.
|
|
30
|
+
|
|
31
|
+
> **Валидация — отдельный слой.** Layout-схема `createForm` НЕ несёт валидаторов. Правила живут в
|
|
32
|
+
> `defineValidationSchema<T>(({ model }) => { validate(model.$.x, [required(), min(50000)]); ... })`
|
|
33
|
+
> из `@reformer/core/validation`; фабрики `required()`, `min(50000)`, `email()` возвращают
|
|
34
|
+
> `Rule<T> = (value) => ValidationError | null` и передаются массивом в `validate(sig, [...])`.
|
|
35
|
+
> Запуск — только внешним раннером `await validateModel(model, schema)` (`Promise<boolean>`, ошибки
|
|
36
|
+
> сам роутит в ноды; `form.validate()`/`form.submit()` schema-валидацию НЕ гоняют).
|
|
37
|
+
|
|
38
|
+
### Type Values
|
|
39
|
+
|
|
40
|
+
- Опциональные числа: `number | null` (конвенция «пользователь очистил поле»)
|
|
41
|
+
- Опциональные строки: `string` (по умолчанию пустая строка) или `string | null`
|
|
42
|
+
- Form-shape тип объявляй как `type`-alias — см. `30-type-safety-recipes.md`
|
|
43
|
+
|
|
44
|
+
### React Hooks Comparison (CRITICALLY IMPORTANT)
|
|
45
|
+
|
|
46
|
+
| Hook | Return Type | Subscribes To | Use Case |
|
|
47
|
+
|------|-------------|---------------|----------|
|
|
48
|
+
| `useFormControl(field)` | `{ value, errors, disabled, touched, valid, invalid, pending, shouldShowError, componentProps }` | Все сигналы поля | Полное состояние поля, инпуты |
|
|
49
|
+
| `useFormControlValue(field)` | `T` (значение напрямую) | Только сигнал value | Условный рендеринг |
|
|
50
|
+
| `useArrayLength(array)` | `number` | Только длина массива | Реактивная длина массива |
|
|
51
|
+
|
|
52
|
+
**CRITICAL**: Не деструктурируй `useFormControlValue`! Он возвращает `T` напрямую, НЕ `{ value: T }`.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// WRONG - will always be undefined!
|
|
56
|
+
const { value: loanType } = useFormControlValue(control.loanType);
|
|
57
|
+
|
|
58
|
+
// CORRECT
|
|
59
|
+
const loanType = useFormControlValue(control.loanType);
|
|
60
|
+
|
|
61
|
+
// CORRECT - useFormControl returns object, destructuring OK
|
|
62
|
+
const { value, errors, disabled } = useFormControl(control.loanType);
|
|
63
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
## 1.5 QUICK START - Minimal Working Form
|
|
2
|
+
|
|
3
|
+
> **Schema-driven UI rule (read first)**: компонент И его пропсы (label, placeholder,
|
|
4
|
+
> options, type) объявляются в **схеме поля** (`component` + `componentProps`).
|
|
5
|
+
> В JSX рендерится один универсальный `<FormField control={form.x} />` из
|
|
6
|
+
> `@reformer/ui-kit` БЕЗ дополнительных props. Не пиши свои `Input`/`Select`/
|
|
7
|
+
> `Checkbox`-обёртки с `label`-prop'ами — это anti-pattern. См.
|
|
8
|
+
> `find_recipe(package="@reformer/ui-kit", topic="form-field-integration")`.
|
|
9
|
+
|
|
10
|
+
Архитектура M1: сначала создаётся **модель данных** (`createModel`), затем **форма**
|
|
11
|
+
(`createForm({ model, schema })`), где схема привязывает поля к сигналам модели
|
|
12
|
+
(`model.$.field`). Layout-схема НЕ несёт валидаторов — валидация живёт в отдельной
|
|
13
|
+
схеме `defineValidationSchema` из `@reformer/core/validation` и запускается внешним
|
|
14
|
+
раннером `validateModel(model, schema)`.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { createModel, createForm, type FormProxy } from '@reformer/core';
|
|
18
|
+
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
19
|
+
import { required, email } from '@reformer/core/validators';
|
|
20
|
+
import { FormField, Input, Button } from '@reformer/ui-kit';
|
|
21
|
+
|
|
22
|
+
// 1. Define form type as `type` alias (not `interface` — see Recipe 2)
|
|
23
|
+
type ContactForm = {
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 2. Model (источник истины значений)
|
|
29
|
+
const model = createModel<ContactForm>({ name: '', email: '' });
|
|
30
|
+
|
|
31
|
+
// 3. Layout-schema: привязка поля к сигналу (model.$.field) + component/componentProps.
|
|
32
|
+
// БЕЗ validators — валидация в отдельной схеме (шаг 4).
|
|
33
|
+
const schema = {
|
|
34
|
+
name: {
|
|
35
|
+
value: model.$.name,
|
|
36
|
+
component: Input,
|
|
37
|
+
componentProps: { label: 'Name', placeholder: 'Your name' },
|
|
38
|
+
},
|
|
39
|
+
email: {
|
|
40
|
+
value: model.$.email,
|
|
41
|
+
component: Input,
|
|
42
|
+
componentProps: { label: 'Email', type: 'email' },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 4. Validation-schema — отдельный слой (@reformer/core/validation)
|
|
47
|
+
const contactValidation = defineValidationSchema<ContactForm>(({ model }) => {
|
|
48
|
+
validate(model.$.name, [required({ message: 'Name is required' })]);
|
|
49
|
+
validate(model.$.email, [required({ message: 'Email is required' }), email({ message: 'Invalid email' })]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 5. Form — ноды поверх сигналов модели
|
|
53
|
+
const form = createForm<ContactForm>({ model, schema });
|
|
54
|
+
|
|
55
|
+
// 6. Use in React component — thin JSX, FormField does ALL heavy lifting
|
|
56
|
+
function ContactFormComponent() {
|
|
57
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
// form.submit()/validate() НЕ гоняют schema-валидацию — только внешний validateModel
|
|
60
|
+
const ok = await validateModel(model, contactValidation); // Promise<boolean>, ошибки сам роутит в ноды
|
|
61
|
+
if (ok) {
|
|
62
|
+
console.log('Form submitted:', model.get());
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<form onSubmit={handleSubmit}>
|
|
68
|
+
<FormField control={form.name} testId="name" />
|
|
69
|
+
<FormField control={form.email} testId="email" />
|
|
70
|
+
<Button type="submit">Send</Button>
|
|
71
|
+
</form>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 7. Pass form to child components via props (NOT context!)
|
|
76
|
+
type FormStepProps = {
|
|
77
|
+
form: FormProxy<ContactForm>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function FormStep({ form }: FormStepProps) {
|
|
81
|
+
return <FormField control={form.name} testId="name" />;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> **Стабильность инстанса.** В React создавай model/schema/form ОДИН раз через `useMemo(() => { … }, [])`
|
|
86
|
+
> — иначе форма пересоздаётся на каждый рендер. См. `28-submit-and-reset.md`, `29-async-preload.md`.
|
|
87
|
+
|
|
88
|
+
### Arrays of objects — `{ array, item }` schema node
|
|
89
|
+
|
|
90
|
+
Массивы объектов принадлежат модели (`model.arrayField`). В схеме объявляются узлом
|
|
91
|
+
`{ array: model.<path>, item: (itemModel) => itemSchema }`, где `item` строит под-схему
|
|
92
|
+
для каждого элемента из его под-модели (`FormModel<Item>`):
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
type PropertyItem = {
|
|
96
|
+
type: 'apartment' | 'house';
|
|
97
|
+
description: string;
|
|
98
|
+
estimatedValue: number;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type MyForm = { properties: PropertyItem[] };
|
|
102
|
+
|
|
103
|
+
const model = createModel<MyForm>({ properties: [] });
|
|
104
|
+
|
|
105
|
+
// под-схема одного элемента: item.$.field — сигнал под-модели элемента
|
|
106
|
+
const propertyItem = (item: FormModel<PropertyItem>) => ({
|
|
107
|
+
type: {
|
|
108
|
+
value: item.$.type,
|
|
109
|
+
component: Select,
|
|
110
|
+
componentProps: { label: 'Тип', options: [/* ... */] },
|
|
111
|
+
},
|
|
112
|
+
description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
|
|
113
|
+
estimatedValue: {
|
|
114
|
+
value: item.$.estimatedValue,
|
|
115
|
+
component: Input,
|
|
116
|
+
componentProps: { label: 'Стоимость', type: 'number' },
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const schema = {
|
|
121
|
+
properties: { array: model.properties, item: propertyItem },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const form = createForm<MyForm>({ model, schema });
|
|
125
|
+
|
|
126
|
+
// Операции над массивом — на модели:
|
|
127
|
+
model.properties.push({ type: 'apartment', description: '', estimatedValue: 0 });
|
|
128
|
+
model.properties.removeAt(0);
|
|
129
|
+
model.properties.length; // реактивная длина
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Подробнее в `10-arrays.md`, `21-array-operations.md` и `find_recipe(topic="form-array")`.
|
|
133
|
+
|
|
134
|
+
### When to write your own field components (advanced — rare)
|
|
135
|
+
|
|
136
|
+
Свои компоненты нужны ТОЛЬКО если:
|
|
137
|
+
|
|
138
|
+
- ты намеренно избегаешь `@reformer/ui-kit` (например, проект уже имеет свою design system)
|
|
139
|
+
- нужен особый низкоуровневый input, который не покрывается `FormField` + `componentProps`
|
|
140
|
+
|
|
141
|
+
В этом случае см. секцию `## 14.5 UI COMPONENT PATTERNS` ниже — но даже там
|
|
142
|
+
паттерн **schema-driven** (label/options не из JSX-props, а из `componentProps` через
|
|
143
|
+
`useFormControl(...).componentProps`).
|