@reformer/ui-kit 6.0.0 → 7.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/dist/components/form-array/form-array-section.d.ts +44 -0
- package/dist/components/form-wizard/form-wizard-actions.d.ts +36 -0
- package/dist/components/form-wizard/form-wizard-progress.d.ts +30 -0
- package/dist/components/form-wizard/form-wizard.d.ts +107 -8
- package/dist/components/form-wizard/step-indicator.d.ts +27 -0
- package/dist/components/state/error-state.d.ts +21 -0
- package/dist/components/state/loading-state.d.ts +18 -0
- package/dist/components/ui/async-boundary.d.ts +4 -2
- package/dist/components/ui/box.d.ts +9 -5
- package/dist/components/ui/collapsible.d.ts +10 -6
- package/dist/components/ui/form-field.d.ts +26 -13
- package/dist/components/ui/input.d.ts +21 -0
- package/dist/components/ui/section.d.ts +9 -5
- package/dist/components/ui/select-resource.d.ts +176 -0
- package/dist/components/ui/select-resource.test.d.ts +1 -0
- package/dist/components/ui/select.d.ts +59 -60
- package/dist/index.d.ts +1 -1
- package/dist/ui/select.js +294 -141
- package/llms.txt +786 -251
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.
|
|
3
3
|
|
|
4
4
|
> Styled form components with Tailwind CSS and Radix UI for @reformer ecosystem
|
|
5
|
-
> Package: @reformer/ui-kit • Version:
|
|
5
|
+
> Package: @reformer/ui-kit • Version: 6.0.0
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
- 01-overview.md — Overview
|
|
@@ -84,34 +84,53 @@ import { Button } from '@reformer/ui-kit/button';
|
|
|
84
84
|
|
|
85
85
|
## 3. Quick Start
|
|
86
86
|
|
|
87
|
-
Минимальная форма из двух полей с валидацией и
|
|
88
|
-
|
|
87
|
+
Минимальная форма из двух полей с валидацией и сабмитом (архитектура M1:
|
|
88
|
+
`createModel` → схема → `createForm({ model, schema })` → `validateFormModel`).
|
|
89
|
+
`FormField` самостоятельно подцепляет `value`/`error`/`pending` через `@reformer/cdk`:
|
|
89
90
|
|
|
90
91
|
```tsx
|
|
91
92
|
import { useMemo } from 'react';
|
|
92
|
-
import { createForm,
|
|
93
|
-
import {
|
|
94
|
-
import {
|
|
93
|
+
import { createModel, createForm, validateFormModel } from '@reformer/core';
|
|
94
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
95
|
+
import { Button, FormField, Input, InputPassword } from '@reformer/ui-kit';
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
type RegistrationForm = {
|
|
97
98
|
email: string;
|
|
98
99
|
password: string;
|
|
99
|
-
}
|
|
100
|
+
};
|
|
100
101
|
|
|
101
102
|
function RegistrationPage() {
|
|
102
|
-
const form = useMemo(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
const { model, form, schema } = useMemo(() => {
|
|
104
|
+
// 1) Модель — источник истины значений.
|
|
105
|
+
const model = createModel<RegistrationForm>({ email: '', password: '' });
|
|
106
|
+
// 2) Схема: лист = { value: сигнал модели, component, componentProps?, validators }.
|
|
107
|
+
const schema = {
|
|
108
|
+
children: [
|
|
109
|
+
{
|
|
110
|
+
value: model.$.email,
|
|
111
|
+
component: Input,
|
|
112
|
+
componentProps: { label: 'Email', type: 'email', testId: 'email' },
|
|
113
|
+
validators: [required({ message: 'Email обязателен' }), email()],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
value: model.$.password,
|
|
117
|
+
component: InputPassword,
|
|
118
|
+
componentProps: { label: 'Пароль', testId: 'password' },
|
|
119
|
+
validators: [required({ message: 'Пароль обязателен' }), minLength(8)],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
// 3) createForm привязывает ноды к сигналам модели → FormProxy.
|
|
124
|
+
const form = createForm<RegistrationForm>({ model, schema });
|
|
125
|
+
return { model, form, schema };
|
|
126
|
+
}, []);
|
|
110
127
|
|
|
111
128
|
const onSubmit = async () => {
|
|
112
129
|
form.markAsTouched();
|
|
113
|
-
|
|
114
|
-
|
|
130
|
+
// 4) Валидация всей модели по схеме.
|
|
131
|
+
const res = await validateFormModel(model, schema);
|
|
132
|
+
if (!res.valid) return;
|
|
133
|
+
console.log('values', model.get());
|
|
115
134
|
};
|
|
116
135
|
|
|
117
136
|
return (
|
|
@@ -122,8 +141,8 @@ function RegistrationPage() {
|
|
|
122
141
|
}}
|
|
123
142
|
className="space-y-4 max-w-md"
|
|
124
143
|
>
|
|
125
|
-
<FormField control={form.email}
|
|
126
|
-
<FormField control={form.password}
|
|
144
|
+
<FormField control={form.email} />
|
|
145
|
+
<FormField control={form.password} />
|
|
127
146
|
<Button type="submit">Зарегистрироваться</Button>
|
|
128
147
|
</form>
|
|
129
148
|
);
|
|
@@ -217,18 +236,29 @@ import { Input } from '@reformer/ui-kit';
|
|
|
217
236
|
> прокидывается — `onChange` просто не вызывается. Поэтому в форме поле должно
|
|
218
237
|
> иметь тип `number | null`, а не `number`.
|
|
219
238
|
|
|
220
|
-
Email-валидация на уровне
|
|
239
|
+
Email-валидация на уровне формы (M1: `createModel` → схема с листом
|
|
240
|
+
`{ value: model.$.email, component, validators }` → `createForm({ model, schema })`):
|
|
221
241
|
|
|
222
242
|
```tsx
|
|
223
|
-
import {
|
|
224
|
-
import {
|
|
225
|
-
import {
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
import { createModel, createForm } from '@reformer/core';
|
|
244
|
+
import { required, email } from '@reformer/core/validators';
|
|
245
|
+
import { Input, FormField } from '@reformer/ui-kit';
|
|
246
|
+
|
|
247
|
+
const model = createModel<{ email: string }>({ email: '' });
|
|
248
|
+
const schema = {
|
|
249
|
+
children: [
|
|
250
|
+
{
|
|
251
|
+
value: model.$.email,
|
|
252
|
+
component: Input,
|
|
253
|
+
componentProps: { type: 'email', label: 'Email', testId: 'email' },
|
|
254
|
+
validators: [required(), email()],
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
const form = createForm<{ email: string }>({ model, schema });
|
|
230
259
|
|
|
231
|
-
|
|
260
|
+
// Через FormField значение/ошибки подцепляются автоматически:
|
|
261
|
+
<FormField control={form.email} testId="email" />;
|
|
232
262
|
```
|
|
233
263
|
|
|
234
264
|
### Anti-patterns
|
|
@@ -291,7 +321,7 @@ import { InputMask } from '@reformer/ui-kit';
|
|
|
291
321
|
нужно делать в behavior `transformValue` или при сабмите.
|
|
292
322
|
- Использовать `mask` для сложных правил (валидация диапазонов, контрольные
|
|
293
323
|
суммы) — `InputMask` только направляет ввод, не валидирует. Валидацию вешать
|
|
294
|
-
через `
|
|
324
|
+
через массив `validators` листа схемы (`validateFormModel`).
|
|
295
325
|
|
|
296
326
|
## 9. InputPassword
|
|
297
327
|
|
|
@@ -398,14 +428,14 @@ import { Textarea } from '@reformer/ui-kit';
|
|
|
398
428
|
имеет логики авто-роста.
|
|
399
429
|
- Полагаться на `maxLength` как валидатор: это soft-лимит на ввод; для бизнес-
|
|
400
430
|
правил (например, `длина <= 500 на русском, <= 1000 на английском`) ставить
|
|
401
|
-
`
|
|
431
|
+
`validators` в лист схемы (`{ value: model.$.field, component, validators }`).
|
|
402
432
|
|
|
403
433
|
## 11. See also
|
|
404
434
|
|
|
405
435
|
- [03-choice-fields.md](03-choice-fields.md) — Select, Checkbox, RadioGroup.
|
|
406
436
|
- [05-form-field-integration.md](05-form-field-integration.md) — как все эти поля автоматически подключаются через `FormField`.
|
|
407
437
|
- [06-troubleshooting.md](06-troubleshooting.md) — «number возвращает строку», «mask пропускает символы», «password toggle не появляется».
|
|
408
|
-
- Эталон: `RegistrationForm.tsx`, `
|
|
438
|
+
- Эталон: `examples/registration-form/RegistrationForm.tsx`, `examples/complex-multy-step-form/schemas/schema.ts` (monorepo examples).
|
|
409
439
|
|
|
410
440
|
## 12. Checkbox
|
|
411
441
|
|
|
@@ -453,12 +483,16 @@ import { Checkbox } from '@reformer/ui-kit';
|
|
|
453
483
|
label сверху):
|
|
454
484
|
|
|
455
485
|
```tsx
|
|
456
|
-
import {
|
|
486
|
+
import { createModel, createForm } from '@reformer/core';
|
|
457
487
|
import { Checkbox, FormField } from '@reformer/ui-kit';
|
|
458
488
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
489
|
+
const model = createModel<{ accept: boolean }>({ accept: false });
|
|
490
|
+
const schema = {
|
|
491
|
+
children: [
|
|
492
|
+
{ value: model.$.accept, component: Checkbox, componentProps: { label: 'Принять' } },
|
|
493
|
+
],
|
|
494
|
+
};
|
|
495
|
+
const form = createForm<{ accept: boolean }>({ model, schema });
|
|
462
496
|
|
|
463
497
|
<FormField control={form.accept} testId="accept" />;
|
|
464
498
|
```
|
|
@@ -534,13 +568,19 @@ const LOAN_TYPES = [
|
|
|
534
568
|
В составе формы:
|
|
535
569
|
|
|
536
570
|
```tsx
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
571
|
+
import { createModel, createForm } from '@reformer/core';
|
|
572
|
+
|
|
573
|
+
const model = createModel<{ loanType: string }>({ loanType: 'consumer' });
|
|
574
|
+
const schema = {
|
|
575
|
+
children: [
|
|
576
|
+
{
|
|
577
|
+
value: model.$.loanType,
|
|
578
|
+
component: RadioGroup,
|
|
579
|
+
componentProps: { options: LOAN_TYPES },
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
const form = createForm<{ loanType: string }>({ model, schema });
|
|
544
584
|
|
|
545
585
|
<FormField control={form.loanType} testId="loan-type" />;
|
|
546
586
|
```
|
|
@@ -562,17 +602,29 @@ const form = createForm<FormSchema<{ loanType: string }>>({
|
|
|
562
602
|
данных:
|
|
563
603
|
|
|
564
604
|
- **Inline**: `options={[…]}` — массив `{ value, label, group? }`.
|
|
565
|
-
- **Resource**: `resource={{ type, load }}` — асинхронная загрузка
|
|
605
|
+
- **Resource**: `resource={{ type, load }}` — асинхронная загрузка со стратегией `type`:
|
|
606
|
+
- `static` — один `load({})` при маунте, без поиска (снимок);
|
|
607
|
+
- `preload` — грузит всё сразу, поиск фильтрует опции **на клиенте**;
|
|
608
|
+
- `partial` — **серверные** поиск (`load({ search })` с debounce ~300 мс) и
|
|
609
|
+
пагинация (`load({ page })` по мере прокрутки списка до `totalCount`).
|
|
610
|
+
|
|
611
|
+
Для `preload`/`partial` в дропдауне появляется поле поиска.
|
|
566
612
|
|
|
567
613
|
### API
|
|
568
614
|
|
|
569
615
|
```typescript
|
|
570
616
|
interface ResourceConfig<T> {
|
|
617
|
+
/** Стратегия загрузки. Если не задана — трактуется как `static`. */
|
|
571
618
|
type: 'static' | 'preload' | 'partial';
|
|
572
|
-
load: (params?:
|
|
619
|
+
load: (params?: {
|
|
620
|
+
search?: string; // серверная фильтрация (partial)
|
|
621
|
+
page?: number; // 1-based, пагинация (partial)
|
|
622
|
+
pageSize?: number;
|
|
623
|
+
}) => Promise<{
|
|
573
624
|
items: Array<{ id: string | number; label: string; value: T; group?: string }>;
|
|
574
|
-
totalCount: number;
|
|
625
|
+
totalCount: number; // общее число опций — для пагинации (partial)
|
|
575
626
|
}>;
|
|
627
|
+
pageSize?: number; // размер страницы для partial (по умолчанию 20)
|
|
576
628
|
}
|
|
577
629
|
|
|
578
630
|
interface SelectProps<T> {
|
|
@@ -593,7 +645,7 @@ interface SelectProps<T> {
|
|
|
593
645
|
| Prop | Тип | Default | Описание |
|
|
594
646
|
| ------------- | --------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
595
647
|
| `options` | `Array<{value,label,group?}>` | — | Inline-варианты. `value` приводится к строке. `group` опционально — варианты с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. |
|
|
596
|
-
| `resource` | `ResourceConfig<T>` | — | Асинхронный
|
|
648
|
+
| `resource` | `ResourceConfig<T>` | — | Асинхронный источник со стратегией `type` (`static`/`preload`/`partial`). Во время первичной загрузки `Select` показывает `Loading...` и блокируется; при пагинации (`partial`) внизу списка — `Loading more...`. |
|
|
597
649
|
| `value` | `string \| null` | `null` | Выбранное значение (всегда строка из `option.value`). |
|
|
598
650
|
| `onChange` | `(value: string \| null) => void` | — | Срабатывает при выборе. При нажатии на крестик (`clearable`) приходит `null`. |
|
|
599
651
|
| `placeholder` | `string` | `'Select an option...'` | Подсказка в триггере. |
|
|
@@ -634,10 +686,10 @@ import { Select } from '@reformer/ui-kit';
|
|
|
634
686
|
/>;
|
|
635
687
|
```
|
|
636
688
|
|
|
637
|
-
Async `resource` (
|
|
689
|
+
Async `resource`, стратегия `preload` (грузим всё, поиск на клиенте):
|
|
638
690
|
|
|
639
691
|
```tsx
|
|
640
|
-
import { Select, type ResourceConfig } from '@reformer/ui-kit
|
|
692
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit';
|
|
641
693
|
|
|
642
694
|
const banksResource: ResourceConfig<string> = {
|
|
643
695
|
type: 'preload',
|
|
@@ -654,6 +706,26 @@ const banksResource: ResourceConfig<string> = {
|
|
|
654
706
|
<Select value={bankId} onChange={setBankId} resource={banksResource} />;
|
|
655
707
|
```
|
|
656
708
|
|
|
709
|
+
Стратегия `partial` (серверные поиск + пагинация больших списков):
|
|
710
|
+
|
|
711
|
+
```tsx
|
|
712
|
+
const usersResource: ResourceConfig<string> = {
|
|
713
|
+
type: 'partial',
|
|
714
|
+
pageSize: 20,
|
|
715
|
+
load: async ({ search = '', page = 1, pageSize = 20 } = {}) => {
|
|
716
|
+
const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
|
|
717
|
+
const { rows, total }: { rows: Array<{ id: number; name: string }>; total: number } =
|
|
718
|
+
await res.json();
|
|
719
|
+
return {
|
|
720
|
+
items: rows.map((u) => ({ id: u.id, value: String(u.id), label: u.name })),
|
|
721
|
+
totalCount: total, // Select догружает страницы, пока items.length < totalCount
|
|
722
|
+
};
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
<Select value={userId} onChange={setUserId} resource={usersResource} clearable />;
|
|
727
|
+
```
|
|
728
|
+
|
|
657
729
|
Grouped options:
|
|
658
730
|
|
|
659
731
|
```tsx
|
|
@@ -687,18 +759,25 @@ Grouped options:
|
|
|
687
759
|
В составе формы:
|
|
688
760
|
|
|
689
761
|
```tsx
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
762
|
+
import { createModel, createForm } from '@reformer/core';
|
|
763
|
+
|
|
764
|
+
const model = createModel<{ city: string }>({ city: '' });
|
|
765
|
+
const schema = {
|
|
766
|
+
children: [
|
|
767
|
+
{
|
|
768
|
+
value: model.$.city,
|
|
769
|
+
component: Select,
|
|
770
|
+
componentProps: {
|
|
771
|
+
placeholder: 'Город',
|
|
772
|
+
options: [
|
|
773
|
+
{ value: 'msk', label: 'Москва' },
|
|
774
|
+
{ value: 'spb', label: 'Санкт-Петербург' },
|
|
775
|
+
],
|
|
776
|
+
},
|
|
699
777
|
},
|
|
700
|
-
|
|
701
|
-
}
|
|
778
|
+
],
|
|
779
|
+
};
|
|
780
|
+
const form = createForm<{ city: string }>({ model, schema });
|
|
702
781
|
|
|
703
782
|
<FormField control={form.city} testId="city" />;
|
|
704
783
|
```
|
|
@@ -722,7 +801,7 @@ const form = createForm<FormSchema<{ city: string }>>({
|
|
|
722
801
|
- [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
|
|
723
802
|
- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.
|
|
724
803
|
- [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».
|
|
725
|
-
- Эталон: `
|
|
804
|
+
- Эталон: `examples/complex-multy-step-form/schemas/schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
|
|
726
805
|
|
|
727
806
|
## 16. Button
|
|
728
807
|
|
|
@@ -1043,35 +1122,40 @@ interface FormFieldProps {
|
|
|
1043
1122
|
|
|
1044
1123
|
```tsx
|
|
1045
1124
|
import { useMemo } from 'react';
|
|
1046
|
-
import {
|
|
1125
|
+
import { createModel, createForm } from '@reformer/core';
|
|
1047
1126
|
import { Button, FormField, Input, Select } from '@reformer/ui-kit';
|
|
1048
1127
|
|
|
1049
|
-
|
|
1128
|
+
type RegistrationForm = {
|
|
1050
1129
|
email: string;
|
|
1051
1130
|
country: string;
|
|
1052
|
-
}
|
|
1131
|
+
};
|
|
1053
1132
|
|
|
1054
1133
|
function RegistrationPage() {
|
|
1055
|
-
const form = useMemo(
|
|
1056
|
-
()
|
|
1057
|
-
|
|
1058
|
-
|
|
1134
|
+
const form = useMemo(() => {
|
|
1135
|
+
const model = createModel<RegistrationForm>({ email: '', country: '' });
|
|
1136
|
+
const schema = {
|
|
1137
|
+
children: [
|
|
1138
|
+
{
|
|
1139
|
+
value: model.$.email,
|
|
1059
1140
|
component: Input,
|
|
1060
|
-
componentProps: { label: 'Email', placeholder: 'you@example.com' },
|
|
1141
|
+
componentProps: { label: 'Email', placeholder: 'you@example.com', testId: 'email' },
|
|
1061
1142
|
},
|
|
1062
|
-
|
|
1143
|
+
{
|
|
1144
|
+
value: model.$.country,
|
|
1063
1145
|
component: Select,
|
|
1064
1146
|
componentProps: {
|
|
1065
1147
|
label: 'Страна',
|
|
1148
|
+
testId: 'country',
|
|
1066
1149
|
options: [
|
|
1067
1150
|
{ value: 'ru', label: 'Россия' },
|
|
1068
1151
|
{ value: 'by', label: 'Беларусь' },
|
|
1069
1152
|
],
|
|
1070
1153
|
},
|
|
1071
1154
|
},
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1155
|
+
],
|
|
1156
|
+
};
|
|
1157
|
+
return createForm<RegistrationForm>({ model, schema });
|
|
1158
|
+
}, []);
|
|
1075
1159
|
|
|
1076
1160
|
return (
|
|
1077
1161
|
<form className="space-y-4">
|
|
@@ -1094,37 +1178,39 @@ function RegistrationPage() {
|
|
|
1094
1178
|
|
|
1095
1179
|
```tsx
|
|
1096
1180
|
import { useMemo } from 'react';
|
|
1181
|
+
import { createForm } from '@reformer/core';
|
|
1097
1182
|
import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
|
|
1098
|
-
import { FormField,
|
|
1099
|
-
import {
|
|
1183
|
+
import { FormField, Input, Section } from '@reformer/ui-kit';
|
|
1184
|
+
import { createCreditApplicationModel } from './schemas/model';
|
|
1100
1185
|
|
|
1101
1186
|
function CreditApplicationPage() {
|
|
1102
|
-
const form = useMemo(() =>
|
|
1103
|
-
|
|
1104
|
-
()
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1187
|
+
const { form, schema } = useMemo(() => {
|
|
1188
|
+
// M1: модель — источник истины; листья схемы ссылаются на её сигналы.
|
|
1189
|
+
const model = createCreditApplicationModel();
|
|
1190
|
+
const schema = createRenderSchema<CreditApplication>(() => ({
|
|
1191
|
+
component: Section,
|
|
1192
|
+
componentProps: { title: 'Заявка', className: 'space-y-4' },
|
|
1193
|
+
children: [
|
|
1194
|
+
{ value: model.$.email, component: Input, componentProps: { testId: 'email' } },
|
|
1195
|
+
{ value: model.$.phone, component: Input, componentProps: { testId: 'phone' } },
|
|
1196
|
+
{ value: model.$.amount, component: Input, componentProps: { testId: 'amount' } },
|
|
1197
|
+
],
|
|
1198
|
+
}));
|
|
1199
|
+
const form = createForm<CreditApplication>({ model, schema });
|
|
1200
|
+
return { form, schema };
|
|
1201
|
+
}, []);
|
|
1116
1202
|
|
|
1117
1203
|
// settings.fieldWrapper применяется к каждому field-узлу автоматически.
|
|
1118
1204
|
return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;
|
|
1119
1205
|
}
|
|
1120
1206
|
```
|
|
1121
1207
|
|
|
1122
|
-
Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example).
|
|
1208
|
+
Эталон: `examples/complex-multy-step-form-renderer/CreditApplicationFormRenderer.tsx` (monorepo example).
|
|
1123
1209
|
|
|
1124
|
-
`testId` рендерер берёт из `componentProps.testId`
|
|
1210
|
+
`testId` рендерер берёт из `componentProps.testId` листа schema:
|
|
1125
1211
|
|
|
1126
1212
|
```tsx
|
|
1127
|
-
{
|
|
1213
|
+
{ value: itemModel.$.bank, component: Input, componentProps: { testId: 'existingLoan-bank' } }
|
|
1128
1214
|
// → <FormField control={...} testId="existingLoan-bank" />
|
|
1129
1215
|
// → data-testid="field-existingLoan-bank", "input-existingLoan-bank", ...
|
|
1130
1216
|
```
|
|
@@ -1164,15 +1250,20 @@ import { InputMask } from '@reformer/ui-kit/input-mask';
|
|
|
1164
1250
|
верхний `Label`:
|
|
1165
1251
|
|
|
1166
1252
|
```tsx
|
|
1253
|
+
import { createModel, createForm } from '@reformer/core';
|
|
1167
1254
|
import { Checkbox, FormField } from '@reformer/ui-kit';
|
|
1168
1255
|
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1256
|
+
const model = createModel<{ accept: boolean }>({ accept: false });
|
|
1257
|
+
const schema = {
|
|
1258
|
+
children: [
|
|
1259
|
+
{
|
|
1260
|
+
value: model.$.accept,
|
|
1261
|
+
component: Checkbox,
|
|
1262
|
+
componentProps: { label: 'Принимаю условия' },
|
|
1263
|
+
},
|
|
1264
|
+
],
|
|
1265
|
+
};
|
|
1266
|
+
const form = createForm<{ accept: boolean }>({ model, schema });
|
|
1176
1267
|
|
|
1177
1268
|
<FormField control={form.accept} testId="accept" />;
|
|
1178
1269
|
// рендерится только Checkbox с label справа + error снизу.
|
|
@@ -1207,7 +1298,7 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
|
1207
1298
|
- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.
|
|
1208
1299
|
- [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».
|
|
1209
1300
|
- CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).
|
|
1210
|
-
- Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
|
|
1301
|
+
- Эталон: `examples/complex-multy-step-form-renderer/CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
|
|
1211
1302
|
|
|
1212
1303
|
## 25. 1. `Input type="number"` возвращает строку, а не число (или `null`)
|
|
1213
1304
|
|
|
@@ -1323,14 +1414,17 @@ const MyLink = React.forwardRef<HTMLAnchorElement, { href: string; children: Rea
|
|
|
1323
1414
|
|
|
1324
1415
|
- Передан `checked` вместо `value` (`<Checkbox checked={...}>`) — пропа
|
|
1325
1416
|
`checked` нет, нужно `value`.
|
|
1326
|
-
- В
|
|
1327
|
-
отрендерится как `false`, и при `setValue(true)` без вмешательства
|
|
1328
|
-
re-render не произойдёт. Указывай `
|
|
1417
|
+
- В модели поле имеет тип `boolean`, но начальное значение `undefined` —
|
|
1418
|
+
компонент отрендерится как `false`, и при `setValue(true)` без вмешательства
|
|
1419
|
+
React re-render не произойдёт. Указывай `accept: false` явно в initial-значениях
|
|
1420
|
+
модели.
|
|
1329
1421
|
|
|
1330
1422
|
```typescript
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1423
|
+
const model = createModel<{ accept: boolean }>({ accept: false }); // false, не undefined!
|
|
1424
|
+
const schema = {
|
|
1425
|
+
children: [{ value: model.$.accept, component: Checkbox }],
|
|
1426
|
+
};
|
|
1427
|
+
const form = createForm<{ accept: boolean }>({ model, schema });
|
|
1334
1428
|
```
|
|
1335
1429
|
|
|
1336
1430
|
## 30. 6. `FormField` не подцепляет ошибки (`<error>` не появляется)
|
|
@@ -1347,13 +1441,18 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
|
1347
1441
|
после `blur` или `markAsTouched`. Если submit-кнопка не вызывает
|
|
1348
1442
|
`form.markAsTouched()` — пользователь вообще не увидит ошибку.
|
|
1349
1443
|
|
|
1350
|
-
**Решение.** На submit
|
|
1444
|
+
**Решение.** На submit обязательно помечаем touched и валидируем модель по схеме
|
|
1445
|
+
(M1: `validateFormModel(model, schema)` — именно он прогоняет `validators` листьев
|
|
1446
|
+
и роутит ошибки в ноды):
|
|
1351
1447
|
|
|
1352
1448
|
```tsx
|
|
1449
|
+
import { validateFormModel } from '@reformer/core';
|
|
1450
|
+
|
|
1353
1451
|
const onSubmit = async () => {
|
|
1354
1452
|
form.markAsTouched();
|
|
1355
|
-
|
|
1356
|
-
|
|
1453
|
+
const res = await validateFormModel(model, schema);
|
|
1454
|
+
if (!res.valid) return;
|
|
1455
|
+
// ... отправка (значения — из model.get())
|
|
1357
1456
|
};
|
|
1358
1457
|
```
|
|
1359
1458
|
|
|
@@ -1465,14 +1564,14 @@ const hasValue = Boolean(value);
|
|
|
1465
1564
|
|
|
1466
1565
|
## 35. 11. JSON-renderer: `Select`-`options` хранятся в реестре, но в дропдауне пусто
|
|
1467
1566
|
|
|
1468
|
-
**Симптом.** Регистрируется
|
|
1567
|
+
**Симптом.** Регистрируется dataSource `LOAN_TYPES` через `reg.dataSource('LOAN_TYPES', list)`,
|
|
1469
1568
|
в JSON-схеме `componentProps: { options: '$LOAN_TYPES' }`, но опции пустые.
|
|
1470
1569
|
|
|
1471
1570
|
**Причина.** `Select` ждёт `options: Array<{value, label, group?}>`, а из
|
|
1472
1571
|
реестра приходит уже обработанная строкой ссылка `'$LOAN_TYPES'`. Нужен
|
|
1473
|
-
правильный синтаксис
|
|
1572
|
+
правильный синтаксис dataSource-ссылки в реестре.
|
|
1474
1573
|
|
|
1475
|
-
**Решение.** Проверь convention для
|
|
1574
|
+
**Решение.** Проверь convention для dataSource-ссылок в
|
|
1476
1575
|
[`renderer-json/03-registry.md`](../../../reformer-renderer-json/docs/llms/03-registry.md).
|
|
1477
1576
|
Внутри `Select` дальнейших магий нет — он просто читает `directOptions`
|
|
1478
1577
|
один в один.
|
|
@@ -1486,19 +1585,47 @@ const hasValue = Boolean(value);
|
|
|
1486
1585
|
## 37. Базовое использование
|
|
1487
1586
|
|
|
1488
1587
|
```tsx
|
|
1489
|
-
import { useRef } from 'react';
|
|
1588
|
+
import { useMemo, useRef, type FC } from 'react';
|
|
1490
1589
|
import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
|
|
1491
|
-
import
|
|
1492
|
-
import type {
|
|
1590
|
+
import { FormField, Input, Checkbox } from '@reformer/ui-kit';
|
|
1591
|
+
import type { FormWizardHandle, FormWizardConfig } from '@reformer/cdk/form-wizard';
|
|
1592
|
+
import { createModel, createForm, validateFormModel, type FormProxy } from '@reformer/core';
|
|
1593
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
1493
1594
|
|
|
1494
1595
|
// Используйте `type`, не `interface`, для structural-совместимости с
|
|
1495
|
-
//
|
|
1596
|
+
// constraint `T extends Record<string, any>` внутри FormWizard generic'а.
|
|
1496
1597
|
type MyForm = {
|
|
1497
1598
|
email: string;
|
|
1498
1599
|
password: string;
|
|
1499
1600
|
confirmation: boolean;
|
|
1500
1601
|
};
|
|
1501
1602
|
|
|
1603
|
+
// M1: модель — источник истины значений; листья схемы ссылаются на её сигналы.
|
|
1604
|
+
const model = createModel<MyForm>({ email: '', password: '', confirmation: false });
|
|
1605
|
+
const schema = {
|
|
1606
|
+
children: [
|
|
1607
|
+
{
|
|
1608
|
+
value: model.$.email,
|
|
1609
|
+
component: Input,
|
|
1610
|
+
componentProps: { label: 'Email', testId: 'email' },
|
|
1611
|
+
validators: [required(), email()],
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
value: model.$.password,
|
|
1615
|
+
component: Input,
|
|
1616
|
+
componentProps: { label: 'Пароль', testId: 'password' },
|
|
1617
|
+
validators: [required(), minLength(8)],
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
value: model.$.confirmation,
|
|
1621
|
+
component: Checkbox,
|
|
1622
|
+
componentProps: { label: 'Подтверждаю' },
|
|
1623
|
+
validators: [required()],
|
|
1624
|
+
},
|
|
1625
|
+
],
|
|
1626
|
+
};
|
|
1627
|
+
const form = createForm<MyForm>({ model, schema });
|
|
1628
|
+
|
|
1502
1629
|
const Step1: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (
|
|
1503
1630
|
<FormField control={control.email} />
|
|
1504
1631
|
);
|
|
@@ -1513,25 +1640,20 @@ const steps: FormWizardStep<MyForm>[] = [
|
|
|
1513
1640
|
{ number: 3, title: 'Готово', icon: '✓', body: <ConfirmationView /> },
|
|
1514
1641
|
];
|
|
1515
1642
|
|
|
1516
|
-
// ⚠️ КРИТИЧНО: `
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1643
|
+
// ⚠️ КРИТИЧНО: `config` это **FormWizardConfig** — объект с ДВУМЯ колбэками
|
|
1644
|
+
// (`validateStep`, `validateAll`), НЕ схемы/массивы валидаторов. Каждый колбэк
|
|
1645
|
+
// возвращает `boolean | Promise<boolean>`: `true` = валидно, идём дальше.
|
|
1646
|
+
// Если колбэк не задан — соответствующий шаг/submit считается валидным (no-op).
|
|
1647
|
+
// Канон M1 — валидировать per-step/полностью через validateFormModel(model, ...).
|
|
1648
|
+
const config: FormWizardConfig = {
|
|
1649
|
+
// step 1-based. Провалидируй нужный шаг (собери под-схему шага и прогони
|
|
1650
|
+
// validateFormModel), верни boolean.
|
|
1651
|
+
validateStep: async (step) => {
|
|
1652
|
+
const stepSchema = { children: [schema.children[step - 1]] };
|
|
1653
|
+
const res = await validateFormModel(model, stepSchema);
|
|
1654
|
+
return res.valid;
|
|
1524
1655
|
},
|
|
1525
|
-
|
|
1526
|
-
required(path.password);
|
|
1527
|
-
minLength(path.password, 8);
|
|
1528
|
-
},
|
|
1529
|
-
};
|
|
1530
|
-
|
|
1531
|
-
const fullValidation: ValidationSchemaFn<MyForm> = (path) => {
|
|
1532
|
-
STEP_VALIDATIONS[1](path);
|
|
1533
|
-
STEP_VALIDATIONS[2](path);
|
|
1534
|
-
required(path.confirmation);
|
|
1656
|
+
validateAll: async () => (await validateFormModel(model, schema)).valid,
|
|
1535
1657
|
};
|
|
1536
1658
|
|
|
1537
1659
|
// ref типизируется явно типом формы; constraint `T extends Record<string, any>`
|
|
@@ -1540,21 +1662,33 @@ const navRef = useRef<FormWizardHandle<MyForm>>(null);
|
|
|
1540
1662
|
|
|
1541
1663
|
// ВАЖНО: prop-level `onSubmit` имеет signature `() => void | Promise<void>` —
|
|
1542
1664
|
// БЕЗ аргумента values. Это by-design (см. FormWizardActionsProps в @reformer/cdk).
|
|
1543
|
-
// Чтобы получить values — читай их из
|
|
1665
|
+
// Чтобы получить values — читай их из модели внутри handler:
|
|
1544
1666
|
const handleSubmit = async () => {
|
|
1545
|
-
const values =
|
|
1667
|
+
const values = model.get();
|
|
1546
1668
|
await api.submit(values);
|
|
1547
1669
|
};
|
|
1548
1670
|
|
|
1549
1671
|
<FormWizard
|
|
1550
1672
|
ref={navRef}
|
|
1551
1673
|
form={form}
|
|
1552
|
-
config={
|
|
1674
|
+
config={config}
|
|
1553
1675
|
steps={steps}
|
|
1554
1676
|
onSubmit={handleSubmit}
|
|
1555
1677
|
/>;
|
|
1556
1678
|
```
|
|
1557
1679
|
|
|
1680
|
+
> **`config` не привязан к типу формы.** `FormWizardConfig` — это `{ validateStep?, validateAll? }`,
|
|
1681
|
+
> оба колбэка возвращают `boolean | Promise<boolean>`. Канон M1 — прогонять
|
|
1682
|
+
> `validateFormModel(model, schema)` (именно он исполняет `validators` листьев;
|
|
1683
|
+
> `form.validate()` по нодам схемные правила НЕ запустит). См. эталон
|
|
1684
|
+
> `examples/complex-multy-step-form/schemas/validation.ts`. Реальный эталон собирает конфиг фабрикой:
|
|
1685
|
+
>
|
|
1686
|
+
> ```tsx
|
|
1687
|
+
> import { makeCreditValidationConfig } from './schemas/validation';
|
|
1688
|
+
> const config = useMemo(() => makeCreditValidationConfig(model), [model]);
|
|
1689
|
+
> // → { validateStep: (step) => Promise<boolean>, validateAll: () => Promise<boolean> }
|
|
1690
|
+
> ```
|
|
1691
|
+
|
|
1558
1692
|
### Альтернатива — imperative submit с values
|
|
1559
1693
|
|
|
1560
1694
|
`navRef.current?.submit(callback)` — отдельный API, ПРИНИМАЕТ `(values: T) =>` callback.
|
|
@@ -1585,8 +1719,14 @@ const handleSaveAndExit = async () => {
|
|
|
1585
1719
|
|
|
1586
1720
|
## 39. RenderNode body (renderer-react / renderer-json)
|
|
1587
1721
|
|
|
1722
|
+
M1: схема без аргумента `path` — листья ссылаются на сигналы модели
|
|
1723
|
+
(`value: model.$.x`), а не на `path.x`:
|
|
1724
|
+
|
|
1588
1725
|
```tsx
|
|
1589
|
-
|
|
1726
|
+
import { createRenderSchema } from '@reformer/renderer-react';
|
|
1727
|
+
import { Box, Input } from '@reformer/ui-kit';
|
|
1728
|
+
|
|
1729
|
+
const renderSchema = createRenderSchema<CreditApplication>(() => ({
|
|
1590
1730
|
selector: 'wizard',
|
|
1591
1731
|
component: FormWizard,
|
|
1592
1732
|
componentProps: {
|
|
@@ -1601,12 +1741,15 @@ const renderSchema = (path) => ({
|
|
|
1601
1741
|
body: {
|
|
1602
1742
|
component: Box,
|
|
1603
1743
|
componentProps: { className: 'space-y-4' },
|
|
1604
|
-
children: [
|
|
1744
|
+
children: [
|
|
1745
|
+
{ value: model.$.loanAmount, component: Input },
|
|
1746
|
+
{ value: model.$.loanTerm, component: Input },
|
|
1747
|
+
],
|
|
1605
1748
|
},
|
|
1606
1749
|
},
|
|
1607
1750
|
],
|
|
1608
1751
|
},
|
|
1609
|
-
});
|
|
1752
|
+
}));
|
|
1610
1753
|
```
|
|
1611
1754
|
|
|
1612
1755
|
ui-kit FormWizard детектирует RenderNode (объект с `.component` без React-element-маркера) и оборачивает в `RenderNodeComponent` с `form={form}`.
|
|
@@ -1685,11 +1828,22 @@ const wizardRef = useRef<FormWizardHandle<MyForm>>(null);
|
|
|
1685
1828
|
|
|
1686
1829
|
<FormWizard ref={wizardRef} ... />
|
|
1687
1830
|
|
|
1688
|
-
// Программная
|
|
1689
|
-
wizardRef.current?.goToStep(2);
|
|
1690
|
-
wizardRef.current?.
|
|
1831
|
+
// Программная навигация и submit (см. FormWizardHandle<T> в @reformer/cdk):
|
|
1832
|
+
wizardRef.current?.goToStep(2); // boolean: false, если предыдущий шаг не завершён
|
|
1833
|
+
await wizardRef.current?.goToNextStep(); // валидирует текущий шаг, затем переходит
|
|
1834
|
+
wizardRef.current?.goToPreviousStep();
|
|
1835
|
+
await wizardRef.current?.validateCurrentStep(); // Promise<boolean>
|
|
1836
|
+
|
|
1837
|
+
// submit принимает callback (values: T) => R | Promise<R>; возвращает R | null
|
|
1838
|
+
// (null — не прошла validateAll):
|
|
1839
|
+
const result = await wizardRef.current?.submit((values) => api.submit(values));
|
|
1691
1840
|
```
|
|
1692
1841
|
|
|
1842
|
+
Доступные поля/методы `FormWizardHandle<T>`: `form`, `currentStep`,
|
|
1843
|
+
`completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`,
|
|
1844
|
+
`validateCurrentStep()`, `goToNextStep()`, `goToPreviousStep()`,
|
|
1845
|
+
`goToStep(step)`, `submit(cb)`.
|
|
1846
|
+
|
|
1693
1847
|
## 43. Базовое использование (TS-flow)
|
|
1694
1848
|
|
|
1695
1849
|
```tsx
|
|
@@ -1737,34 +1891,42 @@ const PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (
|
|
|
1737
1891
|
|
|
1738
1892
|
## 44. Renderer-react RenderSchema
|
|
1739
1893
|
|
|
1740
|
-
|
|
1894
|
+
M1: схема без аргумента `path` (`createRenderSchema<T>(() => ...)`). `control`
|
|
1895
|
+
ссылается на массив модели (`model.<arrayField>` — `ModelArray<T>`); `FormArraySection`
|
|
1896
|
+
резолвит его в `ArrayNode` внутри.
|
|
1741
1897
|
|
|
1742
1898
|
```tsx
|
|
1743
|
-
|
|
1899
|
+
import { createRenderSchema } from '@reformer/renderer-react';
|
|
1900
|
+
import { FormArraySection } from '@reformer/ui-kit/form-array';
|
|
1901
|
+
|
|
1902
|
+
const renderSchema = createRenderSchema<CreditApplication>(() => ({
|
|
1744
1903
|
selector: 'properties-section',
|
|
1745
1904
|
component: FormArraySection,
|
|
1746
1905
|
componentProps: {
|
|
1747
|
-
control:
|
|
1906
|
+
control: model.properties, // ModelArray → резолвится в ArrayNode
|
|
1748
1907
|
itemComponent: PropertyForm, // FC напрямую
|
|
1749
1908
|
title: 'Имущество',
|
|
1750
1909
|
addButtonLabel: '+ Добавить имущество',
|
|
1910
|
+
initialValue: createBlankProperty(),
|
|
1751
1911
|
},
|
|
1752
|
-
});
|
|
1912
|
+
}));
|
|
1753
1913
|
```
|
|
1754
1914
|
|
|
1755
|
-
ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
|
|
1915
|
+
ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
|
|
1916
|
+
|
|
1917
|
+
> Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }` (см. эталон `examples/complex-multy-step-form-renderer/render-schema.ts`).
|
|
1756
1918
|
|
|
1757
1919
|
## 45. JSON (renderer-json)
|
|
1758
1920
|
|
|
1759
1921
|
Два варианта `itemComponent`:
|
|
1760
1922
|
|
|
1761
|
-
### Вариант 1: registry-name (FC зарегистрирован
|
|
1923
|
+
### Вариант 1: registry-name (FC зарегистрирован через reg.component)
|
|
1762
1924
|
|
|
1763
1925
|
```ts
|
|
1764
1926
|
// registry.ts
|
|
1765
1927
|
defineRegistry((reg) => {
|
|
1766
|
-
reg.
|
|
1767
|
-
reg.
|
|
1928
|
+
reg.component('FormArraySection', FormArraySection);
|
|
1929
|
+
reg.component('PropertyForm', PropertyForm);
|
|
1768
1930
|
});
|
|
1769
1931
|
```
|
|
1770
1932
|
|
|
@@ -1817,20 +1979,25 @@ defineRegistry((reg) => {
|
|
|
1817
1979
|
|
|
1818
1980
|
## 46. Props (полный список)
|
|
1819
1981
|
|
|
1820
|
-
| Prop | Type
|
|
1821
|
-
| -------------------- |
|
|
1822
|
-
| `control` | `FormArrayProxy<T> \| ArrayNode<T> \|
|
|
1823
|
-
| `itemComponent` | `ComponentType<{ control: FormProxy<T> }>`
|
|
1824
|
-
| `title` | `string`
|
|
1825
|
-
| `itemLabel` | `string \| (control
|
|
1826
|
-
| `addButtonLabel` | `string`
|
|
1827
|
-
| `removeButtonLabel` | `string`
|
|
1828
|
-
| `emptyMessage` | `string`
|
|
1829
|
-
| `emptyMessageHint` | `string`
|
|
1830
|
-
| `hasItems` | `boolean`
|
|
1831
|
-
| `initialValue` | `Partial<
|
|
1832
|
-
| `
|
|
1833
|
-
| `
|
|
1982
|
+
| Prop | Type | Default | Описание |
|
|
1983
|
+
| -------------------- | ------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------- |
|
|
1984
|
+
| `control` | `FormArrayProxy<T> \| ArrayNode<T> \| undefined` | required | Массив для управления (в RenderSchema — `FieldPathNode`) |
|
|
1985
|
+
| `itemComponent` | `ComponentType<{ control: FormProxy<T> }>` | required | FC для рендера каждого item |
|
|
1986
|
+
| `title` | `string` | — | Заголовок секции (h3) |
|
|
1987
|
+
| `itemLabel` | `string \| (control: FormProxy<T>, index: number) => string` | — | Метка над каждым item |
|
|
1988
|
+
| `addButtonLabel` | `string` | `'+ Добавить'` | Текст кнопки добавления |
|
|
1989
|
+
| `removeButtonLabel` | `string` | `'Удалить'` | Текст кнопки удаления |
|
|
1990
|
+
| `emptyMessage` | `string` | — | Сообщение при пустом массиве |
|
|
1991
|
+
| `emptyMessageHint` | `string` | — | Подсказка под emptyMessage |
|
|
1992
|
+
| `hasItems` | `boolean` | — | `false` → секция полностью скрыта |
|
|
1993
|
+
| `initialValue` | `Partial<T>` | — | Plain-leaf значения для новых items |
|
|
1994
|
+
| `showRemoveOnSingle` | `boolean` | `false` | Показывать «Удалить» при одном item |
|
|
1995
|
+
| `reorderable` | `boolean` | `false` | Показывать кнопки ↑/↓ для перестановки элементов |
|
|
1996
|
+
| `maxItems` | `number` | — | Максимум items (AddButton скрывается при достижении) |
|
|
1997
|
+
| `className` | `string` | `'space-y-3 mt-2'` | Класс `<section>`-обёртки |
|
|
1998
|
+
| `cardClassName` | `string` | `'mb-4 p-4 bg-white rounded border'` | Класс card-обёртки каждого item |
|
|
1999
|
+
| `form` | `FormProxy<unknown>` | авто-инъекция | Проброс `form` (RenderNodeComponent через `__selfManagedChildren`) |
|
|
2000
|
+
| `fieldWrapper` | `ComponentType<FieldWrapperProps>` | авто-инъекция | Field wrapper для дочерних полей (по умолчанию — от родителя) |
|
|
1834
2001
|
|
|
1835
2002
|
## 47. Critical: `initialValue` — PLAIN LEAVES ONLY
|
|
1836
2003
|
|
|
@@ -1846,10 +2013,13 @@ FieldConfig попадает в значение поля → Textarea ренд
|
|
|
1846
2013
|
|
|
1847
2014
|
## 48. Schema-driven (canonical pattern)
|
|
1848
2015
|
|
|
1849
|
-
|
|
2016
|
+
Канон M1: `createModel` → схема, где лист = `{ value: model.$.field, component,
|
|
2017
|
+
componentProps, validators }` → `createForm({ model, schema })`. Объяви InputMask
|
|
2018
|
+
как `component` листа; передай `mask` в `componentProps`:
|
|
1850
2019
|
|
|
1851
2020
|
```ts
|
|
1852
|
-
import {
|
|
2021
|
+
import { createModel, createForm } from '@reformer/core';
|
|
2022
|
+
import { required, pattern } from '@reformer/core/validators';
|
|
1853
2023
|
import { InputMask } from '@reformer/ui-kit';
|
|
1854
2024
|
|
|
1855
2025
|
type ContactForm = {
|
|
@@ -1859,30 +2029,41 @@ type ContactForm = {
|
|
|
1859
2029
|
snils: string;
|
|
1860
2030
|
};
|
|
1861
2031
|
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2032
|
+
const model = createModel<ContactForm>({ phone: '', passport: '', inn: '', snils: '' });
|
|
2033
|
+
|
|
2034
|
+
const schema = {
|
|
2035
|
+
children: [
|
|
2036
|
+
{
|
|
2037
|
+
value: model.$.phone,
|
|
1866
2038
|
component: InputMask,
|
|
1867
|
-
componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99' },
|
|
2039
|
+
componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99', testId: 'phone' },
|
|
2040
|
+
validators: [
|
|
2041
|
+
required({ message: 'Телефон обязателен' }),
|
|
2042
|
+
pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Неверный формат телефона' }),
|
|
2043
|
+
],
|
|
1868
2044
|
},
|
|
1869
|
-
|
|
1870
|
-
value:
|
|
2045
|
+
{
|
|
2046
|
+
value: model.$.passport,
|
|
1871
2047
|
component: InputMask,
|
|
1872
|
-
componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999' },
|
|
2048
|
+
componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999', testId: 'passport' },
|
|
2049
|
+
validators: [required()],
|
|
1873
2050
|
},
|
|
1874
|
-
|
|
1875
|
-
value:
|
|
2051
|
+
{
|
|
2052
|
+
value: model.$.inn,
|
|
1876
2053
|
component: InputMask,
|
|
1877
|
-
componentProps: { label: 'ИНН', mask: '999999999999' },
|
|
2054
|
+
componentProps: { label: 'ИНН', mask: '999999999999', testId: 'inn' },
|
|
2055
|
+
validators: [required(), pattern(/^\d{12}$/, { message: 'ИНН должен содержать 12 цифр' })],
|
|
1878
2056
|
},
|
|
1879
|
-
|
|
1880
|
-
value:
|
|
2057
|
+
{
|
|
2058
|
+
value: model.$.snils,
|
|
1881
2059
|
component: InputMask,
|
|
1882
|
-
componentProps: { label: 'СНИЛС', mask: '999-999-999 99' },
|
|
2060
|
+
componentProps: { label: 'СНИЛС', mask: '999-999-999 99', testId: 'snils' },
|
|
2061
|
+
validators: [required()],
|
|
1883
2062
|
},
|
|
1884
|
-
|
|
1885
|
-
}
|
|
2063
|
+
],
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
const form = createForm<ContactForm>({ model, schema });
|
|
1886
2067
|
```
|
|
1887
2068
|
|
|
1888
2069
|
Render как обычно через `FormField`:
|
|
@@ -1915,24 +2096,30 @@ import { FormField } from '@reformer/ui-kit';
|
|
|
1915
2096
|
## 50. Validation
|
|
1916
2097
|
|
|
1917
2098
|
`InputMask` пишет в значение **то, что ввёл пользователь** (с literal-символами маски).
|
|
1918
|
-
|
|
2099
|
+
Валидаторы — фабрики из `@reformer/core/validators` — кладутся в массив `validators`
|
|
2100
|
+
листа схемы (см. схему выше), а не в отдельную path-функцию. Прогоняются через
|
|
2101
|
+
`validateFormModel(model, schema)`:
|
|
1919
2102
|
|
|
1920
|
-
- `required(
|
|
1921
|
-
- `minLength(
|
|
1922
|
-
- `pattern(
|
|
2103
|
+
- `required({ message })` — на пустоту;
|
|
2104
|
+
- `minLength(18)` — для проверки длины с literal-символами (телефон ровно 18 символов);
|
|
2105
|
+
- `pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message })` — точный формат.
|
|
1923
2106
|
|
|
1924
2107
|
```ts
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2108
|
+
import { validateFormModel } from '@reformer/core';
|
|
2109
|
+
|
|
2110
|
+
// schema — из блока Schema-driven выше (валидаторы уже на листьях phone/inn/…).
|
|
2111
|
+
const onSubmit = async () => {
|
|
2112
|
+
form.markAsTouched();
|
|
2113
|
+
const res = await validateFormModel(model, schema);
|
|
2114
|
+
if (!res.valid) return;
|
|
2115
|
+
await api.submit(model.get());
|
|
1933
2116
|
};
|
|
1934
2117
|
```
|
|
1935
2118
|
|
|
2119
|
+
Cross-field правила (например, «доп. телефон отличается от основного») — это
|
|
2120
|
+
именованные `ModelValidator<value, scope, root>` в том же массиве `validators`
|
|
2121
|
+
(читают корень формы через третий аргумент). Эталон: `examples/complex-multy-step-form/schemas/validation.ts`.
|
|
2122
|
+
|
|
1936
2123
|
## 51. Advanced — strict mask через FormField + children slot
|
|
1937
2124
|
|
|
1938
2125
|
Если нужна автоматическая вставка literal-символов (true input-mask), используй
|
|
@@ -2013,13 +2200,15 @@ ErrorComponent={() => <p>Ошибка загрузки</p>}
|
|
|
2013
2200
|
|
|
2014
2201
|
Внутри RenderSchema (статус подставляется через `patchProps`)
|
|
2015
2202
|
```tsx
|
|
2203
|
+
import { createRenderSchema } from '@reformer/renderer-react';
|
|
2016
2204
|
import { AsyncBoundary } from '@reformer/ui-kit';
|
|
2017
2205
|
|
|
2018
|
-
|
|
2206
|
+
// M1: функция схемы не принимает аргументов — привязка к данным идёт через сигналы модели.
|
|
2207
|
+
const schema = createRenderSchema(() => ({
|
|
2019
2208
|
selector: 'data-boundary',
|
|
2020
2209
|
component: AsyncBoundary,
|
|
2021
2210
|
componentProps: { status: 'loading', LoadingComponent: Spinner },
|
|
2022
|
-
children: [
|
|
2211
|
+
children: [{ value: model.$.email, component: Input }],
|
|
2023
2212
|
}));
|
|
2024
2213
|
// позже: schema.node('data-boundary').patchProps({ status: 'ready' });
|
|
2025
2214
|
```
|
|
@@ -2077,26 +2266,30 @@ export function Box({ className, children }: BoxProps): ReactNode
|
|
|
2077
2266
|
|
|
2078
2267
|
**Examples:**
|
|
2079
2268
|
|
|
2080
|
-
Вертикальный список полей в RenderSchema
|
|
2269
|
+
Вертикальный список полей в RenderSchema (M1: лист = `value` + `component`)
|
|
2081
2270
|
```typescript
|
|
2271
|
+
import { Box, Input, InputPassword } from '@reformer/ui-kit';
|
|
2272
|
+
|
|
2082
2273
|
{
|
|
2083
2274
|
component: Box,
|
|
2084
2275
|
componentProps: { className: 'flex flex-col gap-4' },
|
|
2085
2276
|
children: [
|
|
2086
|
-
{
|
|
2087
|
-
{
|
|
2277
|
+
{ value: model.$.email, component: Input },
|
|
2278
|
+
{ value: model.$.password, component: InputPassword },
|
|
2088
2279
|
],
|
|
2089
2280
|
}
|
|
2090
2281
|
```
|
|
2091
2282
|
|
|
2092
2283
|
Двухколоночная сетка
|
|
2093
2284
|
```typescript
|
|
2285
|
+
import { Box, Input } from '@reformer/ui-kit';
|
|
2286
|
+
|
|
2094
2287
|
{
|
|
2095
2288
|
component: Box,
|
|
2096
2289
|
componentProps: { className: 'grid grid-cols-2 gap-4' },
|
|
2097
2290
|
children: [
|
|
2098
|
-
{
|
|
2099
|
-
{
|
|
2291
|
+
{ value: model.$.firstName, component: Input },
|
|
2292
|
+
{ value: model.$.lastName, component: Input },
|
|
2100
2293
|
],
|
|
2101
2294
|
}
|
|
2102
2295
|
```
|
|
@@ -2285,8 +2478,8 @@ Collapsible - сворачиваемая секция формы.
|
|
|
2285
2478
|
|
|
2286
2479
|
Заголовок-кнопка переключает видимость `children`. Состояние локальное
|
|
2287
2480
|
(`useState`), внешний control пока не поддерживается — для управляемого
|
|
2288
|
-
варианта используй `
|
|
2289
|
-
обычного `Box`.
|
|
2481
|
+
извне варианта используй `createRenderSchema(...).node('selector').setHidden(true)`
|
|
2482
|
+
поверх обычного `Box`/`Section`.
|
|
2290
2483
|
|
|
2291
2484
|
**Signature:**
|
|
2292
2485
|
```typescript
|
|
@@ -2302,8 +2495,10 @@ export function Collapsible({
|
|
|
2302
2495
|
|
|
2303
2496
|
**Examples:**
|
|
2304
2497
|
|
|
2305
|
-
Свёрнута по умолчанию
|
|
2498
|
+
Свёрнута по умолчанию (M1: лист = `value` + `component`)
|
|
2306
2499
|
```typescript
|
|
2500
|
+
import { Collapsible, Textarea, Input } from '@reformer/ui-kit';
|
|
2501
|
+
|
|
2307
2502
|
{
|
|
2308
2503
|
component: Collapsible,
|
|
2309
2504
|
componentProps: {
|
|
@@ -2313,14 +2508,16 @@ className: 'border rounded p-4',
|
|
|
2313
2508
|
titleClassName: 'font-semibold w-full text-left',
|
|
2314
2509
|
},
|
|
2315
2510
|
children: [
|
|
2316
|
-
{
|
|
2317
|
-
{
|
|
2511
|
+
{ value: model.$.notes, component: Textarea },
|
|
2512
|
+
{ value: model.$.tags, component: Input },
|
|
2318
2513
|
],
|
|
2319
2514
|
}
|
|
2320
2515
|
```
|
|
2321
2516
|
|
|
2322
2517
|
Развёрнута, со специальным фоном контента
|
|
2323
2518
|
```typescript
|
|
2519
|
+
import { Collapsible, Textarea } from '@reformer/ui-kit';
|
|
2520
|
+
|
|
2324
2521
|
{
|
|
2325
2522
|
component: Collapsible,
|
|
2326
2523
|
componentProps: {
|
|
@@ -2329,7 +2526,7 @@ defaultOpen: true,
|
|
|
2329
2526
|
contentClassName: 'mt-2 bg-gray-50 p-3 rounded',
|
|
2330
2527
|
},
|
|
2331
2528
|
children: [
|
|
2332
|
-
{
|
|
2529
|
+
{ value: model.$.deliveryAddress, component: Textarea },
|
|
2333
2530
|
],
|
|
2334
2531
|
}
|
|
2335
2532
|
```
|
|
@@ -2366,6 +2563,10 @@ _Source: src/components/ui/collapsible.tsx_
|
|
|
2366
2563
|
|
|
2367
2564
|
**Kind:** `function`
|
|
2368
2565
|
|
|
2566
|
+
Состояние ошибки — карточка с иконкой, заголовком, текстом ошибки и
|
|
2567
|
+
опциональной кнопкой повтора. Типовое применение — показать вместо формы,
|
|
2568
|
+
когда не удалось загрузить данные заявки.
|
|
2569
|
+
|
|
2369
2570
|
**Signature:**
|
|
2370
2571
|
```typescript
|
|
2371
2572
|
export function ErrorState({
|
|
@@ -2377,6 +2578,21 @@ export function ErrorState({
|
|
|
2377
2578
|
}: ErrorStateProps): ReactNode
|
|
2378
2579
|
```
|
|
2379
2580
|
|
|
2581
|
+
**Parameters:**
|
|
2582
|
+
- `props` — - Пропсы: `error` (обязателен), `title`, `onRetry`, `retryLabel`, `className`.
|
|
2583
|
+
|
|
2584
|
+
**Returns:** Разметку блока ошибки.
|
|
2585
|
+
|
|
2586
|
+
**Examples:**
|
|
2587
|
+
|
|
2588
|
+
Ошибка загрузки с повтором
|
|
2589
|
+
```tsx
|
|
2590
|
+
const { error } = useLoadCreditApplication(form, '1');
|
|
2591
|
+
if (error) {
|
|
2592
|
+
return <ErrorState error={error} onRetry={() => window.location.reload()} />;
|
|
2593
|
+
}
|
|
2594
|
+
```
|
|
2595
|
+
|
|
2380
2596
|
_Source: src/components/state/error-state.tsx_
|
|
2381
2597
|
|
|
2382
2598
|
### ExampleCard
|
|
@@ -2468,6 +2684,20 @@ _Source: src/components/ui/example-card.tsx_
|
|
|
2468
2684
|
|
|
2469
2685
|
**Kind:** `function`
|
|
2470
2686
|
|
|
2687
|
+
Готовая UI-секция для динамического массива форм — стилизованная обёртка поверх
|
|
2688
|
+
headless-compound `@reformer/cdk/form-array`. Рендерит заголовок, кнопку
|
|
2689
|
+
«Добавить», карточку с меткой на каждый элемент (плюс опциональные кнопки
|
|
2690
|
+
удаления и перестановки ↑/↓) и сообщение пустого состояния.
|
|
2691
|
+
|
|
2692
|
+
`control` принимает `FormArrayProxy<T>` или уже-резолвленный
|
|
2693
|
+
`ArrayNode<T>`/`ModelArrayNode<T>`. `itemComponent` — единственная форма
|
|
2694
|
+
рендера элемента: `ComponentType<{ control: FormProxy<T> }>` (тот же контракт,
|
|
2695
|
+
что у шага {@link FormWizardStep}). Внутри `RenderNodeComponent` проп `form`
|
|
2696
|
+
инъектится автоматически (маркер `__selfManagedChildren`).
|
|
2697
|
+
|
|
2698
|
+
Проп `hasItems` удобен для toggle-чекбоксов («У меня есть имущество»): при
|
|
2699
|
+
`false` секция скрывается целиком.
|
|
2700
|
+
|
|
2471
2701
|
**Signature:**
|
|
2472
2702
|
```typescript
|
|
2473
2703
|
export function FormArraySection<T extends object>({
|
|
@@ -2489,12 +2719,38 @@ export function FormArraySection<T extends object>({
|
|
|
2489
2719
|
}: FormArraySectionProps<T>): ReactNode
|
|
2490
2720
|
```
|
|
2491
2721
|
|
|
2722
|
+
**Examples:**
|
|
2723
|
+
|
|
2724
|
+
Массив «Имущество» под чекбоксом-переключателем
|
|
2725
|
+
```tsx
|
|
2726
|
+
import { FormArraySection } from '@reformer/ui-kit/form-array';
|
|
2727
|
+
|
|
2728
|
+
function AdditionalInfo({ control }: { control: FormProxy<CreditApplication> }) {
|
|
2729
|
+
const hasProperty = useFormControlValue(control.hasProperty) as boolean;
|
|
2730
|
+
return (
|
|
2731
|
+
<FormArraySection
|
|
2732
|
+
title="Имущество"
|
|
2733
|
+
control={control.properties}
|
|
2734
|
+
itemComponent={PropertyForm}
|
|
2735
|
+
itemLabel="Имущество"
|
|
2736
|
+
addButtonLabel="+ Добавить имущество"
|
|
2737
|
+
emptyMessage='Нажмите "Добавить имущество" для добавления информации'
|
|
2738
|
+
hasItems={hasProperty}
|
|
2739
|
+
initialValue={createBlankProperty()}
|
|
2740
|
+
reorderable
|
|
2741
|
+
/>
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
```
|
|
2745
|
+
|
|
2492
2746
|
_Source: src/components/form-array/form-array-section.tsx_
|
|
2493
2747
|
|
|
2494
2748
|
### FormArraySectionProps
|
|
2495
2749
|
|
|
2496
2750
|
**Kind:** `interface`
|
|
2497
2751
|
|
|
2752
|
+
Пропсы {@link FormArraySection}.
|
|
2753
|
+
|
|
2498
2754
|
**Signature:**
|
|
2499
2755
|
```typescript
|
|
2500
2756
|
export interface FormArraySectionProps<T extends object> {
|
|
@@ -2594,22 +2850,31 @@ export const FormField
|
|
|
2594
2850
|
|
|
2595
2851
|
**Examples:**
|
|
2596
2852
|
|
|
2597
|
-
Standalone в обычной форме
|
|
2853
|
+
Standalone в обычной форме (M1: `createModel` + `createForm({ model, schema })`)
|
|
2598
2854
|
```tsx
|
|
2599
2855
|
import { useMemo } from 'react';
|
|
2600
|
-
import {
|
|
2856
|
+
import { createModel, createForm } from '@reformer/core';
|
|
2857
|
+
import { required } from '@reformer/core/validators';
|
|
2601
2858
|
import { Button, FormField, Input } from '@reformer/ui-kit';
|
|
2602
2859
|
|
|
2603
2860
|
function RegistrationPage() {
|
|
2604
|
-
const form = useMemo(
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2861
|
+
const { form } = useMemo(() => {
|
|
2862
|
+
const model = createModel<{ email: string }>({ email: '' });
|
|
2863
|
+
const schema = {
|
|
2864
|
+
children: [
|
|
2865
|
+
{
|
|
2866
|
+
value: model.$.email,
|
|
2867
|
+
component: Input,
|
|
2868
|
+
componentProps: { label: 'Email', type: 'email', testId: 'email' },
|
|
2869
|
+
validators: [required({ message: 'Email обязателен' })],
|
|
2870
|
+
},
|
|
2871
|
+
],
|
|
2872
|
+
};
|
|
2873
|
+
return { form: createForm<{ email: string }>({ model, schema }) };
|
|
2874
|
+
}, []);
|
|
2610
2875
|
return (
|
|
2611
2876
|
<form>
|
|
2612
|
-
<FormField control={form.email}
|
|
2877
|
+
<FormField control={form.email} />
|
|
2613
2878
|
<Button type="submit">OK</Button>
|
|
2614
2879
|
</form>
|
|
2615
2880
|
);
|
|
@@ -2619,11 +2884,15 @@ return (
|
|
|
2619
2884
|
В качестве `fieldWrapper` для FormRenderer
|
|
2620
2885
|
```tsx
|
|
2621
2886
|
import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
|
|
2622
|
-
import { FormField } from '@reformer/ui-kit';
|
|
2887
|
+
import { Box, FormField, Input } from '@reformer/ui-kit';
|
|
2623
2888
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2889
|
+
// M1: узлы ссылаются на компоненты-значения, листья — на сигналы модели.
|
|
2890
|
+
const schema = createRenderSchema<MyForm>(() => ({
|
|
2891
|
+
component: Box,
|
|
2892
|
+
children: [
|
|
2893
|
+
{ value: model.$.email, component: Input },
|
|
2894
|
+
{ value: model.$.phone, component: Input },
|
|
2895
|
+
],
|
|
2627
2896
|
}));
|
|
2628
2897
|
|
|
2629
2898
|
<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />
|
|
@@ -2670,39 +2939,140 @@ _Source: src/components/ui/form-field.tsx_
|
|
|
2670
2939
|
|
|
2671
2940
|
**Kind:** `const`
|
|
2672
2941
|
|
|
2942
|
+
Готовая многошаговая форма (multi-step wizard) — стилизованная обёртка поверх
|
|
2943
|
+
headless-compound `@reformer/cdk/form-wizard`. Собирает Indicator, тело шагов,
|
|
2944
|
+
Actions (Назад / Далее / Отправить) и Progress в единый layout, работает с
|
|
2945
|
+
одной {@link FormProxy} и декларативным списком {@link FormWizardStep}.
|
|
2946
|
+
|
|
2947
|
+
Один компонент покрывает TS-flow, renderer-react и renderer-json за счёт
|
|
2948
|
+
полиморфного {@link FormWizardStepBody}. Валидация по шагам и submit-валидация
|
|
2949
|
+
задаются через `config` (`{ validateStep, validateAll }`, обычно из
|
|
2950
|
+
`validateFormModel`). Императивный доступ (submit/навигация снаружи дерева) —
|
|
2951
|
+
через `ref` типа `FormWizardHandle<T>`.
|
|
2952
|
+
|
|
2953
|
+
Экспонирует compound-слоты `FormWizard.Indicator` / `.Step` / `.Actions` /
|
|
2954
|
+
`.Progress` для кастомной раскладки.
|
|
2955
|
+
|
|
2673
2956
|
**Signature:**
|
|
2674
2957
|
```typescript
|
|
2675
2958
|
const FormWizard
|
|
2676
2959
|
```
|
|
2677
2960
|
|
|
2961
|
+
**Examples:**
|
|
2962
|
+
|
|
2963
|
+
Кредитная заявка с 3 шагами и внешним submit
|
|
2964
|
+
```tsx
|
|
2965
|
+
import { useMemo, useRef } from 'react';
|
|
2966
|
+
import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
|
|
2967
|
+
import type { FormWizardHandle } from '@reformer/cdk/form-wizard';
|
|
2968
|
+
|
|
2969
|
+
const STEPS: FormWizardStep<CreditApplication>[] = [
|
|
2970
|
+
{ number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
|
|
2971
|
+
{ number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
|
|
2972
|
+
{ number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
|
|
2973
|
+
];
|
|
2974
|
+
|
|
2975
|
+
function CreditForm() {
|
|
2976
|
+
const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
|
|
2977
|
+
const { form, model } = useMemo(() => createCreditForm(), []);
|
|
2978
|
+
const config = useMemo(() => makeValidationConfig(model), [model]);
|
|
2979
|
+
|
|
2980
|
+
const onSubmit = () =>
|
|
2981
|
+
navRef.current?.submit((values) => api.submit(values));
|
|
2982
|
+
|
|
2983
|
+
return (
|
|
2984
|
+
<FormWizard
|
|
2985
|
+
ref={navRef}
|
|
2986
|
+
form={form}
|
|
2987
|
+
config={config}
|
|
2988
|
+
steps={STEPS}
|
|
2989
|
+
onSubmit={onSubmit}
|
|
2990
|
+
/>
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
```
|
|
2994
|
+
|
|
2995
|
+
**See also:**
|
|
2996
|
+
- {@link FormWizardStep} — форма элемента `steps`.
|
|
2997
|
+
- {@link StepIndicator}, {@link FormWizardActions}, {@link FormWizardProgress} — слоты layout'а.
|
|
2998
|
+
|
|
2678
2999
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2679
3000
|
|
|
2680
3001
|
### FormWizardActions
|
|
2681
3002
|
|
|
2682
3003
|
**Kind:** `const`
|
|
2683
3004
|
|
|
3005
|
+
Кнопки навигации wizard'а: «Назад» / «Далее →» / «Отправить». На первом шаге
|
|
3006
|
+
скрывает «Назад», на последнем показывает «Отправить» вместо «Далее». Во время
|
|
3007
|
+
валидации/отправки показывает промежуточные подписи и блокирует кнопку.
|
|
3008
|
+
|
|
3009
|
+
Рендерится из headless-слота `<FormWizard.Actions>` через render-prop, поэтому
|
|
3010
|
+
`prev`/`next`/`submit`/флаги приходят автоматически. Готовый {@link FormWizard}
|
|
3011
|
+
уже подключает этот компонент — использовать напрямую нужно только для кастомной
|
|
3012
|
+
раскладки.
|
|
3013
|
+
|
|
2684
3014
|
**Signature:**
|
|
2685
3015
|
```typescript
|
|
2686
3016
|
export const FormWizardActions: FC<FormWizardActionsProps>
|
|
2687
3017
|
```
|
|
2688
3018
|
|
|
3019
|
+
**Examples:**
|
|
3020
|
+
|
|
3021
|
+
Кастомные подписи в слоте Actions
|
|
3022
|
+
```tsx
|
|
3023
|
+
<FormWizard.Actions onSubmit={onSubmit}>
|
|
3024
|
+
{(actions) => (
|
|
3025
|
+
<FormWizardActions
|
|
3026
|
+
{...actions}
|
|
3027
|
+
submitLabel="Оформить заявку"
|
|
3028
|
+
className="mt-8"
|
|
3029
|
+
/>
|
|
3030
|
+
)}
|
|
3031
|
+
</FormWizard.Actions>
|
|
3032
|
+
```
|
|
3033
|
+
|
|
2689
3034
|
_Source: src/components/form-wizard/form-wizard-actions.tsx_
|
|
2690
3035
|
|
|
2691
3036
|
### FormWizardProgress
|
|
2692
3037
|
|
|
2693
3038
|
**Kind:** `const`
|
|
2694
3039
|
|
|
3040
|
+
Текстовый индикатор прогресса wizard'а — по умолчанию рендерит
|
|
3041
|
+
«Шаг N из M • X% завершено». Формат строки переопределяется пропом `format`.
|
|
3042
|
+
|
|
3043
|
+
Рендерится из headless-слота `<FormWizard.Progress>` через render-prop
|
|
3044
|
+
(`current`/`total`/`percent` приходят автоматически). Готовый {@link FormWizard}
|
|
3045
|
+
уже подключает этот компонент; напрямую нужен только для кастомной раскладки.
|
|
3046
|
+
|
|
2695
3047
|
**Signature:**
|
|
2696
3048
|
```typescript
|
|
2697
3049
|
export const FormWizardProgress: FC<FormWizardProgressProps>
|
|
2698
3050
|
```
|
|
2699
3051
|
|
|
3052
|
+
**Examples:**
|
|
3053
|
+
|
|
3054
|
+
Свой формат строки прогресса
|
|
3055
|
+
```tsx
|
|
3056
|
+
<FormWizard.Progress>
|
|
3057
|
+
{(progress) => (
|
|
3058
|
+
<FormWizardProgress
|
|
3059
|
+
{...progress}
|
|
3060
|
+
format={({ current, total }) => `${current} / ${total}`}
|
|
3061
|
+
/>
|
|
3062
|
+
)}
|
|
3063
|
+
</FormWizard.Progress>
|
|
3064
|
+
```
|
|
3065
|
+
|
|
2700
3066
|
_Source: src/components/form-wizard/form-wizard-progress.tsx_
|
|
2701
3067
|
|
|
2702
3068
|
### FormWizardProps
|
|
2703
3069
|
|
|
2704
3070
|
**Kind:** `interface`
|
|
2705
3071
|
|
|
3072
|
+
Пропсы {@link FormWizard}. Расширяют headless-пропсы из
|
|
3073
|
+
`@reformer/cdk/form-wizard` (`form`, `config`, `onStepChange`, …), добавляя
|
|
3074
|
+
декларативный `steps` и колбэк `onSubmit`.
|
|
3075
|
+
|
|
2706
3076
|
**Signature:**
|
|
2707
3077
|
```typescript
|
|
2708
3078
|
export interface FormWizardProps<
|
|
@@ -2713,43 +3083,71 @@ export interface FormWizardProps<
|
|
|
2713
3083
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2714
3084
|
T extends Record<string, any>,
|
|
2715
3085
|
> extends FormWizardHeadlessProps<T> {
|
|
3086
|
+
/** Внешний CSS-класс корневого контейнера. */
|
|
2716
3087
|
className?: string;
|
|
3088
|
+
/** Декларативный список шагов (см. {@link FormWizardStep}). Порядок = порядок навигации. */
|
|
2717
3089
|
steps: FormWizardStep<T>[];
|
|
3090
|
+
/** Колбэк отправки формы на последнем шаге; вызывается после успешной валидации. */
|
|
2718
3091
|
onSubmit: HeadlessFormWizardActionsProps['onSubmit'];
|
|
2719
3092
|
}
|
|
2720
3093
|
```
|
|
2721
3094
|
|
|
3095
|
+
**See also:**
|
|
3096
|
+
- {@link FormWizardStep} — форма элемента `steps`.
|
|
3097
|
+
- — императивный handle через `ref` (submit/навигация).
|
|
3098
|
+
|
|
2722
3099
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2723
3100
|
|
|
2724
3101
|
### FormWizardStep
|
|
2725
3102
|
|
|
2726
3103
|
**Kind:** `interface`
|
|
2727
3104
|
|
|
3105
|
+
Описание одного шага {@link FormWizard}: порядковый номер, заголовок и иконка
|
|
3106
|
+
для индикатора, плюс полиморфное тело {@link FormWizardStepBody}.
|
|
3107
|
+
|
|
3108
|
+
Массив `FormWizardStep<T>[]` передаётся в проп `steps`. Порядок и `number`
|
|
3109
|
+
задают последовательность навигации; `number` должен быть 1-based и уникальным.
|
|
3110
|
+
|
|
2728
3111
|
**Signature:**
|
|
2729
3112
|
```typescript
|
|
2730
3113
|
export interface FormWizardStep<T> {
|
|
2731
|
-
/**
|
|
3114
|
+
/** Порядковый номер шага (1-based). Уникальный, задаёт порядок навигации. */
|
|
2732
3115
|
number: number;
|
|
2733
|
-
/**
|
|
3116
|
+
/** Заголовок шага, показывается в {@link StepIndicator}. */
|
|
2734
3117
|
title: string;
|
|
2735
|
-
/**
|
|
3118
|
+
/** Иконка шага (эмодзи или строка). Передаётся в headless Indicator. */
|
|
2736
3119
|
icon?: string;
|
|
2737
|
-
/**
|
|
3120
|
+
/** Тело шага — FC | ReactNode | RenderNode<T> (см. {@link FormWizardStepBody}). */
|
|
2738
3121
|
body: FormWizardStepBody<T>;
|
|
2739
3122
|
}
|
|
2740
3123
|
```
|
|
2741
3124
|
|
|
3125
|
+
**Examples:**
|
|
3126
|
+
|
|
3127
|
+
Массив шагов кредитной заявки
|
|
3128
|
+
```tsx
|
|
3129
|
+
const STEPS: FormWizardStep<CreditApplication>[] = [
|
|
3130
|
+
{ number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
|
|
3131
|
+
{ number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
|
|
3132
|
+
{ number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
|
|
3133
|
+
];
|
|
3134
|
+
```
|
|
3135
|
+
|
|
2742
3136
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2743
3137
|
|
|
2744
3138
|
### FormWizardStepBody
|
|
2745
3139
|
|
|
2746
3140
|
**Kind:** `type`
|
|
2747
3141
|
|
|
2748
|
-
Полиморфное тело
|
|
3142
|
+
Полиморфное тело шага {@link FormWizardStep}. Один и тот же {@link FormWizard}
|
|
3143
|
+
покрывает TS-flow, renderer-react и renderer-json за счёт трёх допустимых форм
|
|
3144
|
+
`body`, дискриминация которых выполняется в рантайме по типу значения:
|
|
2749
3145
|
|
|
2750
|
-
-
|
|
2751
|
-
|
|
2752
|
-
-
|
|
3146
|
+
- `ComponentType<{ control: FormProxy<T> }>` — React-компонент; получает
|
|
3147
|
+
`control={form}` (корневой {@link FormProxy}) и сам обращается к нужным полям.
|
|
3148
|
+
- `ReactNode` — готовый JSX или статический контент шага (текст, число и т.п.).
|
|
3149
|
+
- `RenderNode<T>` — RenderSchema-поддерево; рендерится через `RenderNodeComponent`
|
|
3150
|
+
для интеграции с `@reformer/renderer-react`.
|
|
2753
3151
|
|
|
2754
3152
|
**Signature:**
|
|
2755
3153
|
```typescript
|
|
@@ -2759,6 +3157,16 @@ export type FormWizardStepBody<T> =
|
|
|
2759
3157
|
| RenderNode<T>;
|
|
2760
3158
|
```
|
|
2761
3159
|
|
|
3160
|
+
**Examples:**
|
|
3161
|
+
|
|
3162
|
+
Компонент шага получает control
|
|
3163
|
+
```tsx
|
|
3164
|
+
function BasicInfoForm({ control }: { control: FormProxy<CreditApplication> }) {
|
|
3165
|
+
return <FormField control={control.loanAmount} testId="loanAmount" />;
|
|
3166
|
+
}
|
|
3167
|
+
const body: FormWizardStepBody<CreditApplication> = BasicInfoForm;
|
|
3168
|
+
```
|
|
3169
|
+
|
|
2762
3170
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2763
3171
|
|
|
2764
3172
|
### Input
|
|
@@ -2797,6 +3205,27 @@ placeholder="Возраст"
|
|
|
2797
3205
|
/>
|
|
2798
3206
|
```
|
|
2799
3207
|
|
|
3208
|
+
Привязка к полю формы через `useFormControl`
|
|
3209
|
+
```tsx
|
|
3210
|
+
import { useFormControl, type FieldNode } from '@reformer/core';
|
|
3211
|
+
import { Input } from '@reformer/ui-kit';
|
|
3212
|
+
|
|
3213
|
+
function EmailField({ control }: { control: FieldNode<string> }) {
|
|
3214
|
+
const { value, disabled, errors, shouldShowError } = useFormControl(control);
|
|
3215
|
+
return (
|
|
3216
|
+
<Input
|
|
3217
|
+
type="email"
|
|
3218
|
+
value={value}
|
|
3219
|
+
disabled={disabled}
|
|
3220
|
+
onChange={(v) => control.setValue((v ?? '') as string)}
|
|
3221
|
+
onBlur={() => control.markAsTouched()}
|
|
3222
|
+
aria-invalid={shouldShowError}
|
|
3223
|
+
placeholder={shouldShowError ? errors[0]?.message : 'you@example.com'}
|
|
3224
|
+
/>
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
```
|
|
3228
|
+
|
|
2800
3229
|
_Source: src/components/ui/input.tsx_
|
|
2801
3230
|
|
|
2802
3231
|
### InputMask
|
|
@@ -2991,6 +3420,9 @@ _Source: src/components/ui/input.tsx_
|
|
|
2991
3420
|
|
|
2992
3421
|
**Kind:** `function`
|
|
2993
3422
|
|
|
3423
|
+
Состояние загрузки — центрированный спиннер с заголовком и подзаголовком.
|
|
3424
|
+
Типовое применение — показать вместо формы, пока грузятся данные заявки.
|
|
3425
|
+
|
|
2994
3426
|
**Signature:**
|
|
2995
3427
|
```typescript
|
|
2996
3428
|
export function LoadingState({
|
|
@@ -3000,6 +3432,21 @@ export function LoadingState({
|
|
|
3000
3432
|
}: LoadingStateProps): ReactNode
|
|
3001
3433
|
```
|
|
3002
3434
|
|
|
3435
|
+
**Parameters:**
|
|
3436
|
+
- `props` — - Пропсы: `title`, `subtitle`, `className` (все опциональны).
|
|
3437
|
+
|
|
3438
|
+
**Returns:** Разметку блока загрузки.
|
|
3439
|
+
|
|
3440
|
+
**Examples:**
|
|
3441
|
+
|
|
3442
|
+
Загрузка перед рендером формы
|
|
3443
|
+
```tsx
|
|
3444
|
+
const { isLoading } = useLoadCreditApplication(form, '1');
|
|
3445
|
+
if (isLoading) {
|
|
3446
|
+
return <LoadingState />;
|
|
3447
|
+
}
|
|
3448
|
+
```
|
|
3449
|
+
|
|
3003
3450
|
_Source: src/components/state/loading-state.tsx_
|
|
3004
3451
|
|
|
3005
3452
|
### RadioGroup
|
|
@@ -3093,6 +3540,26 @@ export interface RadioOption {
|
|
|
3093
3540
|
|
|
3094
3541
|
_Source: src/components/ui/radio-group.tsx_
|
|
3095
3542
|
|
|
3543
|
+
### ResourceConfig
|
|
3544
|
+
|
|
3545
|
+
**Kind:** `interface`
|
|
3546
|
+
|
|
3547
|
+
Конфигурация асинхронного источника опций для {@link Select}.
|
|
3548
|
+
|
|
3549
|
+
**Signature:**
|
|
3550
|
+
```typescript
|
|
3551
|
+
export interface ResourceConfig<T> {
|
|
3552
|
+
/** Стратегия загрузки. По умолчанию (если не задана) трактуется как `'static'`. */
|
|
3553
|
+
type: ResourceStrategy;
|
|
3554
|
+
/** Функция загрузки опций. Должна вернуть `{ items, totalCount }`. */
|
|
3555
|
+
load: (params?: ResourceLoadParams) => Promise<ResourceResult<T>>;
|
|
3556
|
+
/** Размер страницы для стратегии `partial`. По умолчанию 20. */
|
|
3557
|
+
pageSize?: number;
|
|
3558
|
+
}
|
|
3559
|
+
```
|
|
3560
|
+
|
|
3561
|
+
_Source: src/components/ui/select-resource.ts_
|
|
3562
|
+
|
|
3096
3563
|
### Section
|
|
3097
3564
|
|
|
3098
3565
|
**Kind:** `function`
|
|
@@ -3115,8 +3582,10 @@ export function Section({
|
|
|
3115
3582
|
|
|
3116
3583
|
**Examples:**
|
|
3117
3584
|
|
|
3118
|
-
Заголовок h2 + сетка из двух колонок
|
|
3585
|
+
Заголовок h2 + сетка из двух колонок (M1: лист = `value` + `component`)
|
|
3119
3586
|
```typescript
|
|
3587
|
+
import { Section, Input } from '@reformer/ui-kit';
|
|
3588
|
+
|
|
3120
3589
|
{
|
|
3121
3590
|
component: Section,
|
|
3122
3591
|
componentProps: {
|
|
@@ -3126,20 +3595,22 @@ titleClassName: 'text-xl font-bold',
|
|
|
3126
3595
|
className: 'grid grid-cols-2 gap-4',
|
|
3127
3596
|
},
|
|
3128
3597
|
children: [
|
|
3129
|
-
{
|
|
3130
|
-
{
|
|
3598
|
+
{ value: model.$.firstName, component: Input },
|
|
3599
|
+
{ value: model.$.lastName, component: Input },
|
|
3131
3600
|
],
|
|
3132
3601
|
}
|
|
3133
3602
|
```
|
|
3134
3603
|
|
|
3135
3604
|
Section без заголовка (только обёртка)
|
|
3136
3605
|
```typescript
|
|
3606
|
+
import { Section, Input } from '@reformer/ui-kit';
|
|
3607
|
+
|
|
3137
3608
|
{
|
|
3138
3609
|
component: Section,
|
|
3139
3610
|
componentProps: { className: 'space-y-4 mt-4' },
|
|
3140
3611
|
children: [
|
|
3141
|
-
{
|
|
3142
|
-
{
|
|
3612
|
+
{ value: model.$.address, component: Input },
|
|
3613
|
+
{ value: model.$.city, component: Input },
|
|
3143
3614
|
],
|
|
3144
3615
|
}
|
|
3145
3616
|
```
|
|
@@ -3174,9 +3645,17 @@ _Source: src/components/ui/section.tsx_
|
|
|
3174
3645
|
|
|
3175
3646
|
**Kind:** `const`
|
|
3176
3647
|
|
|
3177
|
-
Выпадающий список на `@radix-ui/react-select`.
|
|
3178
|
-
|
|
3179
|
-
|
|
3648
|
+
Выпадающий список на `@radix-ui/react-select`. Два источника данных: inline
|
|
3649
|
+
`options` и асинхронный `resource` со стратегией загрузки
|
|
3650
|
+
({@link ResourceConfig.type}):
|
|
3651
|
+
|
|
3652
|
+
- `static` — один `load({})` при маунте, без поиска;
|
|
3653
|
+
- `preload` — грузит всё сразу, поиск фильтрует опции на клиенте;
|
|
3654
|
+
- `partial` — серверные поиск (`load({ search })` с debounce) и пагинация
|
|
3655
|
+
(`load({ page })` по мере скролла до `totalCount`).
|
|
3656
|
+
|
|
3657
|
+
Для `preload`/`partial` в дропдауне показывается поле поиска. При
|
|
3658
|
+
`clearable=true` рядом со значением есть крестик сброса в `null`.
|
|
3180
3659
|
|
|
3181
3660
|
**Signature:**
|
|
3182
3661
|
```typescript
|
|
@@ -3196,24 +3675,47 @@ placeholder="Тип кредита"
|
|
|
3196
3675
|
options={[
|
|
3197
3676
|
{ value: 'consumer', label: 'Потребительский' },
|
|
3198
3677
|
{ value: 'mortgage', label: 'Ипотека' },
|
|
3199
|
-
{ value: 'auto', label: 'Авто' },
|
|
3200
3678
|
]}
|
|
3201
3679
|
/>
|
|
3202
3680
|
```
|
|
3203
3681
|
|
|
3204
|
-
|
|
3682
|
+
Серверные поиск и пагинация (resource type='partial')
|
|
3205
3683
|
```tsx
|
|
3206
|
-
import { Select } from '@reformer/ui-kit';
|
|
3684
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit';
|
|
3685
|
+
|
|
3686
|
+
const users: ResourceConfig<string> = {
|
|
3687
|
+
type: 'partial',
|
|
3688
|
+
pageSize: 20,
|
|
3689
|
+
load: async ({ search = '', page = 1, pageSize = 20 }) => {
|
|
3690
|
+
const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
|
|
3691
|
+
const { rows, total } = await res.json();
|
|
3692
|
+
return {
|
|
3693
|
+
items: rows.map((u) => ({ id: u.id, label: u.name, value: u.id })),
|
|
3694
|
+
totalCount: total,
|
|
3695
|
+
};
|
|
3696
|
+
},
|
|
3697
|
+
};
|
|
3207
3698
|
|
|
3208
|
-
<Select
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
{
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3699
|
+
<Select value={userId} onChange={setUserId} resource={users} clearable />
|
|
3700
|
+
```
|
|
3701
|
+
|
|
3702
|
+
Предзагрузка с клиентским поиском (resource type='preload')
|
|
3703
|
+
```tsx
|
|
3704
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit';
|
|
3705
|
+
|
|
3706
|
+
const countries: ResourceConfig<string> = {
|
|
3707
|
+
type: 'preload',
|
|
3708
|
+
load: async () => {
|
|
3709
|
+
const res = await fetch('/api/countries');
|
|
3710
|
+
const items = await res.json();
|
|
3711
|
+
return {
|
|
3712
|
+
items: items.map((c) => ({ id: c.id, label: c.name, value: c.code })),
|
|
3713
|
+
totalCount: items.length,
|
|
3714
|
+
};
|
|
3715
|
+
},
|
|
3716
|
+
};
|
|
3717
|
+
|
|
3718
|
+
<Select value={country} onChange={setCountry} resource={countries} />
|
|
3217
3719
|
```
|
|
3218
3720
|
|
|
3219
3721
|
_Source: src/components/ui/select.tsx_
|
|
@@ -3222,18 +3724,29 @@ _Source: src/components/ui/select.tsx_
|
|
|
3222
3724
|
|
|
3223
3725
|
**Kind:** `function`
|
|
3224
3726
|
|
|
3225
|
-
|
|
3727
|
+
Дропдаун-контent {@link Select} (попап со списком). Обёртка над
|
|
3226
3728
|
`Radix.Select.Content` с порталом, скролл-кнопками сверху/снизу и
|
|
3227
3729
|
`position='popper'` по умолчанию (растягивается под ширину триггера).
|
|
3228
3730
|
|
|
3731
|
+
Опционально принимает `header` (нескроллящаяся шапка, например поле поиска)
|
|
3732
|
+
и `onViewportScroll` (для infinite-scroll — вешается на скроллящийся
|
|
3733
|
+
viewport со списком опций).
|
|
3734
|
+
|
|
3229
3735
|
**Signature:**
|
|
3230
3736
|
```typescript
|
|
3231
3737
|
function SelectContent({
|
|
3232
3738
|
className,
|
|
3233
3739
|
children,
|
|
3234
3740
|
position = 'popper',
|
|
3741
|
+
header,
|
|
3742
|
+
onViewportScroll,
|
|
3235
3743
|
...props
|
|
3236
|
-
}: React.ComponentProps<typeof SelectPrimitive.Content>
|
|
3744
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
|
3745
|
+
/** Нескроллящаяся шапка над списком (например, поле поиска). */
|
|
3746
|
+
header?: React.ReactNode;
|
|
3747
|
+
/** Обработчик прокрутки viewport-а со списком (для infinite-scroll). */
|
|
3748
|
+
onViewportScroll?: React.UIEventHandler<HTMLDivElement>;
|
|
3749
|
+
})
|
|
3237
3750
|
```
|
|
3238
3751
|
|
|
3239
3752
|
**Examples:**
|
|
@@ -3571,11 +4084,33 @@ _Source: src/components/ui/select.tsx_
|
|
|
3571
4084
|
|
|
3572
4085
|
**Kind:** `const`
|
|
3573
4086
|
|
|
4087
|
+
Визуальная цепочка шагов wizard'а — иконки, заголовки и соединительные линии
|
|
4088
|
+
с подсветкой текущего / завершённого шага. Клик (и Enter) по доступному шагу
|
|
4089
|
+
вызывает `goToStep`; недоступные шаги приглушены и не кликабельны. Разметка
|
|
4090
|
+
доступна для скринридеров (`role="navigation"`, `aria-current="step"`,
|
|
4091
|
+
настраиваемые aria-метки).
|
|
4092
|
+
|
|
4093
|
+
Рендерится из headless-слота `<FormWizard.Indicator>` через render-prop
|
|
4094
|
+
(`steps` со статусами и `goToStep` приходят автоматически). Готовый
|
|
4095
|
+
{@link FormWizard} уже подключает этот компонент; напрямую нужен только для
|
|
4096
|
+
кастомной раскладки.
|
|
4097
|
+
|
|
3574
4098
|
**Signature:**
|
|
3575
4099
|
```typescript
|
|
3576
4100
|
export const StepIndicator: FC<StepIndicatorProps>
|
|
3577
4101
|
```
|
|
3578
4102
|
|
|
4103
|
+
**Examples:**
|
|
4104
|
+
|
|
4105
|
+
Индикатор с кастомной aria-меткой контейнера
|
|
4106
|
+
```tsx
|
|
4107
|
+
<FormWizard.Indicator steps={steps}>
|
|
4108
|
+
{(indicator) => (
|
|
4109
|
+
<StepIndicator {...indicator} navAriaLabel="Этапы заявки" className="mb-8" />
|
|
4110
|
+
)}
|
|
4111
|
+
</FormWizard.Indicator>
|
|
4112
|
+
```
|
|
4113
|
+
|
|
3579
4114
|
_Source: src/components/form-wizard/step-indicator.tsx_
|
|
3580
4115
|
|
|
3581
4116
|
### Textarea
|