@reformer/core 11.0.0-beta.5 → 11.0.0-beta.7
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.js +124 -124
- package/dist/create-model-B8U-COv-.js +103 -0
- package/dist/en-CjAqn7h0.js +41 -0
- package/dist/form/behaviors/internals.d.ts +9 -0
- package/dist/form/create-form.d.ts +6 -6
- package/dist/form/factories/node-factory.d.ts +16 -16
- package/dist/form/nodes/array-node.d.ts +3 -3
- package/dist/form/nodes/field-node.d.ts +3 -3
- package/dist/form/nodes/group-node.d.ts +7 -7
- package/dist/form/nodes/model-array-node.d.ts +2 -2
- package/dist/form/types/contracts.d.ts +6 -0
- package/dist/form/types/deep-schema.d.ts +5 -5
- package/dist/form/types/validation-schema.d.ts +13 -1
- package/dist/form/validation/operators.d.ts +6 -1
- package/dist/form/validators/validation-error.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +54 -0
- package/dist/i18n/en.json.d.ts +45 -0
- package/dist/i18n/loader.d.ts +71 -0
- package/dist/i18n/locale-store.d.ts +39 -0
- package/dist/i18n/locale.d.ts +90 -0
- package/dist/i18n/message-format.d.ts +116 -0
- package/dist/i18n/resolve-localized.d.ts +17 -0
- package/dist/i18n/ru.json.d.ts +45 -0
- package/dist/i18n/translator.d.ts +50 -0
- package/dist/i18n/validation-message.d.ts +18 -0
- package/dist/i18n.d.ts +31 -0
- package/dist/i18n.js +303 -0
- package/dist/index-BrNkQj1y.js +105 -0
- package/dist/index.js +548 -601
- package/dist/loader-Dv_Hl7JC.js +216 -0
- package/dist/locale/en.d.ts +2 -0
- package/dist/locale/en.js +5 -0
- package/dist/locale/en.json +42 -0
- package/dist/locale/index.d.ts +18 -0
- package/dist/locale/ru.d.ts +2 -0
- package/dist/locale/ru.js +41 -0
- package/dist/locale/ru.json +42 -0
- package/dist/locale.d.ts +2 -0
- package/dist/locale.js +9 -0
- package/dist/model/create-model.d.ts +17 -3
- package/dist/model/index.d.ts +3 -3
- package/dist/model/model-signals-proxy.d.ts +25 -0
- package/dist/model/types.d.ts +19 -5
- package/dist/model-signals-proxy-2vqS243J.js +250 -0
- package/dist/model.js +18 -16
- package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
- package/dist/platforms/react/field-binding/field-handle.d.ts +50 -0
- package/dist/platforms/react/hooks/types.d.ts +6 -3
- package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
- package/dist/platforms/react/i18n/context.d.ts +13 -0
- package/dist/platforms/react/i18n/hooks.d.ts +59 -0
- package/dist/platforms/react/i18n/provider.d.ts +52 -0
- package/dist/platforms/react/index.d.ts +4 -0
- package/dist/{strategy-DA3JwxsR.js → strategy-BXS_gBfz.js} +7 -7
- package/dist/validation-error-B-U3RsV0.js +11 -0
- package/dist/validation.js +1 -1
- package/dist/validators/email.js +5 -8
- package/dist/validators/future-date.js +8 -11
- package/dist/validators/integer.js +4 -7
- package/dist/validators/is-date.js +5 -8
- package/dist/validators/is-number.js +4 -7
- package/dist/validators/max-age.js +8 -11
- package/dist/validators/max-date.js +8 -11
- package/dist/validators/max-length.js +5 -8
- package/dist/validators/max.js +4 -7
- package/dist/validators/min-age.js +8 -11
- package/dist/validators/min-date.js +8 -11
- package/dist/validators/min-length.js +5 -8
- package/dist/validators/min.js +4 -7
- package/dist/validators/multiple-of.js +5 -8
- package/dist/validators/non-negative.js +4 -7
- package/dist/validators/non-zero.js +4 -7
- package/dist/validators/past-date.js +8 -11
- package/dist/validators/pattern.js +5 -8
- package/dist/validators/phone.js +6 -9
- package/dist/validators/required.js +4 -7
- package/dist/validators/url.js +9 -14
- package/dist/validators.js +84 -106
- package/docs/llms/02-quick-start.md +7 -7
- package/docs/llms/03-api-signatures.md +11 -5
- package/docs/llms/05-common-mistakes.md +1 -1
- package/docs/llms/09-formschema.md +12 -12
- package/docs/llms/10-arrays.md +34 -4
- package/docs/llms/14-extended-mistakes.md +1 -1
- package/docs/llms/15-project-structure.md +15 -13
- package/docs/llms/16-ui-components.md +36 -9
- package/docs/llms/17-nonexistent-api.md +16 -2
- package/docs/llms/29-async-preload.md +4 -4
- package/docs/llms/30-type-safety-recipes.md +1 -1
- package/docs/llms/34-i18n.md +272 -0
- package/llms-index.json +1 -1
- package/llms.txt +1925 -175
- package/package.json +22 -3
- package/dist/create-model-Dxh9cfLp.js +0 -97
- package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
package/dist/validators.js
CHANGED
|
@@ -1,56 +1,52 @@
|
|
|
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
|
|
1
|
+
import { required as d } from "./validators/required.js";
|
|
2
|
+
import { min as b } from "./validators/min.js";
|
|
3
|
+
import { max as L } from "./validators/max.js";
|
|
4
|
+
import { minLength as D } from "./validators/min-length.js";
|
|
5
|
+
import { maxLength as T } from "./validators/max-length.js";
|
|
6
|
+
import { email as w } from "./validators/email.js";
|
|
7
|
+
import { pattern as k } from "./validators/pattern.js";
|
|
8
|
+
import { url as B } from "./validators/url.js";
|
|
9
|
+
import { phone as O } from "./validators/phone.js";
|
|
10
|
+
import { isNumber as G } from "./validators/is-number.js";
|
|
11
|
+
import { integer as I } from "./validators/integer.js";
|
|
12
|
+
import { multipleOf as K } from "./validators/multiple-of.js";
|
|
13
|
+
import { nonNegative as P } from "./validators/non-negative.js";
|
|
14
|
+
import { nonZero as R } from "./validators/non-zero.js";
|
|
15
|
+
import { isDate as V } from "./validators/is-date.js";
|
|
16
|
+
import { minDate as Y } from "./validators/min-date.js";
|
|
17
|
+
import { maxDate as $ } from "./validators/max-date.js";
|
|
18
|
+
import { pastDate as ee } from "./validators/past-date.js";
|
|
19
|
+
import { futureDate as ne } from "./validators/future-date.js";
|
|
20
|
+
import { minAge as ie } from "./validators/min-age.js";
|
|
21
|
+
import { maxAge as le } from "./validators/max-age.js";
|
|
22
|
+
import { v as l } from "./validation-error-B-U3RsV0.js";
|
|
22
23
|
function s(r) {
|
|
23
24
|
if (typeof r != "object" || r === null) return !1;
|
|
24
25
|
const t = r;
|
|
25
26
|
return !(typeof t.name != "string" || t.size !== void 0 && typeof t.size != "number" || t.type !== void 0 && typeof t.type != "string");
|
|
26
27
|
}
|
|
27
|
-
function
|
|
28
|
+
function f(r) {
|
|
28
29
|
return Array.isArray(r) ? r.filter(s) : s(r) ? [r] : null;
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
-
const e = t.split(",").map((
|
|
31
|
+
function m(r, t) {
|
|
32
|
+
const e = t.split(",").map((o) => o.trim().toLowerCase()).filter(Boolean);
|
|
32
33
|
if (e.length === 0) return !0;
|
|
33
34
|
const n = r.name.toLowerCase(), i = (r.type ?? "").toLowerCase();
|
|
34
|
-
return e.some((
|
|
35
|
+
return e.some((o) => o.startsWith(".") ? n.endsWith(o) : o.endsWith("/*") ? i.startsWith(o.slice(0, -1)) : o.includes("/") ? i === o : !1);
|
|
35
36
|
}
|
|
36
|
-
function
|
|
37
|
+
function c(r, t) {
|
|
37
38
|
return (e) => {
|
|
38
39
|
if (e == null || e === "")
|
|
39
40
|
return null;
|
|
40
|
-
const n =
|
|
41
|
+
const n = f(e);
|
|
41
42
|
if (n === null || n.length === 0) return null;
|
|
42
43
|
for (const i of n)
|
|
43
44
|
if (typeof i.size == "number" && i.size > r)
|
|
44
|
-
return {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
fileName: i.name,
|
|
50
|
-
actualSize: i.size,
|
|
51
|
-
...t?.params
|
|
52
|
-
}
|
|
53
|
-
};
|
|
45
|
+
return l("maxFileSize", t, {
|
|
46
|
+
maxFileSize: r,
|
|
47
|
+
fileName: i.name,
|
|
48
|
+
actualSize: i.size
|
|
49
|
+
});
|
|
54
50
|
return null;
|
|
55
51
|
};
|
|
56
52
|
}
|
|
@@ -58,109 +54,91 @@ function a(r, t) {
|
|
|
58
54
|
return (e) => {
|
|
59
55
|
if (e == null || e === "")
|
|
60
56
|
return null;
|
|
61
|
-
const n =
|
|
57
|
+
const n = f(e);
|
|
62
58
|
if (n === null || n.length === 0) return null;
|
|
63
59
|
for (const i of n)
|
|
64
60
|
if (typeof i.size == "number" && i.size < r)
|
|
65
|
-
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fileName: i.name,
|
|
71
|
-
actualSize: i.size,
|
|
72
|
-
...t?.params
|
|
73
|
-
}
|
|
74
|
-
};
|
|
61
|
+
return l("minFileSize", t, {
|
|
62
|
+
minFileSize: r,
|
|
63
|
+
fileName: i.name,
|
|
64
|
+
actualSize: i.size
|
|
65
|
+
});
|
|
75
66
|
return null;
|
|
76
67
|
};
|
|
77
68
|
}
|
|
78
|
-
function
|
|
69
|
+
function x(r, t) {
|
|
79
70
|
return (e) => {
|
|
80
71
|
if (e == null || e === "")
|
|
81
72
|
return null;
|
|
82
|
-
const n =
|
|
73
|
+
const n = f(e);
|
|
83
74
|
if (n === null || n.length === 0) return null;
|
|
84
75
|
for (const i of n)
|
|
85
|
-
if (!
|
|
86
|
-
return {
|
|
87
|
-
code: "fileType",
|
|
88
|
-
message: t?.message ?? "invalid",
|
|
89
|
-
params: { accept: r, fileName: i.name, ...t?.params }
|
|
90
|
-
};
|
|
76
|
+
if (!m(i, r))
|
|
77
|
+
return l("fileType", t, { accept: r, fileName: i.name });
|
|
91
78
|
return null;
|
|
92
79
|
};
|
|
93
80
|
}
|
|
94
|
-
function
|
|
81
|
+
function g(r, t) {
|
|
95
82
|
return (e) => {
|
|
96
83
|
if (e == null || e === "")
|
|
97
84
|
return null;
|
|
98
|
-
const n =
|
|
99
|
-
return n === null || n.length === 0 ? null : n.length > r ? {
|
|
100
|
-
code: "maxFiles",
|
|
101
|
-
message: t?.message ?? "invalid",
|
|
102
|
-
params: { maxFiles: r, actualCount: n.length, ...t?.params }
|
|
103
|
-
} : null;
|
|
85
|
+
const n = f(e);
|
|
86
|
+
return n === null || n.length === 0 ? null : n.length > r ? l("maxFiles", t, { maxFiles: r, actualCount: n.length }) : null;
|
|
104
87
|
};
|
|
105
88
|
}
|
|
106
|
-
function
|
|
89
|
+
function h(r, t) {
|
|
107
90
|
return (e) => {
|
|
108
91
|
if (e == null || e === "")
|
|
109
92
|
return null;
|
|
110
|
-
const n =
|
|
111
|
-
return n === null || n.length === 0 ? null : n.length < r ? {
|
|
112
|
-
code: "minFiles",
|
|
113
|
-
message: t?.message ?? "invalid",
|
|
114
|
-
params: { minFiles: r, actualCount: n.length, ...t?.params }
|
|
115
|
-
} : null;
|
|
93
|
+
const n = f(e);
|
|
94
|
+
return n === null || n.length === 0 ? null : n.length < r ? l("minFiles", t, { minFiles: r, actualCount: n.length }) : null;
|
|
116
95
|
};
|
|
117
96
|
}
|
|
118
|
-
function
|
|
97
|
+
function z(r, t) {
|
|
119
98
|
return (e) => {
|
|
120
99
|
if (e == null || e === "")
|
|
121
100
|
return null;
|
|
122
|
-
const n =
|
|
101
|
+
const n = f(e);
|
|
123
102
|
if (n === null || n.length === 0) return null;
|
|
124
103
|
const i = n.reduce(
|
|
125
|
-
(
|
|
104
|
+
(o, u) => o + (typeof u.size == "number" ? u.size : 0),
|
|
126
105
|
0
|
|
127
106
|
);
|
|
128
|
-
return i > r ? {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
} : null;
|
|
107
|
+
return i > r ? l("maxTotalFileSize", t, {
|
|
108
|
+
maxTotalFileSize: r,
|
|
109
|
+
actualTotal: i
|
|
110
|
+
}) : null;
|
|
133
111
|
};
|
|
134
112
|
}
|
|
135
113
|
export {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
114
|
+
w as email,
|
|
115
|
+
x as fileType,
|
|
116
|
+
ne as futureDate,
|
|
117
|
+
I as integer,
|
|
118
|
+
V as isDate,
|
|
141
119
|
s as isFileLike,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
120
|
+
G as isNumber,
|
|
121
|
+
m as matchesFileAccept,
|
|
122
|
+
L as max,
|
|
123
|
+
le as maxAge,
|
|
124
|
+
$ as maxDate,
|
|
125
|
+
c as maxFileSize,
|
|
126
|
+
g as maxFiles,
|
|
127
|
+
T as maxLength,
|
|
128
|
+
z as maxTotalFileSize,
|
|
129
|
+
b as min,
|
|
130
|
+
ie as minAge,
|
|
131
|
+
Y as minDate,
|
|
154
132
|
a as minFileSize,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
133
|
+
h as minFiles,
|
|
134
|
+
D as minLength,
|
|
135
|
+
K as multipleOf,
|
|
136
|
+
P as nonNegative,
|
|
137
|
+
R as nonZero,
|
|
138
|
+
ee as pastDate,
|
|
139
|
+
k as pattern,
|
|
140
|
+
O as phone,
|
|
141
|
+
d as required,
|
|
142
|
+
f as toFileArray,
|
|
143
|
+
B as url
|
|
166
144
|
};
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';
|
|
19
19
|
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
20
20
|
import { required, email } from '@reformer/core/validators';
|
|
21
|
-
import { FormField,
|
|
21
|
+
import { FormField, Input, Button } from '@reformer/ui-kit';
|
|
22
22
|
|
|
23
23
|
// 1. Define form type as `type` alias (not `interface` — see Recipe 2)
|
|
24
24
|
type ContactForm = {
|
|
@@ -34,12 +34,12 @@ const model = createModel<ContactForm>({ name: '', email: '' });
|
|
|
34
34
|
const schema = {
|
|
35
35
|
name: {
|
|
36
36
|
value: model.$.name,
|
|
37
|
-
component:
|
|
37
|
+
component: Input,
|
|
38
38
|
componentProps: { label: 'Name', placeholder: 'Your name' },
|
|
39
39
|
},
|
|
40
40
|
email: {
|
|
41
41
|
value: model.$.email,
|
|
42
|
-
component:
|
|
42
|
+
component: Input,
|
|
43
43
|
componentProps: { label: 'Email', type: 'email' },
|
|
44
44
|
},
|
|
45
45
|
};
|
|
@@ -113,14 +113,14 @@ const model = createModel<MyForm>({ properties: [] });
|
|
|
113
113
|
const propertyItem = (item: FormModel<PropertyItem>) => ({
|
|
114
114
|
type: {
|
|
115
115
|
value: item.$.type,
|
|
116
|
-
component:
|
|
116
|
+
component: SelectAsync,
|
|
117
117
|
componentProps: { label: 'Тип', options: [/* ... */] },
|
|
118
118
|
},
|
|
119
|
-
description: { value: item.$.description, component:
|
|
119
|
+
description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
|
|
120
120
|
estimatedValue: {
|
|
121
121
|
value: item.$.estimatedValue,
|
|
122
|
-
component:
|
|
123
|
-
componentProps: { label: 'Стоимость'
|
|
122
|
+
component: InputNumber,
|
|
123
|
+
componentProps: { label: 'Стоимость' },
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
|
|
@@ -57,6 +57,12 @@ minAge(years: number, options?: { message?: string })
|
|
|
57
57
|
maxAge(years: number, options?: { message?: string })
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
Кроме `message`, каждое правило принимает `messageKey` (ключ словаря приложения) и `params`.
|
|
61
|
+
Без `message` правило кладёт в ошибку пустую строку, а текст при отображении берётся из словаря
|
|
62
|
+
локали по коду (`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без
|
|
63
|
+
провайдера. Явное `message` важнее словаря, но языком не переключается; для локализуемого текста —
|
|
64
|
+
`messageKey`. Подробно — [34-i18n.md](./34-i18n.md).
|
|
65
|
+
|
|
60
66
|
Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
|
|
61
67
|
`ValidationSchema` и гоняются раннером `validateModel` по требованию.
|
|
62
68
|
|
|
@@ -65,16 +71,16 @@ import { createModel, createForm } from '@reformer/core';
|
|
|
65
71
|
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
66
72
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
67
73
|
|
|
68
|
-
type Loan = { email: string; age: number; amount: number };
|
|
69
|
-
const model = createModel<Loan>({ email: '', age:
|
|
74
|
+
type Loan = { email: string; age: number | null; amount: number | null };
|
|
75
|
+
const model = createModel<Loan>({ email: '', age: null, amount: null });
|
|
70
76
|
|
|
71
77
|
// layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
|
|
72
78
|
const form = createForm({
|
|
73
79
|
model,
|
|
74
80
|
schema: {
|
|
75
|
-
email: { value: model.$.email, component:
|
|
76
|
-
age: { value: model.$.age, component:
|
|
77
|
-
amount: { value: model.$.amount, component:
|
|
81
|
+
email: { value: model.$.email, component: Input },
|
|
82
|
+
age: { value: model.$.age, component: InputNumber },
|
|
83
|
+
amount: { value: model.$.amount, component: InputNumber },
|
|
78
84
|
},
|
|
79
85
|
});
|
|
80
86
|
|
|
@@ -93,7 +93,7 @@ computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => pr
|
|
|
93
93
|
```typescript
|
|
94
94
|
// ❌ WRONG - дерево { value, validators } и позиционная строка удалены
|
|
95
95
|
const schema = {
|
|
96
|
-
email: { value: model.$.email, component:
|
|
96
|
+
email: { value: model.$.email, component: Input, validators: [required(), email()] },
|
|
97
97
|
};
|
|
98
98
|
validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
|
|
99
99
|
|
|
@@ -10,7 +10,7 @@ layout-схеме НЕТ — правила живут в отдельной с
|
|
|
10
10
|
```typescript
|
|
11
11
|
{
|
|
12
12
|
value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
|
|
13
|
-
component:
|
|
13
|
+
component: Input, // React-компонент
|
|
14
14
|
componentProps?: object, // пропсы (label, placeholder, options, type, ...)
|
|
15
15
|
disabled?: boolean,
|
|
16
16
|
updateOn?: 'change' | 'blur' | 'submit',
|
|
@@ -28,7 +28,7 @@ layout-схеме НЕТ — правила живут в отдельной с
|
|
|
28
28
|
import { createModel, createForm } from '@reformer/core';
|
|
29
29
|
import { defineValidationSchema, validate } from '@reformer/core/validation';
|
|
30
30
|
import { required } from '@reformer/core/validators';
|
|
31
|
-
import {
|
|
31
|
+
import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
|
|
32
32
|
|
|
33
33
|
const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
|
|
34
34
|
|
|
@@ -40,22 +40,22 @@ const myValidation = defineValidationSchema<MyForm>(({ model }) => {
|
|
|
40
40
|
const schema = {
|
|
41
41
|
name: {
|
|
42
42
|
value: model.$.name,
|
|
43
|
-
component:
|
|
43
|
+
component: Input,
|
|
44
44
|
componentProps: { label: 'Name', placeholder: 'Enter name' },
|
|
45
45
|
},
|
|
46
46
|
age: {
|
|
47
47
|
value: model.$.age,
|
|
48
|
-
component:
|
|
49
|
-
componentProps: {
|
|
48
|
+
component: InputNumber,
|
|
49
|
+
componentProps: { label: 'Age' },
|
|
50
50
|
},
|
|
51
51
|
agree: {
|
|
52
52
|
value: model.$.agree,
|
|
53
|
-
component:
|
|
53
|
+
component: CheckboxWithLabel,
|
|
54
54
|
componentProps: { label: 'I agree to terms' },
|
|
55
55
|
},
|
|
56
56
|
status: {
|
|
57
57
|
value: model.$.status,
|
|
58
|
-
component:
|
|
58
|
+
component: SelectAsync,
|
|
59
59
|
componentProps: {
|
|
60
60
|
label: 'Status',
|
|
61
61
|
options: [
|
|
@@ -79,9 +79,9 @@ const form = createForm<MyForm>({ model, schema });
|
|
|
79
79
|
import type { FormModel } from '@reformer/core';
|
|
80
80
|
|
|
81
81
|
const addressNodes = (m: FormModel<Address>) => ({
|
|
82
|
-
street: { value: m.$.street, component:
|
|
83
|
-
city: { value: m.$.city, component:
|
|
84
|
-
zip: { value: m.$.zip, component:
|
|
82
|
+
street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
|
|
83
|
+
city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
|
|
84
|
+
zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
const schema = {
|
|
@@ -98,8 +98,8 @@ const schema = {
|
|
|
98
98
|
import type { FormModel } from '@reformer/core';
|
|
99
99
|
|
|
100
100
|
const itemSchema = (item: FormModel<Item>) => ({
|
|
101
|
-
id: { value: item.$.id, component:
|
|
102
|
-
name: { value: item.$.name, component:
|
|
101
|
+
id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
|
|
102
|
+
name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
const schema = {
|
package/docs/llms/10-arrays.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
9
|
import { createModel, createForm, type FormModel } from '@reformer/core';
|
|
10
|
-
import {
|
|
10
|
+
import { Input, InputNumber } from '@reformer/ui-kit';
|
|
11
11
|
|
|
12
12
|
type Item = { id: string; name: string; price: number };
|
|
13
13
|
type MyForm = { items: Item[] };
|
|
@@ -16,9 +16,9 @@ const model = createModel<MyForm>({ items: [] });
|
|
|
16
16
|
|
|
17
17
|
// под-схема одного элемента (item.$.field — сигнал под-модели)
|
|
18
18
|
const itemSchema = (item: FormModel<Item>) => ({
|
|
19
|
-
id: { value: item.$.id, component:
|
|
20
|
-
name: { value: item.$.name, component:
|
|
21
|
-
price: { value: item.$.price, component:
|
|
19
|
+
id: { value: item.$.id, component: Input },
|
|
20
|
+
name: { value: item.$.name, component: Input },
|
|
21
|
+
price: { value: item.$.price, component: InputNumber },
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
const schema = {
|
|
@@ -32,6 +32,36 @@ const form = createForm<MyForm>({ model, schema });
|
|
|
32
32
|
> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
|
|
33
33
|
> См. `30-type-safety-recipes.md`.
|
|
34
34
|
|
|
35
|
+
### Массив как ОДНО значение поля
|
|
36
|
+
|
|
37
|
+
Массив бывает не набором под-форм, а одним значением: мультивыбор, теги, список файлов. Чем он
|
|
38
|
+
является, решает **схема**, а не данные: `{ array, item }` — набор под-форм, а узел поля
|
|
39
|
+
`{ value: model.$.<путь>, component }` — одно значение. Массив, к которому в схеме ничего не
|
|
40
|
+
привязано, в форму не попадает.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
type MyForm = { tags: string[] };
|
|
44
|
+
|
|
45
|
+
const model = createModel<MyForm>({ tags: [] });
|
|
46
|
+
|
|
47
|
+
const schema = {
|
|
48
|
+
tags: { value: model.$.tags, component: SelectMulti }, // поле над массивом целиком
|
|
49
|
+
};
|
|
50
|
+
const form = createForm<MyForm>({ model, schema });
|
|
51
|
+
|
|
52
|
+
form.tags.setValue(['a', 'b']); // FieldNode<string[]>
|
|
53
|
+
model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
|
|
54
|
+
validate(model.$.tags, [required(), maxLength(3)]); // правило получает массив
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `model.$.<массив>` — записываемая ручка значения (`PathAwareSignal`): подходит для `value:` в
|
|
58
|
+
схеме, `validate`/`cross`, `enableWhen`/`copyFrom` и для `model.signalAt(path)`.
|
|
59
|
+
- Узел-массив хранит только массив: запись `null`/`undefined` даёт `[]`. Нужно отличать «не
|
|
60
|
+
выбирали» от «выбрали ничего» — объявляй поле `T[] | null` с начальным `null`: в рантайме это
|
|
61
|
+
лист, и привязывается он так же.
|
|
62
|
+
- Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
|
|
63
|
+
`model.patch(...)`.
|
|
64
|
+
|
|
35
65
|
### Один массив — три слоя (три разных движка)
|
|
36
66
|
|
|
37
67
|
Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
|
|
@@ -71,7 +71,7 @@ import { revalidateWhen, type ValidationError } from '@reformer/core';
|
|
|
71
71
|
// ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
|
|
72
72
|
const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
|
|
73
73
|
root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
|
|
74
|
-
const legacySchema = { field1: { value: model.$.field1, component:
|
|
74
|
+
const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
|
|
75
75
|
validateFormModel(model, legacySchema);
|
|
76
76
|
|
|
77
77
|
// ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
|
|
@@ -2,11 +2,13 @@
|
|
|
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
|
-
|
|
5
|
+
`index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
|
|
6
|
+
`form.schema.*` (layout), `form.behavior.ts` (model behavior), `form.render.ts` (render-layer
|
|
7
|
+
behavior, renderer targets only), `form.validation.ts` (validation rules); every other file has
|
|
8
|
+
no prefix. A wizard may instead keep each
|
|
9
|
+
step's code in `steps/<slug>/` (same file names inside) with a `steps/index.ts` aggregator. The old
|
|
10
|
+
`renderer.schema.*` / `renderer.behavior.ts` / `renderer.wizard.tsx` / `validation.ts` names still
|
|
11
|
+
work with a warning. No `lib/` / `schema/` / `components/` nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
|
|
10
12
|
and rendered with `FormArraySection` — no per-step component files.
|
|
11
13
|
|
|
12
14
|
```
|
|
@@ -20,13 +22,13 @@ src/
|
|
|
20
22
|
│ ├── model.ts # createModel + initial values + empty-array-element factories
|
|
21
23
|
│ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
|
|
22
24
|
│ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
|
|
23
|
-
│ ├── validation.ts
|
|
25
|
+
│ ├── form.validation.ts # ALL validation → { validateStep, validateAll }
|
|
24
26
|
│ ├── data-sources.ts # options + async loaders (dataSources)
|
|
25
27
|
│ └── api.ts # submit + prefill/load
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
|
|
29
|
-
`index.tsx`; validation is a single `validation.ts`.** This is the working default for
|
|
31
|
+
`index.tsx`; validation is a single `form.validation.ts`.** This is the working default for
|
|
30
32
|
almost every form — reach for folders only when a file stops fitting on a screen.
|
|
31
33
|
|
|
32
34
|
### Key Files
|
|
@@ -47,7 +49,7 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
|
|
|
47
49
|
// forms/credit-application/form.schema.ts
|
|
48
50
|
import type { FormModel } from '@reformer/core';
|
|
49
51
|
export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
|
|
50
|
-
loanType: { value: model.$.loanType, component:
|
|
52
|
+
loanType: { value: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } },
|
|
51
53
|
personalData: personalDataNodes(model.$.personalData),
|
|
52
54
|
properties: { array: model.properties, item: propertyItem },
|
|
53
55
|
});
|
|
@@ -95,7 +97,7 @@ forms/
|
|
|
95
97
|
├── schema/ # The form definition
|
|
96
98
|
│ ├── model.ts # createModel factory + initial values + array-element factories
|
|
97
99
|
│ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
|
|
98
|
-
│ ├── validation.ts
|
|
100
|
+
│ ├── form.validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
|
|
99
101
|
│ ├── form.behavior.ts # defineFormBehavior(...)
|
|
100
102
|
│ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
|
|
101
103
|
│
|
|
@@ -107,7 +109,7 @@ forms/
|
|
|
107
109
|
|
|
108
110
|
Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
|
|
109
111
|
`schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
|
|
110
|
-
may co-locate each step's `form.schema.ts` / `validation.ts` / `form.behavior.ts` inside its
|
|
112
|
+
may co-locate each step's `form.schema.ts` / `form.validation.ts` / `form.behavior.ts` inside its
|
|
111
113
|
`steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
|
|
112
114
|
the guide below.
|
|
113
115
|
|
|
@@ -123,8 +125,8 @@ the guide below.
|
|
|
123
125
|
> when registering the MCP server to choose which structure the generators lead with. Default
|
|
124
126
|
> is `minimalist`.
|
|
125
127
|
|
|
126
|
-
> Cross-target variants (renderer-react `
|
|
127
|
-
> renderer-json `
|
|
128
|
-
> `
|
|
128
|
+
> Cross-target variants (renderer-react `form.schema.ts` + `form.render.ts` /
|
|
129
|
+
> renderer-json `form.schema.ts` + `form.render.ts` + `registry.ts`, plus an optional
|
|
130
|
+
> `wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
|
|
129
131
|
> choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —
|
|
130
132
|
> `find_recipe directory-layout`.
|