@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
## 7. FORM TYPE DEFINITION
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без
|
|
3
|
+
Форму данных объявляй через `interface` или `type`-alias — оба варианта работают одинаково (см.
|
|
4
|
+
`30-type-safety-recipes.md`). Тип описывает форму ДАННЫХ (то, что кладётся в `createModel`), без
|
|
5
|
+
узлов/сигналов.
|
|
5
6
|
|
|
6
7
|
```typescript
|
|
7
8
|
// CORRECT form type definition
|
|
8
|
-
|
|
9
|
+
interface MyForm {
|
|
9
10
|
// Required fields
|
|
10
11
|
name: string;
|
|
11
12
|
email: string;
|
|
@@ -28,7 +29,7 @@ type MyForm = {
|
|
|
28
29
|
id: string;
|
|
29
30
|
name: string;
|
|
30
31
|
}>;
|
|
31
|
-
}
|
|
32
|
+
}
|
|
32
33
|
|
|
33
34
|
// Модель создаётся из initial-значений этого типа
|
|
34
35
|
const model = createModel<MyForm>({
|
|
@@ -45,3 +46,9 @@ const model = createModel<MyForm>({
|
|
|
45
46
|
`number | null` / `string | null` работают со встроенными валидаторами напрямую —
|
|
46
47
|
`min`/`max`/`minLength`/`minDate`/`minAge` пропускают пустые значения внутри, guard `if (v != null)`
|
|
47
48
|
не нужен.
|
|
49
|
+
|
|
50
|
+
Какой ноде формы соответствует поле, решает вид значения: объект — `FormGroup`
|
|
51
|
+
(`form.address.city`), массив объектов — `FormArray` (`form.items.at(0)`), примитив, массив
|
|
52
|
+
примитивов, `Date`, `File` — `FormControl`. Вычисляемые поля (цели `compute`) удобно держать
|
|
53
|
+
отдельным типом и объявлять форму пересечением: `type MyFormFull = MyForm & MyFormDerived` — тогда
|
|
54
|
+
на сервер уходит только ввод (`MyForm`), а вычисляемые ключи отрезаются перед отправкой.
|
|
@@ -1,143 +1,191 @@
|
|
|
1
1
|
## 8. SCHEMA FORMAT (CRITICALLY IMPORTANT)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`@reformer/core/validation` и запускаются раннером `validateModel(model, schema)`.
|
|
3
|
+
Схема — **дерево узлов**. Узел привязан к части модели ключом `model` и держит UI-конфиг
|
|
4
|
+
(`component` / `componentProps`). Значения принадлежат модели. Правил валидации в схеме НЕТ — они
|
|
5
|
+
живут в отдельной схеме `defineValidation` из `@reformer/core/validation`.
|
|
7
6
|
|
|
8
|
-
###
|
|
7
|
+
### Четыре вида узла
|
|
8
|
+
|
|
9
|
+
Тип узла — `FormSchemaNode`, закрытый union. Вид определяют ключи `model`, `item`, `part` и
|
|
10
|
+
`children`:
|
|
11
|
+
|
|
12
|
+
| Вид | Ключи | Пример |
|
|
13
|
+
| --------------- | --------------------------------- | --------------------------------------------------- |
|
|
14
|
+
| поле | `model` — лист или массив целиком | `{ model: model.$.email, component: Input }` |
|
|
15
|
+
| массив под-форм | `model` — массив + `item` | `{ model: model.$.phones, item: phoneRow }` |
|
|
16
|
+
| подформа | `model` — группа + `part` | `{ model: model.$.address, part: address }` |
|
|
17
|
+
| контейнер | `children` | `{ component: Section, children: [/* узлы */] }` |
|
|
18
|
+
|
|
19
|
+
Общие ключи любого узла: `component`, `componentProps`, `selector`. У поля ещё `disabled`, у
|
|
20
|
+
массива под-форм — `initialValue` (запасной шаблон новой строки).
|
|
9
21
|
|
|
10
22
|
```typescript
|
|
23
|
+
// Поле
|
|
11
24
|
{
|
|
12
|
-
|
|
25
|
+
model: model.$.fieldName, // ручка значения из model.$ — обязательна
|
|
13
26
|
component: Input, // React-компонент
|
|
14
27
|
componentProps?: object, // пропсы (label, placeholder, options, type, ...)
|
|
15
|
-
disabled?: boolean,
|
|
16
|
-
|
|
17
|
-
debounce?: number,
|
|
28
|
+
disabled?: boolean, // постоянная блокировка поля (вычисляемые total, fullName)
|
|
29
|
+
selector?: string, // имя узла для поведения: schema.node('…')
|
|
18
30
|
}
|
|
19
31
|
```
|
|
20
32
|
|
|
21
|
-
> `
|
|
22
|
-
>
|
|
23
|
-
>
|
|
33
|
+
> **Вложенные узлы читаются только из `children` и из поддерева `part`.** В `componentProps` и под
|
|
34
|
+
> произвольными ключами сборка не заглядывает: узел, положенный туда, полем не станет. В dev
|
|
35
|
+
> сборка предупреждает о ключах узла, которые она не читает.
|
|
36
|
+
|
|
37
|
+
> **Тип закрыт — ошибки ловит компилятор:** опечатка в ключе (`componnet`), группа без `part`,
|
|
38
|
+
> `item` не на массиве, привязка value-фасадом (`model: model.email` вместо `model.$.email`) и
|
|
39
|
+
> узлы под произвольными ключами (`{ email: { model, component } }`) не компилируются.
|
|
40
|
+
|
|
41
|
+
> `disabled` — **ключ узла**, постоянная блокировка: `enable()` группы, массива или формы её не
|
|
42
|
+
> снимает (цикл «выключить форму на время отправки → включить» вычисляемые поля не разблокирует).
|
|
43
|
+
> Снимают её явный `form.field.enable()` и оператор `enableWhen` на этом поле. `componentProps.disabled` — это
|
|
44
|
+
> UI-проп и на состояние поля **не влияет** (частая причина «computed-поле остаётся редактируемым»).
|
|
24
45
|
|
|
25
46
|
### Primitive Fields
|
|
26
47
|
|
|
27
48
|
```typescript
|
|
28
|
-
import { createModel,
|
|
29
|
-
import {
|
|
49
|
+
import { createModel, createFormFromModel, type FormSchemaNode } from '@reformer/core';
|
|
50
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
30
51
|
import { required } from '@reformer/core/validators';
|
|
31
|
-
import { Input,
|
|
52
|
+
import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
|
|
32
53
|
|
|
33
54
|
const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
|
|
34
55
|
|
|
35
56
|
// Валидация — отдельным слоем (запуск: await validateModel(model, myValidation))
|
|
36
|
-
const myValidation =
|
|
57
|
+
const myValidation = defineValidation<MyForm>(({ model }) => {
|
|
37
58
|
validate(model.$.name, [required()]);
|
|
38
59
|
});
|
|
39
60
|
|
|
40
|
-
const schema = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
status: {
|
|
57
|
-
value: model.$.status,
|
|
58
|
-
component: Select,
|
|
59
|
-
componentProps: {
|
|
60
|
-
label: 'Status',
|
|
61
|
-
options: [
|
|
62
|
-
{ value: 'active', label: 'Active' },
|
|
63
|
-
{ value: 'inactive', label: 'Inactive' },
|
|
64
|
-
],
|
|
61
|
+
const schema: FormSchemaNode = {
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
model: model.$.name,
|
|
65
|
+
component: Input,
|
|
66
|
+
componentProps: { label: 'Name', placeholder: 'Enter name' },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
model: model.$.age,
|
|
70
|
+
component: InputNumber,
|
|
71
|
+
componentProps: { label: 'Age' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
model: model.$.agree,
|
|
75
|
+
component: CheckboxWithLabel,
|
|
76
|
+
componentProps: { label: 'I agree to terms' },
|
|
65
77
|
},
|
|
66
|
-
|
|
78
|
+
{
|
|
79
|
+
model: model.$.status,
|
|
80
|
+
component: SelectAsync,
|
|
81
|
+
componentProps: {
|
|
82
|
+
label: 'Status',
|
|
83
|
+
options: [
|
|
84
|
+
{ value: 'active', label: 'Active' },
|
|
85
|
+
{ value: 'inactive', label: 'Inactive' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
67
90
|
};
|
|
68
91
|
|
|
69
|
-
const form =
|
|
92
|
+
const form = createFormFromModel<MyForm>({ model, schema });
|
|
70
93
|
```
|
|
71
94
|
|
|
72
|
-
|
|
95
|
+
Поле данных может называться как угодно — `value`, `schema`, `form`, `model`, `children`: вид ноды
|
|
96
|
+
определяет узел модели, а не имя.
|
|
73
97
|
|
|
74
|
-
|
|
75
|
-
с `.$`/API). Удобно вынести в builder, принимающий `FormModel<Sub>` — симметрично builder'у элемента
|
|
76
|
-
массива. Сигналы под-модели идентичны корневым: `model.address.$.city === model.$.address.city`.
|
|
98
|
+
### Nested Objects — подформа `{ model, part }`
|
|
77
99
|
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
Вложенная группа модели — под-модель `FormModel<Sub>`. Её разметку объявляют один раз функцией от
|
|
101
|
+
под-модели и подключают к группе узлом-подформой. Привязки внутри части идут через `$` полученной
|
|
102
|
+
под-модели:
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
```typescript
|
|
105
|
+
import type { FormModel, FormSchemaNode } from '@reformer/core';
|
|
106
|
+
|
|
107
|
+
const address = (model: FormModel<Address>): FormSchemaNode => ({
|
|
108
|
+
children: [
|
|
109
|
+
{ model: model.$.street, component: Input, componentProps: { label: 'Street' } },
|
|
110
|
+
{ model: model.$.city, component: Input, componentProps: { label: 'City' } },
|
|
111
|
+
{ model: model.$.zip, component: Input, componentProps: { label: 'ZIP' } },
|
|
112
|
+
],
|
|
85
113
|
});
|
|
86
114
|
|
|
87
|
-
const schema = {
|
|
88
|
-
|
|
115
|
+
const schema: FormSchemaNode = {
|
|
116
|
+
children: [
|
|
117
|
+
// Одна часть подключается к любой группе той же формы данных — сколько угодно раз.
|
|
118
|
+
{ model: model.$.registrationAddress, part: address },
|
|
119
|
+
{ model: model.$.residenceAddress, part: address },
|
|
120
|
+
],
|
|
89
121
|
};
|
|
90
122
|
```
|
|
91
123
|
|
|
92
|
-
|
|
124
|
+
Поля группы можно привязать и напрямую, без подформы:
|
|
125
|
+
`{ model: model.$.address.city, component: Input }`.
|
|
93
126
|
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
### Arrays — массив под-форм `{ model, item }`
|
|
128
|
+
|
|
129
|
+
`item` строит разметку строки из под-модели строки (`FormModel<Item>`):
|
|
96
130
|
|
|
97
131
|
```typescript
|
|
98
|
-
import type { FormModel } from '@reformer/core';
|
|
132
|
+
import type { FormModel, FormSchemaNode } from '@reformer/core';
|
|
99
133
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
134
|
+
const itemRow = (item: FormModel<Item>): FormSchemaNode => ({
|
|
135
|
+
children: [
|
|
136
|
+
{ model: item.$.id, component: Input, componentProps: { label: 'ID' } },
|
|
137
|
+
{ model: item.$.name, component: Input, componentProps: { label: 'Name' } },
|
|
138
|
+
],
|
|
103
139
|
});
|
|
104
140
|
|
|
105
|
-
const schema = {
|
|
106
|
-
|
|
141
|
+
const schema: FormSchemaNode = {
|
|
142
|
+
children: [{ model: model.$.items, component: FieldArray, item: itemRow }],
|
|
107
143
|
};
|
|
108
144
|
```
|
|
109
145
|
|
|
146
|
+
Массив **без** `item` и с `component` — одно значение поля (мультивыбор, теги, список файлов):
|
|
147
|
+
`{ model: model.$.tags, component: SelectMulti }`.
|
|
148
|
+
|
|
110
149
|
### createForm API
|
|
111
150
|
|
|
112
151
|
```typescript
|
|
113
|
-
//
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
schema,
|
|
117
|
-
behavior: myBehavior,
|
|
152
|
+
// Сборка одним вызовом: модель, форма, уровень формы и дерево для рендера
|
|
153
|
+
const bundle = createForm<MyForm>({
|
|
154
|
+
initial, // либо готовая модель: model
|
|
155
|
+
schema: (model) => tree, // билдер дерева: узлы держат ручки модели
|
|
156
|
+
behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors
|
|
157
|
+
validation: myRules, // опционально: схема либо { rules?, extras?, strategy? }
|
|
158
|
+
flow: { steps: [{ id: 'loan', rules: loanRules }] }, // опционально: шаги, id = selector шага
|
|
118
159
|
});
|
|
160
|
+
bundle.render.tree; // CompiledSchema — то же дерево после компиляции
|
|
161
|
+
|
|
162
|
+
// Низкоуровневая фабрика: готовые модель и дерево
|
|
163
|
+
const form = createFormFromModel<MyForm>({ model, schema, behavior: myBehavior });
|
|
119
164
|
|
|
120
|
-
// Доступ к нодам
|
|
165
|
+
// Доступ к нодам
|
|
121
166
|
form.name.setValue('John');
|
|
122
|
-
form.address.city.value.value;
|
|
123
|
-
model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели
|
|
167
|
+
form.address.city.value.value; // текущее значение (через сигнал)
|
|
168
|
+
model.items.push({ id: '1', name: 'Item' }); // операции над массивом — на модели (или form.items.push)
|
|
124
169
|
```
|
|
125
170
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
171
|
+
Сборка компилирует дерево один раз (`compileSchema`): у каждого узла — вид (`field | array | part |
|
|
172
|
+
container | text`), идентификатор (`selector` либо позиционный путь `#/0/2`) и область (`#`,
|
|
173
|
+
`#/address`, `#/phones[<ключ>]`). Повтор `selector` в одной области (корень, строка массива,
|
|
174
|
+
подформа) — исключение с путями обоих узлов. В dev правило поведения на узел, которого нет в
|
|
175
|
+
дереве, и шаг `flow.steps`, для которого нет узла с таким `selector`, дают предупреждение в
|
|
176
|
+
консоли. Подробно — [37-compiled-schema.md](./37-compiled-schema.md).
|
|
130
177
|
|
|
131
|
-
###
|
|
178
|
+
### Ноды формы — типы и гарды
|
|
132
179
|
|
|
133
180
|
```typescript
|
|
134
|
-
const form =
|
|
181
|
+
const form = createFormFromModel<MyForm>({ model, schema });
|
|
135
182
|
|
|
136
|
-
form.email; //
|
|
137
|
-
form.address.city; //
|
|
138
|
-
form.items.at(0); //
|
|
183
|
+
form.email; // FormControl<string> — TypeScript знает тип
|
|
184
|
+
form.address.city; // FormControl<string> — вложенный доступ (form.address — FormGroup)
|
|
185
|
+
form.items.at(0); // FormGroup<ItemType> | undefined — форма строки массива
|
|
186
|
+
form.$.value; // поле с именем члена ноды (value, issues, reset, …) — через `$`
|
|
139
187
|
|
|
140
|
-
//
|
|
141
|
-
import {
|
|
142
|
-
if (
|
|
188
|
+
// FormControl / FormGroup / FormArray — интерфейсы, instanceof не работает. Вид ноды — гардами:
|
|
189
|
+
import { isFormControl, isFormGroup, isFormArray } from '@reformer/core';
|
|
190
|
+
if (isFormControl(node)) { /* ... */ }
|
|
143
191
|
```
|
package/docs/llms/10-arrays.md
CHANGED
|
@@ -2,75 +2,143 @@
|
|
|
2
2
|
|
|
3
3
|
Массивы объектов — **model-owned**: данные принадлежат модели (`model.arrayField` — это
|
|
4
4
|
`ModelArray<Item>` с реактивными `push`/`removeAt`/`length`). В схеме массив объявляется узлом
|
|
5
|
-
`{
|
|
6
|
-
|
|
5
|
+
`{ model: model.$.<path>, item: (itemModel) => узел }`, где `item` строит разметку одной строки
|
|
6
|
+
из её под-модели (`FormModel<Item>`).
|
|
7
7
|
|
|
8
8
|
```typescript
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
arrayOf,
|
|
11
|
+
createModel,
|
|
12
|
+
createFormFromModel,
|
|
13
|
+
type FormModel,
|
|
14
|
+
type FormSchemaNode,
|
|
15
|
+
} from '@reformer/core';
|
|
16
|
+
import { FieldArray, Input, InputNumber } from '@reformer/ui-kit';
|
|
11
17
|
|
|
12
18
|
type Item = { id: string; name: string; price: number };
|
|
13
19
|
type MyForm = { items: Item[] };
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// arrayOf(blank) — пустой список и шаблон строки для кнопки «Добавить»
|
|
22
|
+
const blankItem = (): Item => ({ id: '', name: '', price: 0 });
|
|
23
|
+
const model = createModel<MyForm>({ items: arrayOf(blankItem) });
|
|
24
|
+
|
|
25
|
+
// разметка одной строки (item.$.field — ручка под-модели строки)
|
|
26
|
+
const itemRow = (item: FormModel<Item>): FormSchemaNode => ({
|
|
27
|
+
children: [
|
|
28
|
+
{ model: item.$.id, component: Input },
|
|
29
|
+
{ model: item.$.name, component: Input },
|
|
30
|
+
{ model: item.$.price, component: InputNumber },
|
|
31
|
+
],
|
|
22
32
|
});
|
|
23
33
|
|
|
24
|
-
const schema = {
|
|
25
|
-
|
|
34
|
+
const schema: FormSchemaNode = {
|
|
35
|
+
children: [{ model: model.$.items, component: FieldArray, item: itemRow }],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const form = createFormFromModel<MyForm>({ model, schema });
|
|
39
|
+
form.items; // FormArray<Item> — форма каждой строки: form.items.at(i)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Привязка массива — **ручка** `model.$.items`, как у поля и подформы. `item` на привязке, которая
|
|
43
|
+
> массивом не является (лист, группа), — ошибка компиляции.
|
|
44
|
+
|
|
45
|
+
### Массив как ОДНО значение поля
|
|
46
|
+
|
|
47
|
+
Массив бывает не набором под-форм, а одним значением: мультивыбор, теги, список файлов. Чем он
|
|
48
|
+
является, решает **схема**, а не данные: `{ model, item }` — набор под-форм, а узел поля
|
|
49
|
+
`{ model: model.$.<путь>, component }` без `item` — одно значение. Массив, к которому в схеме
|
|
50
|
+
ничего не привязано, в форму не попадает.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
type MyForm = { tags: string[] };
|
|
54
|
+
|
|
55
|
+
const model = createModel<MyForm>({ tags: [] });
|
|
56
|
+
|
|
57
|
+
const schema: FormSchemaNode = {
|
|
58
|
+
children: [{ model: model.$.tags, component: SelectMulti }], // поле над массивом целиком
|
|
26
59
|
};
|
|
60
|
+
const form = createFormFromModel<MyForm>({ model, schema });
|
|
61
|
+
|
|
62
|
+
form.tags.setValue(['a', 'b']); // FormControl<string[]>
|
|
63
|
+
model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
|
|
64
|
+
validate(model.$.tags, [required(), maxLength(3)]); // правило получает массив
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `model.$.<массив>` — записываемая ручка значения (`PathAwareSignal`): подходит для `model:` в
|
|
68
|
+
схеме, `validate`/`cross`, `enableWhen`/`copyFrom` и для `model.signalAt(path)`.
|
|
69
|
+
- Узел-массив хранит только массив: запись `null`/`undefined` даёт `[]`. Нужно отличать «не
|
|
70
|
+
выбирали» от «выбрали ничего» — объявляй поле `T[] | null` с начальным `null`: в рантайме это
|
|
71
|
+
лист, и привязывается он так же.
|
|
72
|
+
- **Контейнер с `null` в типе — значение как есть.** `model.tags` у поля `string[] | null` — это
|
|
73
|
+
`string[] | null`, а не фасад `ModelArray`; `model.$.tags` — лист `PathAwareSignal` без `length`
|
|
74
|
+
и индексов; группа `Address | null` — `Address | null` без под-модели. Такое поле начинают с
|
|
75
|
+
`null`: контейнер, созданный из `[]` или `{}`, в рантайме стал бы фасадом, а тип его не видит.
|
|
76
|
+
- Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
|
|
77
|
+
`model.patch(...)`.
|
|
78
|
+
|
|
79
|
+
### Массив в группе и в строке другого массива
|
|
80
|
+
|
|
81
|
+
Узел `{ model, item }` стоит на любой глубине: массив в группе получает ноду `form.<группа>.<массив>`,
|
|
82
|
+
массив в строке — `form.<массив>.at(i).<массив>`. Разметка строки объявляет свои массивы так же.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const contactRow = (contact: FormModel<Contact>): FormSchemaNode => ({
|
|
86
|
+
children: [
|
|
87
|
+
{ model: contact.$.name, component: Input },
|
|
88
|
+
{ model: contact.$.phones, item: phoneRow }, // массив в строке массива
|
|
89
|
+
],
|
|
90
|
+
});
|
|
27
91
|
|
|
28
|
-
const
|
|
92
|
+
const schema: FormSchemaNode = {
|
|
93
|
+
children: [
|
|
94
|
+
{ model: model.$.details.hotlines, item: phoneRow }, // массив в группе
|
|
95
|
+
{ model: model.$.contacts, item: contactRow },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
29
98
|
```
|
|
30
99
|
|
|
31
|
-
|
|
32
|
-
> он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
|
|
33
|
-
> См. `30-type-safety-recipes.md`.
|
|
100
|
+
Поведение строк вложенного массива — `applyEach` внутри схемы строки, см. `21-array-operations.md`.
|
|
34
101
|
|
|
35
102
|
### Один массив — три слоя (три разных движка)
|
|
36
103
|
|
|
37
104
|
Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
|
|
38
105
|
перепутать, но каждая корректна только в своём контексте:
|
|
39
106
|
|
|
40
|
-
1.
|
|
41
|
-
|
|
42
|
-
**value-proxy** `model.properties` (он несёт `__path`), а **не** через сигнальный
|
|
43
|
-
`model.$.properties`.
|
|
107
|
+
1. **Схема формы** — узел `{ model: model.$.<path>, item: (itemModel) => узел }` среди `children`
|
|
108
|
+
дерева:
|
|
44
109
|
|
|
45
110
|
```typescript
|
|
46
|
-
// узел схемы для createForm
|
|
47
|
-
|
|
111
|
+
// узел схемы для createForm / createFormFromModel
|
|
112
|
+
{ model: model.$.properties, component: FieldArray, item: propertyRow },
|
|
48
113
|
```
|
|
49
114
|
|
|
50
|
-
2.
|
|
51
|
-
|
|
52
|
-
|
|
115
|
+
2. **Схема валидации (`@reformer/core/validation`)** — правила строки объявляются отдельной схемой
|
|
116
|
+
над типом строки и подключаются оператором `applyEach(model.$.<массив>, схема)`. У схемы строки
|
|
117
|
+
своя область: её `cross` получает снимок строки. Запуск — `validateModel(model, schema)`.
|
|
53
118
|
|
|
54
119
|
```typescript
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
validate(
|
|
58
|
-
validate(im.$.estimatedValue, [required(), min(1)]);
|
|
120
|
+
const propertyRules = defineValidation<Property>(({ model }) => {
|
|
121
|
+
validate(model.$.description, [required()]);
|
|
122
|
+
validate(model.$.estimatedValue, [required(), min(1)]);
|
|
59
123
|
});
|
|
124
|
+
|
|
125
|
+
// внутри defineValidation<MyForm>(({ model }) => { ... })
|
|
126
|
+
applyEach(model.$.properties, propertyRules);
|
|
60
127
|
```
|
|
61
128
|
|
|
62
|
-
3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`
|
|
63
|
-
а не со схемой: `<
|
|
129
|
+
3. **CDK / render** — работают с уже **материализованной** нодой `form.<array>` (`FormArray`),
|
|
130
|
+
а не со схемой: `<FieldArray.Root control={form.properties}>` (CDK) или `FieldArraySection` из
|
|
64
131
|
`@reformer/ui-kit` (`control={form.properties}`, `itemComponent`).
|
|
65
132
|
|
|
66
|
-
> **Не путай
|
|
67
|
-
>
|
|
68
|
-
>
|
|
69
|
-
>
|
|
133
|
+
> **Не путай запись по движку.** Схема формы — узел `{ model, item }`; правила строк —
|
|
134
|
+
> `applyEach(model.$.<массив>, схема)` в схеме валидации; `FieldArray.Root control={form.x}` (или
|
|
135
|
+
> `FieldArraySection`) — рендер. `applyEach` в схеме формы не подхватится, а узел `{ model, item }`
|
|
136
|
+
> схема валидации не обходит.
|
|
70
137
|
|
|
71
|
-
### Array operations — на модели
|
|
138
|
+
### Array operations — на модели или на ноде
|
|
72
139
|
|
|
73
|
-
Мутации массива делаются через `ModelArray` (`model.items`)
|
|
140
|
+
Мутации массива делаются через `ModelArray` (`model.items`) либо через ноду формы `FormArray`
|
|
141
|
+
(`form.items`) — нода данными не владеет и передаёт мутацию в тот же массив модели:
|
|
74
142
|
|
|
75
143
|
```typescript
|
|
76
144
|
model.items.push({ id: '1', name: '', price: 0 }); // добавить в конец (плоские значения!)
|
|
@@ -82,8 +150,18 @@ model.items.clear();
|
|
|
82
150
|
model.items.length; // реактивная длина
|
|
83
151
|
model.items.at(0); // под-модель элемента (FormModel<Item>)
|
|
84
152
|
model.items.map((item, i) => item.name); // item — FormModel<Item>
|
|
153
|
+
|
|
154
|
+
// та же мутация через ноду формы (у ноды вставка — insert, у модели — insertAt)
|
|
155
|
+
form.items.push(); // строка по шаблону arrayOf(blank)
|
|
156
|
+
form.items.insert(0, { id: '3', name: '', price: 0 });
|
|
157
|
+
form.items.removeAt(index);
|
|
158
|
+
form.items.length; // число строк (число, не сигнал)
|
|
159
|
+
form.items.keyAt(index); // стабильный ключ строки
|
|
85
160
|
```
|
|
86
161
|
|
|
162
|
+
Формы строк держатся за под-модели элементов и при `move` / `swap` не пересоздаются; ключ строки
|
|
163
|
+
(`keyAt`) не меняется при перестановке — его и берут для React `key`.
|
|
164
|
+
|
|
87
165
|
> **Плоские значения при push.** В `push`/`insertAt` передавай payload из **плоских значений**
|
|
88
166
|
> (`{ id, name, price }`), а НЕ FieldConfig-шаблон (`{ value, component }`). Component/componentProps
|
|
89
167
|
> берутся из `item`-фабрики схемы автоматически.
|
|
@@ -100,27 +178,27 @@ model.items.map((item, i) => item.name); // item — FormModel<Item>
|
|
|
100
178
|
|
|
101
179
|
### Rendering Arrays
|
|
102
180
|
|
|
103
|
-
Каждый элемент массива — под-форма (`
|
|
181
|
+
Каждый элемент массива — под-форма (`FormGroup<Item>`). Итерируй через `form.items.map`:
|
|
104
182
|
|
|
105
183
|
```tsx
|
|
106
|
-
import { useArrayLength } from '@reformer/core';
|
|
184
|
+
import { useArrayLength } from '@reformer/core/react';
|
|
107
185
|
|
|
108
|
-
function ItemsList({ form }: { form:
|
|
186
|
+
function ItemsList({ form }: { form: FormGroup<MyForm> }) {
|
|
109
187
|
const length = useArrayLength(form.items);
|
|
110
188
|
|
|
111
189
|
return (
|
|
112
190
|
<div>
|
|
113
191
|
{form.items.map((item, index) => (
|
|
114
|
-
<div key={index}>
|
|
192
|
+
<div key={form.items.keyAt(index)}>
|
|
115
193
|
<FormField control={item.name} />
|
|
116
194
|
<FormField control={item.price} />
|
|
117
|
-
<button onClick={() =>
|
|
195
|
+
<button onClick={() => form.items.removeAt(index)}>Remove</button>
|
|
118
196
|
</div>
|
|
119
197
|
))}
|
|
120
198
|
|
|
121
199
|
{length === 0 && <p>No items yet</p>}
|
|
122
200
|
|
|
123
|
-
<button onClick={() =>
|
|
201
|
+
<button onClick={() => form.items.push({ id: crypto.randomUUID(), name: '', price: 0 })}>
|
|
124
202
|
Add Item
|
|
125
203
|
</button>
|
|
126
204
|
</div>
|
|
@@ -128,34 +206,36 @@ function ItemsList({ form }: { form: FormProxy<MyForm> }) {
|
|
|
128
206
|
}
|
|
129
207
|
```
|
|
130
208
|
|
|
131
|
-
> В монорепо для массивов используется готовый `
|
|
132
|
-
> (`control={form.items}`, `itemComponent`,
|
|
133
|
-
>
|
|
209
|
+
> В монорепо для массивов используется готовый `FieldArraySection` из `@reformer/ui-kit`
|
|
210
|
+
> (`control={form.items}`, `itemComponent`, add/remove/reorder из коробки; шаблон новой строки —
|
|
211
|
+
> `arrayOf(blank)` в модели, проп `initialValue` — запасной путь). В схеме массив под-форм рисует
|
|
212
|
+
> компонент `FieldArray` узла `{ model, component: FieldArray, item }`.
|
|
213
|
+
> См. `find_recipe(topic="field-array")`.
|
|
134
214
|
|
|
135
215
|
### Array Cross-Validation
|
|
136
216
|
|
|
137
|
-
|
|
138
|
-
(плоские значения), ошибка вешается на
|
|
139
|
-
|
|
217
|
+
Правило над всем массивом пишется оператором `cross` из аргумента схемы: `check` получает снимок
|
|
218
|
+
модели области (`model.get()`, плоские значения), ошибка вешается на поле-носитель. Тип снимка
|
|
219
|
+
выведен из схемы. Правила строк — `applyEach` (см. выше).
|
|
140
220
|
|
|
141
221
|
Носитель — любое **скалярное** поле формы (флаг `hasItems`, итоговая сумма и т.п.), у которого
|
|
142
|
-
есть
|
|
222
|
+
есть ручка `model.$.<field>` и нода в форме:
|
|
143
223
|
|
|
144
224
|
```typescript
|
|
145
|
-
import { cross } from '@reformer/core/validation';
|
|
146
|
-
|
|
147
225
|
type MyForm = { hasItems: boolean; items: Item[] };
|
|
148
226
|
|
|
149
|
-
|
|
150
|
-
cross(model.$.hasItems, (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
227
|
+
defineValidation<MyForm>(({ model, cross }) => {
|
|
228
|
+
cross(model.$.hasItems, (form) => {
|
|
229
|
+
const names = form.items.map((item) => item.name);
|
|
230
|
+
return names.length !== new Set(names).size
|
|
231
|
+
? { code: 'items.duplicate' }
|
|
232
|
+
: null;
|
|
233
|
+
});
|
|
155
234
|
});
|
|
156
235
|
```
|
|
157
236
|
|
|
158
237
|
## See also
|
|
159
238
|
|
|
160
|
-
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`,
|
|
161
|
-
-
|
|
239
|
+
- [03-api-signatures.md](./03-api-signatures.md) — сигнатуры нод `form.<array>` / `ModelArray`, правила строк `applyEach`
|
|
240
|
+
- [37-compiled-schema.md](./37-compiled-schema.md) — область строки `#/items[<ключ>]` и `keyAt`
|
|
241
|
+
- CDK / ui-kit field-array (`FieldArray.Root`, `FieldArraySection`) — `find_recipe(topic="field-array")`
|
|
@@ -39,11 +39,11 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
|
39
39
|
|
|
40
40
|
### Низкоуровневый примитив watchField
|
|
41
41
|
|
|
42
|
-
`watchField` из `@reformer/core` — базовая подписка на изменение сигнала (без debounce и
|
|
42
|
+
`watchField` из `@reformer/core/model` — базовая подписка на изменение сигнала (без debounce и
|
|
43
43
|
AbortSignal). `onChange` построен поверх него. Для простых синхронных реакций:
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
|
-
import { watchField } from '@reformer/core';
|
|
46
|
+
import { watchField } from '@reformer/core/model';
|
|
47
47
|
|
|
48
48
|
// вызывается при каждом изменении (по умолчанию НЕ на инициализации)
|
|
49
49
|
const stop = watchField(model.$.country, (country) => {
|