@reformer/core 11.0.0-beta.1 → 11.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/behaviors.js +134 -134
- package/dist/form/create-core-form.d.ts +70 -0
- package/dist/form/hooks/use-form-bundle.d.ts +26 -0
- package/dist/form/index.d.ts +6 -0
- package/dist/form/validation-config.d.ts +73 -0
- package/dist/form/validation-strategy.d.ts +8 -2
- package/dist/form-model-DhW2HZOB.js +323 -0
- package/dist/index.js +535 -481
- package/dist/state/form-model.d.ts +14 -0
- package/dist/state/index.d.ts +2 -1
- package/dist/state/types.d.ts +39 -11
- package/dist/state.js +18 -18
- package/dist/validation.js +92 -92
- package/docs/llms/02-quick-start.md +14 -7
- package/docs/llms/15-project-structure.md +12 -8
- package/docs/llms/19-reading-values.md +30 -1
- package/docs/llms/20-compute-vs-watch.md +9 -0
- package/llms.txt +374 -20
- package/package.json +1 -1
- package/dist/derived-registry-CluZCQ7U.js +0 -16
- package/dist/form-model-DYCzGGKF.js +0 -286
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { FormModel, PathAwareSignal } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Узел дерева `model.$` — контейнер (группа/массив), а не лист?
|
|
4
|
+
*
|
|
5
|
+
* Контейнерный узел структурно совместим с `ReadonlySignal` (`peek`/`value`/`subscribe`), поэтому
|
|
6
|
+
* duck-typing «есть `peek` ⇒ это лист» на нём даёт ложное срабатывание. Этот guard — надёжный
|
|
7
|
+
* способ различить: обходчикам дерева нужно спускаться в контейнер, а не читать его целиком.
|
|
8
|
+
*
|
|
9
|
+
* @group Model
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const isLeaf = (v: unknown) => isSignalLike(v) && !isModelContainerSignal(v);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function isModelContainerSignal(value: unknown): boolean;
|
|
2
16
|
/**
|
|
3
17
|
* Обойти ВСЕ листовые сигналы модели (включая элементы массивов), вызвав `visit` на каждом.
|
|
4
18
|
*
|
package/dist/state/index.d.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* @module state
|
|
24
24
|
*/
|
|
25
25
|
export { createModel } from './form-model';
|
|
26
|
-
export type { FormModel, ModelArray, ModelObject, ModelValue, ModelSignals, ModelApi, PathAwareSignal, } from './types';
|
|
26
|
+
export type { FormModel, ModelArray, ModelArraySignals, ModelGroupSignals, ModelObject, ModelValue, ModelSignals, ModelApi, PathAwareSignal, } from './types';
|
|
27
|
+
export { isModelContainerSignal } from './form-model';
|
|
27
28
|
export { computeFrom, copyFrom, watchField, transformValue, resetWhen, syncFields, revalidateWhen, } from './behaviors-value';
|
|
28
29
|
export type { BehaviorCleanup } from './behaviors-value';
|
|
29
30
|
export { markDerived, isDerived, unmarkDerived } from './derived-registry';
|
package/dist/state/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Signal } from '@preact/signals-core';
|
|
1
|
+
import { ReadonlySignal, Signal } from '@preact/signals-core';
|
|
2
2
|
/**
|
|
3
3
|
* Сигнал, который знает свой путь в модели (`'personalData.lastName'`).
|
|
4
4
|
* Используется как «ручка поля»: и привязка value в схеме, и идентичность для testId/devtools.
|
|
@@ -81,8 +81,8 @@ export interface ModelArray<U> {
|
|
|
81
81
|
[index: number]: ModelValue<U>;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
84
|
+
* Карта под-сигналов объекта в дереве `model.$` (без свойств самого узла — их добавляет
|
|
85
|
+
* {@link ModelGroupSignals}).
|
|
86
86
|
*
|
|
87
87
|
* @group Model
|
|
88
88
|
*/
|
|
@@ -90,16 +90,41 @@ export type ModelSignals<T> = {
|
|
|
90
90
|
[K in keyof T]: ModelSignalNode<T[K]>;
|
|
91
91
|
};
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* `PathAwareSignal<string>`, а не `ModelSignals<never>`.
|
|
93
|
+
* Свойства контейнерного узла дерева `$`, «поверх» карты детей. Ключи детей вырезаются из
|
|
94
|
+
* {@link ReadonlySignal}: доступ к полю формы всегда выигрывает у одноимённого свойства сигнала
|
|
95
|
+
* (так же ведёт себя рантайм), поэтому `{ value: string }` не порождает конфликта типов.
|
|
97
96
|
* @internal
|
|
98
97
|
*/
|
|
99
|
-
type
|
|
98
|
+
type ContainerSignal<T, TKeys extends PropertyKey> = Omit<ReadonlySignal<T>, TKeys>;
|
|
99
|
+
/**
|
|
100
|
+
* Узел-группа дерева `model.$`: доступ к под-сигналам полей ({@link ModelSignals}) И одновременно
|
|
101
|
+
* {@link ReadonlySignal} агрегированного значения группы — `.value`/`.peek()`/`.subscribe()`.
|
|
102
|
+
*
|
|
103
|
+
* ⚠️ Поле формы с именем `value`/`peek`/`subscribe`/`valueOf`/`toString`/`toJSON`/`brand` затеняет
|
|
104
|
+
* одноимённое свойство сигнала (редкий краевой случай); `subscribe` при этом продолжает работать.
|
|
105
|
+
*
|
|
106
|
+
* @group Model
|
|
107
|
+
*/
|
|
108
|
+
export type ModelGroupSignals<T> = ModelSignals<T> & ContainerSignal<T, keyof T>;
|
|
109
|
+
/**
|
|
110
|
+
* Узел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`
|
|
111
|
+
* и {@link ReadonlySignal} значения массива целиком (реагирует и на правку элемента, и на изменение
|
|
112
|
+
* состава — push/removeAt/move).
|
|
113
|
+
*
|
|
114
|
+
* @group Model
|
|
115
|
+
*/
|
|
116
|
+
export type ModelArraySignals<U, V> = ContainerSignal<V, 'length'> & {
|
|
100
117
|
readonly length: number;
|
|
101
118
|
readonly [index: number]: ModelSignalNode<U>;
|
|
102
|
-
}
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Узел дерева сигналов `model.$`: массив → {@link ModelArraySignals}, Opaque (Date/File/Blob)
|
|
122
|
+
* и примитив → {@link PathAwareSignal}, объект → {@link ModelGroupSignals}. Для примитивных
|
|
123
|
+
* и Opaque массивов элемент — сам сигнал листа (а не под-дерево), поэтому `model.$.tags[0]` — это
|
|
124
|
+
* `PathAwareSignal<string>`, а не `ModelGroupSignals<never>`.
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
type ModelSignalNode<V> = NonNullable<V> extends ReadonlyArray<infer U> ? ModelArraySignals<U, NonNullable<V>> : NonNullable<V> extends Opaque ? PathAwareSignal<V> : NonNullable<V> extends object ? ModelGroupSignals<NonNullable<V>> : PathAwareSignal<V>;
|
|
103
128
|
/**
|
|
104
129
|
* API уровня модели (доступно на корне, под-моделях вложенных объектов-групп и элементов массива).
|
|
105
130
|
*
|
|
@@ -109,8 +134,11 @@ type ModelSignalNode<V> = NonNullable<V> extends ReadonlyArray<infer U> ? {
|
|
|
109
134
|
* @group Model
|
|
110
135
|
*/
|
|
111
136
|
export interface ModelApi<T> {
|
|
112
|
-
/**
|
|
113
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Escape-hatch к сигналам: `model.$.loanType` → `PathAwareSignal<LoanType>`.
|
|
139
|
+
* Сам узел — {@link ReadonlySignal} модели целиком: `model.$.subscribe(v => …)`.
|
|
140
|
+
*/
|
|
141
|
+
readonly $: ModelGroupSignals<T>;
|
|
114
142
|
/** Снимок значений (без подписки) — для submit. */
|
|
115
143
|
get(): T;
|
|
116
144
|
/**
|
package/dist/state.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { c as s } from "./form-model-
|
|
2
|
-
import { c as
|
|
3
|
-
import {
|
|
4
|
-
import { S as D } from "./subscription-manager-705bls97.js";
|
|
1
|
+
import { c as s, a as r, i as o, m as i, u as t } from "./form-model-DhW2HZOB.js";
|
|
2
|
+
import { c as n, a as d, r as l, b as m, d as f, e as u, f as p, s as b, t as k, w as v } from "./behaviors-value-C7dSEhCc.js";
|
|
3
|
+
import { S as F } from "./subscription-manager-705bls97.js";
|
|
5
4
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
F as SubscriptionManager,
|
|
6
|
+
n as computeFrom,
|
|
7
|
+
d as copyFrom,
|
|
9
8
|
s as createModel,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
m as
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
r as isDerived,
|
|
10
|
+
o as isModelContainerSignal,
|
|
11
|
+
i as markDerived,
|
|
12
|
+
l as resetWhen,
|
|
13
|
+
m as revalidateWhen,
|
|
14
|
+
f as runOutsideEffect,
|
|
15
|
+
u as safeCallback,
|
|
16
|
+
p as safeDebouncedCallback,
|
|
17
|
+
b as syncFields,
|
|
18
|
+
k as transformValue,
|
|
19
|
+
t as unmarkDerived,
|
|
20
|
+
v as watchField
|
|
21
21
|
};
|
package/dist/validation.js
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
import { signal as
|
|
2
|
-
import { e as
|
|
1
|
+
import { signal as A, effect as M } from "@preact/signals-core";
|
|
2
|
+
import { e as E } from "./form-model-DhW2HZOB.js";
|
|
3
3
|
import { g as p } from "./signal-node-registry-tiwel-TV.js";
|
|
4
|
+
function B(e, n, t = {}) {
|
|
5
|
+
const { strategy: r = "submit", debounce: o = 0, liveAfterSubmit: i = "change" } = t, s = A(!1);
|
|
6
|
+
let a = 0, c = !1, d, l;
|
|
7
|
+
const w = async (u) => {
|
|
8
|
+
a++, s.value = !0;
|
|
9
|
+
try {
|
|
10
|
+
return await W(e, n, { touch: u });
|
|
11
|
+
} finally {
|
|
12
|
+
--a === 0 && (s.value = !1);
|
|
13
|
+
}
|
|
14
|
+
}, h = () => {
|
|
15
|
+
w(!1);
|
|
16
|
+
}, F = () => {
|
|
17
|
+
if (!o) {
|
|
18
|
+
h();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
l && clearTimeout(l), l = setTimeout(h, o);
|
|
22
|
+
}, k = (u, g) => {
|
|
23
|
+
let S = !0;
|
|
24
|
+
return M(() => {
|
|
25
|
+
if (E(e, u), S) {
|
|
26
|
+
S = !1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
r === "afterFirstSubmit" && !c || g();
|
|
30
|
+
});
|
|
31
|
+
}, m = {
|
|
32
|
+
async validate() {
|
|
33
|
+
return c = !0, w(!0);
|
|
34
|
+
},
|
|
35
|
+
start() {
|
|
36
|
+
if (d) return () => m.dispose();
|
|
37
|
+
const u = r === "afterFirstSubmit" ? i : r;
|
|
38
|
+
return u === "change" ? d = k((g) => void g.value, F) : u === "blur" && (d = k((g) => void p(g)?.touched.value, h)), () => m.dispose();
|
|
39
|
+
},
|
|
40
|
+
dispose() {
|
|
41
|
+
l && (clearTimeout(l), l = void 0), d?.(), d = void 0;
|
|
42
|
+
},
|
|
43
|
+
get isValidating() {
|
|
44
|
+
return s.value;
|
|
45
|
+
},
|
|
46
|
+
get validating() {
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return m;
|
|
51
|
+
}
|
|
4
52
|
let v = null;
|
|
5
53
|
function f(e) {
|
|
6
54
|
if (!v)
|
|
@@ -9,83 +57,83 @@ function f(e) {
|
|
|
9
57
|
);
|
|
10
58
|
return v;
|
|
11
59
|
}
|
|
12
|
-
function b(e,
|
|
13
|
-
let t = e.errors.get(
|
|
14
|
-
return t || e.errors.set(
|
|
60
|
+
function b(e, n) {
|
|
61
|
+
let t = e.errors.get(n);
|
|
62
|
+
return t || e.errors.set(n, t = []), t;
|
|
15
63
|
}
|
|
16
|
-
const y = (e) => e.whenStack.every((
|
|
17
|
-
function D(e,
|
|
18
|
-
const t = f("validate"),
|
|
64
|
+
const y = (e) => e.whenStack.every((n) => n());
|
|
65
|
+
function D(e, n) {
|
|
66
|
+
const t = f("validate"), r = b(t, e);
|
|
19
67
|
if (!y(t)) return;
|
|
20
68
|
const o = e.peek();
|
|
21
|
-
for (const i of
|
|
69
|
+
for (const i of n) {
|
|
22
70
|
const s = i(o, t.model, t.model);
|
|
23
|
-
s &&
|
|
71
|
+
s && r.push(s);
|
|
24
72
|
}
|
|
25
73
|
}
|
|
26
|
-
function N(e,
|
|
27
|
-
const t = f("validateAsync"),
|
|
74
|
+
function N(e, n) {
|
|
75
|
+
const t = f("validateAsync"), r = b(t, e);
|
|
28
76
|
if (!y(t)) return;
|
|
29
77
|
const o = e.peek(), i = t.signal;
|
|
30
|
-
for (const s of
|
|
78
|
+
for (const s of n)
|
|
31
79
|
t.pending.push(
|
|
32
80
|
s(o, { signal: i }).then(
|
|
33
81
|
(a) => {
|
|
34
|
-
a && !i.aborted &&
|
|
82
|
+
a && !i.aborted && r.push(a);
|
|
35
83
|
},
|
|
36
84
|
() => {
|
|
37
85
|
}
|
|
38
86
|
)
|
|
39
87
|
);
|
|
40
88
|
}
|
|
41
|
-
function R(e,
|
|
89
|
+
function R(e, n) {
|
|
42
90
|
const t = f("validateWhen");
|
|
43
91
|
t.whenStack.push(e);
|
|
44
92
|
try {
|
|
45
|
-
|
|
93
|
+
n();
|
|
46
94
|
} finally {
|
|
47
95
|
t.whenStack.pop();
|
|
48
96
|
}
|
|
49
97
|
}
|
|
50
|
-
function _(e,
|
|
51
|
-
const t = f("cross"),
|
|
98
|
+
function _(e, n) {
|
|
99
|
+
const t = f("cross"), r = b(t, e);
|
|
52
100
|
if (!y(t)) return;
|
|
53
|
-
const o =
|
|
54
|
-
o &&
|
|
101
|
+
const o = n(t.model.get());
|
|
102
|
+
o && r.push(o);
|
|
55
103
|
}
|
|
56
|
-
function $(e,
|
|
104
|
+
function $(e, n) {
|
|
57
105
|
f("each");
|
|
58
106
|
const t = e.length;
|
|
59
|
-
for (let
|
|
107
|
+
for (let r = 0; r < t; r++) n(e.at(r));
|
|
60
108
|
}
|
|
61
109
|
function j(...e) {
|
|
62
110
|
const t = f("apply").model;
|
|
63
|
-
for (const
|
|
111
|
+
for (const r of e) r({ model: t });
|
|
64
112
|
}
|
|
65
113
|
function z(e) {
|
|
66
114
|
return e;
|
|
67
115
|
}
|
|
68
|
-
const
|
|
69
|
-
function
|
|
70
|
-
let t =
|
|
71
|
-
t ||
|
|
72
|
-
let
|
|
73
|
-
return
|
|
116
|
+
const x = /* @__PURE__ */ new WeakMap();
|
|
117
|
+
function L(e, n) {
|
|
118
|
+
let t = x.get(e);
|
|
119
|
+
t || x.set(e, t = /* @__PURE__ */ new WeakMap());
|
|
120
|
+
let r = t.get(n);
|
|
121
|
+
return r || t.set(n, r = { fields: /* @__PURE__ */ new Set(), ac: null }), r;
|
|
74
122
|
}
|
|
75
|
-
const
|
|
76
|
-
for (const
|
|
123
|
+
const P = (e) => {
|
|
124
|
+
for (const n of e.values()) for (const t of n) if (t.severity !== "warning") return !0;
|
|
77
125
|
return !1;
|
|
78
126
|
};
|
|
79
|
-
function
|
|
80
|
-
return e.aborted ? Promise.resolve() : new Promise((
|
|
81
|
-
e.addEventListener("abort", () =>
|
|
127
|
+
function T(e) {
|
|
128
|
+
return e.aborted ? Promise.resolve() : new Promise((n) => {
|
|
129
|
+
e.addEventListener("abort", () => n(), { once: !0 });
|
|
82
130
|
});
|
|
83
131
|
}
|
|
84
|
-
async function
|
|
85
|
-
const
|
|
86
|
-
|
|
132
|
+
async function W(e, n, t) {
|
|
133
|
+
const r = L(e, n);
|
|
134
|
+
r.ac?.abort();
|
|
87
135
|
const o = new AbortController();
|
|
88
|
-
|
|
136
|
+
r.ac = o;
|
|
89
137
|
const i = {
|
|
90
138
|
model: e,
|
|
91
139
|
errors: /* @__PURE__ */ new Map(),
|
|
@@ -95,73 +143,25 @@ async function V(e, r, t) {
|
|
|
95
143
|
}, s = v;
|
|
96
144
|
v = i;
|
|
97
145
|
try {
|
|
98
|
-
|
|
146
|
+
n({ model: e });
|
|
99
147
|
} finally {
|
|
100
148
|
v = s;
|
|
101
149
|
}
|
|
102
|
-
if (i.pending.length && await Promise.race([Promise.all(i.pending),
|
|
150
|
+
if (i.pending.length && await Promise.race([Promise.all(i.pending), T(o.signal)]), o.signal.aborted) return !1;
|
|
103
151
|
const a = new Set(i.errors.keys());
|
|
104
|
-
for (const c of
|
|
152
|
+
for (const c of r.fields) a.has(c) || p(c)?.setErrors([]);
|
|
105
153
|
for (const c of a) p(c)?.setErrors(i.errors.get(c) ?? []);
|
|
106
154
|
if (t?.touch) for (const c of a) p(c)?.markAsTouched();
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
function G(e, r, t = {}) {
|
|
110
|
-
const { strategy: n = "submit", debounce: o = 0, liveAfterSubmit: i = "change" } = t, s = M(!1);
|
|
111
|
-
let a = 0, c = !1, w = !1, d, l;
|
|
112
|
-
const k = async (u) => {
|
|
113
|
-
a++, s.value = !0;
|
|
114
|
-
try {
|
|
115
|
-
return await V(e, r, { touch: u });
|
|
116
|
-
} finally {
|
|
117
|
-
--a === 0 && (s.value = !1);
|
|
118
|
-
}
|
|
119
|
-
}, h = () => {
|
|
120
|
-
k(!1);
|
|
121
|
-
}, A = () => {
|
|
122
|
-
if (!o) {
|
|
123
|
-
h();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
l && clearTimeout(l), l = setTimeout(h, o);
|
|
127
|
-
}, S = (u, g) => {
|
|
128
|
-
let x = !0;
|
|
129
|
-
return E(() => {
|
|
130
|
-
if (L(e, u), x) {
|
|
131
|
-
x = !1;
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
n === "afterFirstSubmit" && !c || g();
|
|
135
|
-
});
|
|
136
|
-
}, m = {
|
|
137
|
-
async validate() {
|
|
138
|
-
return c = !0, k(!0);
|
|
139
|
-
},
|
|
140
|
-
start() {
|
|
141
|
-
if (w || d) return () => m.dispose();
|
|
142
|
-
const u = n === "afterFirstSubmit" ? i : n;
|
|
143
|
-
return u === "change" ? d = S((g) => void g.value, A) : u === "blur" && (d = S((g) => void p(g)?.touched.value, h)), () => m.dispose();
|
|
144
|
-
},
|
|
145
|
-
dispose() {
|
|
146
|
-
w = !0, l && (clearTimeout(l), l = void 0), d?.(), d = void 0;
|
|
147
|
-
},
|
|
148
|
-
get isValidating() {
|
|
149
|
-
return s.value;
|
|
150
|
-
},
|
|
151
|
-
get validating() {
|
|
152
|
-
return s;
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
return m;
|
|
155
|
+
return r.fields = a, !P(i.errors);
|
|
156
156
|
}
|
|
157
157
|
export {
|
|
158
158
|
j as apply,
|
|
159
|
-
|
|
159
|
+
B as createFormValidation,
|
|
160
160
|
_ as cross,
|
|
161
161
|
z as defineValidationSchema,
|
|
162
162
|
$ as each,
|
|
163
163
|
D as validate,
|
|
164
164
|
N as validateAsync,
|
|
165
|
-
|
|
165
|
+
W as validateModel,
|
|
166
166
|
R as validateWhen
|
|
167
167
|
};
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
> `Checkbox`-обёртки с `label`-prop'ами — это anti-pattern. См.
|
|
8
8
|
> `find_recipe(package="@reformer/ui-kit", topic="form-field-integration")`.
|
|
9
9
|
|
|
10
|
-
Архитектура M1:
|
|
11
|
-
(`
|
|
12
|
-
|
|
10
|
+
Архитектура M1: **модель данных** — источник истины, схема привязывает поля к её сигналам
|
|
11
|
+
(`model.$.field`), а сборка идёт ОДНИМ вызовом `createCoreForm` (для рендера ui-kit;
|
|
12
|
+
в рендерерах — `createReactForm` / `createJsonForm`), который создаёт модель, строит форму,
|
|
13
|
+
запускает поведение и собирает валидацию. Layout-схема НЕ несёт валидаторов — валидация живёт в отдельной
|
|
13
14
|
схеме `defineValidationSchema` из `@reformer/core/validation` и запускается внешним
|
|
14
15
|
раннером `validateModel(model, schema)`.
|
|
15
16
|
|
|
16
17
|
```typescript
|
|
17
|
-
import { createModel,
|
|
18
|
+
import { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';
|
|
18
19
|
import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
|
|
19
20
|
import { required, email } from '@reformer/core/validators';
|
|
20
21
|
import { FormField, Input, Button } from '@reformer/ui-kit';
|
|
@@ -49,8 +50,14 @@ const contactValidation = defineValidationSchema<ContactForm>(({ model }) => {
|
|
|
49
50
|
validate(model.$.email, [required({ message: 'Email is required' }), email({ message: 'Invalid email' })]);
|
|
50
51
|
});
|
|
51
52
|
|
|
52
|
-
// 5.
|
|
53
|
-
|
|
53
|
+
// 5. Сборка ОДНИМ вызовом: модель + ноды поверх её сигналов + валидация.
|
|
54
|
+
// В React оборачивают в useFormBundle — ленивый useState, фабрика зовётся один раз.
|
|
55
|
+
const contact = createCoreForm<ContactForm>({
|
|
56
|
+
model,
|
|
57
|
+
schema: buildSchema, // билдер (model) => tree
|
|
58
|
+
validation: contactValidation,
|
|
59
|
+
});
|
|
60
|
+
const form = contact.form;
|
|
54
61
|
|
|
55
62
|
// 6. Use in React component — thin JSX, FormField does ALL heavy lifting
|
|
56
63
|
function ContactFormComponent() {
|
|
@@ -121,7 +128,7 @@ const schema = {
|
|
|
121
128
|
properties: { array: model.properties, item: propertyItem },
|
|
122
129
|
};
|
|
123
130
|
|
|
124
|
-
const form =
|
|
131
|
+
const { form } = createCoreForm<MyForm>({ model, schema: () => schema });
|
|
125
132
|
|
|
126
133
|
// Операции над массивом — на модели:
|
|
127
134
|
model.properties.push({ type: 'apartment', description: '', estimatedValue: 0 });
|
|
@@ -15,7 +15,7 @@ src/
|
|
|
15
15
|
│
|
|
16
16
|
├── forms/
|
|
17
17
|
│ └── [form-name]/ # Form module — flat, one file per concern
|
|
18
|
-
│ ├── index.tsx # entry + whole form:
|
|
18
|
+
│ ├── index.tsx # entry + whole form: ONE createCoreForm call → <FormWizard> with all steps inline; arrays via FormArraySection
|
|
19
19
|
│ ├── types.ts # form type + enums + { value, label } option type + constant dictionaries
|
|
20
20
|
│ ├── model.ts # createModel + initial values + empty-array-element factories
|
|
21
21
|
│ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
|
|
@@ -60,11 +60,15 @@ export const creditApplicationBehavior = defineFormBehavior<CreditApplicationFor
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
// forms/credit-application/index.tsx — entry: assembles the form, renders <FormWizard> with all steps inline
|
|
63
|
-
import {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
import { createCoreForm } from '@reformer/core';
|
|
64
|
+
// ONE call: model + form + behavior + validation. In the page: useFormBundle(createCreditApplicationForm).
|
|
65
|
+
export const createCreditApplicationForm = () =>
|
|
66
|
+
createCoreForm({
|
|
67
|
+
model: createCreditApplicationModel(),
|
|
68
|
+
schema: creditApplicationSchema, // builder (model) => tree
|
|
69
|
+
behavior: creditApplicationBehavior,
|
|
70
|
+
validation: creditApplicationValidation, // { steps, extras } → bundle.validation
|
|
71
|
+
});
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
### Scaling up: folders (large forms)
|
|
@@ -90,10 +94,10 @@ forms/
|
|
|
90
94
|
│
|
|
91
95
|
├── schema/ # The form definition
|
|
92
96
|
│ ├── model.ts # createModel factory + initial values + array-element factories
|
|
93
|
-
│ ├── schema.ts #
|
|
97
|
+
│ ├── schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
|
|
94
98
|
│ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
|
|
95
99
|
│ ├── behavior.ts # defineFormBehavior(...)
|
|
96
|
-
│ └── create-form.ts # Assembly:
|
|
100
|
+
│ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
|
|
97
101
|
│
|
|
98
102
|
└── components/
|
|
99
103
|
├── steps/ # One component per wizard step
|
|
@@ -29,9 +29,38 @@ model.$.email.peek(); // нереактивный снимок
|
|
|
29
29
|
model.$.address.city.value; // сигнал вложенного поля (≡ model.address.$.city у под-модели)
|
|
30
30
|
|
|
31
31
|
// весь объект
|
|
32
|
-
model.get(); // снимок { email, address: { city }, ... } — для submit
|
|
32
|
+
model.get(); // НЕреактивный снимок { email, address: { city }, ... } — для submit
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
> ⚠️ `model.get()` и `model.isDirty()` читают через `peek()` — внутри `effect`/`computed` они НЕ
|
|
36
|
+
> создают зависимостей. `computed(() => model.get())` никогда не пересчитается. Для реактивного
|
|
37
|
+
> чтения всего объекта — узел дерева `$` (ниже).
|
|
38
|
+
|
|
39
|
+
### Подписка на группу и на модель целиком
|
|
40
|
+
|
|
41
|
+
Каждый узел дерева `$` — сигнал: лист отдаёт `PathAwareSignal`, а корень, вложенные объекты-группы
|
|
42
|
+
и массивы — `ReadonlySignal` агрегированного значения поддерева. Поэтому подписаться можно на любом
|
|
43
|
+
уровне, а не только на конкретном поле.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
model.$.subscribe((all) => autosave(all)); // любое изменение модели; all: T целиком
|
|
47
|
+
model.$.address.subscribe((addr) => ...); // только поддерево address
|
|
48
|
+
model.$.address.value; // реактивный снимок группы
|
|
49
|
+
model.$.address.peek(); // нереактивный снимок группы
|
|
50
|
+
model.$.items.subscribe((rows) => ...); // массив: и правка элемента, и push/removeAt/move
|
|
51
|
+
|
|
52
|
+
// узел — обычный ReadonlySignal, поэтому принимается операциями слоя данных
|
|
53
|
+
watchField(model.$.address, (addr) => geocode(addr));
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Дети узла доступны как раньше — `model.$.address.city` по-прежнему сигнал поля. Подписчик вызывается
|
|
57
|
+
сразу с текущим значением (семантика `Signal.subscribe`), а `model.set(...)`/`model.reset()`
|
|
58
|
+
уведомляют один раз, а не по разу на поле.
|
|
59
|
+
|
|
60
|
+
> ⚠️ Доступ к полю выигрывает у свойства сигнала: если в форме есть поле с именем `value`, `peek`,
|
|
61
|
+
> `subscribe`, `valueOf`, `toString`, `toJSON` или `brand`, то `model.$.<группа>.<это имя>` вернёт
|
|
62
|
+
> сигнал поля. `subscribe` при этом продолжает работать.
|
|
63
|
+
|
|
35
64
|
### В behaviors — читаем model напрямую
|
|
36
65
|
|
|
37
66
|
`compute`/`onChange`/условия `when` читают значения из value-модели (`model.field`) —
|
|
@@ -121,6 +121,14 @@ import { watchField } from '@reformer/core';
|
|
|
121
121
|
const stop = watchField(model.$.country, () => { model.city = ''; });
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
Источником может быть не только поле: узлы-контейнеры дерева `$` (корень, группы, массивы) — тоже
|
|
125
|
+
`ReadonlySignal`, поэтому за поддеревом целиком следят без перечисления полей.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
watchField(model.$.address, (addr) => geocode(addr)); // любое поле address
|
|
129
|
+
watchField(model.$, (all) => autosave(all)); // любое поле формы
|
|
130
|
+
```
|
|
131
|
+
|
|
124
132
|
### Rule of Thumb
|
|
125
133
|
|
|
126
134
|
| Scenario | Use |
|
|
@@ -129,6 +137,7 @@ const stop = watchField(model.$.country, () => { model.city = ''; });
|
|
|
129
137
|
| Производное с явными зависимостями | `computeFrom` |
|
|
130
138
|
| Async-реакция, обновление componentProps | `onChange` (debounce + AbortSignal) |
|
|
131
139
|
| Простая синхронная реакция (примитив) | `watchField` |
|
|
140
|
+
| Реакция на любое изменение группы/формы | `watchField(model.$.<группа>)` или `.subscribe` |
|
|
132
141
|
|
|
133
142
|
### Chained computeds
|
|
134
143
|
|