@reformer/renderer-json 8.0.0 → 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/llms.txt ADDED
@@ -0,0 +1,2580 @@
1
+ # ReFormer Renderer Json - LLM Integration Guide
2
+ # AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.
3
+
4
+ > JSON-based form renderer for @reformer ecosystem
5
+ > Package: @reformer/renderer-json • Version: 6.0.0
6
+
7
+ ## Table of Contents
8
+ - 01-overview.md — Overview
9
+ - 02-json-schema.md — JSON Schema
10
+ - 03-registry.md — Component Registry
11
+ - 04-troubleshooting.md — Troubleshooting / FAQ
12
+ - 05-cookbook.md — Cookbook
13
+ - 06-validation.md — Validation
14
+ - 07-form-wizard.md — FormWizard
15
+ - 08-i18n.md — i18n / Локализация
16
+ - API Reference (auto-generated from JSDoc)
17
+
18
+ ## 1. Installation
19
+
20
+ ```bash
21
+ npm install @reformer/renderer-json @reformer/renderer-react @reformer/core
22
+ ```
23
+
24
+ Опционально для готовых UI-компонентов:
25
+
26
+ ```bash
27
+ npm install @reformer/ui-kit
28
+ ```
29
+
30
+ ## 2. Import Patterns
31
+
32
+ ```typescript
33
+ // recommended
34
+ import {
35
+ JsonFormRenderer,
36
+ JsonRendererProvider,
37
+ defineRegistry,
38
+ FIELD_WRAPPER,
39
+ convertJsonToM1Tree,
40
+ type JsonFormSchema,
41
+ } from '@reformer/renderer-json';
42
+ ```
43
+
44
+ ## 3. Quick Start
45
+
46
+ Ключевая идея M1: **модель (`FormModel`) — источник данных, JSON-схема — layout**. Форма строится из той же JSON-схемы через `convertJsonToM1Tree`, а `JsonFormRenderer` получает модель через `JsonRendererProvider` settings (`model`). `JsonFormRenderer` НЕ имеет `form`-пропа — это by-design: JSON статичен, модель runtime.
47
+
48
+ Минимальный рабочий монтаж:
49
+
50
+ ```tsx
51
+ import { useMemo } from 'react';
52
+ import { createForm, createModel } from '@reformer/core';
53
+ import { Input, Box, FormField } from '@reformer/ui-kit';
54
+ import {
55
+ JsonFormRenderer,
56
+ JsonRendererProvider,
57
+ defineRegistry,
58
+ FIELD_WRAPPER,
59
+ convertJsonToM1Tree,
60
+ type JsonFormSchema,
61
+ } from '@reformer/renderer-json';
62
+
63
+ type MyForm = { email: string };
64
+
65
+ // 1. JSON-схема — чистые данные, операторы-строки, никаких React-импортов.
66
+ const jsonSchema: JsonFormSchema = {
67
+ version: '1.0',
68
+ root: {
69
+ component: '$component(Box)',
70
+ children: [
71
+ { selector: 'email', value: '$model(email)', component: '$component(Input)',
72
+ componentProps: { label: 'Email' } },
73
+ ],
74
+ },
75
+ };
76
+
77
+ // 2. Реестр: имена из JSON → React-компоненты.
78
+ const registry = defineRegistry((reg) => {
79
+ reg.component('Input', Input);
80
+ reg.component('Box', Box);
81
+ reg.component(FIELD_WRAPPER, FormField);
82
+ });
83
+
84
+ function MyFormPage() {
85
+ // 3. Модель + форма строятся ИЗ JSON-схемы (единая схема, без отдельной схемы формы).
86
+ const { model } = useMemo(() => {
87
+ const model = createModel<MyForm>({ email: '' });
88
+ createForm<MyForm>({ model, schema: convertJsonToM1Tree(jsonSchema, registry, model) });
89
+ return { model };
90
+ }, []);
91
+
92
+ // 4. Модель прокидывается через провайдер; JsonFormRenderer биндит листья к её сигналам.
93
+ return (
94
+ <JsonRendererProvider settings={{ registry, model }}>
95
+ <JsonFormRenderer<MyForm> schema={jsonSchema} />
96
+ </JsonRendererProvider>
97
+ );
98
+ }
99
+ ```
100
+
101
+ **Почему `model` через провайдер, а не `<JsonFormRenderer form={...}/>`?** Под M1 листья схемы (`value: '$model(path)'`) биндятся к сигналам модели (`model.signalAt(path)`) конвертером. Модель обязательна и передаётся через `JsonRendererProvider` settings — `JsonFormRenderer` без неё бросит `settings.model is required (M1)`. Сам рендерер принимает только `{ schema, renderBehavior?, onSchemaReady?, validate? }`.
102
+
103
+ ## 4. Key Concepts
104
+
105
+ - **JSON-схема** — дерево `JsonNode` (см. [02-json-schema.md](02-json-schema.md)). Узлы: **field** (`value: '$model(...)'`), **array** (`array` + `item.$template`), **container** (`component` + `children`).
106
+ - **Операторы** — строки `$model(path)` / `$component(Name)` / `$dataSource(NAME)`. Только они резолвятся; голые строки идут как есть.
107
+ - **Модель (`model`)** — `FormModel`, источник данных. Передаётся в `JsonRendererProvider` settings; листья биндятся к её сигналам.
108
+ - **Реестр** — карта имени из `$component(...)`/`$dataSource(...)` на React-компонент или source-значение. Без регистрации схема не сконвертируется (ошибка `Component "X" not found in registry`).
109
+ - **`FIELD_WRAPPER`** — зарезервированный ключ реестра (`'$fieldWrapper'`) для компонента-обёртки полей (label, error, hint). Обычно `FormField` из `@reformer/ui-kit`.
110
+ - **`convertJsonToM1Tree`** — конвертер JSON → RenderNode-дерево для `createForm({ model, schema })`.
111
+ - **`renderBehavior`** — TS-функция `RenderBehaviorFn<T>` (hideWhen/patchProps/onInit), применяется поверх готовой схемы; в JSON поведение не выражается.
112
+
113
+ ## 5. Components and exports
114
+
115
+ | Export | Purpose |
116
+ | ----------------------------------------------- | -------------------------------------------------------------------------- |
117
+ | `JsonFormRenderer` | Главный компонент-рендерер. Пропы: `{ schema, renderBehavior?, onSchemaReady?, validate? }`. |
118
+ | `JsonRendererProvider` | Контекст-провайдер: реестр (`registry`), модель (`model`), настройки. |
119
+ | `useJsonRendererSettings` | Хук для чтения текущих настроек контекста. |
120
+ | `defineRegistry` | Builder реестра компонентов и dataSource-значений. |
121
+ | `FIELD_WRAPPER` | Ключ реестра (`'$fieldWrapper'`) для компонента-обёртки полей. |
122
+ | `JsonFormSchema`, `JsonNode` | Типы JSON-схемы (`JsonFieldNode`/`JsonArrayNode`/`JsonContainerNode`). |
123
+ | `isFieldNode`, `isArrayNode`, `isContainerNode` | Type guards для узлов. |
124
+ | `parseOperator`, `isModelOp`, `isComponentOp`, `isDataSourceOp` | Разбор и type-guards строк-операторов. |
125
+ | `ModelOp`, `ComponentOp`, `DataSourceOp` | Template-literal типы операторов. |
126
+ | `convertJsonToM1Tree` | JSON → сырое RenderNode-дерево (для `createForm({ model, schema })`). |
127
+ | `createRenderSchemaFromJsonM1` | JSON → `RenderSchemaFn` (низкоуровневый, для `FormRenderer`/`JsonFormRenderer`). |
128
+ | `SchemaErrorPanel` | Панель ошибок валидации схемы (рисуется при `validate` + невалидной схеме). |
129
+ | `formSchemaMetaSchema`, `buildFormSchemaMetaSchema`, `getComponentNames`, `getDataSourceNames` | Мета-схема form-DSL + утилиты (ajv-free). |
130
+
131
+ > `validateFormSchema` живёт в отдельной точке входа `@reformer/renderer-json/validate` (тянет ajv, не попадает в render-бандл). `JsonFormRenderer` грузит её динамически при `validate={true}`.
132
+
133
+ ## 6. See also
134
+
135
+ - [02-json-schema.md](02-json-schema.md) — формат `JsonFormSchema`/`JsonNode` и синтаксис операторов.
136
+ - [03-registry.md](03-registry.md) — как наполнять реестр.
137
+ - [04-troubleshooting.md](04-troubleshooting.md) — частые ошибки.
138
+ - [05-cookbook.md](05-cookbook.md) — массивы, dataSource-функции, миграция из TS RenderSchema.
139
+
140
+ ## 7. Key Concepts
141
+
142
+ - **`JsonFormSchema`** — корневой документ: `version` (для миграций), опциональный `$schema` (путь к мета-схеме для IDE), единственный корневой узел `root`.
143
+ - **`JsonNode`** — узел дерева. Дискриминированный union по строке-оператору, которую он несёт:
144
+ - **field-node** (`JsonFieldNode`) — лист: `value: '$model(path)'` + опциональный `component: '$component(Name)'` (дефолт — Input). Не имеет `children`. Несёт **только layout** — валидаторов в JSON нет, оператора `$validator(...)` не существует. Валидация значений — отдельная TS-схема над моделью, см. [06-validation.md](06-validation.md).
145
+ - **array-node** (`JsonArrayNode`) — массив: `array: '$model(path)'` + `item: { $template: <JsonNode> }` + опциональный `initialValue` (литерал нового элемента для кнопки «Добавить»).
146
+ - **container-node** (`JsonContainerNode`) — контейнер (Box/Section/Wizard/Step): `component: '$component(Name)'` + опциональные `children`.
147
+ - **Операторы** — единственный способ привязки (см. [`operators.ts`](../../src/operators.ts)):
148
+ - `'$model(path)'` — путь к полю/массиву модели (лист → `model.signalAt(path)`, массив → value-прокси массива).
149
+ - `'$component(Name)'` — имя компонента в реестре (`reg.component`).
150
+ - `'$dataSource(NAME)'` — имя registry-source (`reg.dataSource`): options, itemLabel, константы, loading-компоненты.
151
+ - `'$fn(name)'` — имя функции в реестре (`reg.fn`): форматтеры, компараторы, itemLabel, обработчики. Резолвится в саму функцию (передаётся в проп как есть). Отдельный от `$dataSource` вид — `validate` ловит перепутанные `$fn`/`$dataSource` и `reg.fn` бросает на не-функцию.
152
+ - `'$locale(key)'` — ключ строки для сервиса локализации (`reg.locale`). Резолвится в **строку на этапе конвертации** (`registry.getLocale().resolve(key)`); промах ключа / нет сервиса → сам ключ. Только `componentProps` (label/placeholder/title/aria-*), не структурные позиции. С параметрами — структурная форма `{ "$locale": "key", "params": { … } }` (объект, params — литералы). Реактивный/markdown-текст — компонент `$component(I18n)`, см. [08-i18n.md](08-i18n.md).
153
+ - **`selector`** — plain-строка, id узла для render-behavior (`schema.node('…')`, `hideWhen`, `patchProps`). **Не** путь модели.
154
+ - **`componentProps`** — что прокидывается в React-компонент. Значения могут содержать строки-операторы (`'$dataSource(NAME)'`, `'$model(...)'`, `'$component(...)'`, `'$fn(...)'`, `'$locale(...)'`) или вложенные `JsonNode` — конвертер резолвит их рекурсивно. Обычные значения (числа, инлайн-массивы options, `label`) идут как есть.
155
+
156
+ ## 8. Type Guards
157
+
158
+ Порядок важен: `isArrayNode` проверяй **первым** (array-node тоже несёт `$model`, но в поле `array`).
159
+
160
+ ```typescript
161
+ import { isArrayNode, isFieldNode, isContainerNode, type JsonNode } from '@reformer/renderer-json';
162
+
163
+ function inspect(node: JsonNode) {
164
+ if (isArrayNode(node)) {
165
+ // node.array: '$model(...)', node.item.$template: JsonNode
166
+ } else if (isFieldNode(node)) {
167
+ // node.value: '$model(...)', node.component?: '$component(...)'
168
+ } else if (isContainerNode(node)) {
169
+ // node.component: '$component(...)', node.children?: JsonNode[]
170
+ }
171
+ }
172
+ ```
173
+
174
+ ## 9. Examples
175
+
176
+ Минимальная схема с одним полем (лист `value` + контейнер `Box`):
177
+
178
+ ```typescript
179
+ import type { JsonFormSchema } from '@reformer/renderer-json';
180
+
181
+ const schema: JsonFormSchema = {
182
+ version: '1.0',
183
+ root: {
184
+ component: '$component(Box)',
185
+ children: [
186
+ {
187
+ selector: 'email',
188
+ value: '$model(email)',
189
+ component: '$component(Input)',
190
+ componentProps: { label: 'Email' },
191
+ },
192
+ ],
193
+ },
194
+ };
195
+ ```
196
+
197
+ Вложенный путь к полю и ссылка на dataSource-константу в `componentProps`:
198
+
199
+ ```typescript
200
+ {
201
+ value: '$model(personalData.firstName)',
202
+ component: '$component(Input)',
203
+ }
204
+
205
+ {
206
+ value: '$model(loanType)',
207
+ component: '$component(Select)',
208
+ componentProps: { label: 'Тип кредита', options: '$dataSource(LOAN_TYPES)' },
209
+ }
210
+ ```
211
+
212
+ Функция из реестра (`$fn`) и локализованный текст (`$locale`) в `componentProps`:
213
+
214
+ ```typescript
215
+ {
216
+ value: '$model(email)',
217
+ component: '$component(Input)',
218
+ componentProps: {
219
+ label: '$locale(fields.email.label)', // → строка из сервиса локализации
220
+ placeholder: '$locale(fields.email.placeholder)',
221
+ format: '$fn(formatEmail)', // → сама функция из reg.fn, передаётся в проп
222
+ },
223
+ }
224
+ ```
225
+
226
+ Массив с шаблоном элемента (`array` + `item.$template` + `initialValue`). Внутри `$template` пути `$model(...)` резолвятся **относительно элемента** массива. `itemLabel` — функция `(control, index) => string`, поэтому идиоматично через `$fn`:
227
+
228
+ ```typescript
229
+ {
230
+ selector: 'properties-array',
231
+ array: '$model(properties)',
232
+ initialValue: { type: 'apartment', description: '', estimatedValue: 0, hasEncumbrance: false },
233
+ componentProps: {
234
+ title: '$locale(properties.title)',
235
+ addButtonLabel: '$locale(properties.add)',
236
+ itemLabel: '$fn(propertyItemLabel)',
237
+ },
238
+ item: {
239
+ $template: {
240
+ component: '$component(Box)',
241
+ componentProps: { className: 'space-y-3' },
242
+ children: [
243
+ { value: '$model(type)', component: '$component(Select)',
244
+ componentProps: { label: 'Тип', options: '$dataSource(PROPERTY_TYPES)' } },
245
+ { value: '$model(estimatedValue)', component: '$component(Input)',
246
+ componentProps: { label: 'Стоимость', type: 'number' } },
247
+ ],
248
+ },
249
+ },
250
+ }
251
+ ```
252
+
253
+ ## 10. Anti-patterns
254
+
255
+ - **Голые строки вместо операторов** — `component: 'Input'` или `value: 'email'` не резолвятся. Нужны операторы: `component: '$component(Input)'`, `value: '$model(email)'`. Template-literal типы (`ModelOp`/`ComponentOp`) отловят это на этапе компиляции.
256
+ - **Использовать `selector` как путь к полю** — `selector` это id для behavior; путь задаётся только через `value`/`array` оператором `$model(...)`.
257
+ - **Забыть `item.$template` у массива** — array-node требует `array` **и** `item: { $template }`. Без `$template` `isArrayNode` вернёт false и узел не отрендерится как массив.
258
+ - **`initialValue` как FieldConfig** — это plain-литерал по форме элемента (`{ field: value }`), а не `{ value, component }`. Клонируется через `JSON.parse(JSON.stringify(...))`, поэтому только сериализуемые значения.
259
+ - **Ссылаться на `$dataSource(NAME)` без регистрации** — при `validate` неизвестное имя даст ошибку; без валидации строка просто прокинется как есть (молчаливый баг).
260
+ - **Класть `validators` в field-node** — `JsonFieldNode` несёт только layout, поля `validators` в нём нет и оператора `$validator(...)` не существует. Валидация значений живёт в отдельной TS-схеме над моделью (`validateFormModel`), а не в JSON — см. [06-validation.md](06-validation.md).
261
+ - **Путать `$fn` и `$dataSource`** — функции регистрируй через `reg.fn` и ссылайся через `$fn(name)`, данные — через `reg.dataSource`/`$dataSource(NAME)`. Перекрёстное использование (`$fn(LOAN_TYPES)`, `$dataSource(comparator)`) `validateFormSchema` отклонит, а рантайм бросит. `reg.fn` дополнительно бросает при регистрации не-функции.
262
+ - **Аргументы у `$fn`** — оператор передаёт функцию **по ссылке**; биндинга аргументов (`$fn(goToStep, 2)`) нет. Нужен предзаданный аргумент — зарегистрируй уже связанную функцию через `reg.fn`.
263
+ - **`$locale` для reactive-переключения языка** — ключ резолвится в строку **на этапе конвертации** (иначе signal уронил бы строковые компоненты). Смена языка = новый сервис в `reg.locale` + пересборка дерева, а не «живое» обновление. Для live-переключения и markdown/rich — компонент `$component(I18n)` + `LocaleProvider`, см. [08-i18n.md](08-i18n.md).
264
+ - **Динамический/составной ключ `$locale`** — ключ это статичный литерал (`$locale(fields.email.label)`); вложить в него `$model(...)`/выражение нельзя. При наличии каталога опечатка ключа ловится на `validate`; без каталога промах молча деградирует до самого ключа.
265
+
266
+ ## 11. See also
267
+
268
+ - [01-overview.md](01-overview.md) — как схема монтируется через `model` + `JsonRendererProvider`.
269
+ - [03-registry.md](03-registry.md) — какие компоненты и source можно зарегистрировать.
270
+ - [05-cookbook.md](05-cookbook.md) — `$template`, dataSource-функции, миграция из TS RenderSchema.
271
+ - [06-validation.md](06-validation.md) — валидация значений (TS-схема над моделью + инъекция в wizard).
272
+ - [Типы JsonFormSchema/JsonNode](../../src/types/json-schema.ts) и [операторы](../../src/operators.ts).
273
+
274
+ ## 12. Key Concepts
275
+
276
+ - **component** — любой React-компонент, зарегистрированный под именем и доступный в схеме как `component: '$component(name)'`. Один метод `reg.component(name, Component)` регистрирует и компоненты-листья (Input/Select — узел несёт `value: '$model(path)'`), и контейнеры-обёртки (Box/Section/FormField — узел несёт `children`). Роль узла (лист vs контейнер) определяется **структурой узла в схеме** (`value` vs `children`), а не тем, как компонент зарегистрирован.
277
+ - **dataSource value** — именованная константа, функция или React-компонент, на которые ссылаются строкой `'$dataSource(NAME)'` из `componentProps`. Регистрируется через `reg.dataSource(name, value)`.
278
+ - **fn** — функция (форматтер/компаратор/itemLabel/обработчик), на которую ссылаются строкой `'$fn(name)'` из `componentProps`. Регистрируется через `reg.fn(name, fn)`. Отдельный от `dataSource` вид: `reg.fn` бросает при регистрации не-функции, а `validateFormSchema` ловит перепутанные `$fn`/`$dataSource`. Рантайм передаёт функцию в проп по ссылке (как `$dataSource`-функцию), новизна — в статической проверке.
279
+ - **locale service** — сервис локализации для `$locale(key)` (строковый путь) и компонента `I18n` (реактивный путь). Регистрируется через `reg.locale(serviceOrResolver)` (ключ `LOCALE_SERVICE`) и/или подаётся в `LocaleProvider`. Интерфейс `LocaleService`: `resolve(key, params?) => string` (+ опциональный `render(key, params?) => ReactNode` для markdown/JSX, + `keys` для validate-проверки). Фабрики без зависимостей: `createLocaleResolver(catalog)` (плоский каталог), `createLocaleService(table)` (`ключ → (params) => строка` — параметры/склонение без ICU). ICU (`intl-messageformat`) и markdown подключает потребитель своей реализацией сервиса. Полный гид — [08-i18n.md](08-i18n.md).
280
+ - **`FIELD_WRAPPER`** — зарезервированное имя (`'$fieldWrapper'`) для контейнера-обёртки полей (label, error, hint). Обычно регистрируется как `FormField` из `@reformer/ui-kit`.
281
+
282
+ ## 13. Builder API
283
+
284
+ | Method | Purpose |
285
+ | -------------------------------- | ----------------------------------------------------- |
286
+ | `reg.component(name, Component)` | Регистрирует компонент (лист или контейнер — роль решает структура узла). |
287
+ | `reg.dataSource(name, value)` | Регистрирует dataSource-значение (константу или функцию). |
288
+ | `reg.fn(name, fn)` | Регистрирует функцию для `$fn(name)` (бросает на не-функцию). |
289
+ | `reg.locale(serviceOrResolver)` | Регистрирует единственный сервис локализации для `$locale(key)`. |
290
+
291
+ ## 14. Examples
292
+
293
+ Минимальный реестр:
294
+
295
+ ```typescript
296
+ import { defineRegistry, FIELD_WRAPPER } from '@reformer/renderer-json';
297
+ import { Input, Select, Box, FormField } from '@reformer/ui-kit';
298
+
299
+ const registry = defineRegistry((reg) => {
300
+ reg.component('Input', Input);
301
+ reg.component('Select', Select);
302
+ reg.component('Box', Box);
303
+ reg.component(FIELD_WRAPPER, FormField);
304
+ });
305
+ ```
306
+
307
+ dataSource values для `componentProps` (в схеме — ссылка `'$dataSource(NAME)'`):
308
+
309
+ ```typescript
310
+ const registry = defineRegistry((reg) => {
311
+ reg.component('Select', Select);
312
+ reg.dataSource('LOAN_TYPES', [
313
+ { value: 'consumer', label: 'Потребительский' },
314
+ { value: 'mortgage', label: 'Ипотека' },
315
+ ]);
316
+ });
317
+
318
+ // В JSON-схеме (лист + операторы):
319
+ {
320
+ value: '$model(loanType)',
321
+ component: '$component(Select)',
322
+ componentProps: { options: '$dataSource(LOAN_TYPES)' },
323
+ }
324
+ ```
325
+
326
+ Функции (`$fn`) и сервис локализации (`$locale`):
327
+
328
+ ```typescript
329
+ import { defineRegistry, createLocaleResolver } from '@reformer/renderer-json';
330
+
331
+ const registry = defineRegistry((reg) => {
332
+ reg.component('Input', Input);
333
+ // функции — форматтеры, компараторы, itemLabel
334
+ reg.fn('propertyItemLabel', (_control, index) => `Имущество #${index + 1}`);
335
+ reg.fn('formatCurrency', (v: number) => `${v} ₽`);
336
+ // единственный сервис локализации; каталог включает validate-time проверку ключей
337
+ reg.locale(
338
+ createLocaleResolver({
339
+ 'fields.email.label': 'Email',
340
+ 'fields.email.placeholder': 'you@example.com',
341
+ })
342
+ );
343
+ });
344
+
345
+ // В JSON-схеме:
346
+ {
347
+ value: '$model(email)',
348
+ component: '$component(Input)',
349
+ componentProps: {
350
+ label: '$locale(fields.email.label)', // → строка
351
+ format: '$fn(formatCurrency)', // → функция по ссылке
352
+ },
353
+ }
354
+ ```
355
+
356
+ Смена языка — пересобрать сервис на другом каталоге и передать новый ref в `reg.locale` (плюс пересборка дерева); `$locale` резолвится в строку при конвертации, «живого» переключения нет.
357
+
358
+ ## 15. Anti-patterns
359
+
360
+ - **Забыть зарегистрировать `FIELD_WRAPPER`** — поля будут рендериться без обёртки (нет label/error). В большинстве случаев это ошибка.
361
+ - **Регистрировать React-element вместо компонента** — `reg.component('Input', <Input />)` не сработает, нужно передавать сам тип компонента: `reg.component('Input', Input)`.
362
+ - **Ожидать наследования между вложенными `JsonRendererProvider`** — реестр сливается через `withParent`, дубли решаются по приоритету (внешний > внутренний).
363
+ - **Использовать `$dataSource(NAME)` без регистрации** — без `validate` строка просто прокинется в проп как есть (молчаливый баг); с `validate` даст ошибку `unknown dataSource "NAME"`.
364
+ - **Ссылаться на dataSource как на компонент** — `component: '$component(LoadingState)'`, где `LoadingState` зарегистрирован через `reg.dataSource`, бросит `Entry "..." is a 'dataSource' and cannot be used as $component(...)`. dataSource — только для значений в `componentProps`.
365
+ - **Регистрировать функцию как `dataSource` и ссылаться `$fn`** (или наоборот) — виды раздельны: `$fn(name)` резолвит только `reg.fn`-записи, `$dataSource(NAME)` — только `reg.dataSource`. Перекрёстная ссылка бросит `Entry "..." is a '...' and cannot be used as $fn(...)` и отклонится на `validate`.
366
+ - **Регистрировать несколько сервисов локализации** — сервис один (кладётся под `LOCALE_SERVICE`); повторный `reg.locale(...)` перезапишет предыдущий. Разные языки — это разные каталоги, передаваемые в `reg.locale` по одному за раз.
367
+
368
+ ## 16. See also
369
+
370
+ - [01-overview.md](01-overview.md) — как реестр прокидывается через `JsonRendererProvider`.
371
+ - [02-json-schema.md](02-json-schema.md) — где имена появляются в схеме (операторы `$component`/`$dataSource`/`$fn`/`$locale`).
372
+
373
+ ## 17. Component "X" not found in registry
374
+
375
+ Имя из оператора `$component(X)` (или `$dataSource(X)`) не зарегистрировано в реестре. Проверь:
376
+
377
+ - `defineRegistry` действительно содержит `reg.component('X', ...)` или `reg.dataSource('X', ...)`.
378
+ - В схеме используется оператор, а не голая строка: `component: '$component(X)'`, а не `component: 'X'`.
379
+ - `JsonFormRenderer` обёрнут в `JsonRendererProvider` с этим реестром.
380
+ - Если используются вложенные провайдеры — реестр внутреннего провайдера наследуется через `withParent`, но дубли разрешаются в пользу внешнего.
381
+
382
+ ## 18. Field renders without label/error
383
+
384
+ Не зарегистрирован контейнер с ключом `FIELD_WRAPPER`. Добавь:
385
+
386
+ ```typescript
387
+ import { FIELD_WRAPPER } from '@reformer/renderer-json';
388
+ import { FormField } from '@reformer/ui-kit';
389
+
390
+ reg.component(FIELD_WRAPPER, FormField);
391
+ ```
392
+
393
+ ## 19. No model signal for "..." / settings.model is required
394
+
395
+ Два разных симптома одной причины — модель.
396
+
397
+ - `JsonFormRenderer: settings.model is required (M1)` — не передана модель. Под M1 листья схемы биндятся к сигналам `FormModel`; передай её в `JsonRendererProvider settings={{ registry, model }}`.
398
+ - `[JsonRenderer/M1] No model signal for "path"` (warn) — путь в `$model(path)` не соответствует структуре модели. Проверь: `value: '$model(personalData.firstName)'` — поле `firstName` реально существует внутри `personalData` в `createModel(...)` initial-значениях.
399
+
400
+ ## 20. componentProps string passes through as plain string
401
+
402
+ Строка `'$dataSource(NAME)'` в `componentProps` ссылается на source, который не зарегистрирован — конвертер оставляет её как есть. Используй `reg.dataSource('NAME', value)` либо передавай значение литералом напрямую. Голые строки (без `$dataSource(...)`) намеренно не резолвятся — это обычные значения пропа (`label`, `placeholder`).
403
+
404
+ ## 21. useJsonRendererSettings throws outside provider
405
+
406
+ `useJsonRendererSettings` в dev-режиме бросает, если вызван вне `JsonRendererProvider` **или** если в провайдере не передан `registry`. Оберни вызывающий компонент в провайдер с реестром.
407
+
408
+ ## 22. "version" missing / invalid schema (при validate)
409
+
410
+ `validate={true}` прогоняет схему через мета-схему (ajv) + обход имён операторов. Типичные ошибки: узел не подходит ни под field/array/container (нет ни `value`, ни `array`+`item`, ни `component`), голая строка вместо оператора, неизвестное `$component(...)`/`$dataSource(...)` имя. Ошибки рисуются в `SchemaErrorPanel` вместо формы. `$model(...)`-пути мета-схема не проверяет (только синтаксис) — они динамичны.
411
+
412
+ ## 23. Behavior selector matches nothing
413
+
414
+ `hideWhen`/`patchProps` ищут узел по `selector`. Убедись, что у узла он явно задан (`selector: 'mortgage-section'`), и что значение совпадает с тем, на которое смотрит behavior. `selector` — plain-строка, НЕ оператор.
415
+
416
+ ## 24. $template inside array doesn't render rows
417
+
418
+ Массив — это array-node, а не container с `itemComponent`. Проверь:
419
+
420
+ - Узел использует `array: '$model(path)'` **и** `item: { $template: <JsonNode> }` (оба обязательны — иначе `isArrayNode` вернёт false).
421
+ - Внутри `$template` пути `$model(...)` заданы **относительно элемента** (`value: '$model(type)'`, а не `'$model(properties[0].type)'`).
422
+ - Есть `initialValue` (plain-литерал по форме элемента) — иначе кнопка «Добавить» создаст пустой элемент без сигналов для полей шаблона.
423
+
424
+ ## 25. Массив рендерится без строк / пустой при добавлении
425
+
426
+ `initialValue` должен быть **полным** plain-объектом по форме элемента (все поля, что есть в `$template`). Если передать частичный объект (`{ type: 'apartment' }` без `estimatedValue`/`description`), под-модель нового элемента не получит сигналов для недостающих полей и они не отрендерятся. `initialValue` клонируется через `JSON.parse(JSON.stringify(...))` — только сериализуемые значения, никакого FieldConfig.
427
+
428
+ ## 26. Toggle-видимость секции массива
429
+
430
+ Условный показ секции (напр. массив `properties` виден только когда `hasProperty === true`) делается **не** кастомным блоком, а через `renderBehavior`:
431
+
432
+ ```typescript
433
+ import { hideWhen } from '@reformer/renderer-react';
434
+
435
+ const renderBehavior: RenderBehaviorFn<MyForm> = (schema) => {
436
+ hideWhen(schema.node('properties-array'), () => model.signalAt('hasProperty').value !== true);
437
+ };
438
+ ```
439
+
440
+ `renderBehavior` передаётся пропом в `JsonFormRenderer`; узел адресуется по своему `selector`.
441
+
442
+ ## 27. See also
443
+
444
+ - [01-overview.md](01-overview.md)
445
+ - [02-json-schema.md](02-json-schema.md)
446
+ - [03-registry.md](03-registry.md)
447
+ - [05-cookbook.md](05-cookbook.md)
448
+
449
+ ## 28. Монтаж формы из JSON (M1) { #mounting }
450
+
451
+ **Problem.** JSON-схема статична, а данные и форма — runtime. Нужно связать их без React-glue на каждой странице.
452
+
453
+ **Solution.** Модель (`FormModel`) — источник данных, форма строится из **той же** JSON-схемы через `convertJsonToM1Tree`, а `JsonFormRenderer` получает модель через `JsonRendererProvider` settings. `JsonFormRenderer` не имеет `form`-пропа (by-design).
454
+
455
+ ```tsx
456
+ import { useMemo } from 'react';
457
+ import { createForm, createModel } from '@reformer/core';
458
+ import {
459
+ JsonFormRenderer,
460
+ JsonRendererProvider,
461
+ convertJsonToM1Tree,
462
+ type JsonFormSchema,
463
+ } from '@reformer/renderer-json';
464
+ import rawJsonSchema from './json-schema.json';
465
+ import { createRegistry } from './registry';
466
+
467
+ const jsonSchema = rawJsonSchema as unknown as JsonFormSchema; // «схема пришла строкой»
468
+
469
+ export function MyFormPage() {
470
+ const registry = useMemo(() => createRegistry(), []);
471
+ const { model } = useMemo(() => {
472
+ const model = createModel<MyForm>(initialValues);
473
+ // Форма строится из JSON: конвертер биндит листья к сигналам модели.
474
+ createForm<MyForm>({ model, schema: convertJsonToM1Tree(jsonSchema, registry, model) });
475
+ return { model };
476
+ }, [registry]);
477
+
478
+ return (
479
+ <JsonRendererProvider settings={{ registry, model }}>
480
+ <JsonFormRenderer<MyForm> schema={jsonSchema} validate={import.meta.env.DEV} />
481
+ </JsonRendererProvider>
482
+ );
483
+ }
484
+ ```
485
+
486
+ **Notes.**
487
+
488
+ - `convertJsonToM1Tree` бросает при битой схеме (неизвестный `$component`) **до** рендера. Оберни в try/catch, если хочешь показать `SchemaErrorPanel` вместо краша (см. `buildModelAndForm` в эталоне).
489
+ - `validate={import.meta.env.DEV}` — детекцию dev нельзя «запечь» в пакет; приложение передаёт значение из своего окружения.
490
+ - Поведение (compute/enableWhen/navigation) идёт в `createForm({ behavior })`; render-behavior (hideWhen/patchProps/onInit) — отдельным пропом `renderBehavior`.
491
+
492
+ ## 29. $template для массивов { #template-arrays }
493
+
494
+ **Problem.** В JSON нельзя выразить функцию `(itemPath) => RenderNode` для item-шаблона. Массив должен остаться декларативным.
495
+
496
+ **Solution.** Array-node несёт `array: '$model(path)'` + `item: { $template: <JsonNode> }` + `initialValue`. Внутри `$template` пути `$model(...)` резолвятся **относительно элемента** массива. Массивы под M1 рендерятся native-веткой конвертера (`{ array, item }`) — отдельный контейнер-компонент не нужен.
497
+
498
+ ```typescript
499
+ {
500
+ selector: 'properties-array',
501
+ array: '$model(properties)',
502
+ initialValue: { type: 'apartment', description: '', estimatedValue: 0, hasEncumbrance: false },
503
+ componentProps: {
504
+ title: 'Имущество',
505
+ addButtonLabel: '+ Добавить имущество',
506
+ itemLabel: '$dataSource(PROPERTY_ITEM_LABEL_SOURCE_FN)',
507
+ emptyMessage: 'Нажмите "Добавить имущество"',
508
+ },
509
+ item: {
510
+ $template: {
511
+ component: '$component(Box)',
512
+ componentProps: { className: 'space-y-3' },
513
+ children: [
514
+ { value: '$model(type)', component: '$component(Select)',
515
+ componentProps: { label: 'Тип', options: '$dataSource(PROPERTY_TYPES)' } },
516
+ { value: '$model(estimatedValue)', component: '$component(Input)',
517
+ componentProps: { label: 'Стоимость', type: 'number' } },
518
+ { value: '$model(description)', component: '$component(Textarea)',
519
+ componentProps: { label: 'Описание', rows: 2 } },
520
+ ],
521
+ },
522
+ },
523
+ }
524
+ ```
525
+
526
+ **Notes.**
527
+
528
+ - `array` **и** `item.$template` обязательны оба — иначе узел не считается array-node (`isArrayNode`).
529
+ - `initialValue` — полный plain-объект по форме элемента (все поля из `$template`). Клонируется через `JSON.parse(JSON.stringify(...))`; не FieldConfig. Частичный `initialValue` → у нового элемента нет сигналов для недостающих полей.
530
+ - Внутри `$template` пути относительны элементу (`'$model(type)'`, а не `'$model(properties[0].type)'`).
531
+ - Вложенный массив в массиве — новый array-node внутри `$template` со своим `array`/`item`.
532
+
533
+ ## 30. dataSource-значения и функции { #datasource }
534
+
535
+ **Problem.** Нужно передать в проп массив options, функцию (`itemLabel: (form, index) => string`) или React-компонент (`LoadingComponent`) — а JSON хранит только примитивы и объекты.
536
+
537
+ **Solution.** Регистрируешь значение через `reg.dataSource('NAME', value)`, в JSON-схеме ссылаешься оператором `'$dataSource(NAME)'`. Конвертер при обходе `componentProps` подставит зарегистрированное значение.
538
+
539
+ ```typescript
540
+ import { createElement } from 'react';
541
+ import { defineRegistry } from '@reformer/renderer-json';
542
+ import { LoadingState, ErrorState } from '@reformer/ui-kit';
543
+
544
+ const registry = defineRegistry((reg) => {
545
+ // 1. Константа: массив options.
546
+ reg.dataSource('LOAN_TYPES', [
547
+ { value: 'consumer', label: 'Потребительский' },
548
+ { value: 'mortgage', label: 'Ипотека' },
549
+ ]);
550
+
551
+ // 2. React-компонент как dataSource (для AsyncBoundary.LoadingComponent).
552
+ reg.dataSource('LoadingState', LoadingState);
553
+ reg.dataSource('ErrorStateDefault', () => createElement(ErrorState, { error: 'Ошибка' }));
554
+
555
+ // 3. Функция: itemLabel для array-секции.
556
+ reg.dataSource('PROPERTY_ITEM_LABEL_SOURCE_FN', (_form, index: number) => `Имущество #${index + 1}`);
557
+
558
+ // 4. Computed-константа.
559
+ reg.dataSource('CURRENT_YEAR_PLUS_ONE', new Date().getFullYear() + 1);
560
+ });
561
+ ```
562
+
563
+ ```typescript
564
+ // В JSON-схеме — ссылки операторами:
565
+ {
566
+ selector: 'data-boundary',
567
+ component: '$component(AsyncBoundary)',
568
+ componentProps: {
569
+ status: 'loading',
570
+ LoadingComponent: '$dataSource(LoadingState)', // → React-компонент
571
+ ErrorComponent: '$dataSource(ErrorStateDefault)',
572
+ },
573
+ children: [
574
+ { value: '$model(loanType)', component: '$component(Select)',
575
+ componentProps: { options: '$dataSource(LOAN_TYPES)' } }, // → массив
576
+ { value: '$model(carYear)', component: '$component(Input)',
577
+ componentProps: { type: 'number', max: '$dataSource(CURRENT_YEAR_PLUS_ONE)' } }, // → число
578
+ ],
579
+ }
580
+ ```
581
+
582
+ **Notes.**
583
+
584
+ - Резолв происходит только для строк `'$dataSource(NAME)'`. Голые строки (`label`, `placeholder`) и инлайн-массивы options идут как есть.
585
+ - Если имя не зарегистрировано: без `validate` строка `'$dataSource(NAME)'` останется строкой (молчаливый баг); с `validate` — ошибка `unknown dataSource "NAME"`.
586
+ - dataSource нельзя использовать как имя `component` (`component: '$component(LoadingState)'`, где `LoadingState` — dataSource, бросит `Entry "..." is a 'dataSource' and cannot be used as $component(...)`). dataSource — только для значений в `componentProps`.
587
+
588
+ ## 31. Инъекция runtime-сущностей в компонент (form, validation) { #inject-runtime }
589
+
590
+ **Problem.** Компоненту (напр. wizard) нужен `FormProxy` или validation-конфиг — рантайм-сущности, которые нельзя выразить в статичном JSON.
591
+
592
+ **Solution.** Инжектируй их через `renderBehavior` + `onInit`/`patchProps` до первого рендера. Узел адресуется по `selector`.
593
+
594
+ ```typescript
595
+ import { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';
596
+
597
+ function createMyRenderBehavior(
598
+ form: FormProxy<MyForm>,
599
+ model: FormModel<MyForm>
600
+ ): RenderBehaviorFn<MyForm> {
601
+ return (schema) => {
602
+ // JSON-схема не знает про FormProxy/валидацию — инъектим их в wizard до первого рендера.
603
+ onInit(schema.node('wizard'), () => {
604
+ schema.node('wizard').patchProps({ form, ...makeValidationConfig(model) });
605
+ });
606
+ // Остальное поведение (visibility/navigation) — из shared render-behavior.
607
+ createSharedRenderBehavior(form)(schema);
608
+ };
609
+ }
610
+
611
+ // <JsonFormRenderer schema={jsonSchema} renderBehavior={createMyRenderBehavior(form, model)} />
612
+ ```
613
+
614
+ **Notes.**
615
+
616
+ - `onInit(node, fn)` — build-time hook, вызывается один раз до первого рендера ноды.
617
+ - `patchProps` мержит переданные пропы в `componentProps` ноды.
618
+
619
+ ## 32. Вся форма read-only / view-mode { #readonly }
620
+
621
+ **Problem.** Нужно показать заполненную форму «только для просмотра» — все поля недоступны для ввода. Тянет искать флаг `settings.readonly` / `settings.mode`.
622
+
623
+ **Solution.** Такого флага **нет**. Настройки рендерера (`JsonRendererSettings`) — это `registry` + `model` поверх `RendererSettings`, а `RendererSettings` несёт только `fieldWrapper` (см. [json-renderer-context.tsx](../../src/context/json-renderer-context.tsx), renderer-react `RendererSettings`). Read-only задаётся **на уровне модели**: `form.disable()` каскадит `disabled` по всему поддереву — `GroupNode.onDisable()` рекурсивно зовёт `field.disable()` на всех детях, а рендерер пробрасывает per-field `disabled: state.disabled` в компонент. Один вызов на корне → вся форма read-only.
624
+
625
+ ```typescript
626
+ // Вариант A — сразу после сборки формы (самый прямой):
627
+ const model = createModel<MyForm>(initialValues);
628
+ const form = createForm<MyForm>({ model, schema: convertJsonToM1Tree(jsonSchema, registry, model) });
629
+ form.disable(); // каскад disabled по всему дереву → вся форма read-only
630
+ ```
631
+
632
+ ```typescript
633
+ // Вариант B — из render-behavior (например, включить view-mode по условию/после загрузки данных):
634
+ import { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';
635
+
636
+ function createReadonlyBehavior(form: FormProxy<MyForm>): RenderBehaviorFn<MyForm> {
637
+ return (schema) => {
638
+ onInit(schema.node('wizard'), () => {
639
+ form.disable(); // корневой FormProxy отдаёт disable() (делегирует в GroupNode)
640
+ });
641
+ };
642
+ }
643
+ ```
644
+
645
+ **Notes.**
646
+
647
+ - `form.disable()` — публичный метод узла (`FormNode.disable()`): ставит статус `disabled` и вызывает hook `onDisable`, который у группы каскадит на всех детей рекурсивно. Обратно — `form.enable()`.
648
+ - **Caveat:** поле с явным `componentProps.disabled` **перебивает** каскад. Рендерер собирает пропы как `{ value, disabled: state.disabled, ...componentProps }` — спред `componentProps` идёт ПОСЛЕ `disabled`, поэтому `componentProps.disabled: false` вернёт полю доступность даже при `form.disable()`. Не задавай `disabled` в JSON, если хочешь глобальный каскад.
649
+ - Отключённые узлы не валидируются и не попадают в `getValue()` — для чистого view-mode это обычно желаемо; если нужен submit disabled-значений, снимай `disable()` перед сбором.
650
+ - `settings.readonly` / `settings.mode` не существует — model-level каскад (`form.disable()`) это канонический механизм view-mode.
651
+
652
+ ## 33. Migration from TS RenderSchema { #migration }
653
+
654
+ **Problem.** Есть готовая `RenderSchemaFn<T>` (TS-вариант с `path.email`, React-компонентами по ссылке) — нужно перенести её в JSON-схему.
655
+
656
+ **Solution.** Покомпонентная карта замен. Ключевое: TS-ссылки (`path.email`, `Box`, `LOAN_TYPES`) → строки-операторы.
657
+
658
+ | TS RenderSchema (`@reformer/renderer-react`) | JSON-схема (`@reformer/renderer-json`, M1) |
659
+ | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
660
+ | `{ value: path.email, component: Input }` | `{ value: '$model(email)', component: '$component(Input)' }` |
661
+ | `{ value: path.personalData.firstName, component: Input }` | `{ value: '$model(personalData.firstName)', component: '$component(Input)' }` |
662
+ | `{ component: Box, componentProps: { className: 'grid' }, children: [...] }` | `{ component: '$component(Box)', componentProps: { className: 'grid' }, children: [...] }` |
663
+ | `{ component: Section, componentProps: { title: 'X' }, children: [...] }` | `{ component: '$component(Section)', componentProps: { title: 'X' }, children: [...] }` |
664
+ | `{ selector: 'mortgage-section', component: Section, ... }` | то же — `selector` сохраняется (plain-строка) |
665
+ | `componentProps: { options: LOAN_TYPES }` (импорт константы) | `componentProps: { options: '$dataSource(LOAN_TYPES)' }` + `reg.dataSource('LOAN_TYPES', LOAN_TYPES)` |
666
+ | `componentProps: { LoadingComponent: LoadingState }` | `componentProps: { LoadingComponent: '$dataSource(LoadingState)' }` + `reg.dataSource('LoadingState', ...)` |
667
+ | `{ array: path.properties, item: (ip) => ({...}), initialValue: () => ({...}) }` | `{ array: '$model(properties)', item: { $template: {...} }, initialValue: {...} }` |
668
+
669
+ ```typescript
670
+ // После: JSON
671
+ const schema: JsonFormSchema = {
672
+ version: '1.0',
673
+ root: {
674
+ component: '$component(Box)',
675
+ componentProps: { className: 'space-y-4' },
676
+ children: [
677
+ { value: '$model(email)', component: '$component(Input)', componentProps: { label: 'Email' } },
678
+ {
679
+ component: '$component(Section)',
680
+ componentProps: { title: 'Адрес' },
681
+ children: [{ value: '$model(address.city)', component: '$component(Input)' }],
682
+ },
683
+ ],
684
+ },
685
+ };
686
+
687
+ const registry = defineRegistry((reg) => {
688
+ reg.component('Input', Input);
689
+ reg.component('Box', Box);
690
+ reg.component('Section', Section);
691
+ reg.component(FIELD_WRAPPER, FormField);
692
+ });
693
+ ```
694
+
695
+ **Notes.**
696
+
697
+ - В JSON `children` всегда отдельное поле узла (не `componentProps.children`).
698
+ - field/array/container взаимоисключающи: `value` → лист, `array`+`item` → массив, `component`+`children` → контейнер. Дискриминация в конвертере: array → field → container.
699
+ - Поведение (`hideWhen`, `onInit`, lifecycle) **не** переезжает в JSON — остаётся TS-функцией `RenderBehaviorFn<T>` и передаётся пропом `renderBehavior`. В эталоне TS- и JSON-варианты переиспользуют один shared behavior.
700
+
701
+ ## 34. See also
702
+
703
+ - [01-overview.md](01-overview.md) — монтаж через `model` + `JsonRendererProvider`.
704
+ - [02-json-schema.md](02-json-schema.md) — справочник по узлам `JsonNode` и операторам.
705
+ - [03-registry.md](03-registry.md) — все методы `defineRegistry`.
706
+ - [04-troubleshooting.md](04-troubleshooting.md) — типичные ошибки.
707
+
708
+ ## 35. Mental model — почему валидаторов нет в JSON { #mental-model }
709
+
710
+ Одна ключевая мысль: **JSON-DSL несёт только layout, а не правила валидации.**
711
+
712
+ - `JsonFieldNode` — это `{ selector?, value, component?, componentProps?, wrapper? }` и **ничего больше**. Поля `validators` в нём нет (см. [json-schema.ts](../../src/types/json-schema.ts) — интерфейс `JsonFieldNode`).
713
+ - Операторы DSL — только `$model(...)`, `$component(...)`, `$dataSource(...)` (см. [operators.ts](../../src/operators.ts)). Оператора `$validator(...)` **не существует**.
714
+ - `JsonFormRenderer` с пропом `validate` (и функция `validateFormSchema`) проверяют **структуру** схемы через ajv: корректность узлов + синтаксис операторов + известность имён `$component`/`$dataSource`. Это НЕ валидация введённых пользователем значений (см. [validate.ts](../../src/validate.ts)).
715
+ - Значит, валидацию значений выражают **отдельной TS-схемой над МОДЕЛЬЮ** (`FormModel`), а не в JSON. Схема исполняется `validateFormModel(model, schema)` — тем же движком, что и в TS-варианте формы. Одна валидация на все варианты рендера.
716
+
717
+ Дальше — три шага: (1) построить схему над моделью, (2) обернуть её в `{ validateStep, validateAll }`, (3) инъектировать в wizard через render-behavior.
718
+
719
+ ## 36. Шаг 1 — построить схему валидации над моделью { #build-schema }
720
+
721
+ Схема валидации — дерево узлов движка M1: лист `{ value: signal, validators: [...] }`, контейнер `{ children: [...] }`. `value` — сигнал модели (`model.$.path`), НЕ строка-оператор `$model(...)` (операторы — только для layout-JSON; схема валидации это обычный TS-код). Фабрики правил импортируются из `@reformer/core/validators`.
722
+
723
+ ```typescript
724
+ import { type FormModel } from '@reformer/core';
725
+ import { required, min, max, minLength, email } from '@reformer/core/validators';
726
+
727
+ type M = FormModel<CreditForm>;
728
+
729
+ // Под-схема одного шага: дерево field-узлов { value, validators }.
730
+ const step1 = (model: M) => ({
731
+ children: [
732
+ { value: model.$.loanType, validators: [required({ message: 'Выберите тип кредита' })] },
733
+ {
734
+ value: model.$.loanAmount,
735
+ validators: [required(), min(50000, { message: 'Минимум 50 000 ₽' }), max(10000000)],
736
+ },
737
+ ],
738
+ });
739
+
740
+ const step2 = (model: M) => ({
741
+ children: [
742
+ { value: model.$.personalData.firstName, validators: [required(), minLength(2)] },
743
+ { value: model.$.email, validators: [required(), email()] },
744
+ ],
745
+ });
746
+
747
+ // Карта шагов + полная схема (объединение для submit).
748
+ const STEP_BUILDERS = [step1, step2];
749
+ ```
750
+
751
+ Условные группы (`{ when, children }`), cross-field правила, секции массивов — тот же движок, что в TS-форме. Полное описание узлов схемы и `validateFormModel` — в `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md) (не дублируем здесь).
752
+
753
+ ## 37. Шаг 2 — исполнить: `{ validateStep, validateAll }` { #execute }
754
+
755
+ `validateFormModel(model, schema)` прогоняет схему по текущим значениям модели и **сам роутит ошибки в ноды формы** (UI подсветит проблемные поля). Возвращает `{ valid, errors }`. Оборачиваем в две функции — контракт `FormWizardConfig` из `@reformer/cdk` (см. [form-wizard/types.ts](../../../reformer-cdk/src/components/form-wizard/types.ts): `validateStep?(step): boolean | Promise<boolean>`, `validateAll?(): boolean | Promise<boolean>`).
756
+
757
+ ```typescript
758
+ import { validateFormModel } from '@reformer/core';
759
+
760
+ export function makeValidationConfig(model: M) {
761
+ // Схема зависит только от ФОРМЫ модели, не от значений — строим дерево один раз.
762
+ const stepSchemas = STEP_BUILDERS.map((build) => build(model));
763
+ const fullSchema = { children: stepSchemas };
764
+
765
+ return {
766
+ validateStep: async (step: number): Promise<boolean> => {
767
+ const res = await validateFormModel(model, stepSchemas[step - 1] ?? { children: [] });
768
+ return res.valid; // ошибки уже проставлены в ноды текущего шага
769
+ },
770
+ validateAll: async (): Promise<boolean> => {
771
+ const res = await validateFormModel(model, fullSchema);
772
+ return res.valid;
773
+ },
774
+ };
775
+ }
776
+ ```
777
+
778
+ ## 38. Шаг 3 — инъекция в wizard через render-behavior { #inject }
779
+
780
+ JSON-схема статична и не знает про `FormProxy`/validation-конфиг — это рантайм-сущности. Инжектируем их в wizard-ноду через `renderBehavior` + `onInit` + `patchProps` до первого рендера (общий приём — см. [05-cookbook.md#inject-runtime](05-cookbook.md#inject-runtime)). Wizard-узел JSON-схемы должен нести `selector: 'wizard'`, чтобы адресоваться через `schema.node('wizard')`.
781
+
782
+ ```typescript
783
+ import { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';
784
+ import type { FormProxy, FormModel } from '@reformer/core';
785
+
786
+ export function createJsonRenderBehavior(
787
+ form: FormProxy<CreditForm>,
788
+ model: FormModel<CreditForm>
789
+ ): RenderBehaviorFn<CreditForm> {
790
+ return (schema) => {
791
+ // JSON не выражает FormProxy/валидацию — инъектим их в wizard до первого рендера.
792
+ onInit(schema.node('wizard'), () => {
793
+ schema.node('wizard').patchProps({ form, ...makeValidationConfig(model) });
794
+ });
795
+ // Остальное поведение (visibility/navigation) — из shared render-behavior.
796
+ createSharedRenderBehavior(form)(schema);
797
+ };
798
+ }
799
+
800
+ // <JsonFormRenderer schema={jsonSchema} renderBehavior={createJsonRenderBehavior(form, model)} />
801
+ ```
802
+
803
+ `patchProps({ form, validateStep, validateAll })` кладёт `form` + оба колбэка в `componentProps` wizard-ноды. Wizard-компонент читает их как `FormWizardConfig` и запускает `validateStep` на «Далее», `validateAll` на submit.
804
+
805
+ ## 39. Полный рабочий пример { #full-example }
806
+
807
+ Зеркалит golden-эталон (`complex-multy-step-form-renderer-json`): валидация — TS-схема над моделью, переиспользуемая всеми вариантами рендера, инъектируется в JSON-wizard. Submit и навигация между шагами приходят **не** отсюда, а из shared render-behavior — JSON-вариант лишь до-инъектит `form`/валидацию и делегирует остальное (см. [07-form-wizard.md](07-form-wizard.md)).
808
+
809
+ ```typescript
810
+ // validation.ts — TS-схема над МОДЕЛЬЮ (не JSON)
811
+ import { validateFormModel, type FormModel } from '@reformer/core';
812
+ import { required, min, minLength, email } from '@reformer/core/validators';
813
+ import type { CreditForm } from './types';
814
+
815
+ type M = FormModel<CreditForm>;
816
+
817
+ const step1 = (model: M) => ({
818
+ children: [
819
+ { value: model.$.loanType, validators: [required({ message: 'Выберите тип кредита' })] },
820
+ { value: model.$.loanAmount, validators: [required(), min(50000, { message: 'Минимум 50 000 ₽' })] },
821
+ ],
822
+ });
823
+ const step2 = (model: M) => ({
824
+ children: [
825
+ { value: model.$.personalData.firstName, validators: [required(), minLength(2)] },
826
+ { value: model.$.email, validators: [required(), email()] },
827
+ ],
828
+ });
829
+ const STEP_BUILDERS = [step1, step2];
830
+
831
+ /** Контракт FormWizardConfig: per-step + полная валидация через validateFormModel. */
832
+ export function makeValidationConfig(model: M) {
833
+ const stepSchemas = STEP_BUILDERS.map((build) => build(model));
834
+ const fullSchema = { children: stepSchemas };
835
+ return {
836
+ validateStep: async (step: number) =>
837
+ (await validateFormModel(model, stepSchemas[step - 1] ?? { children: [] })).valid,
838
+ validateAll: async () => (await validateFormModel(model, fullSchema)).valid,
839
+ };
840
+ }
841
+ ```
842
+
843
+ ```typescript
844
+ // render-behavior.ts — инъекция form + валидации в JSON-wizard
845
+ import { onInit, type RenderBehaviorFn } from '@reformer/renderer-react';
846
+ import type { FormProxy, FormModel } from '@reformer/core';
847
+ import type { CreditForm } from './types';
848
+ import { makeValidationConfig } from './validation';
849
+ // Единый behavior (submit/навигация/visibility) — общий для TS- и JSON-варианта. См. 07-form-wizard.md.
850
+ import { createSharedRenderBehavior } from './render-behavior.shared';
851
+
852
+ export function createJsonRenderBehavior(
853
+ form: FormProxy<CreditForm>,
854
+ model: FormModel<CreditForm>
855
+ ): RenderBehaviorFn<CreditForm> {
856
+ return (schema) => {
857
+ // (1) Инъекция рантайм-сущностей: form + validation-конфиг в wizard-ноду.
858
+ onInit(schema.node('wizard'), () => {
859
+ schema.node('wizard').patchProps({ form, ...makeValidationConfig(model) });
860
+ });
861
+ // (2) Submit + навигация между шагами + visibility — из shared render-behavior.
862
+ // БЕЗ этого вызова форма валидирует, но НЕ сабмитит и не навигирует (golden
863
+ // `complex-multy-step-form-renderer-json/render-behavior.ts` делегирует так же).
864
+ createSharedRenderBehavior(form)(schema);
865
+ };
866
+ }
867
+ ```
868
+
869
+ ## 40. Anti-patterns
870
+
871
+ - **Ждать, что `validate={true}` (или `validateFormSchema`) валидирует значения** — этот проп проверяет только СТРУКТУРУ схемы через ajv (узлы + синтаксис операторов + имена компонентов). Введённые пользователем значения он не трогает. Валидацию значений исполняет `validateFormModel` над моделью.
872
+ - **Пытаться добавить `validators` в JSON field-node** — `JsonFieldNode` несёт только layout (`value`/`component`/`componentProps`/`wrapper`). Поля `validators` в нём нет, оператора `$validator(...)` не существует. TypeScript отклонит лишнее поле; даже если протащить через `as`, конвертер его проигнорирует.
873
+ - **Строить схему валидации по значениям, а не по shape модели** — узел это `{ value: model.$.path, validators }`, где `value` — сигнал (стабильная ссылка на форму модели), а не текущее значение поля. Схему собирают один раз на `model`; значения читаются движком в момент прогона `validateFormModel`.
874
+ - **Забыть `selector: 'wizard'` у wizard-ноды** — без `selector` узел не адресуется через `schema.node('wizard')`, `onInit`/`patchProps` не найдут его и валидация не инъектируется (submit пройдёт без блокировки).
875
+
876
+ ## 41. See also
877
+
878
+ - [07-form-wizard.md](07-form-wizard.md) — end-to-end wizard в JSON: submit (`onComponentEvent`), навигация (`renderEffect`) и инъекция этой валидации в одном месте.
879
+ - [02-json-schema.md](02-json-schema.md) — справочник по узлам `JsonNode` (field-node несёт только layout).
880
+ - [05-cookbook.md#inject-runtime](05-cookbook.md#inject-runtime) — общий приём инъекции runtime-сущностей через `onInit`/`patchProps`.
881
+ - `@reformer/core` [13-multi-step.md](../../../reformer/docs/llms/13-multi-step.md) — узлы схемы валидации, `validateFormModel`, STEP_SCHEMAS.
882
+ - [Типы JsonFormSchema/JsonNode](../../src/types/json-schema.ts) и [validate.ts](../../src/validate.ts) — структурная валидация схемы.
883
+
884
+ ## 42. Половина 1 — layout шагов в JSON { #json-shape }
885
+
886
+ Wizard — обычная container-нода со `selector: 'wizard'` (чтобы адресоваться через `schema.node('wizard')`). Шаги лежат в **`componentProps.steps`** — массив container-нод, а **не** в top-level `children`. Каждый шаг — нода `$component(Step)` с `componentProps: { title, icon }` и собственным `children` (поддерево layout шага).
887
+
888
+ ```json
889
+ {
890
+ "selector": "wizard",
891
+ "component": "$component(Wizard)",
892
+ "componentProps": {
893
+ "className": "bg-white p-8 rounded-lg shadow-md",
894
+ "steps": [
895
+ {
896
+ "component": "$component(Step)",
897
+ "componentProps": { "title": "Кредит", "icon": "💰" },
898
+ "children": [
899
+ {
900
+ "selector": "mortgage-section",
901
+ "component": "$component(Section)",
902
+ "componentProps": { "title": "Ипотека" },
903
+ "children": [
904
+ { "value": "$model(loanAmount)", "component": "$component(Input)",
905
+ "componentProps": { "label": "Сумма кредита (₽)", "type": "number" } }
906
+ ]
907
+ }
908
+ ]
909
+ },
910
+ {
911
+ "component": "$component(Step)",
912
+ "componentProps": { "title": "Заявитель", "icon": "🧑" },
913
+ "children": [
914
+ { "value": "$model(personalData.firstName)", "component": "$component(Input)",
915
+ "componentProps": { "label": "Имя" } }
916
+ ]
917
+ }
918
+ ]
919
+ }
920
+ }
921
+ ```
922
+
923
+ Ground truth: golden `complex-multy-step-form-renderer-json/json-schema.json` (wizard-нода — `selector: 'wizard'`, `steps` внутри `componentProps`, каждый шаг — `$component(Step)` + `componentProps.title/icon` + `children`).
924
+
925
+ > Отличие от `@reformer/renderer-react`: там шаг — объект `{ number, title, icon, body }`, где `body` — самостоятельный `RenderNode` (см. renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms)). В JSON-DSL нельзя вписать `RenderNode` как значение пропа, поэтому шаг выражается **container-нодой** `Step` + `children`, а wizard-компонент адаптирует эту форму под `step.body`.
926
+
927
+ ## 43. Регистрируй свой wizard-компонент в реестре { #register }
928
+
929
+ `$component(Wizard)` — это **запись в реестре**, а не библиотечный экспорт: имя резолвится через registry (`reg.component('Wizard', <твой компонент>)`). Имя произвольное — важно лишь совпадение строки в JSON и ключа в реестре.
930
+
931
+ Канонический shipped-компонент — `FormWizard` из `@reformer/ui-kit/form-wizard` (см. renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms)). Он принимает `componentProps.form`, `componentProps.steps` и `FormWizardConfig` (`validateStep`/`validateAll`), а `step.body` полиморфен (`FC | ReactNode | RenderNode<T>`). Твой зарегистрированный компонент должен быть совместим с этим контрактом.
932
+
933
+ ```typescript
934
+ import { defineRegistry } from '@reformer/renderer-json';
935
+ import { Step } from '@reformer/cdk/form-wizard';
936
+ import { MyWizard } from './MyWizard'; // тонкая обёртка над ui-kit FormWizard
937
+
938
+ const registry = defineRegistry((reg) => {
939
+ reg.component('Wizard', MyWizard); // ← имя из JSON `$component(Wizard)`
940
+ reg.component('Step', Step); // container-нода шага
941
+ // ...остальные компоненты (Input, Select, Section, ...)
942
+ });
943
+ ```
944
+
945
+ > В golden-эталоне под `$component(Wizard)` зарегистрирован app-shim `RendererFormWizard`: он снимает `title`/`icon` c `componentProps` Step-ноды, а сам Step-узел кладёт в `step.body` ui-kit `FormWizard`. Shim — деталь приложения, **не** канон библиотеки; регистрируй под этим именем любой совместимый с `FormWizard` компонент.
946
+
947
+ ## 44. Половина 2 — поведение в одном render-behavior { #render-behavior }
948
+
949
+ Один `RenderBehaviorFn<T>` навешивает всё рантайм-поведение на wizard-ноду. Порядок: (a) инъекция `form` + валидации через `onInit`; (b) submit через `onComponentEvent`; (c) навигация через `renderEffect` + `wizardRef`; (d) условные секции через `hideWhen`. Семантику хелперов см. renderer-react [03-render-behavior.md](../../../reformer-renderer-react/docs/llms/03-render-behavior.md).
950
+
951
+ ```typescript
952
+ import {
953
+ onInit,
954
+ onComponentEvent,
955
+ renderEffect,
956
+ hideWhen,
957
+ type RenderBehaviorFn,
958
+ } from '@reformer/renderer-react';
959
+ import type { FormProxy, FormModel } from '@reformer/core';
960
+ import type { FormWizardHandle } from '@reformer/cdk/form-wizard';
961
+ import type { CreditForm } from './types';
962
+ import { makeValidationConfig } from './validation'; // см. 06-validation.md
963
+ import { submitCreditApplication } from './api';
964
+
965
+ export function createWizardRenderBehavior(
966
+ form: FormProxy<CreditForm>,
967
+ model: FormModel<CreditForm>
968
+ ): RenderBehaviorFn<CreditForm> {
969
+ return (schema) => {
970
+ const wizard = schema.node('wizard');
971
+ const wizardRef = wizard.getRef<FormWizardHandle<CreditForm>>();
972
+
973
+ // (a) Инъекция рантайма: form + validateStep/validateAll в wizard-ноду до первого рендера.
974
+ // Валидация — TS-схема над МОДЕЛЬЮ (не JSON). Детали — 06-validation.md.
975
+ onInit(wizard, () => {
976
+ wizard.patchProps({ form, ...makeValidationConfig(model) });
977
+ });
978
+
979
+ // (b) Submit: onComponentEvent получает те же аргументы, что и оригинальный проп onSubmit.
980
+ onComponentEvent(wizard, 'onSubmit', async (values: CreditForm) => {
981
+ await submitCreditApplication(values);
982
+ });
983
+
984
+ // (c) Навигация: реактивный эффект принимает СХЕМУ (не ноду); wizardRef доступен после mount.
985
+ renderEffect(schema, () => {
986
+ if (form.loanType.value.value === 'mortgage') {
987
+ wizardRef.current?.goToStep(1);
988
+ }
989
+ });
990
+
991
+ // (d) Условные секции: реактивно по сигналам формы (читай сигнал целиком: `.value.value`).
992
+ hideWhen(schema.node('mortgage-section'), () => form.loanType.value.value !== 'mortgage');
993
+ };
994
+ }
995
+
996
+ // <JsonFormRenderer schema={jsonSchema} renderBehavior={createWizardRenderBehavior(form, model)} />
997
+ ```
998
+
999
+ Ground truth: golden `complex-multy-step-form-renderer/render-behavior.ts` — `onComponentEvent(schema.node('wizard'), 'onSubmit', ...)`, `renderEffect(schema, () => wizardRef.current?.goToStep(1))`, `hideWhen(...)`; инъекция `form`+валидации — `complex-multy-step-form-renderer-json/render-behavior.ts`.
1000
+
1001
+ ## 45. Anti-patterns
1002
+
1003
+ - **Класть шаги в top-level `children` wizard-ноды** — шаги живут в `componentProps.steps`. Top-level `children` wizard-компонент не читает как шаги.
1004
+ - **Ждать шаг как `{ number, title, icon, body }` в JSON** — это форма renderer-react. В JSON шаг — container-нода `$component(Step)` + `componentProps.title/icon` + `children`.
1005
+ - **Считать `RendererFormWizard` библиотечным экспортом** — это app-shim эталона. Wizard-компонент подключается через реестр под любым именем; канон — ui-kit `FormWizard`.
1006
+ - **Забыть `createWizardRenderBehavior` (только `onInit` с валидацией)** — форма будет валидировать, но `onSubmit`/навигация не подключатся: submit-less форма. Submit и навигация приходят из этого же behavior.
1007
+ - **`renderEffect(node, ...)` вместо `renderEffect(schema, ...)`** — первый аргумент `renderEffect` это схема, а не узел (в отличие от `hideWhen`/`onComponentEvent`).
1008
+ - **Забыть `selector: 'wizard'`** — без селектора `schema.node('wizard')` не адресует узел, инъекция/submit/навигация не навесятся.
1009
+
1010
+ ## 46. See also
1011
+
1012
+ - [06-validation.md](06-validation.md) — `makeValidationConfig` (TS-схема над моделью), инъекция `validateStep`/`validateAll` в wizard.
1013
+ - [05-cookbook.md#inject-runtime](05-cookbook.md#inject-runtime) — общий приём инъекции runtime-сущностей (`form`) через `onInit`/`patchProps`.
1014
+ - renderer-react [03-render-behavior.md](../../../reformer-renderer-react/docs/llms/03-render-behavior.md) — семантика `hideWhen`/`renderEffect`/`onComponentEvent`/`onInit`.
1015
+ - renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms) — канонический `FormWizard`, форма шага `{ number, title, icon, body }`.
1016
+
1017
+ ## 47. Два пути (важно выбрать правильный)
1018
+
1019
+ Значение попадает в один из двух приёмников, и это определяет путь:
1020
+
1021
+ | Приёмник | Что можно | Путь |
1022
+ |---|---|---|
1023
+ | **Строковый проп** (`label`, `placeholder`, `title`, `aria-*`) | только `string`, **статично** | `$locale(key)` / `{ $locale, params }` |
1024
+ | **Контент** (видимый текст, сообщения) | `string` **или** rich/JSX, **реактивно** | компонент `$component(I18n)` |
1025
+
1026
+ Причина: `componentProps` спредятся в компонент как есть — signal/ReactNode в `label` уронит его (`Objects are not valid as a React child`). Строковый путь резолвится один раз при конвертации (смена языка = пересборка дерева); реактивный путь читает сервис из React-контекста на этапе рендера (live-переключение).
1027
+
1028
+ ## 48. Сервис локализации
1029
+
1030
+ ```ts
1031
+ interface LocaleService {
1032
+ resolve(key: string, params?: Record<string, unknown>): string; // строка (обязателен)
1033
+ render?(key: string, params?: Record<string, unknown>): ReactNode; // rich/markdown (опционален)
1034
+ keys?: readonly string[]; // validate-time проверка ключей
1035
+ }
1036
+ ```
1037
+
1038
+ Фабрики (0-dependency):
1039
+
1040
+ ```ts
1041
+ import { createLocaleResolver, createLocaleService } from '@reformer/renderer-json';
1042
+
1043
+ // плоский каталог ключ→строка
1044
+ createLocaleResolver({ 'fields.email.label': 'Email' });
1045
+
1046
+ // таблица ключ→(params)=>строка — параметры и склонение без ICU (стиль cdk createMessageResolver)
1047
+ createLocaleService({
1048
+ 'fields.min': (p) => `Минимум ${p?.count} символов`,
1049
+ 'users.count': (p) => plural(Number(p?.count), ['пользователь', 'пользователя', 'пользователей']),
1050
+ });
1051
+ ```
1052
+
1053
+ ### ICU MessageFormat (подключает потребитель)
1054
+
1055
+ Стандарт для склонения по CLDR-правилам. Библиотека — `intl-messageformat`; сервис пишешь сам:
1056
+
1057
+ ```ts
1058
+ import { IntlMessageFormat } from 'intl-messageformat';
1059
+
1060
+ function createIcuLocaleService(catalog: Record<string, string>, locale: string): LocaleService {
1061
+ return {
1062
+ resolve: (key, params) =>
1063
+ key in catalog ? (new IntlMessageFormat(catalog[key], locale).format(params) as string) : key,
1064
+ keys: Object.keys(catalog),
1065
+ };
1066
+ }
1067
+ // catalog: { 'users.count': 'There {count, plural, one {is # user} other {are # users}}' }
1068
+ ```
1069
+
1070
+ ### Markdown / rich (подключает потребитель через `render`)
1071
+
1072
+ ```ts
1073
+ import ReactMarkdown from 'react-markdown';
1074
+
1075
+ const service: LocaleService = {
1076
+ resolve: (k) => catalog[k] ?? k,
1077
+ render: (k, p) => <ReactMarkdown>{format(catalog[k] ?? k, p)}</ReactMarkdown>,
1078
+ keys: Object.keys(catalog),
1079
+ };
1080
+ ```
1081
+
1082
+ ## 49. Путь 1 — строковые пропы (статично)
1083
+
1084
+ Без параметров — строковый оператор:
1085
+
1086
+ ```jsonc
1087
+ { "value": "$model(email)", "component": "$component(Input)",
1088
+ "componentProps": { "label": "$locale(fields.email.label)" } }
1089
+ ```
1090
+
1091
+ С параметрами — **структурная форма** (объект, не оператор — без парсинга аргументов):
1092
+
1093
+ ```jsonc
1094
+ { "value": "$model(password)", "component": "$component(Input)",
1095
+ "componentProps": { "label": { "$locale": "fields.min", "params": { "count": 8 } } } }
1096
+ // → resolve("fields.min", { count: 8 }) → "Минимум 8 символов"
1097
+ ```
1098
+
1099
+ Параметры — обычно **литералы** (путь статичный). Можно сослаться и на `$model(path)` в `params` — тогда берётся **снимок** текущего значения (`.peek()`, на момент конвертации) и печатается dev-warning: снимок не реактивен, при изменении модели строка не обновится. Для живого значения — `I18n` (путь 2).
1100
+
1101
+ Регистрация сервиса — в реестре (для конвертера):
1102
+
1103
+ ```ts
1104
+ const registry = defineRegistry((reg) => {
1105
+ reg.locale(createLocaleService({ 'fields.min': (p) => `Минимум ${p?.count} символов` }));
1106
+ });
1107
+ ```
1108
+
1109
+ ## 50. Путь 2 — реактивный компонент `I18n`
1110
+
1111
+ Для markdown/rich и/или реактивных параметров и live-переключения языка. Регистрируется как компонент, оборачивается `LocaleProvider`:
1112
+
1113
+ ```tsx
1114
+ import { I18n, LocaleProvider, createLocaleService, JsonFormRenderer } from '@reformer/renderer-json';
1115
+
1116
+ const services = { ru: createLocaleService(ruTable), en: createLocaleService(enTable) };
1117
+
1118
+ function App() {
1119
+ const [lang, setLang] = useState<'ru' | 'en'>('ru');
1120
+ const registry = useMemo(() => defineRegistry((reg) => {
1121
+ reg.component('I18n', I18n);
1122
+ // ...остальные компоненты
1123
+ }), []);
1124
+ return (
1125
+ <LocaleProvider service={services[lang]}>
1126
+ <JsonRendererProvider settings={{ registry, model }}>
1127
+ <JsonFormRenderer schema={schema} />
1128
+ </JsonRendererProvider>
1129
+ </LocaleProvider>
1130
+ );
1131
+ }
1132
+ ```
1133
+
1134
+ ```jsonc
1135
+ // в схеме — контейнер-узел; values.count приходит сигналом и обновляется вживую
1136
+ { "component": "$component(I18n)",
1137
+ "componentProps": { "id": "users.count", "values": { "count": "$model(userCount)" } } }
1138
+ ```
1139
+
1140
+ `I18n` рендерит `service.render?.()` (markdown/JSX) либо `service.resolve()` (строка) — формат решает сервис. Перерисовывается при смене сигнала `values.count` и при смене `service` в `LocaleProvider` (переключение языка).
1141
+
1142
+ ## 51. Валидация
1143
+
1144
+ `validateFormSchema` проверяет ключи `$locale(...)` и структурной формы `{ $locale }` против `localeKeys` (из `service.keys` или явного списка) — опечатка ключа ловится до рендера. Нет каталога → проверка мягко пропускается (ключи свободные).
1145
+
1146
+ ## 52. Anti-patterns
1147
+
1148
+ - **markdown/JSX в строковый проп** — `label: "$locale(...)"` с сервисом, возвращающим ReactNode, уронит компонент. Rich-контент только через `$component(I18n)` (или свой `$component(RichText)`).
1149
+ - **реактивный параметр в строковом `$locale`** — `{ $locale, params: { count: "$model(...)" } }` берётся снимком и **не обновляется** (путь статичный); в dev печатается warning. Для реактивных model-параметров → `I18n` с `values`.
1150
+ - **`I18n` без `LocaleProvider`** — без провайдера сервис по умолчанию отдаёт сам ключ (fallback-to-key). Оберни поддерево в `LocaleProvider`.
1151
+ - **ждать live-переключения от `$locale`** — строковый путь статичен; смена языка для label/placeholder требует пересборки (новый сервис в `reg.locale` + пере-render). Live только у `I18n`.
1152
+ - **тащить ICU/markdown в схему** — форматтер живёт в реализации `LocaleService` у потребителя, не в JSON.
1153
+
1154
+ ## 53. See also
1155
+
1156
+ - [02-json-schema.md](02-json-schema.md) — где `$locale`/`I18n` появляются в схеме.
1157
+ - [03-registry.md](03-registry.md) — `reg.locale`, `createLocaleService`, регистрация `I18n`.
1158
+ - [06-validation.md](06-validation.md) — валидация значений (отдельно от локализации).
1159
+
1160
+ ## 54. API Reference
1161
+
1162
+ _Auto-generated from JSDoc on public exports._
1163
+
1164
+ ### buildFormSchemaMetaSchema
1165
+
1166
+ **Kind:** `function`
1167
+
1168
+ Конкретная мета-схема: базовая + (если заданы `componentNames`) сужение `$component(...)` до
1169
+ enum допустимых значений (`["$component(Input)", "$component(Select)", …]`). `$dataSource`-имена
1170
+ JSON Schema'й не покрыть (вложены в произвольный componentProps) — их проверяет рекурсивный обход
1171
+ в `validateFormSchema`.
1172
+
1173
+ enum, а не regex-`pattern`: ajv перечисляет допустимые имена в тексте ошибки, IDE даёт
1174
+ автодополнение по значениям, а имена не нужно экранировать под regex (напр. `$fieldWrapper`).
1175
+
1176
+ **Signature:**
1177
+ ```typescript
1178
+ export function buildFormSchemaMetaSchema(opts?: {
1179
+ componentNames?: string[];
1180
+ }): Record<string, unknown>
1181
+ ```
1182
+
1183
+ **Examples:**
1184
+
1185
+ ```ts
1186
+ const schema = buildFormSchemaMetaSchema({ componentNames: getComponentNames(registry) });
1187
+ ```
1188
+
1189
+ _Source: src/schema/index.ts_
1190
+
1191
+ ### ComponentMetadata
1192
+
1193
+ **Kind:** `interface`
1194
+
1195
+ Запись реестра. Хранит значение (компонент / dataSource / функцию / сервис локализации) и его роль.
1196
+
1197
+ **Signature:**
1198
+ ```typescript
1199
+ export interface ComponentMetadata<P = any> {
1200
+ component: ComponentType<P> | unknown;
1201
+ type: 'component' | 'dataSource' | 'fn' | 'locale';
1202
+ description?: string;
1203
+ }
1204
+ ```
1205
+
1206
+ **Examples:**
1207
+
1208
+ ```typescript
1209
+ const meta: ComponentMetadata = registry.get('Input')!;
1210
+ meta.type; // 'component'
1211
+ ```
1212
+
1213
+ _Source: src/registry/types.ts_
1214
+
1215
+ ### ComponentOp
1216
+
1217
+ **Kind:** `type`
1218
+
1219
+ Строка-оператор ссылки на компонент реестра: `` `$component(${name})` ``.
1220
+
1221
+ **Signature:**
1222
+ ```typescript
1223
+ export type ComponentOp = `$component(${string})`;
1224
+ ```
1225
+
1226
+ _Source: src/operators.ts_
1227
+
1228
+ ### ComponentRegistry
1229
+
1230
+ **Kind:** `interface`
1231
+
1232
+ Read-only API реестра, доступное в рантайме.
1233
+
1234
+ Создаётся через {@link defineRegistry}. Передаётся в `JsonRendererProvider`
1235
+ через `settings.registry`.
1236
+
1237
+ **Signature:**
1238
+ ```typescript
1239
+ export interface ComponentRegistry {
1240
+ get(name: string): ComponentMetadata | undefined;
1241
+ getDataSource<T = unknown>(name: string): T | undefined;
1242
+ /**
1243
+ * Единственный сервис локализации (для оператора `$locale(key)`), если зарегистрирован через
1244
+ * `reg.locale(...)`. Опционален — сторонние реализации `ComponentRegistry` без него продолжают
1245
+ * компилироваться; при его отсутствии `$locale` мягко деградирует до самого ключа.
1246
+ */
1247
+ getLocale?(): LocaleService | undefined;
1248
+ has(name: string): boolean;
1249
+ names(): string[];
1250
+ }
1251
+ ```
1252
+
1253
+ **Examples:**
1254
+
1255
+ ```typescript
1256
+ if (registry.has('Input')) {
1257
+ registry.get('Input'); // ComponentMetadata
1258
+ }
1259
+ registry.names(); // ['Input', 'Box', 'LOAN_TYPES', ...]
1260
+ ```
1261
+
1262
+ _Source: src/registry/types.ts_
1263
+
1264
+ ### convertJsonToM1Tree
1265
+
1266
+ **Kind:** `function`
1267
+
1268
+ Сырое дерево RenderNode из JSON (M1) — для `createForm({ model, schema })`. Листья привязываются
1269
+ к сигналам модели (`'$model(path)'` → `model.signalAt`), компоненты/источники — из реестра.
1270
+
1271
+ **Signature:**
1272
+ ```typescript
1273
+ export function convertJsonToM1Tree<T>(
1274
+ schema: JsonFormSchema,
1275
+ registry: ComponentRegistry,
1276
+ model: FormModel<T>
1277
+ ): RenderNode<T>
1278
+ ```
1279
+
1280
+ **Parameters:**
1281
+ - `schema` — - JSON-схема формы ({@link JsonFormSchema}).
1282
+ - `registry` — - Реестр компонентов/источников (см. {@link defineRegistry}).
1283
+ - `model` — - Модель данных — источник значений (`FormModel`).
1284
+
1285
+ **Returns:** Корневой {@link RenderNode} — кладётся в `createForm({ schema })`.
1286
+
1287
+ **Examples:**
1288
+
1289
+ Собрать форму из JSON-схемы (M1)
1290
+ ```ts
1291
+ import { createForm } from '@reformer/core';
1292
+
1293
+ const form = createForm<MyForm>({
1294
+ model,
1295
+ schema: convertJsonToM1Tree(jsonSchema, registry, model),
1296
+ behavior,
1297
+ });
1298
+ ```
1299
+
1300
+ _Source: src/converter/json-to-render-schema.ts_
1301
+
1302
+ ### createLocaleResolver
1303
+
1304
+ **Kind:** `function`
1305
+
1306
+ Строит {@link LocaleService} из каталога `ключ → строка`. Промах ключа → сам ключ (fallback-to-key,
1307
+ зеркало `createMessageResolver` из `@reformer/cdk`). `keys` каталога включает validate-time проверку
1308
+ опечаток. Смена языка — пересобрать сервис на другом каталоге и передать новый ref в `reg.locale`.
1309
+
1310
+ **Signature:**
1311
+ ```typescript
1312
+ export function createLocaleResolver(catalog: Record<string, string>): LocaleService
1313
+ ```
1314
+
1315
+ **Parameters:**
1316
+ - `catalog` — - Таблица `ключ → локализованная строка`.
1317
+
1318
+ **Returns:**
1319
+
1320
+ **Examples:**
1321
+
1322
+ ```ts
1323
+ import { defineRegistry } from '@reformer/renderer-json';
1324
+ import { createLocaleResolver } from '@reformer/renderer-json';
1325
+
1326
+ const registry = defineRegistry((reg) => {
1327
+ reg.locale(createLocaleResolver({
1328
+ 'fields.email.label': 'Email',
1329
+ 'fields.email.placeholder': 'you@example.com',
1330
+ }));
1331
+ });
1332
+ // В схеме: componentProps: { label: '$locale(fields.email.label)' }
1333
+ // '$locale(fields.unknown)' при наличии каталога → ошибка на validateFormSchema.
1334
+ ```
1335
+
1336
+ _Source: src/locale/locale-service.ts_
1337
+
1338
+ ### createLocaleService
1339
+
1340
+ **Kind:** `function`
1341
+
1342
+ Строит {@link LocaleService} из таблицы `ключ → (params) => строка` — 0-dependency вариант для
1343
+ параметров и склонения без ICU (стиль `createMessageResolver` из `@reformer/cdk`). Промах ключа →
1344
+ сам ключ. `keys` таблицы включает validate-time проверку опечаток.
1345
+
1346
+ Для ICU-склонения по CLDR-правилам подключи `intl-messageformat` в СВОЁМ сервисе:
1347
+ `reg.locale({ resolve: (k, p) => new IntlMessageFormat(catalog[k], lng).format(p) as string, keys })`.
1348
+ Для markdown добавь `render` (напр. через `react-markdown`).
1349
+
1350
+ **Signature:**
1351
+ ```typescript
1352
+ export function createLocaleService(
1353
+ table: Record<string, (params?: LocaleParams) => string>
1354
+ ): LocaleService
1355
+ ```
1356
+
1357
+ **Parameters:**
1358
+ - `table` — - Таблица `ключ → (params) => локализованная строка`.
1359
+
1360
+ **Returns:**
1361
+
1362
+ **Examples:**
1363
+
1364
+ ```ts
1365
+ import { createLocaleService } from '@reformer/renderer-json';
1366
+
1367
+ const ru = createLocaleService({
1368
+ 'fields.min': (p) => `Минимум ${p?.count} символов`,
1369
+ 'users.count': (p) => {
1370
+ const n = Number(p?.count);
1371
+ const forms = ['пользователь', 'пользователя', 'пользователей'];
1372
+ const idx = n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
1373
+ return `${n} ${forms[idx]}`;
1374
+ },
1375
+ });
1376
+ ru.resolve('fields.min', { count: 3 }); // 'Минимум 3 символов'
1377
+ ```
1378
+
1379
+ _Source: src/locale/locale-service.ts_
1380
+
1381
+ ### createRenderSchemaFromJsonM1
1382
+
1383
+ **Kind:** `function`
1384
+
1385
+ `RenderSchemaFn` из JSON (M1) — для `FormRenderer`/`JsonFormRenderer`. Листья привязываются к
1386
+ сигналам модели (`'$model(path)'` → `model.signalAt`), компоненты/источники — из реестра.
1387
+
1388
+ В отличие от {@link convertJsonToM1Tree} (который возвращает готовое дерево), здесь результат —
1389
+ ленивая функция-фабрика дерева, как её ждёт `createRenderSchema`. Обычно вызывается внутри
1390
+ {@link JsonFormRenderer}; напрямую нужен для интеграции с `FormRenderer` без JSON-обёртки.
1391
+
1392
+ **Signature:**
1393
+ ```typescript
1394
+ export function createRenderSchemaFromJsonM1<T>(
1395
+ schema: JsonFormSchema,
1396
+ registry: ComponentRegistry,
1397
+ model: FormModel<T>
1398
+ ): RenderSchemaFn<T>
1399
+ ```
1400
+
1401
+ **Parameters:**
1402
+ - `schema` — - JSON-схема формы ({@link JsonFormSchema}).
1403
+ - `registry` — - Реестр компонентов/источников (см. {@link defineRegistry}).
1404
+ - `model` — - Модель данных — источник значений (`FormModel`).
1405
+
1406
+ **Returns:** `RenderSchemaFn<T>` — фабрика {@link RenderNode}-дерева для `createRenderSchema`.
1407
+
1408
+ **Examples:**
1409
+
1410
+ ```ts
1411
+ import { createRenderSchema, FormRenderer } from '@reformer/renderer-react';
1412
+
1413
+ const fn = createRenderSchemaFromJsonM1<MyForm>(jsonSchema, registry, model);
1414
+ const proxy = createRenderSchema<MyForm>(fn);
1415
+ <FormRenderer render={proxy} />;
1416
+ ```
1417
+
1418
+ _Source: src/converter/json-to-render-schema.ts_
1419
+
1420
+ ### DataSourceOp
1421
+
1422
+ **Kind:** `type`
1423
+
1424
+ Строка-оператор ссылки на registry-source: `` `$dataSource(${name})` ``.
1425
+
1426
+ **Signature:**
1427
+ ```typescript
1428
+ export type DataSourceOp = `$dataSource(${string})`;
1429
+ ```
1430
+
1431
+ _Source: src/operators.ts_
1432
+
1433
+ ### defaultLocaleResolver
1434
+
1435
+ **Kind:** `const`
1436
+
1437
+ Дефолтный резолвер: отдаёт сам ключ (fallback-to-key). Применяется, когда сервис не зарегистрирован.
1438
+
1439
+ **Signature:**
1440
+ ```typescript
1441
+ export const defaultLocaleResolver: LocaleResolver
1442
+ ```
1443
+
1444
+ _Source: src/locale/locale-service.ts_
1445
+
1446
+ ### defineRegistry
1447
+
1448
+ **Kind:** `function`
1449
+
1450
+ Создаёт реестр компонентов через builder-callback.
1451
+
1452
+ Реестр обязателен для работы {@link JsonFormRenderer} — иначе компоненты,
1453
+ упомянутые в JSON-схеме, не отрезолвятся.
1454
+
1455
+ **Signature:**
1456
+ ```typescript
1457
+ export function defineRegistry(fn: (reg: RegistryBuilder) => void): ComponentRegistry
1458
+ ```
1459
+
1460
+ **Parameters:**
1461
+ - `fn` — - Builder-callback. Получает {@link RegistryBuilder} с методами `component`, `dataSource`.
1462
+
1463
+ **Returns:** Готовый {@link ComponentRegistry}, который кладётся в `JsonRendererProvider`.
1464
+
1465
+ **Examples:**
1466
+
1467
+ ```typescript
1468
+ import { defineRegistry, FIELD_WRAPPER } from '@reformer/renderer-json';
1469
+ import { Input, Select, FormField } from '@reformer/ui-kit';
1470
+
1471
+ const registry = defineRegistry((reg) => {
1472
+ reg.component('Input', Input);
1473
+ reg.component('Select', Select);
1474
+ reg.component(FIELD_WRAPPER, FormField);
1475
+ reg.dataSource('LOAN_TYPES', [
1476
+ { value: 'consumer', label: 'Потребительский' },
1477
+ { value: 'mortgage', label: 'Ипотека' },
1478
+ ]);
1479
+ });
1480
+ ```
1481
+
1482
+ **See also:**
1483
+ - [docs/llms/03-registry.md](../../docs/llms/03-registry.md)
1484
+
1485
+ _Source: src/registry/component-registry.ts_
1486
+
1487
+ ### FIELD_WRAPPER
1488
+
1489
+ **Kind:** `const`
1490
+
1491
+ Зарезервированный ключ реестра для контейнера-обёртки полей.
1492
+
1493
+ Зарегистрируй компонент под этим именем (обычно `FormField` из `@reformer/ui-kit`),
1494
+ чтобы каждое поле получало label, error и hint автоматически.
1495
+
1496
+ **Signature:**
1497
+ ```typescript
1498
+ export const FIELD_WRAPPER
1499
+ ```
1500
+
1501
+ **Examples:**
1502
+
1503
+ ```typescript
1504
+ import { defineRegistry, FIELD_WRAPPER } from '@reformer/renderer-json';
1505
+ import { FormField } from '@reformer/ui-kit';
1506
+
1507
+ const registry = defineRegistry((reg) => {
1508
+ reg.component(FIELD_WRAPPER, FormField);
1509
+ });
1510
+ ```
1511
+
1512
+ _Source: src/registry/constants.ts_
1513
+
1514
+ ### FnOp
1515
+
1516
+ **Kind:** `type`
1517
+
1518
+ Строка-оператор ссылки на функцию реестра: `` `$fn(${name})` ``.
1519
+
1520
+ **Signature:**
1521
+ ```typescript
1522
+ export type FnOp = `$fn(${string})`;
1523
+ ```
1524
+
1525
+ _Source: src/operators.ts_
1526
+
1527
+ ### formSchemaMetaSchema
1528
+
1529
+ **Kind:** `const`
1530
+
1531
+ Базовая мета-схема form-DSL (draft-07): структура узлов + синтаксис операторов, имена компонентов
1532
+ НЕ ограничены (паттерн `$component(...)` открыт). Для сужения до конкретного реестра —
1533
+ {@link buildFormSchemaMetaSchema}. Используется как `$schema` в IDE и как база валидатора.
1534
+
1535
+ **Signature:**
1536
+ ```typescript
1537
+ export const formSchemaMetaSchema
1538
+ ```
1539
+
1540
+ **Examples:**
1541
+
1542
+ Подключить как `$schema` в JSON-файле схемы
1543
+ ```ts
1544
+ // record можно записать в файл и сослаться на него из "$schema" JSON-схемы.
1545
+ formSchemaMetaSchema.$schema; // 'http://json-schema.org/draft-07/schema#'
1546
+ ```
1547
+
1548
+ _Source: src/schema/index.ts_
1549
+
1550
+ ### getComponentNames
1551
+
1552
+ **Kind:** `function`
1553
+
1554
+ Имена компонентов реестра (тип `component`) — то, что валидно в `$component(...)`.
1555
+
1556
+ **Signature:**
1557
+ ```typescript
1558
+ export function getComponentNames(registry: ComponentRegistry): string[]
1559
+ ```
1560
+
1561
+ **Parameters:**
1562
+ - `registry` — - Реестр (см. {@link defineRegistry}).
1563
+
1564
+ **Returns:** Массив имён компонентов.
1565
+
1566
+ **Examples:**
1567
+
1568
+ Сузить мета-схему до компонентов реестра
1569
+ ```ts
1570
+ const names = getComponentNames(registry); // ['Input', 'Select', 'Box', ...]
1571
+ const schema = buildFormSchemaMetaSchema({ componentNames: names });
1572
+ ```
1573
+
1574
+ _Source: src/schema/index.ts_
1575
+
1576
+ ### getDataSourceNames
1577
+
1578
+ **Kind:** `function`
1579
+
1580
+ Имена registry-dataSource (`reg.dataSource`) — то, что валидно в `$dataSource(...)`:
1581
+ options/itemLabel/константы/loading-компоненты.
1582
+
1583
+ **Signature:**
1584
+ ```typescript
1585
+ export function getDataSourceNames(registry: ComponentRegistry): string[]
1586
+ ```
1587
+
1588
+ **Parameters:**
1589
+ - `registry` — - Реестр (см. {@link defineRegistry}).
1590
+
1591
+ **Returns:** Массив имён источников.
1592
+
1593
+ **Examples:**
1594
+
1595
+ ```ts
1596
+ getDataSourceNames(registry); // ['LOAN_TYPES', 'GENDERS', 'LoadingState', ...]
1597
+ ```
1598
+
1599
+ _Source: src/schema/index.ts_
1600
+
1601
+ ### getFnNames
1602
+
1603
+ **Kind:** `function`
1604
+
1605
+ Имена функций реестра (`reg.fn`) — то, что валидно в `$fn(...)`: форматтеры/компараторы/itemLabel/
1606
+ обработчики. Отдельно от {@link getDataSourceNames}, поэтому `validateFormSchema` ловит перепутанные
1607
+ `$fn`/`$dataSource`.
1608
+
1609
+ **Signature:**
1610
+ ```typescript
1611
+ export function getFnNames(registry: ComponentRegistry): string[]
1612
+ ```
1613
+
1614
+ **Parameters:**
1615
+ - `registry` — - Реестр (см. {@link defineRegistry}).
1616
+
1617
+ **Returns:** Массив имён функций.
1618
+
1619
+ _Source: src/schema/index.ts_
1620
+
1621
+ ### getLocaleKeys
1622
+
1623
+ **Kind:** `function`
1624
+
1625
+ Известные ключи локализации сервиса реестра (`reg.locale` с каталогом) — то, что валидно в
1626
+ `$locale(...)`. `undefined`, если сервис не зарегистрирован или задан голым резолвером без `keys`
1627
+ (тогда `validateFormSchema` мягко пропускает проверку ключей, как для `$model`-путей).
1628
+
1629
+ **Signature:**
1630
+ ```typescript
1631
+ export function getLocaleKeys(registry: ComponentRegistry): readonly string[] | undefined
1632
+ ```
1633
+
1634
+ **Parameters:**
1635
+ - `registry` — - Реестр (см. {@link defineRegistry}).
1636
+
1637
+ **Returns:** Массив ключей либо `undefined`.
1638
+
1639
+ _Source: src/schema/index.ts_
1640
+
1641
+ ### I18n
1642
+
1643
+ **Kind:** `function`
1644
+
1645
+ Реактивный локализованный текст. Читает {@link LocaleService} из {@link LocaleProvider} (live-смена
1646
+ языка) и разворачивает `values`-сигналы (live-обновление параметров). Если сервис умеет `render` —
1647
+ рендерит rich-контент (markdown/JSX), иначе строку через `resolve`.
1648
+
1649
+ **Signature:**
1650
+ ```typescript
1651
+ export function I18n({ id, values }: I18nProps): ReactNode
1652
+ ```
1653
+
1654
+ **Parameters:**
1655
+ - `props` — - {@link I18nProps} (`id` + опциональные `values`).
1656
+
1657
+ **Returns:** Локализованный `ReactNode`.
1658
+
1659
+ _Source: src/locale/i18n.tsx_
1660
+
1661
+ ### I18nProps
1662
+
1663
+ **Kind:** `interface`
1664
+
1665
+ Props {@link I18n}.
1666
+
1667
+ **Signature:**
1668
+ ```typescript
1669
+ export interface I18nProps {
1670
+ /** Ключ сообщения в сервисе локализации. */
1671
+ id: string;
1672
+ /**
1673
+ * Параметры интерполяции/склонения. Значения могут быть сигналами (`$model(...)`) — тогда текст
1674
+ * обновляется при их изменении — или литералами.
1675
+ */
1676
+ values?: LocaleParams;
1677
+ }
1678
+ ```
1679
+
1680
+ _Source: src/locale/i18n.tsx_
1681
+
1682
+ ### isArrayNode
1683
+
1684
+ **Kind:** `function`
1685
+
1686
+ Type-guard: узел — массив (`array: '$model(...)'` + `item.$template`). Проверять ПЕРВЫМ
1687
+ (лист/контейнер отсеиваются после, т.к. массив тоже несёт `$model`).
1688
+
1689
+ **Signature:**
1690
+ ```typescript
1691
+ export function isArrayNode(node: JsonNode): node is JsonArrayNode
1692
+ ```
1693
+
1694
+ **Parameters:**
1695
+ - `node` — - Узел JSON-схемы.
1696
+
1697
+ **Returns:** `true`, если узел — {@link JsonArrayNode}.
1698
+
1699
+ **Examples:**
1700
+
1701
+ Сузить тип узла перед доступом к `item.$template`
1702
+ ```ts
1703
+ if (isArrayNode(node)) {
1704
+ node.array; // ModelOp
1705
+ node.item.$template; // JsonNode
1706
+ }
1707
+ ```
1708
+
1709
+ _Source: src/types/json-schema.ts_
1710
+
1711
+ ### isComponentOp
1712
+
1713
+ **Kind:** `const`
1714
+
1715
+ Type-guard: строка — оператор `"$component(...)"` (ссылка на компонент реестра).
1716
+
1717
+ **Signature:**
1718
+ ```typescript
1719
+ export const isComponentOp
1720
+ ```
1721
+
1722
+ **Parameters:**
1723
+ - `v` — - Проверяемое значение.
1724
+
1725
+ **Returns:** `true`, если `v` — {@link ComponentOp}.
1726
+
1727
+ **Examples:**
1728
+
1729
+ ```ts
1730
+ if (isComponentOp(node.component)) {
1731
+ const name = parseOperator(node.component)!.arg; // 'Select'
1732
+ }
1733
+ ```
1734
+
1735
+ _Source: src/operators.ts_
1736
+
1737
+ ### isContainerNode
1738
+
1739
+ **Kind:** `function`
1740
+
1741
+ Type-guard: узел — контейнер (`component: '$component(...)'`, без `value`/`array`).
1742
+
1743
+ **Signature:**
1744
+ ```typescript
1745
+ export function isContainerNode(node: JsonNode): node is JsonContainerNode
1746
+ ```
1747
+
1748
+ **Parameters:**
1749
+ - `node` — - Узел JSON-схемы.
1750
+
1751
+ **Returns:** `true`, если узел — {@link JsonContainerNode}.
1752
+
1753
+ **Examples:**
1754
+
1755
+ Сузить тип узла перед обходом `children`
1756
+ ```ts
1757
+ if (isContainerNode(node)) {
1758
+ node.component; // ComponentOp
1759
+ node.children?.forEach(walk);
1760
+ }
1761
+ ```
1762
+
1763
+ _Source: src/types/json-schema.ts_
1764
+
1765
+ ### isDataSourceOp
1766
+
1767
+ **Kind:** `const`
1768
+
1769
+ Type-guard: строка — оператор `"$dataSource(...)"` (ссылка на registry-source).
1770
+
1771
+ **Signature:**
1772
+ ```typescript
1773
+ export const isDataSourceOp
1774
+ ```
1775
+
1776
+ **Parameters:**
1777
+ - `v` — - Проверяемое значение.
1778
+
1779
+ **Returns:** `true`, если `v` — {@link DataSourceOp}.
1780
+
1781
+ **Examples:**
1782
+
1783
+ ```ts
1784
+ if (isDataSourceOp(props.options)) {
1785
+ const name = parseOperator(props.options)!.arg; // 'LOAN_TYPES'
1786
+ }
1787
+ ```
1788
+
1789
+ _Source: src/operators.ts_
1790
+
1791
+ ### isFieldNode
1792
+
1793
+ **Kind:** `function`
1794
+
1795
+ Type-guard: узел — лист (`value: '$model(...)'`).
1796
+
1797
+ **Signature:**
1798
+ ```typescript
1799
+ export function isFieldNode(node: JsonNode): node is JsonFieldNode
1800
+ ```
1801
+
1802
+ **Parameters:**
1803
+ - `node` — - Узел JSON-схемы.
1804
+
1805
+ **Returns:** `true`, если узел — {@link JsonFieldNode}.
1806
+
1807
+ **Examples:**
1808
+
1809
+ Сузить тип узла перед доступом к `value`/`component`
1810
+ ```ts
1811
+ if (isFieldNode(node)) {
1812
+ node.value; // ModelOp
1813
+ node.component; // ComponentOp | undefined
1814
+ }
1815
+ ```
1816
+
1817
+ _Source: src/types/json-schema.ts_
1818
+
1819
+ ### isFnOp
1820
+
1821
+ **Kind:** `const`
1822
+
1823
+ Type-guard: строка — оператор `"$fn(...)"` (ссылка на функцию реестра).
1824
+
1825
+ **Signature:**
1826
+ ```typescript
1827
+ export const isFnOp
1828
+ ```
1829
+
1830
+ **Parameters:**
1831
+ - `v` — - Проверяемое значение.
1832
+
1833
+ **Returns:** `true`, если `v` — {@link FnOp}.
1834
+
1835
+ **Examples:**
1836
+
1837
+ ```ts
1838
+ if (isFnOp(props.itemLabel)) {
1839
+ const name = parseOperator(props.itemLabel)!.arg; // 'propertyItemLabel'
1840
+ }
1841
+ ```
1842
+
1843
+ _Source: src/operators.ts_
1844
+
1845
+ ### isLocaleOp
1846
+
1847
+ **Kind:** `const`
1848
+
1849
+ Type-guard: строка — оператор `"$locale(...)"` (ключ строки для сервиса локализации).
1850
+
1851
+ **Signature:**
1852
+ ```typescript
1853
+ export const isLocaleOp
1854
+ ```
1855
+
1856
+ **Parameters:**
1857
+ - `v` — - Проверяемое значение.
1858
+
1859
+ **Returns:** `true`, если `v` — {@link LocaleOp}.
1860
+
1861
+ **Examples:**
1862
+
1863
+ ```ts
1864
+ if (isLocaleOp(props.label)) {
1865
+ const key = parseOperator(props.label)!.arg; // 'fields.email.label'
1866
+ }
1867
+ ```
1868
+
1869
+ _Source: src/operators.ts_
1870
+
1871
+ ### isModelOp
1872
+
1873
+ **Kind:** `const`
1874
+
1875
+ Type-guard: строка — оператор `"$model(...)"` (привязка к полю/массиву модели).
1876
+
1877
+ **Signature:**
1878
+ ```typescript
1879
+ export const isModelOp
1880
+ ```
1881
+
1882
+ **Parameters:**
1883
+ - `v` — - Проверяемое значение.
1884
+
1885
+ **Returns:** `true`, если `v` — {@link ModelOp}.
1886
+
1887
+ **Examples:**
1888
+
1889
+ Сузить тип значения prop до ModelOp
1890
+ ```ts
1891
+ if (isModelOp(value)) {
1892
+ const path = parseOperator(value)!.arg; // 'loanType'
1893
+ }
1894
+ ```
1895
+
1896
+ _Source: src/operators.ts_
1897
+
1898
+ ### JsonArrayNode
1899
+
1900
+ **Kind:** `interface`
1901
+
1902
+ Массив форм: данные из модели (`$model`) + шаблон элемента (`$template`).
1903
+
1904
+ **Signature:**
1905
+ ```typescript
1906
+ export interface JsonArrayNode {
1907
+ /** Id для render-behavior. */
1908
+ selector?: string;
1909
+ /** Привязка к массиву модели: `'$model(coBorrowers)'`. */
1910
+ array: ModelOp;
1911
+ /** Шаблон под-схемы элемента. */
1912
+ item: { $template: JsonNode };
1913
+ /**
1914
+ * «Пустой» элемент для кнопки «Добавить» (литерал-объект по форме элемента).
1915
+ * Нужен, т.к. листья шаблона несут `value: '$model(...)'`, а не литерал-дефолт.
1916
+ */
1917
+ initialValue?: Record<string, unknown>;
1918
+ /** Оформление секции массива (title/addButtonLabel/itemLabel/emptyMessage/…). */
1919
+ componentProps?: Record<string, unknown>;
1920
+ }
1921
+ ```
1922
+
1923
+ _Source: src/types/json-schema.ts_
1924
+
1925
+ ### JsonContainerNode
1926
+
1927
+ **Kind:** `interface`
1928
+
1929
+ Контейнер (Box/Section/Wizard/Step/…) с дочерними узлами.
1930
+
1931
+ **Signature:**
1932
+ ```typescript
1933
+ export interface JsonContainerNode {
1934
+ /** Id для render-behavior. */
1935
+ selector?: string;
1936
+ /** Компонент-контейнер из реестра: `'$component(Section)'`. */
1937
+ component: ComponentOp;
1938
+ /** Props компонента; значения могут содержать строки-операторы или вложенные узлы. */
1939
+ componentProps?: Record<string, unknown>;
1940
+ /** Дочерние узлы. */
1941
+ children?: JsonNode[];
1942
+ }
1943
+ ```
1944
+
1945
+ _Source: src/types/json-schema.ts_
1946
+
1947
+ ### JsonFieldNode
1948
+
1949
+ **Kind:** `interface`
1950
+
1951
+ Лист формы: значение из модели (`$model`) + опциональный компонент (`$component`, дефолт — Input).
1952
+
1953
+ **Signature:**
1954
+ ```typescript
1955
+ export interface JsonFieldNode {
1956
+ /** Id для render-behavior (hideWhen/patchProps). Опционален. */
1957
+ selector?: string;
1958
+ /** Привязка к сигналу модели: `'$model(personalData.lastName)'`. */
1959
+ value: ModelOp;
1960
+ /** Компонент поля из реестра: `'$component(Select)'`. Опционален. */
1961
+ component?: ComponentOp;
1962
+ /** Props компонента; значения могут содержать строки-операторы (`'$dataSource(NAME)'`) или вложенные узлы. */
1963
+ componentProps?: Record<string, unknown>;
1964
+ /** Обёртка поля (например, FormField). */
1965
+ wrapper?: JsonNode;
1966
+ }
1967
+ ```
1968
+
1969
+ _Source: src/types/json-schema.ts_
1970
+
1971
+ ### JsonFormRenderer
1972
+
1973
+ **Kind:** `function`
1974
+
1975
+ Главный компонент пакета. Рендерит форму, описанную JSON-схемой.
1976
+
1977
+ Должен использоваться внутри {@link JsonRendererProvider}, который снабжает рендерер
1978
+ реестром компонентов. Без реестра компонент бросит исключение при попытке резолва.
1979
+
1980
+ **Signature:**
1981
+ ```typescript
1982
+ export function JsonFormRenderer<T>({
1983
+ schema,
1984
+ renderBehavior,
1985
+ onSchemaReady,
1986
+ validate = false,
1987
+ }: JsonFormRendererProps<T>): ReactNode
1988
+ ```
1989
+
1990
+ **Examples:**
1991
+
1992
+ Форма из JSON-схемы (M1)
1993
+ ```tsx
1994
+ import { useMemo } from 'react';
1995
+ import { createModel } from '@reformer/core';
1996
+ import { Input, Box, FormField } from '@reformer/ui-kit';
1997
+ import {
1998
+ JsonFormRenderer,
1999
+ JsonRendererProvider,
2000
+ defineRegistry,
2001
+ FIELD_WRAPPER,
2002
+ type JsonFormSchema,
2003
+ } from '@reformer/renderer-json';
2004
+
2005
+ // Привязки — строки-операторы: '$model(...)', '$component(...)', '$dataSource(...)'.
2006
+ const schema: JsonFormSchema = {
2007
+ version: '1.0',
2008
+ root: {
2009
+ component: '$component(Box)',
2010
+ children: [
2011
+ {
2012
+ value: '$model(email)',
2013
+ component: '$component(Input)',
2014
+ componentProps: { label: 'Email' },
2015
+ },
2016
+ ],
2017
+ },
2018
+ };
2019
+
2020
+ type MyForm = { email: string };
2021
+
2022
+ function MyFormPage() {
2023
+ // M1: модель — источник истины значений; листья схемы биндятся к её сигналам.
2024
+ const model = useMemo(() => createModel<MyForm>({ email: '' }), []);
2025
+ const registry = useMemo(() => defineRegistry((reg) => {
2026
+ reg.component('Input', Input);
2027
+ reg.component('Box', Box);
2028
+ reg.component(FIELD_WRAPPER, FormField); // системная обёртка полей
2029
+ }), []);
2030
+
2031
+ // Модель передаётся через провайдер (settings.model), НЕ пропом рендерера.
2032
+ return (
2033
+ <JsonRendererProvider settings={{ registry, model }}>
2034
+ <JsonFormRenderer<MyForm> schema={schema} validate={import.meta.env.DEV} />
2035
+ </JsonRendererProvider>
2036
+ );
2037
+ }
2038
+ ```
2039
+
2040
+ **Note**: `JsonFormRenderer` принимает ТОЛЬКО `{ schema, renderBehavior?, onSchemaReady?, validate? }`.
2041
+ Под M1 модель (`FormModel`) передаётся через {@link JsonRendererProvider} settings (`model`);
2042
+ листья JSON-схемы биндятся к её сигналам конвертером {@link createRenderSchemaFromJsonM1}.
2043
+
2044
+ **See also:**
2045
+ - [docs/llms/01-overview.md](../../docs/llms/01-overview.md)
2046
+
2047
+ _Source: src/components/json-form-renderer.tsx_
2048
+
2049
+ ### JsonFormRendererProps
2050
+
2051
+ **Kind:** `interface`
2052
+
2053
+ Props of {@link JsonFormRenderer}.
2054
+
2055
+ **Signature:**
2056
+ ```typescript
2057
+ export interface JsonFormRendererProps<T> {
2058
+ /** JSON-схема формы. См. {@link JsonFormSchema}. */
2059
+ schema: JsonFormSchema;
2060
+ /** Опциональный behavior: hideWhen/patchProps/onComponentEvent поверх готовой схемы. */
2061
+ renderBehavior?: RenderBehaviorFn<T>;
2062
+ /** Колбэк, получающий построенный `RenderSchemaProxy` для внешних манипуляций. */
2063
+ onSchemaReady?: (schema: RenderSchemaProxy<T>) => void;
2064
+ /**
2065
+ * Валидировать JSON-схему против мета-схемы перед рендером. При ошибках рисует
2066
+ * {@link SchemaErrorPanel} вместо формы. ajv грузится **динамически** (`import('../validate')`) —
2067
+ * в prod-бандл не попадает, пока `validate` не включён.
2068
+ *
2069
+ * По умолчанию `false`. Чтобы валидировать только в dev, приложение передаёт значение из
2070
+ * СВОЕГО окружения: `validate={import.meta.env.DEV}` — детекцию dev нельзя «запечь» в пакет,
2071
+ * т.к. `import.meta.env.DEV` инлайнится в `false` при production-сборке самого пакета.
2072
+ */
2073
+ validate?: boolean;
2074
+ }
2075
+ ```
2076
+
2077
+ _Source: src/components/json-form-renderer.tsx_
2078
+
2079
+ ### JsonFormSchema
2080
+
2081
+ **Kind:** `interface`
2082
+
2083
+ Корневая JSON-схема формы.
2084
+
2085
+ **Signature:**
2086
+ ```typescript
2087
+ export interface JsonFormSchema {
2088
+ /**
2089
+ * Путь к мета-схеме для IDE (VSCode подсветит структуру/синтаксис/имена `$component`).
2090
+ * Игнорируется конвертером. Сгенерировать конкретную мета-схему: `gen-form-json-schema.ts`.
2091
+ */
2092
+ $schema?: string;
2093
+ /** Версия схемы (для миграций). */
2094
+ version?: string;
2095
+ /** Корневой узел. */
2096
+ root: JsonNode;
2097
+ }
2098
+ ```
2099
+
2100
+ **Examples:**
2101
+
2102
+ ```ts
2103
+ const schema: JsonFormSchema = {
2104
+ version: '1.0',
2105
+ root: {
2106
+ component: '$component(Box)',
2107
+ children: [{ value: '$model(email)', component: '$component(Input)' }],
2108
+ },
2109
+ };
2110
+ ```
2111
+
2112
+ _Source: src/types/json-schema.ts_
2113
+
2114
+ ### JsonNode
2115
+
2116
+ **Kind:** `type`
2117
+
2118
+ Узел JSON-схемы (M1).
2119
+
2120
+ **Signature:**
2121
+ ```typescript
2122
+ export type JsonNode = JsonFieldNode | JsonArrayNode | JsonContainerNode;
2123
+ ```
2124
+
2125
+ _Source: src/types/json-schema.ts_
2126
+
2127
+ ### JsonOperator
2128
+
2129
+ **Kind:** `type`
2130
+
2131
+ Любой строковый оператор JSON-схемы.
2132
+
2133
+ **Signature:**
2134
+ ```typescript
2135
+ export type JsonOperator = ModelOp | ComponentOp | DataSourceOp | FnOp | LocaleOp;
2136
+ ```
2137
+
2138
+ _Source: src/operators.ts_
2139
+
2140
+ ### JsonRendererProvider
2141
+
2142
+ **Kind:** `function`
2143
+
2144
+ Провайдер настроек для {@link JsonFormRenderer}. Прокидывает реестр и
2145
+ fieldWrapper во вложенные компоненты через React Context.
2146
+
2147
+ Поддерживает вложенность: внутренний провайдер сливается с внешним
2148
+ (внешний имеет приоритет в случае дублей имён в реестре).
2149
+
2150
+ **Signature:**
2151
+ ```typescript
2152
+ export function JsonRendererProvider({ settings, children }: JsonRendererProviderProps): ReactNode
2153
+ ```
2154
+
2155
+ **Examples:**
2156
+
2157
+ M1: реестр + модель через settings
2158
+ ```tsx
2159
+ // Модель — источник истины (M1); листья схемы биндятся к её сигналам.
2160
+ const model = useMemo(() => createModel<MyForm>({ email: '' }), []);
2161
+ const registry = useMemo(() => defineRegistry((reg) => {
2162
+ reg.component('Input', Input);
2163
+ reg.component(FIELD_WRAPPER, FormField);
2164
+ }), []);
2165
+
2166
+ <JsonRendererProvider settings={{ registry, model }}>
2167
+ <JsonFormRenderer<MyForm> schema={schema} />
2168
+ </JsonRendererProvider>
2169
+ ```
2170
+
2171
+ _Source: src/context/json-renderer-context.tsx_
2172
+
2173
+ ### JsonRendererProviderProps
2174
+
2175
+ **Kind:** `interface`
2176
+
2177
+ Props {@link JsonRendererProvider}.
2178
+
2179
+ **Signature:**
2180
+ ```typescript
2181
+ export interface JsonRendererProviderProps {
2182
+ /** Настройки рендерера, как минимум содержащие `registry`. */
2183
+ settings: JsonRendererSettings;
2184
+ /** Дочернее поддерево, в котором доступен `JsonFormRenderer` и `useJsonRendererSettings`. */
2185
+ children: ReactNode;
2186
+ }
2187
+ ```
2188
+
2189
+ _Source: src/context/json-renderer-context.tsx_
2190
+
2191
+ ### JsonRendererSettings
2192
+
2193
+ **Kind:** `interface`
2194
+
2195
+ Расширенные настройки рендерера: всё из `RendererSettings` плюс реестр.
2196
+
2197
+ **Signature:**
2198
+ ```typescript
2199
+ export interface JsonRendererSettings extends RendererSettings {
2200
+ /** Реестр компонентов и source-значений. См. {@link defineRegistry}. */
2201
+ registry?: ComponentRegistry;
2202
+ /**
2203
+ * Модель данных (M1, единая схема). Если задана — JSON-листья биндятся к сигналам модели
2204
+ * (`model.signalAt(selector)`), а форма строится из той же JSON-схемы (без отдельной схемы формы).
2205
+ */
2206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2207
+ model?: FormModel<any>;
2208
+ }
2209
+ ```
2210
+
2211
+ _Source: src/context/json-renderer-context.tsx_
2212
+
2213
+ ### LOCALE_SERVICE
2214
+
2215
+ **Kind:** `const`
2216
+
2217
+ Зарезервированный ключ реестра для единственного сервиса локализации.
2218
+
2219
+ `reg.locale(...)` кладёт сервис под этот ключ; оператор `"$locale(key)"` резолвится через него
2220
+ (`registry.getLocale()`). Ключ намеренно отличается от имени оператора, чтобы `$dataSource`/`$fn`
2221
+ не могли достать сервис локализации по имени.
2222
+
2223
+ **Signature:**
2224
+ ```typescript
2225
+ export const LOCALE_SERVICE
2226
+ ```
2227
+
2228
+ **Examples:**
2229
+
2230
+ ```typescript
2231
+ import { defineRegistry, createLocaleResolver } from '@reformer/renderer-json';
2232
+
2233
+ const registry = defineRegistry((reg) => {
2234
+ reg.locale(createLocaleResolver({ 'fields.email.label': 'Email' }));
2235
+ });
2236
+ ```
2237
+
2238
+ _Source: src/registry/constants.ts_
2239
+
2240
+ ### LocaleOp
2241
+
2242
+ **Kind:** `type`
2243
+
2244
+ Строка-оператор ссылки на ключ локализации: `` `$locale(${key})` ``.
2245
+
2246
+ **Signature:**
2247
+ ```typescript
2248
+ export type LocaleOp = `$locale(${string})`;
2249
+ ```
2250
+
2251
+ _Source: src/operators.ts_
2252
+
2253
+ ### LocaleParams
2254
+
2255
+ **Kind:** `type`
2256
+
2257
+ Параметры для интерполяции/склонения (ICU-values, i18next-values и т.п.).
2258
+
2259
+ **Signature:**
2260
+ ```typescript
2261
+ export type LocaleParams = Record<string, unknown>;
2262
+ ```
2263
+
2264
+ _Source: src/locale/locale-service.ts_
2265
+
2266
+ ### LocaleProvider
2267
+
2268
+ **Kind:** `function`
2269
+
2270
+ Провайдер сервиса локализации для реактивного пути. Оберни форму (или её часть) и передавай
2271
+ новый `service` при смене языка — вложенные `I18n` перерисуются без пересборки схемы.
2272
+
2273
+ **Signature:**
2274
+ ```typescript
2275
+ export function LocaleProvider({ service, children }: LocaleProviderProps): ReactNode
2276
+ ```
2277
+
2278
+ **Examples:**
2279
+
2280
+ Переключение языка
2281
+ ```tsx
2282
+ import { LocaleProvider, createLocaleService, JsonFormRenderer } from '@reformer/renderer-json';
2283
+
2284
+ const services = { ru: createLocaleService(ruTable), en: createLocaleService(enTable) };
2285
+ function App() {
2286
+ const [lang, setLang] = useState<'ru' | 'en'>('ru');
2287
+ return (
2288
+ <LocaleProvider service={services[lang]}>
2289
+ <JsonFormRenderer schema={schema} />
2290
+ </LocaleProvider>
2291
+ );
2292
+ }
2293
+ ```
2294
+
2295
+ _Source: src/locale/locale-context.tsx_
2296
+
2297
+ ### LocaleProviderProps
2298
+
2299
+ **Kind:** `interface`
2300
+
2301
+ Props {@link LocaleProvider}.
2302
+
2303
+ **Signature:**
2304
+ ```typescript
2305
+ export interface LocaleProviderProps {
2306
+ /** Текущий сервис локализации. Смена ссылки (напр. при переключении языка) перерендеривает все `I18n`. */
2307
+ service: LocaleService;
2308
+ /** Поддерево, в котором `I18n`/`useLocale` видят этот сервис. */
2309
+ children: ReactNode;
2310
+ }
2311
+ ```
2312
+
2313
+ _Source: src/locale/locale-context.tsx_
2314
+
2315
+ ### LocaleResolver
2316
+
2317
+ **Kind:** `type`
2318
+
2319
+ Резолвер ключа (+опциональные параметры) в строку.
2320
+
2321
+ **Signature:**
2322
+ ```typescript
2323
+ export type LocaleResolver = (key: string, params?: LocaleParams) => string;
2324
+ ```
2325
+
2326
+ _Source: src/locale/locale-service.ts_
2327
+
2328
+ ### LocaleService
2329
+
2330
+ **Kind:** `interface`
2331
+
2332
+ Сервис локализации в реестре / контексте. `resolve` обязателен (строковый путь); `render`
2333
+ опционален (rich/markdown-путь через `Trans`/`RichText`); `keys` (если задан — напр., из каталога)
2334
+ включает проверку опечаток ключей на этапе `validateFormSchema`.
2335
+
2336
+ **Signature:**
2337
+ ```typescript
2338
+ export interface LocaleService {
2339
+ /** Ключ (+params) → локализованная строка (при промахе — сам ключ). Для label/placeholder и fallback `Trans`. */
2340
+ resolve: LocaleResolver;
2341
+ /**
2342
+ * Ключ (+params) → rich-контент (markdown/JSX). Опционален. Используется `Trans`/`RichText`, когда
2343
+ * нужен не голый текст, а разметка. Реализуется потребителем (напр. через `react-markdown`).
2344
+ */
2345
+ render?(key: string, params?: LocaleParams): ReactNode;
2346
+ /** Множество известных ключей. Есть → `validate` ловит `unknown locale key`; нет → проверка мягко пропускается (как для `$model`). */
2347
+ keys?: readonly string[];
2348
+ }
2349
+ ```
2350
+
2351
+ _Source: src/locale/locale-service.ts_
2352
+
2353
+ ### ModelOp
2354
+
2355
+ **Kind:** `type`
2356
+
2357
+ Строка-оператор привязки к полю/массиву модели: `` `$model(${path})` ``.
2358
+
2359
+ **Signature:**
2360
+ ```typescript
2361
+ export type ModelOp = `$model(${string})`;
2362
+ ```
2363
+
2364
+ _Source: src/operators.ts_
2365
+
2366
+ ### ParsedOperator
2367
+
2368
+ **Kind:** `interface`
2369
+
2370
+ Разобранный оператор: тип + аргумент (путь/имя/ключ).
2371
+
2372
+ **Signature:**
2373
+ ```typescript
2374
+ export interface ParsedOperator {
2375
+ op: 'model' | 'component' | 'dataSource' | 'fn' | 'locale';
2376
+ arg: string;
2377
+ }
2378
+ ```
2379
+
2380
+ _Source: src/operators.ts_
2381
+
2382
+ ### parseOperator
2383
+
2384
+ **Kind:** `function`
2385
+
2386
+ Разбор строки-оператора `"$op(arg)"`. Возвращает `null` для не-операторов (обычных строк),
2387
+ чтобы вызывающий оставил значение как есть.
2388
+
2389
+ **Signature:**
2390
+ ```typescript
2391
+ export function parseOperator(value: unknown): ParsedOperator | null
2392
+ ```
2393
+
2394
+ **Parameters:**
2395
+ - `value` — - Проверяемое значение (не-строки сразу дают `null`).
2396
+
2397
+ **Returns:**
2398
+
2399
+ **Examples:**
2400
+
2401
+ ```ts
2402
+ parseOperator('$model(loanType)'); // { op: 'model', arg: 'loanType' }
2403
+ parseOperator('$dataSource(LOAN_TYPES)'); // { op: 'dataSource', arg: 'LOAN_TYPES' }
2404
+ parseOperator('Введите сумму'); // null (обычная строка)
2405
+ ```
2406
+
2407
+ _Source: src/operators.ts_
2408
+
2409
+ ### RegistryBuilder
2410
+
2411
+ **Kind:** `interface`
2412
+
2413
+ Builder, который попадает в callback {@link defineRegistry}.
2414
+
2415
+ **Signature:**
2416
+ ```typescript
2417
+ export interface RegistryBuilder {
2418
+ /**
2419
+ * Зарегистрировать React-компонент под именем — доступен в схеме как
2420
+ * `$component(name)`. Одним методом регистрируются и компоненты-листья
2421
+ * (Input/Select), и контейнеры (Box/Section/FormField): роль узла (лист vs
2422
+ * контейнер) определяется структурой узла схемы (`value` vs `children`),
2423
+ * а не регистрацией.
2424
+ */
2425
+ component<P>(name: string, component: ComponentType<P>, description?: string): void;
2426
+ dataSource<T>(name: string, value: T, description?: string): void;
2427
+ /**
2428
+ * Зарегистрировать функцию под именем — доступна в схеме как `$fn(name)` (форматтеры,
2429
+ * компараторы, `itemLabel`, обработчики). Отдельный от `dataSource` вид: бросает при регистрации,
2430
+ * если передана не функция, и `validateFormSchema` отклоняет перепутанные `$fn`/`$dataSource`.
2431
+ */
2432
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2433
+ fn<F extends (...args: any[]) => any>(name: string, fn: F, description?: string): void;
2434
+ /**
2435
+ * Зарегистрировать единственный сервис локализации для оператора `$locale(key)`. Принимает
2436
+ * {@link LocaleService} (каталог через `createLocaleResolver` — включает validate-time проверку
2437
+ * ключей) либо голый резолвер `(key) => string`. Кладётся под зарезервированный ключ `LOCALE_SERVICE`.
2438
+ */
2439
+ locale(service: LocaleService | LocaleResolver, description?: string): void;
2440
+ }
2441
+ ```
2442
+
2443
+ **Examples:**
2444
+
2445
+ ```typescript
2446
+ defineRegistry((reg: RegistryBuilder) => {
2447
+ reg.component('Input', Input);
2448
+ reg.component('Box', Box);
2449
+ reg.dataSource('LOAN_TYPES', LOAN_TYPES);
2450
+ reg.fn('formatCurrency', formatCurrency);
2451
+ reg.locale(createLocaleResolver({ 'fields.email.label': 'Email' }));
2452
+ });
2453
+ ```
2454
+
2455
+ _Source: src/registry/types.ts_
2456
+
2457
+ ### SchemaErrorPanel
2458
+
2459
+ **Kind:** `function`
2460
+
2461
+ Список ошибок схемы (path + message), с пометкой источника. Без внешних UI-зависимостей
2462
+ (инлайн-стили), помечен `role="alert"` и `data-testid="schema-error-panel"`.
2463
+
2464
+ Обычно рендерится автоматически внутри {@link JsonFormRenderer} (при `validate` и невалидной
2465
+ схеме). Можно использовать напрямую для показа результата `validateFormSchema().errors`.
2466
+
2467
+ **Signature:**
2468
+ ```typescript
2469
+ export function SchemaErrorPanel({ errors }: SchemaErrorPanelProps): ReactNode
2470
+ ```
2471
+
2472
+ **Parameters:**
2473
+ - `props` — - {@link SchemaErrorPanelProps} (массив `errors`).
2474
+
2475
+ **Returns:** Панель со списком ошибок.
2476
+
2477
+ **Examples:**
2478
+
2479
+ Показать ошибки валидации схемы вручную
2480
+ ```tsx
2481
+ import { validateFormSchema } from '@reformer/renderer-json/validate';
2482
+
2483
+ const { valid, errors } = validateFormSchema(schema, { registry });
2484
+ if (!valid) return <SchemaErrorPanel errors={errors} />;
2485
+ ```
2486
+
2487
+ _Source: src/components/schema-error-panel.tsx_
2488
+
2489
+ ### SchemaErrorPanelProps
2490
+
2491
+ **Kind:** `interface`
2492
+
2493
+ Props of {@link SchemaErrorPanel}.
2494
+
2495
+ **Signature:**
2496
+ ```typescript
2497
+ export interface SchemaErrorPanelProps {
2498
+ /** Человекочитаемые ошибки (`validateFormSchema().errors`). */
2499
+ errors: string[];
2500
+ }
2501
+ ```
2502
+
2503
+ _Source: src/components/schema-error-panel.tsx_
2504
+
2505
+ ### unwrapSignalValues
2506
+
2507
+ **Kind:** `function`
2508
+
2509
+ Разворачивает значения-сигналы в обычные (читает `.value`), литералы отдаёт как есть. Если сигналов
2510
+ не было — возвращает исходный объект (стабильная ссылка). Pure — тестируется без DOM.
2511
+
2512
+ **Signature:**
2513
+ ```typescript
2514
+ export function unwrapSignalValues(values?: LocaleParams): LocaleParams | undefined
2515
+ ```
2516
+
2517
+ _Source: src/locale/use-signal-value.ts_
2518
+
2519
+ ### useJsonRendererSettings
2520
+
2521
+ **Kind:** `function`
2522
+
2523
+ Хук для чтения текущих настроек {@link JsonRendererProvider}.
2524
+
2525
+ В режиме разработки бросает исключение, если вызван вне провайдера или
2526
+ если в провайдере не передан `registry`.
2527
+
2528
+ **Signature:**
2529
+ ```typescript
2530
+ export function useJsonRendererSettings(): JsonRendererSettings
2531
+ ```
2532
+
2533
+ **Returns:** Текущие {@link JsonRendererSettings}.
2534
+
2535
+ **Examples:**
2536
+
2537
+ ```tsx
2538
+ function MyControl() {
2539
+ const { registry } = useJsonRendererSettings();
2540
+ return <span>{registry?.has('Input') ? 'ready' : 'not registered'}</span>;
2541
+ }
2542
+ ```
2543
+
2544
+ _Source: src/context/json-renderer-context.tsx_
2545
+
2546
+ ### useLocale
2547
+
2548
+ **Kind:** `function`
2549
+
2550
+ Текущий сервис локализации из {@link LocaleProvider}. Без провайдера — fallback-to-key.
2551
+ Используется внутри {@link I18n}; вызывай напрямую для собственного localized-компонента.
2552
+
2553
+ **Signature:**
2554
+ ```typescript
2555
+ export function useLocale(): LocaleService
2556
+ ```
2557
+
2558
+ **Returns:** Активный {@link LocaleService}.
2559
+
2560
+ _Source: src/locale/locale-context.tsx_
2561
+
2562
+ ### useSignalValues
2563
+
2564
+ **Kind:** `function`
2565
+
2566
+ Реактивно разворачивает `values`-сигналы. Перерендер компонента при изменении любого сигнала;
2567
+ литералы проходят как есть. Снапшот кэшируется (стабильная ссылка при неизменных значениях) —
2568
+ иначе `useSyncExternalStore` зациклится.
2569
+
2570
+ **Signature:**
2571
+ ```typescript
2572
+ export function useSignalValues(values?: LocaleParams): LocaleParams | undefined
2573
+ ```
2574
+
2575
+ **Parameters:**
2576
+ - `values` — - Параметры сообщения (сигналы и/или литералы).
2577
+
2578
+ **Returns:** Развёрнутые значения для передачи в `LocaleService.resolve/render`.
2579
+
2580
+ _Source: src/locale/use-signal-value.ts_