@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.
Files changed (95) hide show
  1. package/dist/behaviors.js +124 -124
  2. package/dist/create-model-B8U-COv-.js +103 -0
  3. package/dist/en-CjAqn7h0.js +41 -0
  4. package/dist/form/behaviors/internals.d.ts +9 -0
  5. package/dist/form/create-form.d.ts +6 -6
  6. package/dist/form/factories/node-factory.d.ts +16 -16
  7. package/dist/form/nodes/array-node.d.ts +3 -3
  8. package/dist/form/nodes/field-node.d.ts +3 -3
  9. package/dist/form/nodes/group-node.d.ts +7 -7
  10. package/dist/form/nodes/model-array-node.d.ts +2 -2
  11. package/dist/form/types/contracts.d.ts +6 -0
  12. package/dist/form/types/deep-schema.d.ts +5 -5
  13. package/dist/form/types/validation-schema.d.ts +13 -1
  14. package/dist/form/validation/operators.d.ts +6 -1
  15. package/dist/form/validators/validation-error.d.ts +8 -0
  16. package/dist/i18n/descriptor.d.ts +54 -0
  17. package/dist/i18n/en.json.d.ts +45 -0
  18. package/dist/i18n/loader.d.ts +71 -0
  19. package/dist/i18n/locale-store.d.ts +39 -0
  20. package/dist/i18n/locale.d.ts +90 -0
  21. package/dist/i18n/message-format.d.ts +116 -0
  22. package/dist/i18n/resolve-localized.d.ts +17 -0
  23. package/dist/i18n/ru.json.d.ts +45 -0
  24. package/dist/i18n/translator.d.ts +50 -0
  25. package/dist/i18n/validation-message.d.ts +18 -0
  26. package/dist/i18n.d.ts +31 -0
  27. package/dist/i18n.js +303 -0
  28. package/dist/index-BrNkQj1y.js +105 -0
  29. package/dist/index.js +548 -601
  30. package/dist/loader-Dv_Hl7JC.js +216 -0
  31. package/dist/locale/en.d.ts +2 -0
  32. package/dist/locale/en.js +5 -0
  33. package/dist/locale/en.json +42 -0
  34. package/dist/locale/index.d.ts +18 -0
  35. package/dist/locale/ru.d.ts +2 -0
  36. package/dist/locale/ru.js +41 -0
  37. package/dist/locale/ru.json +42 -0
  38. package/dist/locale.d.ts +2 -0
  39. package/dist/locale.js +9 -0
  40. package/dist/model/create-model.d.ts +17 -3
  41. package/dist/model/index.d.ts +3 -3
  42. package/dist/model/model-signals-proxy.d.ts +25 -0
  43. package/dist/model/types.d.ts +19 -5
  44. package/dist/model-signals-proxy-2vqS243J.js +250 -0
  45. package/dist/model.js +18 -16
  46. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  47. package/dist/platforms/react/field-binding/field-handle.d.ts +50 -0
  48. package/dist/platforms/react/hooks/types.d.ts +6 -3
  49. package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
  50. package/dist/platforms/react/i18n/context.d.ts +13 -0
  51. package/dist/platforms/react/i18n/hooks.d.ts +59 -0
  52. package/dist/platforms/react/i18n/provider.d.ts +52 -0
  53. package/dist/platforms/react/index.d.ts +4 -0
  54. package/dist/{strategy-DA3JwxsR.js → strategy-BXS_gBfz.js} +7 -7
  55. package/dist/validation-error-B-U3RsV0.js +11 -0
  56. package/dist/validation.js +1 -1
  57. package/dist/validators/email.js +5 -8
  58. package/dist/validators/future-date.js +8 -11
  59. package/dist/validators/integer.js +4 -7
  60. package/dist/validators/is-date.js +5 -8
  61. package/dist/validators/is-number.js +4 -7
  62. package/dist/validators/max-age.js +8 -11
  63. package/dist/validators/max-date.js +8 -11
  64. package/dist/validators/max-length.js +5 -8
  65. package/dist/validators/max.js +4 -7
  66. package/dist/validators/min-age.js +8 -11
  67. package/dist/validators/min-date.js +8 -11
  68. package/dist/validators/min-length.js +5 -8
  69. package/dist/validators/min.js +4 -7
  70. package/dist/validators/multiple-of.js +5 -8
  71. package/dist/validators/non-negative.js +4 -7
  72. package/dist/validators/non-zero.js +4 -7
  73. package/dist/validators/past-date.js +8 -11
  74. package/dist/validators/pattern.js +5 -8
  75. package/dist/validators/phone.js +6 -9
  76. package/dist/validators/required.js +4 -7
  77. package/dist/validators/url.js +9 -14
  78. package/dist/validators.js +84 -106
  79. package/docs/llms/02-quick-start.md +7 -7
  80. package/docs/llms/03-api-signatures.md +11 -5
  81. package/docs/llms/05-common-mistakes.md +1 -1
  82. package/docs/llms/09-formschema.md +12 -12
  83. package/docs/llms/10-arrays.md +34 -4
  84. package/docs/llms/14-extended-mistakes.md +1 -1
  85. package/docs/llms/15-project-structure.md +15 -13
  86. package/docs/llms/16-ui-components.md +36 -9
  87. package/docs/llms/17-nonexistent-api.md +16 -2
  88. package/docs/llms/29-async-preload.md +4 -4
  89. package/docs/llms/30-type-safety-recipes.md +1 -1
  90. package/docs/llms/34-i18n.md +272 -0
  91. package/llms-index.json +1 -1
  92. package/llms.txt +1925 -175
  93. package/package.json +22 -3
  94. package/dist/create-model-Dxh9cfLp.js +0 -97
  95. package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
@@ -1,56 +1,52 @@
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";
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 o(r) {
28
+ function f(r) {
28
29
  return Array.isArray(r) ? r.filter(s) : s(r) ? [r] : null;
29
30
  }
30
- function u(r, t) {
31
- const e = t.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean);
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((l) => l.startsWith(".") ? n.endsWith(l) : l.endsWith("/*") ? i.startsWith(l.slice(0, -1)) : l.includes("/") ? i === l : !1);
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 m(r, t) {
37
+ function c(r, t) {
37
38
  return (e) => {
38
39
  if (e == null || e === "")
39
40
  return null;
40
- const n = o(e);
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
- code: "maxFileSize",
46
- message: t?.message ?? "invalid",
47
- params: {
48
- maxFileSize: r,
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 = o(e);
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
- code: "minFileSize",
67
- message: t?.message ?? "invalid",
68
- params: {
69
- minFileSize: r,
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 p(r, t) {
69
+ function x(r, t) {
79
70
  return (e) => {
80
71
  if (e == null || e === "")
81
72
  return null;
82
- const n = o(e);
73
+ const n = f(e);
83
74
  if (n === null || n.length === 0) return null;
84
75
  for (const i of n)
85
- if (!u(i, r))
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 c(r, t) {
81
+ function g(r, t) {
95
82
  return (e) => {
96
83
  if (e == null || e === "")
97
84
  return null;
98
- const n = o(e);
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 x(r, t) {
89
+ function h(r, t) {
107
90
  return (e) => {
108
91
  if (e == null || e === "")
109
92
  return null;
110
- const n = o(e);
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 g(r, t) {
97
+ function z(r, t) {
119
98
  return (e) => {
120
99
  if (e == null || e === "")
121
100
  return null;
122
- const n = o(e);
101
+ const n = f(e);
123
102
  if (n === null || n.length === 0) return null;
124
103
  const i = n.reduce(
125
- (l, f) => l + (typeof f.size == "number" ? f.size : 0),
104
+ (o, u) => o + (typeof u.size == "number" ? u.size : 0),
126
105
  0
127
106
  );
128
- return i > r ? {
129
- code: "maxTotalFileSize",
130
- message: t?.message ?? "invalid",
131
- params: { maxTotalFileSize: r, actualTotal: i, ...t?.params }
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
- T as email,
137
- p as fileType,
138
- ee as futureDate,
139
- G as integer,
140
- R as isDate,
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
- 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,
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
- 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,
164
- o as toFileArray,
165
- k as url
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, InputField, Button } from '@reformer/ui-kit';
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: InputField,
37
+ component: Input,
38
38
  componentProps: { label: 'Name', placeholder: 'Your name' },
39
39
  },
40
40
  email: {
41
41
  value: model.$.email,
42
- component: InputField,
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: SelectField,
116
+ component: SelectAsync,
117
117
  componentProps: { label: 'Тип', options: [/* ... */] },
118
118
  },
119
- description: { value: item.$.description, component: TextareaField, componentProps: { label: 'Описание' } },
119
+ description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
120
120
  estimatedValue: {
121
121
  value: item.$.estimatedValue,
122
- component: InputField,
123
- componentProps: { label: 'Стоимость', type: 'number' },
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: 0, amount: 0 });
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: InputField },
76
- age: { value: model.$.age, component: InputField },
77
- amount: { value: model.$.amount, component: InputField },
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: InputField, validators: [required(), email()] },
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: InputField, // React-компонент
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 { InputField, SelectField, CheckboxField } from '@reformer/ui-kit';
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: InputField,
43
+ component: Input,
44
44
  componentProps: { label: 'Name', placeholder: 'Enter name' },
45
45
  },
46
46
  age: {
47
47
  value: model.$.age,
48
- component: InputField,
49
- componentProps: { type: 'number', label: 'Age' },
48
+ component: InputNumber,
49
+ componentProps: { label: 'Age' },
50
50
  },
51
51
  agree: {
52
52
  value: model.$.agree,
53
- component: CheckboxField,
53
+ component: CheckboxWithLabel,
54
54
  componentProps: { label: 'I agree to terms' },
55
55
  },
56
56
  status: {
57
57
  value: model.$.status,
58
- component: SelectField,
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: InputField, componentProps: { label: 'Street' } },
83
- city: { value: m.$.city, component: InputField, componentProps: { label: 'City' } },
84
- zip: { value: m.$.zip, component: InputField, componentProps: { label: 'ZIP' } },
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: InputField, componentProps: { label: 'ID' } },
102
- name: { value: item.$.name, component: InputField, componentProps: { label: 'Name' } },
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 = {
@@ -7,7 +7,7 @@
7
7
 
8
8
  ```typescript
9
9
  import { createModel, createForm, type FormModel } from '@reformer/core';
10
- import { InputField } from '@reformer/ui-kit';
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: InputField },
20
- name: { value: item.$.name, component: InputField },
21
- price: { value: item.$.price, component: InputField, componentProps: { type: 'number' } },
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: InputField, validators: [legacyRule] } };
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`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
6
- carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
7
- render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
8
- `renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
9
- nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
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 # ALL validation → { validateStep, validateAll }
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: SelectField, componentProps: { /* ... */ } },
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 # defineValidationSchema + validateModel config ({ validateStep, validateAll })
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 `renderer.schema.ts` + `renderer.behavior.ts` /
127
- > renderer-json `renderer.schema.ts` + `renderer.behavior.ts` + `registry.ts`, plus an optional
128
- > `renderer.wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
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`.