@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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Signal } from '@preact/signals-core';
|
|
2
|
+
import { PathAwareSignal } from './types';
|
|
3
|
+
export declare const isIndexKey: (key: string) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Глубокое сравнение значений модели: объекты и массивы — по содержимому, даты — по времени,
|
|
6
|
+
* `NaN` равен себе, остальное (включая файлы) — по ссылке.
|
|
7
|
+
*
|
|
8
|
+
* То же сравнение стоит за `isDirty` модели и за сверкой «модель изменилась, пока шла проверка»
|
|
9
|
+
* при отправке и переходе шага: значение, изменённое и возвращённое, изменением не считается.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* deepEqual({ date: new Date(0), tags: ['a'] }, { date: new Date(0), tags: ['a'] }); // true
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare const deepEqual: (a: unknown, b: unknown) => boolean;
|
|
17
|
+
/** Фабрика нового элемента массива. */
|
|
18
|
+
type Blank = () => unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Образец того же места модели внутри шаблона родительского массива. Ленивый: шаблон родителя
|
|
21
|
+
* ради него вызывается, только когда образец действительно понадобился.
|
|
22
|
+
*/
|
|
23
|
+
type Shape = () => unknown;
|
|
24
|
+
/**
|
|
25
|
+
* Массив модели с шаблоном нового элемента.
|
|
26
|
+
*
|
|
27
|
+
* Шаблон — фабрика значения, которое получает `push()` / `insertAt(i)` без аргумента: кнопка
|
|
28
|
+
* «Добавить» секции массива, `model.items.push()` в поведении. Частичное значение
|
|
29
|
+
* (`push({ type: 'house' })`) дополняется из шаблона. Объявляется там же, где остальные
|
|
30
|
+
* начальные значения, — в модели; схеме и JSX знать его не нужно.
|
|
31
|
+
*
|
|
32
|
+
* Возвращает обычный массив: тип поля остаётся `U[]`. Шаблон переживает `set` / `patch` / `reset`
|
|
33
|
+
* модели. Вложенные массивы объявляются внутри шаблона элемента — тогда и элементы, пришедшие
|
|
34
|
+
* обычными данными (загрузка с сервера), получают их шаблоны.
|
|
35
|
+
*
|
|
36
|
+
* Шаблон привязан к самому массиву, поэтому до `createModel` его нельзя копировать:
|
|
37
|
+
* `structuredClone`, `JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона, и
|
|
38
|
+
* `push()` без значения бросит ошибку. Начальные значения с `arrayOf` держат в фабрике
|
|
39
|
+
* (`const initial = () => ({ items: arrayOf(blank) })`), а не в константе, которую клонируют.
|
|
40
|
+
*
|
|
41
|
+
* @typeParam U - Тип элемента массива.
|
|
42
|
+
* @param blank - Фабрика нового элемента. Вызывается на каждое добавление — значение не делится
|
|
43
|
+
* между элементами.
|
|
44
|
+
* @param items - Начальные элементы. По умолчанию массив пуст.
|
|
45
|
+
* @returns Массив начальных значений с шаблоном.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const blankPhone = () => ({ number: '' });
|
|
50
|
+
* const blankCoBorrower = () => ({ name: '', phones: arrayOf(blankPhone) });
|
|
51
|
+
*
|
|
52
|
+
* const model = createModel({ coBorrowers: arrayOf(blankCoBorrower) });
|
|
53
|
+
*
|
|
54
|
+
* model.coBorrowers.push(); // { name: '', phones: [] }
|
|
55
|
+
* model.coBorrowers.at(0).phones.push(); // { number: '' }
|
|
56
|
+
* model.coBorrowers.push({ name: 'Анна' }); // { name: 'Анна', phones: [] } — остальное из шаблона
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @group Model
|
|
60
|
+
*/
|
|
61
|
+
export declare function arrayOf<U>(blank: () => U, items?: readonly U[]): U[];
|
|
62
|
+
export type ModelNode = ModelLeafNode | ModelGroupNode | ModelArrayNode;
|
|
63
|
+
/** Лист модели, чей это сигнал; `undefined` — сигнал не из модели. @internal */
|
|
64
|
+
export declare function leafOfSignal(handle: object): {
|
|
65
|
+
reference(): unknown;
|
|
66
|
+
} | undefined;
|
|
67
|
+
declare class ModelLeafNode {
|
|
68
|
+
path: string;
|
|
69
|
+
readonly kind: "leaf";
|
|
70
|
+
readonly signal: PathAwareSignal<unknown>;
|
|
71
|
+
private initial;
|
|
72
|
+
constructor(initial: unknown, path: string);
|
|
73
|
+
rebase(path: string): void;
|
|
74
|
+
/** Реактивное чтение (внутри effect/computed создаёт зависимость). */
|
|
75
|
+
read(): unknown;
|
|
76
|
+
/** Нереактивный снимок. */
|
|
77
|
+
peek(): unknown;
|
|
78
|
+
set(value: unknown): void;
|
|
79
|
+
/**
|
|
80
|
+
* Вернуть значение к снимку. Производное поле (цель `compute`, поле сервера с `markDerived`)
|
|
81
|
+
* сброс не пишет — как `set` и `patch`: значением владеет поведение, а снимок такого поля снят
|
|
82
|
+
* до первого расчёта, и `compute` пересчитал бы его только при смене источников.
|
|
83
|
+
*/
|
|
84
|
+
resetToInitial(): void;
|
|
85
|
+
captureInitial(): void;
|
|
86
|
+
/** Значение точки отсчёта — копия: правка полученного снимок не трогает. */
|
|
87
|
+
reference(): unknown;
|
|
88
|
+
dirty(): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Отличается ли значение от снимка. Производное поле отличием не считается: его пишет поведение,
|
|
91
|
+
* а не пользователь — иначе форма без единой правки была бы «изменённой».
|
|
92
|
+
*/
|
|
93
|
+
differsFrom(snapshot: unknown): boolean;
|
|
94
|
+
}
|
|
95
|
+
export declare class ModelGroupNode {
|
|
96
|
+
path: string;
|
|
97
|
+
readonly kind: "group";
|
|
98
|
+
readonly children: Map<string, ModelNode>;
|
|
99
|
+
/**
|
|
100
|
+
* @param shape Образец той же группы из шаблона родительского массива: из него вложенные
|
|
101
|
+
* массивы берут шаблоны, если значение пришло обычными данными (см. {@link ModelArrayNode}).
|
|
102
|
+
*/
|
|
103
|
+
constructor(initial: Record<string, unknown>, path: string, shape?: Shape);
|
|
104
|
+
rebase(path: string): void;
|
|
105
|
+
/** Реактивное чтение поддерева (внутри effect/computed подписывает на все листья группы). */
|
|
106
|
+
read(): Record<string, unknown>;
|
|
107
|
+
peek(): Record<string, unknown>;
|
|
108
|
+
/**
|
|
109
|
+
* Полная установка: значение ожидается целиком. Ключи, которых в нём нет, остаются прежними —
|
|
110
|
+
* как у {@link patch}, — но в dev о них сообщает предупреждение: неполный объект в `set` — почти
|
|
111
|
+
* всегда ошибка вызывающего кода, а не намерение.
|
|
112
|
+
*/
|
|
113
|
+
set(value: unknown): void;
|
|
114
|
+
/**
|
|
115
|
+
* Частичное обновление: записываются только переданные ключи. Вложенная группа сливается так
|
|
116
|
+
* же, массив заменяется целиком.
|
|
117
|
+
*/
|
|
118
|
+
patch(value: unknown): void;
|
|
119
|
+
/**
|
|
120
|
+
* Запись значения по ключам группы. Лишние ключи значения игнорируются, производные поля не
|
|
121
|
+
* затираются.
|
|
122
|
+
*
|
|
123
|
+
* @param missing - Куда складывать пути ключей, для которых значение не пришло. Передаётся при
|
|
124
|
+
* полной установке ({@link set}); при частичной ({@link patch}) — `undefined`.
|
|
125
|
+
* @param partial - Частичное обновление: ключ со значением `undefined` — «не трогать», как в
|
|
126
|
+
* частичном значении элемента массива (`DeepPartial` допускает `{ ключ: undefined }`).
|
|
127
|
+
*/
|
|
128
|
+
private write;
|
|
129
|
+
resetToInitial(): void;
|
|
130
|
+
captureInitial(): void;
|
|
131
|
+
dirty(): boolean;
|
|
132
|
+
/** Отличается ли группа от снимка её значения (строка массива сверяется со снимком массива). */
|
|
133
|
+
differsFrom(snapshot: unknown): boolean;
|
|
134
|
+
}
|
|
135
|
+
export declare class ModelArrayNode {
|
|
136
|
+
path: string;
|
|
137
|
+
private readonly shape?;
|
|
138
|
+
readonly kind: "array";
|
|
139
|
+
readonly items: Signal<ModelNode[]>;
|
|
140
|
+
private initial;
|
|
141
|
+
private blank;
|
|
142
|
+
private fallback;
|
|
143
|
+
private sample;
|
|
144
|
+
/** @param shape Образец того же массива из шаблона родителя (см. {@link ModelGroupNode}). */
|
|
145
|
+
constructor(initial: unknown[], path: string, shape?: Shape | undefined);
|
|
146
|
+
/** Шаблон по старшинству: свой (`arrayOf`) → из шаблона родителя → запасной извне. */
|
|
147
|
+
private template;
|
|
148
|
+
private readonly itemShape;
|
|
149
|
+
private build;
|
|
150
|
+
/**
|
|
151
|
+
* Значение нового элемента: из шаблона; переданный объект накладывается поверх шаблона —
|
|
152
|
+
* недостающие ключи берутся из шаблона, поэтому `push({ type: 'house' })` даёт полный элемент.
|
|
153
|
+
*/
|
|
154
|
+
private fresh;
|
|
155
|
+
/** Запасной шаблон извне (сборка формы передаёт `initialValue`): шаблон модели главнее. */
|
|
156
|
+
provideBlank(blank: Blank): void;
|
|
157
|
+
rebase(path: string): void;
|
|
158
|
+
private reindex;
|
|
159
|
+
/**
|
|
160
|
+
* Индекс за концом массива — ошибка, а не «в конец»: `splice` молча сдвигал бы элемент на
|
|
161
|
+
* другой индекс, и вызывающий код получал бы путь, которого не просил.
|
|
162
|
+
*/
|
|
163
|
+
private beyondEnd;
|
|
164
|
+
push(value?: unknown): void;
|
|
165
|
+
insertAt(index: number, value?: unknown): void;
|
|
166
|
+
removeAt(index: number): void;
|
|
167
|
+
move(from: number, to: number): void;
|
|
168
|
+
swap(a: number, b: number): void;
|
|
169
|
+
clear(): void;
|
|
170
|
+
/**
|
|
171
|
+
* Реактивное чтение массива. Читаем `items.value` (а не `.peek()`) — внутри effect/computed это
|
|
172
|
+
* подписка на СОСТАВ массива, поэтому push/removeAt/move ретригерят наравне с правкой элемента.
|
|
173
|
+
*/
|
|
174
|
+
read(): unknown[];
|
|
175
|
+
peek(): unknown[];
|
|
176
|
+
set(value: unknown): void;
|
|
177
|
+
resetToInitial(): void;
|
|
178
|
+
captureInitial(): void;
|
|
179
|
+
/** Элементы точки отсчёта — копия: правка полученного снимок не трогает. */
|
|
180
|
+
reference(): unknown[];
|
|
181
|
+
dirty(): boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Отличается ли массив от снимка: другая длина либо отличается элемент. Сверка идёт по узлам, а
|
|
184
|
+
* не по значению целиком — производные поля строк (цели `compute` поведения строки) не в счёт.
|
|
185
|
+
*/
|
|
186
|
+
differsFrom(snapshot: unknown): boolean;
|
|
187
|
+
}
|
|
188
|
+
/** Начальное значение элемента как шаблон: фабрика — как есть, значение — копией. */
|
|
189
|
+
export declare const blankFrom: (initialValue: unknown) => Blank;
|
|
190
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Signal } from '@preact/signals-core';
|
|
2
|
+
import { PathAwareSignal } from './types';
|
|
3
|
+
import { ModelNode, ModelGroupNode, ModelArrayNode } from './model-nodes';
|
|
4
|
+
/**
|
|
5
|
+
* Узел дерева `$`: лист → сам {@link PathAwareSignal}, группа/массив → контейнерный узел —
|
|
6
|
+
* {@link ReadonlySignal} агрегированного значения ПЛЮС доступ к детям по имени/индексу.
|
|
7
|
+
*
|
|
8
|
+
* Узел массива вдобавок принимает запись `.value` — замену массива целиком (см. шапку модуля).
|
|
9
|
+
* Любая другая запись в контейнер — ошибка: свойство, молча осевшее на прокси, выглядело бы как
|
|
10
|
+
* удачная запись, а модель осталась бы прежней.
|
|
11
|
+
*
|
|
12
|
+
* Порядок разрешения ключа: служебные `__path`/`__kind` → ребёнок → свойство сигнала. Дети идут
|
|
13
|
+
* раньше свойств сигнала — тот же приоритет, что у {@link makeFormModel} (поле формы затеняет метод
|
|
14
|
+
* API). Поэтому имена `value`/`peek`/`subscribe`/`valueOf`/`toString`/`toJSON`/`brand` де-факто
|
|
15
|
+
* зарезервированы: одноимённое поле формы затенит свойство сигнала (`subscribe` при этом продолжает
|
|
16
|
+
* работать — он замкнут на агрегат, а не читает `.value` через прокси).
|
|
17
|
+
*
|
|
18
|
+
* `has`/`ownKeys` намеренно НЕ показывают свойства сигнала — ровно как `__path`, доступный через
|
|
19
|
+
* `get`, но невидимый для `in`/`Object.keys`. Так `Object.keys(model.$.<group>)` остаётся списком
|
|
20
|
+
* полей, а потребители, перечисляющие модель, не видят служебных ключей.
|
|
21
|
+
*/
|
|
22
|
+
export declare function signalsProxy(node: ModelNode): any;
|
|
23
|
+
/**
|
|
24
|
+
* Узел модели за контейнерной ручкой дерева `$` (группа или массив). Для листа и для всего, что
|
|
25
|
+
* ручкой не является, — `undefined`.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare function containerNodeOf(handle: unknown): ModelGroupNode | ModelArrayNode | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Узел дерева `model.$` — контейнер (группа/массив), а не лист?
|
|
32
|
+
*
|
|
33
|
+
* Контейнерный узел структурно совместим с `ReadonlySignal` (`peek`/`value`/`subscribe`), поэтому
|
|
34
|
+
* duck-typing «есть `peek` ⇒ это лист» на нём даёт ложное срабатывание. Этот guard — надёжный
|
|
35
|
+
* способ различить: обходчикам дерева нужно спускаться в контейнер, а не читать его целиком.
|
|
36
|
+
*
|
|
37
|
+
* @group Model
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const isLeaf = (v: unknown) => isSignalLike(v) && !isModelContainerSignal(v);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function isModelContainerSignal(value: unknown): boolean;
|
|
44
|
+
/** Узел-массив дерева `model.$` — ручка значения массива целиком. */
|
|
45
|
+
export declare function isModelArraySignal(value: unknown): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* «Ручка значения», к которой можно привязать поле: лист (сигнал, не контейнер) либо узел-массив
|
|
48
|
+
* дерева `model.$` (значение — массив целиком, `.value` читается и пишется).
|
|
49
|
+
*
|
|
50
|
+
* Группа сюда не входит: она читается целиком, но не пишется и полем не бывает. Этой проверкой
|
|
51
|
+
* `createForm` и рендереры отличают узел-поле схемы (`{ model, component }`) от контейнера.
|
|
52
|
+
*
|
|
53
|
+
* @group Model
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const model = createModel({ name: '', tags: [] as string[], address: { city: '' } });
|
|
57
|
+
* isValueSignal(model.$.name); // true — лист
|
|
58
|
+
* isValueSignal(model.$.tags); // true — массив как значение
|
|
59
|
+
* isValueSignal(model.$.address); // false — группа
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function isValueSignal(value: unknown): value is Signal<unknown>;
|
|
63
|
+
export declare function resolveSignalAt(root: ModelGroupNode, path: string): PathAwareSignal<unknown> | undefined;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ModelGroupNode } from './model-nodes';
|
|
2
|
+
import { FormModel, ModelArray } from './types';
|
|
3
|
+
export declare const rootByFacade: WeakMap<object, ModelGroupNode>;
|
|
4
|
+
export declare function makeFormModel(group: ModelGroupNode): any;
|
|
5
|
+
/**
|
|
6
|
+
* Value-фасад для ручки {@link modelOf}: массив → {@link ModelArray}, объект → под-модель
|
|
7
|
+
* {@link FormModel}.
|
|
8
|
+
*
|
|
9
|
+
* @group Model
|
|
10
|
+
*/
|
|
11
|
+
export type ModelOf<V> = NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : FormModel<NonNullable<V>>;
|
|
12
|
+
/**
|
|
13
|
+
* Value-фасад по ручке дерева `model.$`: группа → под-модель {@link FormModel}, массив →
|
|
14
|
+
* {@link ModelArray}. Обратная операция к `model.$`: `modelOf(model.$.address) === model.address`.
|
|
15
|
+
*
|
|
16
|
+
* Ручка находит фасад по ИДЕНТИЧНОСТИ, а не по пути. Путь для этого не годится: он абсолютный
|
|
17
|
+
* (`items.0.phones`) и меняется при перестановке строк, а области поведения и формы строк
|
|
18
|
+
* вложенные — поиск по пути от под-модели строки ничего не находит.
|
|
19
|
+
*
|
|
20
|
+
* Лист ручкой контейнера не является — функция бросает. В частности, группа или массив,
|
|
21
|
+
* созданные из начального `null`, в модели — лист: вид узла фиксируется при создании.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam V - Значение, которое хранит ручка (выводится из `peek()`).
|
|
24
|
+
* @param handle - Узел-группа или узел-массив дерева `model.$`.
|
|
25
|
+
* @returns Под-модель группы либо фасад массива.
|
|
26
|
+
* @throws TypeError если `handle` — лист или не ручка модели.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const model = createModel({ address: { city: '' }, phones: [{ number: '' }] });
|
|
31
|
+
*
|
|
32
|
+
* modelOf(model.$.address).city = 'Казань'; // под-модель: чтение, запись, get/set/patch
|
|
33
|
+
* modelOf(model.$.phones).push({ number: '+7' }); // фасад массива: мутации и обход
|
|
34
|
+
* modelOf(model.$.phones[0]).number; // строка массива — тоже группа
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @group Model
|
|
38
|
+
*/
|
|
39
|
+
export declare function modelOf<V extends object | null | undefined>(handle: {
|
|
40
|
+
peek(): V;
|
|
41
|
+
}): ModelOf<V>;
|
|
42
|
+
/**
|
|
43
|
+
* Ручка массива по привязке: сама ручка `model.$.<массив>` либо value-фасад `model.<массив>`.
|
|
44
|
+
* Для всего остального — `undefined`.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export declare function arrayHandleOf(binding: unknown): object | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Ручка группы по привязке: `model.$.<группа>` либо под-модель `model.<группа>`. Для всего
|
|
51
|
+
* остального — `undefined`.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
export declare function groupHandleOf(binding: unknown): object | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Запасной шаблон нового элемента массива — от того, кто строит модель извне: сборка формы
|
|
58
|
+
* передаёт сюда `initialValue` узла-массива. Действует, только если модель своего шаблона не
|
|
59
|
+
* объявила (`arrayOf`): так живут формы, чья модель строится из данных без кода. Что это узел
|
|
60
|
+
* схемы, модель не знает.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
export declare function provideArrayBlank(handle: unknown, initialValue: unknown): void;
|
|
65
|
+
/**
|
|
66
|
+
* Value-фасад модели (под-модель или фасад массива), а не обычный объект?
|
|
67
|
+
*
|
|
68
|
+
* Обходчикам чужих деревьев, в которых могут лежать фасады модели, в фасад спускаться нельзя:
|
|
69
|
+
* чтение его ключей — это реактивное чтение значений модели.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* Точка отсчёта модели по ручке, к которой привязана нода формы: сигналу листа или фасаду
|
|
75
|
+
* массива. К ней сброс формы (`form.reset()`) возвращает ноду — к тому же, к чему `model.reset()`
|
|
76
|
+
* возвращает модель: последний `captureInitial` либо значение создания модели.
|
|
77
|
+
*
|
|
78
|
+
* `undefined` — ручка не из модели (нода над собственным сигналом): сбрасывать её не к чему,
|
|
79
|
+
* кроме значения, с которым она создана. @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare function referenceOf(handle: unknown): {
|
|
82
|
+
readonly value: unknown;
|
|
83
|
+
} | undefined;
|
|
84
|
+
export declare function isModelFacade(value: unknown): boolean;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* не здесь. См. также `aggregateInto`, где коалесинг реализован на уровне самого оператора.
|
|
20
20
|
*
|
|
21
21
|
* @group Utils
|
|
22
|
-
* @module
|
|
22
|
+
* @module model/safe-effect
|
|
23
23
|
*/
|
|
24
24
|
/**
|
|
25
25
|
* Создает callback, который выполняется вне контекста effect
|
|
@@ -68,6 +68,54 @@ export declare function safeCallback<TArgs extends unknown[]>(callback: (...args
|
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
70
|
export declare function runOutsideEffect(fn: () => void | Promise<void>): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Выполнить сборку так, чтобы записи, отложенные ею, выполнились до возврата, — вместе с теми, что
|
|
73
|
+
* они породили (копия `syncFields`, нормализация `transformValue`, сброс выключенного поля).
|
|
74
|
+
*
|
|
75
|
+
* Без этого первые записи поведения приходили бы на микротаске, уже после сборки, и точка отсчёта
|
|
76
|
+
* модели, снятая в конце сборки, их бы не видела: пара `syncFields` попала бы в неё рассогласованной,
|
|
77
|
+
* и `form.reset()` вернул бы её не к собранному состоянию, а к полусобранному.
|
|
78
|
+
*
|
|
79
|
+
* Записи выполняются вне `batch()` — как на микротаске: встречные эффекты флашатся синхронно (см.
|
|
80
|
+
* комментарий модуля). Вложенная сборка своих записей не выполняет — это сделает внешняя.
|
|
81
|
+
*
|
|
82
|
+
* @param build - Сборка: подключение поведения.
|
|
83
|
+
* @returns То, что вернула сборка.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
export declare function runAssemblyWrites<R>(build: () => R): R;
|
|
87
|
+
/**
|
|
88
|
+
* Дождаться, пока отработают отложенные записи текущего тика — вместе с теми, что они породили
|
|
89
|
+
* (цепочка расчётов: каждая запись будит следующий расчёт, а тот снова пишет отложенно).
|
|
90
|
+
*
|
|
91
|
+
* Ждёт только микротаски: запись после таймера (debounce) или после `await` в async-callback сюда не
|
|
92
|
+
* входит — это уже не «тот же тик».
|
|
93
|
+
*
|
|
94
|
+
* Счётчик общий для рантайма, а не для одной модели: отложенные записи ставит {@link runOutsideEffect},
|
|
95
|
+
* а он моделей не различает. Нужен проверкам «по действию» — отправке и переходу шага: правила должны
|
|
96
|
+
* увидеть модель после записей поведения того же тика (колбэк `onChange`, сброс `resetOnDisable`).
|
|
97
|
+
*
|
|
98
|
+
* @returns Промис ожидания; `undefined` — отложенных записей сейчас нет, ждать нечего.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* model.loanType = 'mortgage'; // поведение допишет зависимые поля на микротаске
|
|
103
|
+
* await deferredWritesSettled();
|
|
104
|
+
* const values = model.get(); // уже с записями поведения
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare function deferredWritesSettled(): Promise<void> | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Отложенные записи одной операции: задание выполняется на микротаске ({@link runOutsideEffect}),
|
|
110
|
+
* а `cancelAll` отменяет все ещё не выполненные — при снятии операции (запись после cleanup не
|
|
111
|
+
* случится) или когда новое задание делает ожидающие устаревшими (побеждает последнее).
|
|
112
|
+
*
|
|
113
|
+
* @internal
|
|
114
|
+
*/
|
|
115
|
+
export declare function deferredWrites(): {
|
|
116
|
+
schedule(job: () => void): void;
|
|
117
|
+
cancelAll(): void;
|
|
118
|
+
};
|
|
71
119
|
/**
|
|
72
120
|
* Создает версию callback с поддержкой debounce, безопасную для effect
|
|
73
121
|
*
|
|
@@ -76,7 +124,7 @@ export declare function runOutsideEffect(fn: () => void | Promise<void>): () =>
|
|
|
76
124
|
* async-rejection) маршрутизируются dev-логгером (logDeferError).
|
|
77
125
|
*
|
|
78
126
|
* @param callback - Функция для выполнения
|
|
79
|
-
* @param withDebounce -
|
|
127
|
+
* @param withDebounce - Обёртка, откладывающая вызов (debounce)
|
|
80
128
|
* @returns Обёрнутая функция
|
|
81
129
|
*
|
|
82
130
|
* @example
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { ReadonlySignal, Signal } from '@preact/signals-core';
|
|
2
|
+
/**
|
|
3
|
+
* Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
|
|
4
|
+
* Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
|
|
5
|
+
*
|
|
6
|
+
* @group Model
|
|
7
|
+
*/
|
|
8
|
+
export type PathAwareSignal<T> = Signal<T> & {
|
|
9
|
+
/** Путь поля в модели (dot-нотация). На элементах массива включает индекс. */
|
|
10
|
+
readonly __path: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Спец-объекты, которые трактуются как листья (не разворачиваются в под-модель).
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
type Opaque = Date | File | Blob;
|
|
17
|
+
/**
|
|
18
|
+
* Значение одного ключа в {@link DeepPartial}: вложенная группа — частично, массив и спец-объект
|
|
19
|
+
* (Date/File/Blob) — целиком.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
type DeepPartialValue<V> = NonNullable<V> extends ReadonlyArray<unknown> | Opaque ? V : NonNullable<V> extends object ? DeepPartial<NonNullable<V>> | Extract<V, null | undefined> : V;
|
|
23
|
+
/**
|
|
24
|
+
* Частичное значение модели для {@link ModelApi.patch}: любой ключ можно опустить на любой
|
|
25
|
+
* глубине вложенных групп. Массив передаётся целиком — он заменяется, а не сливается.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* type Profile = { name: string; address: { city: string; street: string }; tags: string[] };
|
|
30
|
+
* const update: DeepPartial<Profile> = { address: { city: 'Казань' } };
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @group Model
|
|
34
|
+
*/
|
|
35
|
+
export type DeepPartial<T> = {
|
|
36
|
+
[K in keyof T]?: DeepPartialValue<T[K]>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* `null` входит в тип `V`. Без `strictNullChecks` у потребителя `null` подходит любому типу —
|
|
40
|
+
* тогда признак ложен, и тип контейнера остаётся фасадом, как без `null`.
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
type IncludesNull<V> = null extends string ? false : null extends V ? true : false;
|
|
44
|
+
/**
|
|
45
|
+
* Фасад элемента массива при обходе (`at`/`map`/`forEach`): массив → {@link ModelArray},
|
|
46
|
+
* Opaque (Date/File/Blob) и примитив → значение, объект → под-модель {@link FormModel}.
|
|
47
|
+
* Зеркалит листовой/объектный сплит {@link ModelValue} (но объект → под-модель с API),
|
|
48
|
+
* чтобы `files: File[]`/`dates: Date[]`/вложенные массивы не мис-типизировались как `FormModel`.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
type ModelArrayItem<U> = IncludesNull<U> extends true ? U : NonNullable<U> extends ReadonlyArray<infer E> ? ModelArray<E> : NonNullable<U> extends Opaque ? U : NonNullable<U> extends object ? FormModel<NonNullable<U>> : U;
|
|
52
|
+
/**
|
|
53
|
+
* Значение поля в value-доступе модели:
|
|
54
|
+
* - массив → {@link ModelArray}
|
|
55
|
+
* - спец-объект (Date/File/Blob) → как есть
|
|
56
|
+
* - объект → под-модель {@link FormModel} (value-доступ + `.$`-сигналы + API get/set/patch/…);
|
|
57
|
+
* промоутится рантаймом (`makeFormModel`); сигналы идентичны `model.$.<path>`
|
|
58
|
+
* - примитив → значение
|
|
59
|
+
* - тип с `null` (`File[] | null`, `Address | null`) → значение как есть, без фасада: поле,
|
|
60
|
+
* которое бывает `null`, начинают с `null`, а такое поле в рантайме — лист (значение целиком).
|
|
61
|
+
* Контейнер, созданный из `[]` или `{}`, стал бы фасадом — nullable-контейнер начинают с `null`.
|
|
62
|
+
*
|
|
63
|
+
* @group Model
|
|
64
|
+
*/
|
|
65
|
+
export type ModelValue<V> = IncludesNull<V> extends true ? V : NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : NonNullable<V> extends Opaque ? V : NonNullable<V> extends object ? FormModel<NonNullable<V>> : V;
|
|
66
|
+
/**
|
|
67
|
+
* Карта value-полей объекта (value-половина {@link FormModel}): поля доступны как обычные свойства
|
|
68
|
+
* (чтение реактивно внутри `effect`/`computed`, запись — присваиванием). Вложенные объекты-поля
|
|
69
|
+
* резолвятся в под-модели {@link FormModel} (см. {@link ModelValue}), массивы — в {@link ModelArray}.
|
|
70
|
+
*
|
|
71
|
+
* @group Model
|
|
72
|
+
*/
|
|
73
|
+
export type ModelObject<T> = {
|
|
74
|
+
[K in keyof T]: ModelValue<T[K]>;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Реактивный массив модели. Мутации (`push`/`removeAt`/…) меняют длину реактивно;
|
|
78
|
+
* `map`/`forEach`/`at` отдают под-модель элемента ({@link FormModel}) для объектных элементов.
|
|
79
|
+
*
|
|
80
|
+
* @group Model
|
|
81
|
+
*/
|
|
82
|
+
export interface ModelArray<U> {
|
|
83
|
+
/**
|
|
84
|
+
* Путь массива в модели (dot-нотация). Предоставляется рантаймом (value-прокси) и требуется
|
|
85
|
+
* рендер-слою для резолва узла массива (напр. `ArrayRenderNode` в `@reformer/renderer-react`).
|
|
86
|
+
*/
|
|
87
|
+
readonly __path: string;
|
|
88
|
+
/** Реактивная длина. */
|
|
89
|
+
readonly length: number;
|
|
90
|
+
/**
|
|
91
|
+
* Добавить элемент в конец (значение элемента целиком). Без значения — элемент по шаблону
|
|
92
|
+
* массива (`arrayOf(blank)` в модели); шаблона нет — ошибка.
|
|
93
|
+
*/
|
|
94
|
+
push(item?: U): void;
|
|
95
|
+
/** Вставить элемент по индексу. Без значения — элемент по шаблону, как у {@link push}. */
|
|
96
|
+
insertAt(index: number, item?: U): void;
|
|
97
|
+
/** Удалить элемент по индексу. */
|
|
98
|
+
removeAt(index: number): void;
|
|
99
|
+
/** Переместить элемент. */
|
|
100
|
+
move(from: number, to: number): void;
|
|
101
|
+
/** Поменять местами два элемента. */
|
|
102
|
+
swap(a: number, b: number): void;
|
|
103
|
+
/** Очистить массив. */
|
|
104
|
+
clear(): void;
|
|
105
|
+
/** Элемент по индексу: объект → под-модель, массив → {@link ModelArray}, лист → значение. */
|
|
106
|
+
at(index: number): NonNullable<U> extends object ? ModelArrayItem<U> : ModelArrayItem<U> | undefined;
|
|
107
|
+
/** Map по элементам (объект → {@link FormModel}, Opaque/примитив → значение, массив → {@link ModelArray}). */
|
|
108
|
+
map<R>(fn: (item: ModelArrayItem<U>, index: number) => R): R[];
|
|
109
|
+
/** Итерация по элементам (см. {@link ModelArrayItem}). */
|
|
110
|
+
forEach(fn: (item: ModelArrayItem<U>, index: number) => void): void;
|
|
111
|
+
/** Снимок массива значений (без подписки). */
|
|
112
|
+
toArray(): U[];
|
|
113
|
+
/** Обход `for…of` / spread — те же элементы, что у {@link forEach}. */
|
|
114
|
+
[Symbol.iterator](): IterableIterator<ModelArrayItem<U>>;
|
|
115
|
+
/** Индексный value-доступ. */
|
|
116
|
+
[index: number]: ModelValue<U>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Карта под-сигналов объекта в дереве `model.$` (без свойств самого узла — их добавляет
|
|
120
|
+
* {@link ModelGroupSignals}).
|
|
121
|
+
*
|
|
122
|
+
* @group Model
|
|
123
|
+
*/
|
|
124
|
+
export type ModelSignals<T> = {
|
|
125
|
+
[K in keyof T]: ModelSignalNode<T[K]>;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Свойства контейнерного узла дерева `$`, «поверх» карты детей. Ключи детей вырезаются из
|
|
129
|
+
* {@link ReadonlySignal}: доступ к полю формы всегда выигрывает у одноимённого свойства сигнала
|
|
130
|
+
* (так же ведёт себя рантайм), поэтому `{ value: string }` не порождает конфликта типов.
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
type ContainerSignal<T, TKeys extends PropertyKey> = Omit<ReadonlySignal<T>, TKeys>;
|
|
134
|
+
/**
|
|
135
|
+
* Узел-группа дерева `model.$`: доступ к под-сигналам полей ({@link ModelSignals}) И одновременно
|
|
136
|
+
* {@link ReadonlySignal} агрегированного значения группы — `.value`/`.peek()`/`.subscribe()`.
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ Поле формы с именем `value`/`peek`/`subscribe`/`valueOf`/`toString`/`toJSON`/`brand` затеняет
|
|
139
|
+
* одноимённое свойство сигнала (редкий краевой случай); `subscribe` при этом продолжает работать.
|
|
140
|
+
*
|
|
141
|
+
* @group Model
|
|
142
|
+
*/
|
|
143
|
+
export type ModelGroupSignals<T> = ModelSignals<T> & ContainerSignal<T, keyof T>;
|
|
144
|
+
/**
|
|
145
|
+
* Узел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`
|
|
146
|
+
* и сигнал значения массива целиком (реагирует и на правку элемента, и на изменение состава —
|
|
147
|
+
* push/removeAt/move).
|
|
148
|
+
*
|
|
149
|
+
* В отличие от группы, узел ЗАПИСЫВАЕМ: `model.$.tags.value = ['a']` заменяет массив целиком. Это
|
|
150
|
+
* делает его {@link PathAwareSignal} — к нему привязывают поле (`{ model: model.$.tags, component }`),
|
|
151
|
+
* правило (`validate(model.$.tags, …)`) и поведение, как к листу. Так массив служит ОДНИМ значением
|
|
152
|
+
* поля: мультивыбор, теги, список файлов.
|
|
153
|
+
*
|
|
154
|
+
* `V` — объявленный тип поля, включая `undefined`. Поле с `null` в типе (`string[] | null`) — не
|
|
155
|
+
* узел-массив, а лист {@link PathAwareSignal}: его начинают с `null`, и `length` с индексами у него
|
|
156
|
+
* в рантайме нет (см. {@link ModelValue}).
|
|
157
|
+
*
|
|
158
|
+
* @group Model
|
|
159
|
+
*/
|
|
160
|
+
export type ModelArraySignals<U, V> = Signal<V> & {
|
|
161
|
+
/** Путь массива в модели (dot-нотация). */
|
|
162
|
+
readonly __path: string;
|
|
163
|
+
readonly length: number;
|
|
164
|
+
readonly [index: number]: ModelSignalNode<U>;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Узел дерева сигналов `model.$`: массив → {@link ModelArraySignals}, Opaque (Date/File/Blob)
|
|
168
|
+
* и примитив → {@link PathAwareSignal}, объект → {@link ModelGroupSignals}. Для примитивных
|
|
169
|
+
* и Opaque массивов элемент — сам сигнал листа (а не под-дерево), поэтому `model.$.tags[0]` — это
|
|
170
|
+
* `PathAwareSignal<string>`, а не `ModelGroupSignals<never>`. Тип с `null` — лист
|
|
171
|
+
* {@link PathAwareSignal}: такое поле начинают с `null`, и под-сигналов у него нет.
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
type ModelSignalNode<V> = IncludesNull<V> extends true ? PathAwareSignal<V> : NonNullable<V> extends ReadonlyArray<infer U> ? ModelArraySignals<U, V> : NonNullable<V> extends Opaque ? PathAwareSignal<V> : NonNullable<V> extends object ? ModelGroupSignals<NonNullable<V>> : PathAwareSignal<V>;
|
|
175
|
+
/**
|
|
176
|
+
* API уровня модели (доступно на корне, под-моделях вложенных объектов-групп и элементов массива).
|
|
177
|
+
*
|
|
178
|
+
* ⚠️ Имена методов (`$`/`get`/`set`/`patch`/`isDirty`/`reset`/`signalAt`/`captureInitial`)
|
|
179
|
+
* зарезервированы: одноимённое поле формы их затеняет (редкий краевой случай).
|
|
180
|
+
*
|
|
181
|
+
* @group Model
|
|
182
|
+
*/
|
|
183
|
+
export interface ModelApi<T> {
|
|
184
|
+
/**
|
|
185
|
+
* Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`.
|
|
186
|
+
* Сам узел — {@link ReadonlySignal} модели целиком: `model.$.subscribe(v => …)`.
|
|
187
|
+
*/
|
|
188
|
+
readonly $: ModelGroupSignals<T>;
|
|
189
|
+
/** Снимок значений (без подписки) — для submit. */
|
|
190
|
+
get(): T;
|
|
191
|
+
/**
|
|
192
|
+
* Полная установка значений: принимает объект целиком (все ключи `T`) и записывает их в модель.
|
|
193
|
+
* Производными полями (цели `compute`) владеет compute — их значения из payload игнорируются.
|
|
194
|
+
* Не меняет initial-снимок. Для частичного обновления (только переданные ключи) — {@link ModelApi.patch}.
|
|
195
|
+
*
|
|
196
|
+
* Если в объекте не хватает ключей (значение пришло без типа — например, с сервера), эти поля
|
|
197
|
+
* остаются прежними, а в dev выводится предупреждение со списком.
|
|
198
|
+
*/
|
|
199
|
+
set(value: T): void;
|
|
200
|
+
/**
|
|
201
|
+
* Частичное слияние значений (load/patch с сервера): обновляет только переданные ключи,
|
|
202
|
+
* отсутствующие ключи НЕ трогаются. Вложенные группы сливаются так же, массив заменяется
|
|
203
|
+
* целиком. Не меняет initial-снимок.
|
|
204
|
+
*/
|
|
205
|
+
patch(value: DeepPartial<T>): void;
|
|
206
|
+
/**
|
|
207
|
+
* Отличаются ли текущие значения от initial-снимка (value-diff). Производные поля — цели
|
|
208
|
+
* `compute` / `computeFrom` и поля с `markDerived` — не в счёт: их пишет поведение, а не
|
|
209
|
+
* пользователь, и форма без единой правки изменённой не считается.
|
|
210
|
+
*/
|
|
211
|
+
isDirty(): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Сбросить значения к initial-снимку. Производные поля сброс не пишет, как `set` и `patch`:
|
|
214
|
+
* цель `compute` пересчитает поведение, когда вернутся её источники.
|
|
215
|
+
*
|
|
216
|
+
* Ноды формы не трогает: замечания погаснут при следующей проверке. Сброс для пользователя —
|
|
217
|
+
* `form.reset()`: та же точка отсчёта и чистые ноды.
|
|
218
|
+
*/
|
|
219
|
+
reset(): void;
|
|
220
|
+
/**
|
|
221
|
+
* Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). К нему возвращают
|
|
222
|
+
* и `model.reset()`, и `form.reset()`.
|
|
223
|
+
*/
|
|
224
|
+
captureInitial(): void;
|
|
225
|
+
/**
|
|
226
|
+
* Резолв строкового пути в сигнал (для error-routing/мостов): лист либо массив целиком
|
|
227
|
+
* (ручка значения массива — см. {@link ModelArraySignals}). Для пути группы — `undefined`.
|
|
228
|
+
*/
|
|
229
|
+
signalAt(path: string): PathAwareSignal<unknown> | undefined;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* FormModel — под-модель объекта `T`: value-доступ ({@link ModelObject}) + `.$`-сигналы + {@link ModelApi}
|
|
233
|
+
* (get/set/patch/isDirty/reset/signalAt). Вложенные объекты-группы модели — тоже {@link FormModel}
|
|
234
|
+
* (доступны как `model.<group>`), поэтому `model.<group>.$.<field>` эквивалентно `model.$.<group>.<field>`.
|
|
235
|
+
*
|
|
236
|
+
* @group Model
|
|
237
|
+
*/
|
|
238
|
+
export type FormModel<T> = ModelObject<T> & ModelApi<T>;
|
|
239
|
+
export {};
|