@reformer/core 9.0.0-beta.1 → 9.0.0-beta.2
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 +52 -57
- package/dist/behaviors-Br4Im38V.js +263 -0
- package/dist/behaviors.js +146 -146
- package/dist/core/model/behaviors.d.ts +15 -0
- package/dist/core/nodes/group-node.d.ts +32 -2
- package/dist/core/nodes/model-array-node.d.ts +8 -1
- package/dist/core/types/deep-schema.d.ts +23 -1
- package/dist/core/types/form-proxy.d.ts +26 -1
- package/dist/core/types/validation-schema.d.ts +18 -8
- package/dist/core/utils/derived-registry.d.ts +29 -0
- package/dist/core/utils/form-submitter.d.ts +6 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/safe-effect.d.ts +27 -7
- package/dist/core/validation/validators/required.d.ts +2 -1
- package/dist/hooks/types.d.ts +30 -0
- package/dist/hooks/useSignalSubscription.d.ts +8 -1
- package/dist/index.js +766 -758
- package/dist/validators/pattern.js +7 -6
- package/dist/validators/required.js +5 -9
- package/llms.txt +149 -22
- package/package.json +1 -1
- package/dist/behaviors-O9a1Djj9.js +0 -147
package/README.md
CHANGED
|
@@ -30,33 +30,35 @@ npm install @reformer/core@beta # Active development is underway, so you can try
|
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
|
|
33
|
+
ReFormer is built around the **M1 architecture**: a reactive `FormModel` owns the values, a single
|
|
34
|
+
schema binds field config (component / validators) to the model's signals, and `createForm({ model, schema })`
|
|
35
|
+
wires them into a typed form.
|
|
36
|
+
|
|
33
37
|
```tsx
|
|
34
38
|
import { useMemo } from 'react';
|
|
35
39
|
import {
|
|
40
|
+
createModel,
|
|
36
41
|
createForm,
|
|
42
|
+
validateFormModel,
|
|
37
43
|
useFormControl,
|
|
38
|
-
|
|
39
|
-
email,
|
|
40
|
-
validate,
|
|
41
|
-
watchField,
|
|
42
|
-
FieldNode,
|
|
43
|
-
type ValidationSchemaFn,
|
|
44
|
-
type BehaviorSchemaFn,
|
|
44
|
+
type FieldNode,
|
|
45
45
|
} from '@reformer/core';
|
|
46
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
47
|
+
import { defineFormBehavior, onChange } from '@reformer/core/behaviors';
|
|
46
48
|
|
|
47
49
|
// 0. Simple FormField component
|
|
48
50
|
function FormField({ label, control }: { label: string; control: FieldNode<string> }) {
|
|
49
|
-
const { value, errors } = useFormControl(control);
|
|
51
|
+
const { value, errors, shouldShowError } = useFormControl(control);
|
|
50
52
|
|
|
51
53
|
return (
|
|
52
54
|
<div>
|
|
53
55
|
<label>{label}</label>
|
|
54
56
|
<input
|
|
55
|
-
value={value}
|
|
57
|
+
value={value ?? ''}
|
|
56
58
|
onChange={(e) => control.setValue(e.target.value)}
|
|
57
59
|
onBlur={() => control.markAsTouched()}
|
|
58
60
|
/>
|
|
59
|
-
{
|
|
61
|
+
{shouldShowError && <span className="error">{errors[0].message}</span>}
|
|
60
62
|
</div>
|
|
61
63
|
);
|
|
62
64
|
}
|
|
@@ -69,62 +71,55 @@ interface RegistrationForm {
|
|
|
69
71
|
confirmPassword: string;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
// 2.
|
|
73
|
-
const
|
|
74
|
-
username:
|
|
75
|
-
email:
|
|
76
|
-
password:
|
|
77
|
-
confirmPassword:
|
|
74
|
+
// 2. Reactive model — the source of truth for values
|
|
75
|
+
const model = createModel<RegistrationForm>({
|
|
76
|
+
username: '',
|
|
77
|
+
email: '',
|
|
78
|
+
password: '',
|
|
79
|
+
confirmPassword: '',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// 3. Single schema — binds field config to model signals (`model.$.<field>`).
|
|
83
|
+
// Validators are inline factories from `@reformer/core/validators`.
|
|
84
|
+
// Cross-field validation is a ModelValidator `(value, model) => error | null`.
|
|
85
|
+
const schema = {
|
|
86
|
+
children: [
|
|
87
|
+
{ value: model.$.username, component: Input, validators: [required(), minLength(2)] },
|
|
88
|
+
{ value: model.$.email, component: Input, validators: [required(), email()] },
|
|
89
|
+
{ value: model.$.password, component: Input, validators: [required(), minLength(8)] },
|
|
90
|
+
{
|
|
91
|
+
value: model.$.confirmPassword,
|
|
92
|
+
component: Input,
|
|
93
|
+
validators: [
|
|
94
|
+
required(),
|
|
95
|
+
(value, m) =>
|
|
96
|
+
value && m.password && value !== m.password
|
|
97
|
+
? { code: 'mismatch', message: 'Passwords do not match' }
|
|
98
|
+
: null,
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
],
|
|
78
102
|
};
|
|
79
103
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
email(path.email);
|
|
86
|
-
|
|
87
|
-
required(path.password);
|
|
88
|
-
required(path.confirmPassword);
|
|
89
|
-
|
|
90
|
-
// Cross-field validation: вешается на поле-носитель ошибки, соседнее поле читается через `root`
|
|
91
|
-
validate(path.confirmPassword, (value, _control, root) => {
|
|
92
|
-
const password = root.password.value.value;
|
|
93
|
-
if (value && password && value !== password) {
|
|
94
|
-
return { code: 'mismatch', message: 'Passwords do not match' };
|
|
95
|
-
}
|
|
96
|
-
return null;
|
|
104
|
+
// 4. Behavior — declarative reactive logic, run by `createForm({ behavior })`.
|
|
105
|
+
// Clear confirmPassword whenever password changes.
|
|
106
|
+
const behavior = defineFormBehavior<RegistrationForm>(({ model }) => {
|
|
107
|
+
onChange(model.$.password, () => {
|
|
108
|
+
if (model.confirmPassword) model.confirmPassword = '';
|
|
97
109
|
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// 4. Behavior schema
|
|
101
|
-
const behaviorSchema: BehaviorSchemaFn<RegistrationForm> = (path) => {
|
|
102
|
-
// Clear confirmPassword when password changes (if not empty)
|
|
103
|
-
watchField(path.password, (_, ctx) => {
|
|
104
|
-
const confirmValue = ctx.form.confirmPassword.value.value;
|
|
105
|
-
if (confirmValue) {
|
|
106
|
-
ctx.form.confirmPassword.setValue('', { emitEvent: false });
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
+
});
|
|
110
111
|
|
|
111
112
|
// 5. Registration form component
|
|
112
113
|
function RegistrationFormExample() {
|
|
113
|
-
const form = useMemo(
|
|
114
|
-
() =>
|
|
115
|
-
createForm<RegistrationForm>({
|
|
116
|
-
form: formSchema,
|
|
117
|
-
validation: validationSchema,
|
|
118
|
-
behavior: behaviorSchema,
|
|
119
|
-
}),
|
|
120
|
-
[]
|
|
121
|
-
);
|
|
114
|
+
const form = useMemo(() => createForm<RegistrationForm>({ model, schema, behavior }), []);
|
|
122
115
|
|
|
123
116
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
124
117
|
e.preventDefault();
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
118
|
+
form.touchAll();
|
|
119
|
+
// Validate the whole model against the schema (sync + async); errors route into the form.
|
|
120
|
+
const { valid } = await validateFormModel(model, schema);
|
|
121
|
+
if (valid) {
|
|
122
|
+
console.log('Form data:', model.get());
|
|
128
123
|
}
|
|
129
124
|
};
|
|
130
125
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { effect as a } from "@preact/signals-core";
|
|
2
|
+
var l = /* @__PURE__ */ ((n) => (n.THROW = "throw", n.LOG = "log", n.CONVERT = "convert", n))(l || {});
|
|
3
|
+
class m {
|
|
4
|
+
static handle(e, t, r = "throw") {
|
|
5
|
+
const s = this.extractMessage(e);
|
|
6
|
+
switch (process.env.NODE_ENV !== "production" && console.error(`[${t}]`, e), r) {
|
|
7
|
+
case "throw":
|
|
8
|
+
throw e;
|
|
9
|
+
case "log":
|
|
10
|
+
return;
|
|
11
|
+
case "convert":
|
|
12
|
+
return {
|
|
13
|
+
code: "validator_error",
|
|
14
|
+
message: s,
|
|
15
|
+
params: { field: t }
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Извлечь сообщение из ошибки
|
|
21
|
+
*
|
|
22
|
+
* Обрабатывает различные типы ошибок:
|
|
23
|
+
* - Error объекты → error.message
|
|
24
|
+
* - Строки → возвращает как есть
|
|
25
|
+
* - Объекты с message → извлекает message
|
|
26
|
+
* - Другое → String(error)
|
|
27
|
+
*
|
|
28
|
+
* @param error Ошибка для извлечения сообщения
|
|
29
|
+
* @returns Сообщение ошибки
|
|
30
|
+
* @private
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* FormErrorHandler.extractMessage(new Error('Test'));
|
|
35
|
+
* // 'Test'
|
|
36
|
+
*
|
|
37
|
+
* FormErrorHandler.extractMessage('String error');
|
|
38
|
+
* // 'String error'
|
|
39
|
+
*
|
|
40
|
+
* FormErrorHandler.extractMessage({ message: 'Object error' });
|
|
41
|
+
* // 'Object error'
|
|
42
|
+
*
|
|
43
|
+
* FormErrorHandler.extractMessage(null);
|
|
44
|
+
* // 'null'
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
static extractMessage(e) {
|
|
48
|
+
return e instanceof Error ? e.message : typeof e == "string" ? e : typeof e == "object" && e !== null && "message" in e ? String(e.message) : String(e);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Создать ValidationError с заданными параметрами
|
|
52
|
+
*
|
|
53
|
+
* Утилитная функция для создания ValidationError объектов
|
|
54
|
+
*
|
|
55
|
+
* @param code Код ошибки
|
|
56
|
+
* @param message Сообщение ошибки
|
|
57
|
+
* @param field Поле (опционально)
|
|
58
|
+
* @returns ValidationError объект
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const error = FormErrorHandler.createValidationError(
|
|
63
|
+
* 'required',
|
|
64
|
+
* 'This field is required',
|
|
65
|
+
* 'email'
|
|
66
|
+
* );
|
|
67
|
+
* // { code: 'required', message: 'This field is required', field: 'email' }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
static createValidationError(e, t, r) {
|
|
71
|
+
return {
|
|
72
|
+
code: e,
|
|
73
|
+
message: t,
|
|
74
|
+
params: r ? { field: r } : void 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Проверить, является ли объект ValidationError
|
|
79
|
+
*
|
|
80
|
+
* Type guard для ValidationError
|
|
81
|
+
*
|
|
82
|
+
* @param value Значение для проверки
|
|
83
|
+
* @returns true если value является ValidationError
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* if (FormErrorHandler.isValidationError(result)) {
|
|
88
|
+
* console.log(result.code); // OK, типобезопасно
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
static isValidationError(e) {
|
|
93
|
+
return typeof e == "object" && e !== null && "code" in e && "message" in e && typeof e.code == "string" && typeof e.message == "string";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const u = /* @__PURE__ */ new WeakMap();
|
|
97
|
+
function w(n) {
|
|
98
|
+
u.set(n, (u.get(n) ?? 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
function E(n) {
|
|
101
|
+
const e = u.get(n);
|
|
102
|
+
e !== void 0 && (e <= 1 ? u.delete(n) : u.set(n, e - 1));
|
|
103
|
+
}
|
|
104
|
+
function O(n) {
|
|
105
|
+
return u.has(n);
|
|
106
|
+
}
|
|
107
|
+
const h = /* @__PURE__ */ new WeakMap();
|
|
108
|
+
function D(n, e) {
|
|
109
|
+
h.set(n, e);
|
|
110
|
+
}
|
|
111
|
+
function p(n) {
|
|
112
|
+
return h.get(n);
|
|
113
|
+
}
|
|
114
|
+
const y = "runOutsideEffect";
|
|
115
|
+
function g(n) {
|
|
116
|
+
return typeof n == "object" && n !== null && typeof n.then == "function";
|
|
117
|
+
}
|
|
118
|
+
function v(n, e) {
|
|
119
|
+
try {
|
|
120
|
+
const t = n();
|
|
121
|
+
g(t) && t.then(void 0, (r) => m.handle(r, e, l.LOG));
|
|
122
|
+
} catch (t) {
|
|
123
|
+
m.handle(t, e, l.LOG);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function F(n) {
|
|
127
|
+
return (...e) => {
|
|
128
|
+
i(() => n(...e));
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function i(n) {
|
|
132
|
+
let e = !1;
|
|
133
|
+
return queueMicrotask(() => {
|
|
134
|
+
e || v(n, y);
|
|
135
|
+
}), () => {
|
|
136
|
+
e = !0;
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function V(n, e) {
|
|
140
|
+
return () => {
|
|
141
|
+
e(() => v(n, "safeDebouncedCallback"));
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function W(n, e, t, r) {
|
|
145
|
+
return a(() => {
|
|
146
|
+
const s = n.map((f) => f.value);
|
|
147
|
+
if (r?.when && !r.when(...s)) return;
|
|
148
|
+
const o = t(...s);
|
|
149
|
+
e.peek() !== o && (e.value = o);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function N(n, e, t) {
|
|
153
|
+
return a(() => {
|
|
154
|
+
const r = n.value;
|
|
155
|
+
if (t?.when && !t.when()) return;
|
|
156
|
+
const s = t?.transform ? t.transform(r) : r;
|
|
157
|
+
e.peek() !== s && (e.value = s);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function x(n, e, t) {
|
|
161
|
+
let r = !0;
|
|
162
|
+
return a(() => {
|
|
163
|
+
const s = n.value;
|
|
164
|
+
r && (r = !1, !t?.immediate) || e(s);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function k(n, e, t) {
|
|
168
|
+
return a(() => {
|
|
169
|
+
const r = e(), s = p(n);
|
|
170
|
+
s && i(() => {
|
|
171
|
+
r ? s.enable() : (s.disable(), t?.resetOnDisable && s.reset());
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function C(n, e, t) {
|
|
176
|
+
return k(n, () => !e(), t);
|
|
177
|
+
}
|
|
178
|
+
function M(n, e) {
|
|
179
|
+
let t = !1;
|
|
180
|
+
return a(() => {
|
|
181
|
+
const r = n.value;
|
|
182
|
+
t || i(() => {
|
|
183
|
+
const s = e(r);
|
|
184
|
+
if (n.peek() !== s) {
|
|
185
|
+
t = !0;
|
|
186
|
+
try {
|
|
187
|
+
n.value = s;
|
|
188
|
+
} finally {
|
|
189
|
+
t = !1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function j(n, e, t) {
|
|
196
|
+
const r = t?.resetValue ?? null;
|
|
197
|
+
return a(() => {
|
|
198
|
+
const s = e();
|
|
199
|
+
i(() => {
|
|
200
|
+
s && n.peek() !== r && (n.value = r);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function L(n, e, t) {
|
|
205
|
+
const r = t?.transform;
|
|
206
|
+
let s = !1;
|
|
207
|
+
const o = a(() => {
|
|
208
|
+
const c = n.value;
|
|
209
|
+
s || i(() => {
|
|
210
|
+
s = !0;
|
|
211
|
+
try {
|
|
212
|
+
const d = r ? r(c) : c;
|
|
213
|
+
e.peek() !== d && (e.value = d);
|
|
214
|
+
} finally {
|
|
215
|
+
s = !1;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}), f = a(() => {
|
|
219
|
+
const c = e.value;
|
|
220
|
+
s || i(() => {
|
|
221
|
+
s = !0;
|
|
222
|
+
try {
|
|
223
|
+
n.peek() !== c && (n.value = c);
|
|
224
|
+
} finally {
|
|
225
|
+
s = !1;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
return () => {
|
|
230
|
+
o(), f();
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function T(n, e) {
|
|
234
|
+
let t = !0;
|
|
235
|
+
return a(() => {
|
|
236
|
+
if (n.forEach((r) => r.value), t) {
|
|
237
|
+
t = !1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
i(() => e());
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
l as E,
|
|
245
|
+
m as F,
|
|
246
|
+
j as a,
|
|
247
|
+
T as b,
|
|
248
|
+
N as c,
|
|
249
|
+
D as d,
|
|
250
|
+
k as e,
|
|
251
|
+
F as f,
|
|
252
|
+
p as g,
|
|
253
|
+
V as h,
|
|
254
|
+
O as i,
|
|
255
|
+
W as j,
|
|
256
|
+
C as k,
|
|
257
|
+
w as m,
|
|
258
|
+
i as r,
|
|
259
|
+
L as s,
|
|
260
|
+
M as t,
|
|
261
|
+
E as u,
|
|
262
|
+
x as w
|
|
263
|
+
};
|