@reformer/core 11.0.0-beta.6 → 11.0.0-beta.8
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 +7 -3
- package/dist/behaviors.js +216 -179
- package/dist/create-model-BBRHhHwK.js +30 -0
- package/dist/en-CjAqn7h0.js +41 -0
- package/dist/form/behaviors/collections.d.ts +14 -1
- package/dist/form/behaviors/context.d.ts +18 -2
- package/dist/form/behaviors/index.d.ts +3 -1
- package/dist/form/behaviors/internals.d.ts +9 -1
- package/dist/form/behaviors/schema-operators.d.ts +76 -0
- package/dist/form/behaviors/types.d.ts +15 -3
- package/dist/form/create-core-form.d.ts +5 -3
- package/dist/form/create-form.d.ts +28 -53
- package/dist/form/form-bundle.d.ts +136 -0
- package/dist/form/index.d.ts +6 -1
- package/dist/form/nodes/field-node.d.ts +2 -2
- package/dist/form/nodes/group-node.d.ts +1 -1
- package/dist/form/nodes/model-array-node.d.ts +10 -5
- package/dist/form/schema-controller.d.ts +137 -0
- package/dist/form/schema-subtree.d.ts +28 -0
- package/dist/form/signal-node-registry.d.ts +22 -16
- package/dist/form/types/contracts.d.ts +6 -0
- package/dist/form/types/deep-schema.d.ts +2 -2
- package/dist/form/types/form-proxy.d.ts +1 -1
- package/dist/form/types/schema-node.d.ts +37 -12
- package/dist/form/types/validation-schema.d.ts +13 -1
- package/dist/form/validation/context.d.ts +6 -1
- package/dist/form/validation/index.d.ts +1 -1
- package/dist/form/validation/operators.d.ts +74 -7
- package/dist/form/validation/types.d.ts +2 -1
- package/dist/form/validators/validation-error.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +54 -0
- package/dist/i18n/en.json.d.ts +45 -0
- package/dist/i18n/loader.d.ts +71 -0
- package/dist/i18n/locale-store.d.ts +39 -0
- package/dist/i18n/locale.d.ts +90 -0
- package/dist/i18n/message-format.d.ts +116 -0
- package/dist/i18n/resolve-localized.d.ts +17 -0
- package/dist/i18n/ru.json.d.ts +45 -0
- package/dist/i18n/translator.d.ts +50 -0
- package/dist/i18n/validation-message.d.ts +18 -0
- package/dist/i18n.d.ts +31 -0
- package/dist/i18n.js +303 -0
- package/dist/index-BrNkQj1y.js +105 -0
- package/dist/index.js +737 -743
- package/dist/loader-Dv_Hl7JC.js +216 -0
- package/dist/locale/en.d.ts +2 -0
- package/dist/locale/en.js +5 -0
- package/dist/locale/en.json +42 -0
- package/dist/locale/index.d.ts +18 -0
- package/dist/locale/ru.d.ts +2 -0
- package/dist/locale/ru.js +41 -0
- package/dist/locale/ru.json +42 -0
- package/dist/locale.d.ts +2 -0
- package/dist/locale.js +9 -0
- package/dist/model/create-model.d.ts +17 -3
- package/dist/model/index.d.ts +8 -3
- package/dist/model/model-nodes.d.ts +67 -4
- package/dist/model/model-signals-proxy.d.ts +33 -1
- package/dist/model/model-value-proxy.d.ts +76 -0
- package/dist/model/types.d.ts +26 -9
- package/dist/model-value-proxy-caTmAiUd.js +419 -0
- package/dist/model.js +21 -17
- package/dist/node-BeV_lV5i.js +100 -0
- package/dist/platforms/react/form-bundle-context.d.ts +27 -0
- package/dist/platforms/react/hooks/types.d.ts +6 -3
- package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
- package/dist/platforms/react/i18n/context.d.ts +13 -0
- package/dist/platforms/react/i18n/hooks.d.ts +59 -0
- package/dist/platforms/react/i18n/provider.d.ts +52 -0
- package/dist/platforms/react/index.d.ts +1 -0
- package/dist/strategy-DgzELWno.js +204 -0
- package/dist/validation-error-B-U3RsV0.js +11 -0
- package/dist/validation.js +7 -6
- package/dist/validators/email.js +5 -8
- package/dist/validators/future-date.js +8 -11
- package/dist/validators/integer.js +4 -7
- package/dist/validators/is-date.js +5 -8
- package/dist/validators/is-number.js +4 -7
- package/dist/validators/max-age.js +8 -11
- package/dist/validators/max-date.js +8 -11
- package/dist/validators/max-length.js +5 -8
- package/dist/validators/max.js +4 -7
- package/dist/validators/min-age.js +8 -11
- package/dist/validators/min-date.js +8 -11
- package/dist/validators/min-length.js +5 -8
- package/dist/validators/min.js +4 -7
- package/dist/validators/multiple-of.js +5 -8
- package/dist/validators/non-negative.js +4 -7
- package/dist/validators/non-zero.js +4 -7
- package/dist/validators/past-date.js +8 -11
- package/dist/validators/pattern.js +5 -8
- package/dist/validators/phone.js +6 -9
- package/dist/validators/required.js +4 -7
- package/dist/validators/url.js +9 -14
- package/dist/validators.js +84 -106
- package/docs/llms/03-api-signatures.md +10 -4
- package/docs/llms/09-formschema.md +5 -5
- package/docs/llms/10-arrays.md +52 -3
- package/docs/llms/16-ui-components.md +2 -2
- package/docs/llms/18-conditional-fields.md +1 -1
- package/docs/llms/21-array-operations.md +26 -0
- package/docs/llms/28-submit-and-reset.md +3 -3
- package/docs/llms/29-async-preload.md +4 -4
- package/docs/llms/33-validation-strategy.md +2 -2
- package/docs/llms/34-i18n.md +272 -0
- package/docs/llms/35-unified-contract.md +246 -0
- package/llms-index.json +1 -1
- package/llms.txt +2746 -229
- package/package.json +22 -3
- package/dist/create-model-Dxh9cfLp.js +0 -97
- package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
- package/dist/node-DGEFMTQu.js +0 -18
- package/dist/strategy-DA3JwxsR.js +0 -170
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Signal } from '@preact/signals-core';
|
|
2
|
+
/**
|
|
3
|
+
* Хуки жизненного цикла узла схемы. Каждый хук опционален; повторная регистрация перезаписывает
|
|
4
|
+
* предыдущее значение.
|
|
5
|
+
*
|
|
6
|
+
* @group Schema
|
|
7
|
+
*/
|
|
8
|
+
export interface NodeLifecycleHooks {
|
|
9
|
+
/** Срабатывает один раз при mount узла. Может вернуть cleanup-функцию. */
|
|
10
|
+
onMount?: () => void | (() => void);
|
|
11
|
+
/** Срабатывает один раз при unmount узла. */
|
|
12
|
+
onUnmount?: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Хранилище переопределений одной области схемы. Ключ каждой карты — `selector` узла.
|
|
16
|
+
*
|
|
17
|
+
* @group Schema
|
|
18
|
+
*/
|
|
19
|
+
export interface SchemaOverrideMaps {
|
|
20
|
+
hiddenOverrides: Map<string, boolean | null>;
|
|
21
|
+
propsOverrides: Map<string, Record<string, unknown> | null>;
|
|
22
|
+
refRegistry: Map<string, {
|
|
23
|
+
current: any;
|
|
24
|
+
}>;
|
|
25
|
+
/** Условия скрытия узлов: selector → реактивное условие. */
|
|
26
|
+
conditionRegistry: Map<string, () => boolean>;
|
|
27
|
+
/** Реактивные эффекты, живущие, пока область смонтирована (`renderEffect`). */
|
|
28
|
+
effectRegistry: Array<() => void | (() => void)>;
|
|
29
|
+
/** Обработчики событий компонентов: selector → { имя пропа → обработчик }. */
|
|
30
|
+
callbackRegistry: Map<string, Map<string, (...args: any[]) => any>>;
|
|
31
|
+
/** Хуки жизненного цикла узла: selector → { onMount, onUnmount }. */
|
|
32
|
+
lifecycleRegistry: Map<string, NodeLifecycleHooks>;
|
|
33
|
+
/** Общий счётчик изменений области. Узлы на него не подписываются — см. `versionFor`. */
|
|
34
|
+
version: Signal<number>;
|
|
35
|
+
/**
|
|
36
|
+
* Версия-сигнал одного селектора (создаётся лениво). Узел подписывается только на сигнал СВОЕГО
|
|
37
|
+
* селектора, поэтому `setHidden` / `patchProps` уведомляют один узел, а не всё дерево.
|
|
38
|
+
*/
|
|
39
|
+
versionFor(selector: string): Signal<number>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Управление одним узлом схемы. Получается через `schema.node(selector)`.
|
|
43
|
+
*
|
|
44
|
+
* @group Schema
|
|
45
|
+
*/
|
|
46
|
+
export interface SchemaNodeControl {
|
|
47
|
+
/** Принудительно скрыть или показать узел, минуя условие `hideWhen`. */
|
|
48
|
+
setHidden(value: boolean): this;
|
|
49
|
+
/** Убрать принудительное значение — снова действует условие `hideWhen`. */
|
|
50
|
+
resetHidden(): this;
|
|
51
|
+
/** Подмешать объект в `componentProps` узла. */
|
|
52
|
+
patchProps(partial: Record<string, unknown>): this;
|
|
53
|
+
/** Убрать подмешанные пропсы. */
|
|
54
|
+
resetProps(): this;
|
|
55
|
+
/**
|
|
56
|
+
* Ref на компонент узла. Создаётся один раз на селектор; рендерер вешает его на компонент.
|
|
57
|
+
* Компонент обязан принимать `ref`.
|
|
58
|
+
*/
|
|
59
|
+
getRef<H>(): {
|
|
60
|
+
current: H | null;
|
|
61
|
+
};
|
|
62
|
+
/** @internal Селектор узла. */
|
|
63
|
+
__selector: string;
|
|
64
|
+
/** @internal Хранилище области. */
|
|
65
|
+
__overrideMaps: SchemaOverrideMaps;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Схема одной области: адресует узлы по `selector`.
|
|
69
|
+
*
|
|
70
|
+
* @group Schema
|
|
71
|
+
*/
|
|
72
|
+
export interface SchemaScope {
|
|
73
|
+
/** Управление узлом области по его `selector`. */
|
|
74
|
+
node(selector: string): SchemaNodeControl;
|
|
75
|
+
/** @internal Хранилище области — его читает рендерер. */
|
|
76
|
+
__overrideMaps: SchemaOverrideMaps;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Схема-контроллер сборки: области по под-моделям.
|
|
80
|
+
*
|
|
81
|
+
* @group Schema
|
|
82
|
+
*/
|
|
83
|
+
export interface SchemaController {
|
|
84
|
+
/**
|
|
85
|
+
* Область под-модели: корня сборки, строки массива или группы подформы. Создаётся при первом
|
|
86
|
+
* обращении; повторное обращение отдаёт ту же область.
|
|
87
|
+
*/
|
|
88
|
+
scopeOf(subModel: object): SchemaScope;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Пустое хранилище переопределений области.
|
|
92
|
+
*
|
|
93
|
+
* @group Schema
|
|
94
|
+
*/
|
|
95
|
+
export declare function createSchemaOverrideMaps(): SchemaOverrideMaps;
|
|
96
|
+
/**
|
|
97
|
+
* Схема области поверх хранилища: `node(selector)` → управление узлом.
|
|
98
|
+
*
|
|
99
|
+
* @param maps - Хранилище области; по умолчанию создаётся новое.
|
|
100
|
+
* @group Schema
|
|
101
|
+
*/
|
|
102
|
+
export declare function createSchemaScope(maps?: SchemaOverrideMaps): SchemaScope;
|
|
103
|
+
/**
|
|
104
|
+
* Схема-контроллер одной сборки формы.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* const controller = createSchemaController();
|
|
109
|
+
* const root = controller.scopeOf(model);
|
|
110
|
+
* root.node('mortgage').setHidden(true);
|
|
111
|
+
*
|
|
112
|
+
* controller.scopeOf(model.items.at(0)); // область строки — свои селекторы
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @group Schema
|
|
116
|
+
*/
|
|
117
|
+
export declare function createSchemaController(): SchemaController;
|
|
118
|
+
/**
|
|
119
|
+
* Селекторы, на которые в области записаны правила, но которых нет в её дереве. Такое правило
|
|
120
|
+
* ничего не делает — сборка и рендерер в dev предупреждают о нём.
|
|
121
|
+
*
|
|
122
|
+
* Ref в проверку не входит: его разрешено брать по абсолютному пути модели
|
|
123
|
+
* (`phones.0.number`), а такой строки при сборке может ещё не быть.
|
|
124
|
+
*
|
|
125
|
+
* @param maps - Хранилище области.
|
|
126
|
+
* @param tree - Дерево (поддерево) области.
|
|
127
|
+
* @returns Селекторы с правилами, не найденные в дереве.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* const scope = controller.scopeOf(model);
|
|
132
|
+
* unknownSchemaSelectors(scope.__overrideMaps, tree); // ['mortgage'] — узла с таким selector нет
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* @group Schema
|
|
136
|
+
*/
|
|
137
|
+
export declare function unknownSchemaSelectors(maps: SchemaOverrideMaps, tree: unknown): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Поддерево схемы для под-модели: содержимое строки массива (`item`) или подформы (`part`).
|
|
3
|
+
*
|
|
4
|
+
* Билдер поддерева зовут два места — сборка формы и рендерер. Оба обязаны получить ОДНО дерево:
|
|
5
|
+
* на объектах его узлов держится идентичность (ключи рендера, записи схемы-контроллера), а сам
|
|
6
|
+
* билдер не обязан быть чистым. Поэтому дерево строится один раз на пару «билдер + под-модель»
|
|
7
|
+
* и дальше отдаётся из памяти.
|
|
8
|
+
*
|
|
9
|
+
* Память привязана к под-модели: исчезла строка массива — исчезли и её поддеревья.
|
|
10
|
+
*
|
|
11
|
+
* @group Utilities
|
|
12
|
+
* @module form/schema-subtree
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Поддерево схемы для под-модели — результат `builder(subModel)`, один на пару.
|
|
16
|
+
*
|
|
17
|
+
* @typeParam N - Тип узла, который возвращает билдер.
|
|
18
|
+
* @param builder - Билдер поддерева: `item` узла-массива или `part` узла-подформы.
|
|
19
|
+
* @param subModel - Под-модель строки массива или группы.
|
|
20
|
+
* @returns Узел поддерева; повторный вызов с той же парой отдаёт тот же объект.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const row = model.items.at(0);
|
|
25
|
+
* schemaSubtree(itemSchema, row) === schemaSubtree(itemSchema, row); // true
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function schemaSubtree<N>(builder: (subModel: any) => N, subModel: object): N;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { Signal } from '@preact/signals-core';
|
|
2
1
|
import { FormNode } from './nodes/form-node';
|
|
3
2
|
/**
|
|
4
|
-
* Связать
|
|
3
|
+
* Связать ручку модели с её нодой формы.
|
|
5
4
|
*
|
|
6
|
-
* Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого
|
|
7
|
-
*
|
|
5
|
+
* Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого поля,
|
|
6
|
+
* группы и массива под-форм. Прикладной код обычно этот реестр не заполняет напрямую.
|
|
8
7
|
*
|
|
9
|
-
* @param signal -
|
|
10
|
-
* @param node - Нода формы, отвечающая за
|
|
8
|
+
* @param signal - Ручка из {@link FormModel}: сигнал листа, узел-массив или узел-группа дерева `model.$`
|
|
9
|
+
* @param node - Нода формы, отвечающая за эту часть модели
|
|
11
10
|
*
|
|
12
11
|
* @example Привязка листовых полей при построении формы
|
|
13
12
|
* ```typescript
|
|
@@ -18,19 +17,20 @@ import { FormNode } from './nodes/form-node';
|
|
|
18
17
|
* if (sig && node) registerSignalNode(sig, node);
|
|
19
18
|
* ```
|
|
20
19
|
*
|
|
21
|
-
* @see {@link getNodeForSignal} - обратный поиск ноды по
|
|
20
|
+
* @see {@link getNodeForSignal} - обратный поиск ноды по ручке
|
|
22
21
|
* @group Utilities
|
|
23
22
|
*/
|
|
24
|
-
export declare function registerSignalNode(signal:
|
|
23
|
+
export declare function registerSignalNode(signal: object, node: FormNode<any>): void;
|
|
25
24
|
/**
|
|
26
|
-
* Найти ноду формы по
|
|
25
|
+
* Найти ноду формы по ручке модели.
|
|
27
26
|
*
|
|
28
|
-
* Используется state-операциями behavior (`enableWhen`/`disableWhen`)
|
|
29
|
-
* валидации в ноды. Возвращает `undefined`, если форма
|
|
30
|
-
*
|
|
27
|
+
* Используется state-операциями behavior (`enableWhen`/`disableWhen`), подключением под-схем
|
|
28
|
+
* (`apply`/`applyEach`) и роутингом ошибок валидации в ноды. Возвращает `undefined`, если форма
|
|
29
|
+
* ещё не построена или эта часть модели в форме не материализована (например, массив без
|
|
30
|
+
* item-схемы).
|
|
31
31
|
*
|
|
32
|
-
* @param signal -
|
|
33
|
-
* @returns Нода формы
|
|
32
|
+
* @param signal - Ручка из {@link FormModel}: `model.$.<поле>`, `model.$.<группа>`, `model.$.<массив>`
|
|
33
|
+
* @returns Нода формы — поля, группы или массива — либо `undefined`
|
|
34
34
|
*
|
|
35
35
|
* @example Роутинг ошибок валидации в ноду поля
|
|
36
36
|
* ```typescript
|
|
@@ -40,7 +40,13 @@ export declare function registerSignalNode(signal: Signal<any>, node: FormNode<a
|
|
|
40
40
|
* getNodeForSignal(sig)?.setErrors([{ code: 'required', message: 'Обязательно' }]);
|
|
41
41
|
* ```
|
|
42
42
|
*
|
|
43
|
-
* @
|
|
43
|
+
* @example Нода группы и строки массива
|
|
44
|
+
* ```typescript
|
|
45
|
+
* getNodeForSignal(model.$.address)?.disable(); // группа целиком
|
|
46
|
+
* getNodeForSignal(model.$.phones[0])?.markAsTouched(); // форма строки массива
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @see {@link registerSignalNode} - регистрация связи ручка→нода
|
|
44
50
|
* @group Utilities
|
|
45
51
|
*/
|
|
46
|
-
export declare function getNodeForSignal(signal:
|
|
52
|
+
export declare function getNodeForSignal(signal: object): FormNode<any> | undefined;
|
|
@@ -75,7 +75,13 @@ export type AsyncValidatorFn<T = FormValue> = (value: T, options?: AsyncValidato
|
|
|
75
75
|
*/
|
|
76
76
|
export interface ValidationError {
|
|
77
77
|
code: string;
|
|
78
|
+
/**
|
|
79
|
+
* Готовый текст ошибки. Пустая строка — «автор правила текста не задавал»: отображаемую строку
|
|
80
|
+
* тогда даёт резолвер по `code` (словарь локали `validation.<code>`).
|
|
81
|
+
*/
|
|
78
82
|
message: string;
|
|
83
|
+
/** Ключ сообщения в словаре приложения; при наличии в активной локали важнее `message`. */
|
|
84
|
+
messageKey?: string;
|
|
79
85
|
params?: Record<string, FormValue>;
|
|
80
86
|
/** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
|
|
81
87
|
severity?: 'error' | 'warning';
|
|
@@ -6,7 +6,7 @@ import { ValidatorFn, AsyncValidatorFn, AnyFunction } from './index';
|
|
|
6
6
|
* @group Types
|
|
7
7
|
* @category Configuration Types
|
|
8
8
|
* @remarks
|
|
9
|
-
* В схеме `
|
|
9
|
+
* В схеме `createFormFromModel({ model, schema })` лист связывают через **`value: model.$.field`**: harvest
|
|
10
10
|
* ловит узел по идентичности `Signal` (`node.value instanceof Signal` — см. `create-form.ts`) и
|
|
11
11
|
* нормализует его в `valueSignal`. Писать `valueSignal:` **в самой схеме `createForm` нельзя** —
|
|
12
12
|
* harvest его не подхватит (проверка идёт по `node.value`), и `component`/`componentProps` этого
|
|
@@ -68,7 +68,7 @@ type NonEmptyFieldConfig<T> = RequireAtLeastOne<FieldConfig<T>>;
|
|
|
68
68
|
* Использует NonNullable для корректной обработки опциональных полей.
|
|
69
69
|
*
|
|
70
70
|
* ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
|
|
71
|
-
* контейнер), передаваемой в `
|
|
71
|
+
* контейнер), передаваемой в `createFormFromModel({ model, schema })`. `FormSchema` — это data-shaped конфиг
|
|
72
72
|
* (ключи = поля данных), а не узел дерева.
|
|
73
73
|
*
|
|
74
74
|
* @group Types
|
|
@@ -13,16 +13,17 @@ export interface SchemaArrayControl {
|
|
|
13
13
|
/** Реактивная длина. */
|
|
14
14
|
readonly length: number;
|
|
15
15
|
at(index: number): unknown;
|
|
16
|
-
push(item
|
|
16
|
+
push(item?: unknown): void;
|
|
17
17
|
removeAt(index: number): void;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Узел единой схемы M1 — layout-дерево, обходимое `
|
|
20
|
+
* Узел единой схемы M1 — layout-дерево, обходимое `createFormFromModel({ model, schema })`
|
|
21
21
|
* и рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).
|
|
22
22
|
*
|
|
23
23
|
* Узел совмещает несколько ролей (различаются рантаймом по форме):
|
|
24
|
-
* - **поле** —
|
|
25
|
-
* -
|
|
24
|
+
* - **поле** — `{ model: model.$.x, component, componentProps }`;
|
|
25
|
+
* - **массив под-форм** — `{ model: model.$.items, item: (model) => узел }`;
|
|
26
|
+
* - **подформа** — `{ model: model.$.group, part: (model) => узел }`;
|
|
26
27
|
* - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
|
|
27
28
|
* - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
|
|
28
29
|
*
|
|
@@ -33,9 +34,20 @@ export interface SchemaArrayControl {
|
|
|
33
34
|
*/
|
|
34
35
|
export interface FormSchemaNode {
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* Привязка узла к части модели — ручка из дерева `model.$`. Чем узел является, решают ручка и
|
|
38
|
+
* соседние ключи:
|
|
39
|
+
* - лист или массив (`model.$.email`, `model.$.tags`) — **поле**;
|
|
40
|
+
* - массив вместе с {@link FormSchemaNode.item} — **массив под-форм**;
|
|
41
|
+
* - группа вместе с {@link FormSchemaNode.part} — **подформа**.
|
|
42
|
+
*
|
|
43
|
+
* Узел распознаётся по значению, а не по имени ключа: в записи «имя поля → узел» поле данных
|
|
44
|
+
* может называться `model`, и тогда под этим ключом лежит обычный вложенный узел.
|
|
45
|
+
*/
|
|
46
|
+
model?: unknown;
|
|
47
|
+
/**
|
|
48
|
+
* Прежняя запись привязки поля — то же, что {@link FormSchemaNode.model}.
|
|
49
|
+
*
|
|
50
|
+
* @deprecated Пишите `model: model.$.<path>`.
|
|
39
51
|
*/
|
|
40
52
|
value?: unknown;
|
|
41
53
|
/**
|
|
@@ -65,14 +77,27 @@ export interface FormSchemaNode {
|
|
|
65
77
|
* выводит на своём месте в порядке следования.
|
|
66
78
|
*/
|
|
67
79
|
children?: readonly (FormSchemaNode | string | number | Signal<any>)[];
|
|
68
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Прежняя запись привязки массива под-форм — фасад `model.<path>` (вместе с `item`).
|
|
82
|
+
*
|
|
83
|
+
* @deprecated Пишите `model: model.$.<path>`.
|
|
84
|
+
*/
|
|
69
85
|
array?: SchemaArrayControl;
|
|
70
86
|
/** Схема элемента массива: под-модель элемента → узел поддерева. */
|
|
71
|
-
item?: (
|
|
87
|
+
item?: (model: any) => FormSchemaNode;
|
|
88
|
+
/**
|
|
89
|
+
* Подформа: под-модель группы → узел поддерева. Часть объявляется один раз и подключается к
|
|
90
|
+
* любой группе той же формы данных: `{ model: model.$.registrationAddress, part: address }`.
|
|
91
|
+
* Привязки внутри части идут через `$` полученной под-модели.
|
|
92
|
+
*/
|
|
93
|
+
part?: (model: any) => FormSchemaNode;
|
|
72
94
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
95
|
+
* Шаблон нового элемента массива для кнопки «Добавить»: либо готовое значение, либо фабрика
|
|
96
|
+
* `() => value`. Запасной путь — для форм, чья модель создаётся из данных без кода: шаблон,
|
|
97
|
+
* объявленный в модели (`arrayOf(blank)`), главнее.
|
|
98
|
+
*
|
|
99
|
+
* Тип не различает варианты (union `unknown | (() => unknown)` схлопывается в `unknown`) —
|
|
100
|
+
* рантайм различает по `typeof initialValue === 'function'`.
|
|
76
101
|
*/
|
|
77
102
|
initialValue?: unknown;
|
|
78
103
|
/** Свободная вложенность: record-of-fields и произвольные под-узлы. */
|
|
@@ -17,8 +17,20 @@ export type Validator<TForm, TField> = (value: TField, scope: unknown, root: For
|
|
|
17
17
|
*/
|
|
18
18
|
export interface ValidateOptions {
|
|
19
19
|
/** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
|
|
20
|
-
*
|
|
20
|
+
* берётся из словаря локали по `code` (`validation.<code>`; см. `resolveValidationError` в
|
|
21
|
+
* `@reformer/core/i18n`). Явное сообщение важнее словаря, но не переключается вместе с языком —
|
|
22
|
+
* для локализуемого текста используйте {@link ValidateOptions.messageKey}. */
|
|
21
23
|
message?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Ключ сообщения в словаре приложения — локализуемый текст автора правила. Если ключ есть в
|
|
26
|
+
* активной локали, он важнее и `message`, и текста по `code`; если нет — в ход идёт `message`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* required({ messageKey: 'profile.name.required', message: 'Name is required' });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
messageKey?: string;
|
|
22
34
|
/** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
|
|
23
35
|
params?: Record<string, FormValue>;
|
|
24
36
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { FormModel, PathAwareSignal, ValidationError } from '../../index';
|
|
2
2
|
/** Сток одного прогона: модель scope, накопленные ошибки, незавершённые async-правила, гейты. */
|
|
3
3
|
export interface VContext {
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Модель текущей области — источник снапшота для `cross` и модель для `apply`. В корне прогона
|
|
6
|
+
* это вся модель; внутри `apply(ручка, схема)` / `applyEach` — под-модель группы или строки.
|
|
7
|
+
*/
|
|
5
8
|
model: FormModel<unknown>;
|
|
9
|
+
/** Корень прогона — модель, с которой вызван раннер. Правило получает его третьим аргументом. */
|
|
10
|
+
root: FormModel<unknown>;
|
|
6
11
|
/** Накопленные ошибки по сигналу (ключ — идентичность `PathAwareSignal`). */
|
|
7
12
|
errors: Map<PathAwareSignal<unknown>, ValidationError[]>;
|
|
8
13
|
/** Незавершённые async-правила (раннер их дожидается). */
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
* @module form/validation
|
|
19
19
|
*/
|
|
20
20
|
export type { Rule, AsyncRule, ValidationSchema } from './types';
|
|
21
|
-
export { validate, validateAsync, validateWhen, cross, each, apply } from './operators';
|
|
21
|
+
export { validate, validateAsync, validateWhen, cross, each, apply, applyEach } from './operators';
|
|
22
22
|
export { defineValidationSchema, validateModel } from './run';
|
|
23
23
|
export * from './strategy';
|
|
@@ -3,9 +3,13 @@ import { AsyncRule, Rule, ValidationSchema } from './types';
|
|
|
3
3
|
/**
|
|
4
4
|
* Синхронные правила поля.
|
|
5
5
|
*
|
|
6
|
+
* Поле — лист либо массив целиком: `model.$.<массив>` — такая же ручка значения, и правило
|
|
7
|
+
* получает массив (мультивыбор, теги, список файлов).
|
|
8
|
+
*
|
|
6
9
|
* @example
|
|
7
10
|
* ```ts
|
|
8
11
|
* validate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);
|
|
12
|
+
* validate(model.$.tags, [required(), maxLength(3)]); // tags: string[] — массив как значение
|
|
9
13
|
* ```
|
|
10
14
|
*/
|
|
11
15
|
export declare function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void;
|
|
@@ -28,15 +32,78 @@ export declare function validateAsync<TField>(sig: PathAwareSignal<TField>, rule
|
|
|
28
32
|
*/
|
|
29
33
|
export declare function validateWhen(cond: () => boolean, cb: () => void): void;
|
|
30
34
|
/**
|
|
31
|
-
* Cross-field правило: `fn` получает СНАПШОТ модели
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* Cross-field правило: `fn` получает СНАПШОТ модели текущей области (`model.get()`) и вешает
|
|
36
|
+
* ошибку на `sig`.
|
|
37
|
+
*
|
|
38
|
+
* Область — та модель, с которой запущена схема: в корне прогона это вся форма, а в схеме,
|
|
39
|
+
* подключённой через {@link apply} / {@link applyEach}, — под-модель группы или строки массива.
|
|
40
|
+
* Поэтому правила подформы пишутся один раз и не знают, где она стоит.
|
|
34
41
|
*/
|
|
35
42
|
export declare function cross<TSnapshot>(sig: PathAwareSignal<unknown>, fn: (form: TSnapshot) => ValidationError | null): void;
|
|
43
|
+
/** Привязка к группе: ручка `model.$.<группа>` (тип значения — из её `peek()`). */
|
|
44
|
+
interface GroupBinding<V> {
|
|
45
|
+
peek(): V;
|
|
46
|
+
}
|
|
47
|
+
/** Привязка к массиву под-форм: ручка `model.$.<массив>`. */
|
|
48
|
+
interface ArrayBinding<U> {
|
|
49
|
+
peek(): readonly U[] | null | undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Применить схему к КАЖДОМУ элементу массива под-форм. Схема элемента — обычная
|
|
53
|
+
* `ValidationSchema` над типом элемента: тот же набор правил подключается и к группе через
|
|
54
|
+
* {@link apply}.
|
|
55
|
+
*
|
|
56
|
+
* У каждого элемента своя область: `cross` внутри схемы получает снапшот элемента.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const propertyRules = defineValidationSchema<Property>(({ model }) => {
|
|
61
|
+
* validate(model.$.type, [required()]);
|
|
62
|
+
* cross<Property>(model.$.estimatedValue, (property) =>
|
|
63
|
+
* property.hasEncumbrance && property.estimatedValue < 100_000 ? tooCheap : null
|
|
64
|
+
* );
|
|
65
|
+
* });
|
|
66
|
+
*
|
|
67
|
+
* applyEach(model.$.properties, propertyRules);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyEach<U extends object>(array: ArrayBinding<U> | ModelArray<U>, schema: ValidationSchema<U>): void;
|
|
71
|
+
/**
|
|
72
|
+
* Применить под-правила к КАЖДОМУ элементу массива — прежняя запись {@link applyEach}.
|
|
73
|
+
*
|
|
74
|
+
* Колбэк получает под-модель элемента, но области не создаёт: `cross` внутри него видит снапшот
|
|
75
|
+
* корня прогона.
|
|
76
|
+
*
|
|
77
|
+
* @deprecated Пишите `applyEach(model.$.<массив>, schema)`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function each<U extends object>(arr: ArrayBinding<U> | ModelArray<U>, itemFn: (item: FormModel<U>) => void): void;
|
|
36
80
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
81
|
+
* Подключить схему подформы к группе модели — одной или нескольким.
|
|
82
|
+
*
|
|
83
|
+
* Схема запускается в области группы: `model` в ней — под-модель, `cross` получает снапшот
|
|
84
|
+
* группы. Привязка — ручка `model.$.<группа>`, как у узла-подформы схемы и у `apply` поведения.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* const addressRules = defineValidationSchema<Address>(({ model }) => {
|
|
89
|
+
* validate(model.$.city, [required()]);
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* apply(model.$.registrationAddress, addressRules);
|
|
93
|
+
* validateWhen(
|
|
94
|
+
* () => !model.sameAsRegistration,
|
|
95
|
+
* () => apply(model.$.residenceAddress, addressRules)
|
|
96
|
+
* );
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare function apply<V extends object | null | undefined>(group: GroupBinding<V> | GroupBinding<V>[], schema: ValidationSchema<NonNullable<V>>): void;
|
|
100
|
+
/**
|
|
101
|
+
* Композиция схем над ТОЙ ЖЕ моделью области — так форма собирается из схем шагов.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const formRules = defineValidationSchema<Form>(() => apply(loanRules, contactsRules));
|
|
106
|
+
* ```
|
|
39
107
|
*/
|
|
40
|
-
export declare function each<U extends object>(arr: ModelArray<U>, itemFn: (item: FormModel<U>) => void): void;
|
|
41
|
-
/** Композиция под-схем в текущую (над той же моделью scope). Заменяет пошаговую группировку. */
|
|
42
108
|
export declare function apply<T>(...schemas: ValidationSchema<T>[]): void;
|
|
109
|
+
export {};
|
|
@@ -12,7 +12,8 @@ export type CallableRule = (value: unknown, model: unknown, root: unknown) => Va
|
|
|
12
12
|
* (`required()`/`min()`/… — они `(value, model, root) => …`) и inline-правила `(value) => …`
|
|
13
13
|
* ОБА присваиваются в `Rule<TField>[]` без `any`, при этом сохраняется проверка типа поля
|
|
14
14
|
* (`validate(model.$.age, [email()])` подсветится — `email` ждёт `string`, поле `number`).
|
|
15
|
-
* На вызове раннер приводит правило к callable и передаёт `(value,
|
|
15
|
+
* На вызове раннер приводит правило к callable и передаёт `(value, scope, root)`: модель текущей
|
|
16
|
+
* области (в корне прогона — вся модель) и корень прогона.
|
|
16
17
|
*/
|
|
17
18
|
export type Rule<TField> = (value: TField, scope: never, root: never) => ValidationError | null;
|
|
18
19
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FormValue, ValidationError } from '../types/contracts';
|
|
2
|
+
import { ValidateOptions } from '../types/validation-schema';
|
|
3
|
+
/**
|
|
4
|
+
* @param code - Код ошибки; по нему ищется текст в словаре локали (`validation.<code>`).
|
|
5
|
+
* @param options - Опции правила: явное сообщение, ключ сообщения, дополнительные параметры.
|
|
6
|
+
* @param params - Параметры, которые правило заполняет само; параметры из `options` важнее.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validationError(code: string, options: ValidateOptions | undefined, params?: Record<string, FormValue>): ValidationError;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Messages } from './locale';
|
|
2
|
+
/**
|
|
3
|
+
* Бренд описателя. `Symbol.for` — чтобы описатель узнавался и через границу двух копий модуля
|
|
4
|
+
* (разные чанки, разные версии пакета на странице).
|
|
5
|
+
*/
|
|
6
|
+
export declare const MESSAGE_DESCRIPTOR: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Описатель сообщения. Намеренно без ключей `value`, `array`, `item`, `component`: обход схемы в
|
|
9
|
+
* `createForm` узнаёт по ним узел, а описатель узлом не является.
|
|
10
|
+
*/
|
|
11
|
+
export interface MessageDescriptor {
|
|
12
|
+
readonly [MESSAGE_DESCRIPTOR]: true;
|
|
13
|
+
/** Ключ сообщения в словаре приложения. */
|
|
14
|
+
readonly key: string;
|
|
15
|
+
/** Значения подстановок. */
|
|
16
|
+
readonly values?: Readonly<Record<string, unknown>>;
|
|
17
|
+
/** Английский текст на случай, если ключа в локали нет. */
|
|
18
|
+
readonly defaultMessage?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Текст, который можно задать строкой или описателем. */
|
|
21
|
+
export type LocalizableText = string | MessageDescriptor;
|
|
22
|
+
/**
|
|
23
|
+
* Создаёт описатель сообщения.
|
|
24
|
+
*
|
|
25
|
+
* @param key - Ключ в словаре приложения.
|
|
26
|
+
* @param values - Значения подстановок.
|
|
27
|
+
* @param defaultMessage - Английский текст на случай, если ключа в локали нет.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* { value: model.$.email, component: Input, componentProps: { label: msg('profile.email.label') } }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function msg(key: string, values?: Readonly<Record<string, unknown>>, defaultMessage?: string): MessageDescriptor;
|
|
35
|
+
/** Является ли значение описателем сообщения. */
|
|
36
|
+
export declare function isMessageDescriptor(value: unknown): value is MessageDescriptor;
|
|
37
|
+
/**
|
|
38
|
+
* `msg` с типизированными ключами: принимает английский словарь приложения, проверяет ключ при
|
|
39
|
+
* компиляции и подставляет английский текст как `defaultMessage` — без провайдера и при промахе
|
|
40
|
+
* по локали форма покажет его, а не ключ.
|
|
41
|
+
*
|
|
42
|
+
* @param defaults - Английский словарь приложения.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import appEn from './locales/en.json';
|
|
47
|
+
*
|
|
48
|
+
* export const { msg } = defineMessages(appEn);
|
|
49
|
+
* msg('profile.email.label'); // опечатка в ключе — ошибка компиляции
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function defineMessages<D extends Messages>(defaults: D): {
|
|
53
|
+
msg(key: keyof D & string, values?: Readonly<Record<string, unknown>>): MessageDescriptor;
|
|
54
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
"format.fileSize.b": "{value} B",
|
|
3
|
+
"format.fileSize.kb": "{value} KB",
|
|
4
|
+
"format.fileSize.mb": "{value} MB",
|
|
5
|
+
"format.fileSize.gb": "{value} GB",
|
|
6
|
+
"format.fileSize.tb": "{value} TB",
|
|
7
|
+
|
|
8
|
+
"validation.required": "This field is required",
|
|
9
|
+
"validation.pattern": "Invalid format",
|
|
10
|
+
"validation.email": "Enter a valid email address",
|
|
11
|
+
"validation.url": "Enter a valid URL",
|
|
12
|
+
"validation.url_protocol": "The URL must use one of these protocols: {allowedProtocols}",
|
|
13
|
+
"validation.phone": "Enter a valid phone number",
|
|
14
|
+
|
|
15
|
+
"validation.min": "Must be at least {min}",
|
|
16
|
+
"validation.max": "Must be at most {max}",
|
|
17
|
+
"validation.minLength": "Enter at least {minLength, plural, one{# character} other{# characters}}",
|
|
18
|
+
"validation.maxLength": "Enter no more than {maxLength, plural, one{# character} other{# characters}}",
|
|
19
|
+
"validation.isNumber": "Enter a number",
|
|
20
|
+
"validation.integer": "Enter a whole number",
|
|
21
|
+
"validation.multipleOf": "Must be a multiple of {multipleOf}",
|
|
22
|
+
"validation.nonNegative": "Must not be negative",
|
|
23
|
+
"validation.nonZero": "Must not be zero",
|
|
24
|
+
|
|
25
|
+
"validation.date_invalid": "Enter a valid date",
|
|
26
|
+
"validation.date_min": "The date must not be earlier than {minDate}",
|
|
27
|
+
"validation.date_max": "The date must not be later than {maxDate}",
|
|
28
|
+
"validation.date_past": "The date must not be in the past",
|
|
29
|
+
"validation.date_future": "The date must not be in the future",
|
|
30
|
+
"validation.date_min_age": "Age must be at least {minAge}",
|
|
31
|
+
"validation.date_max_age": "Age must be at most {maxAge}",
|
|
32
|
+
|
|
33
|
+
"validation.fileType": "File {fileName} has a type that is not allowed. Allowed: {accept}",
|
|
34
|
+
"validation.maxFileSize": "File {fileName} is larger than {maxFileSize}",
|
|
35
|
+
"validation.minFileSize": "File {fileName} is smaller than {minFileSize}",
|
|
36
|
+
"validation.maxFiles": "No more than {maxFiles, plural, one{# file} other{# files}}",
|
|
37
|
+
"validation.minFiles": "At least {minFiles, plural, one{# file} other{# files}}",
|
|
38
|
+
"validation.maxTotalFileSize": "The total size of the files exceeds {maxTotalFileSize}",
|
|
39
|
+
"validation.fileExists": "File {fileName} has already been added",
|
|
40
|
+
"validation.uploadAborted": "Upload cancelled",
|
|
41
|
+
"validation.uploadFailed": "Failed to upload {fileName}"
|
|
42
|
+
}
|
|
43
|
+
;
|
|
44
|
+
|
|
45
|
+
export default _default;
|