@reformer/ui-kit 7.0.0-beta.1 → 7.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +774 -251
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -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,13 @@ 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`, `credit-application-schema.ts` (monorepo examples).
|
|
409
438
|
|
|
410
439
|
## 12. Checkbox
|
|
411
440
|
|
|
@@ -453,12 +482,16 @@ import { Checkbox } from '@reformer/ui-kit';
|
|
|
453
482
|
label сверху):
|
|
454
483
|
|
|
455
484
|
```tsx
|
|
456
|
-
import {
|
|
485
|
+
import { createModel, createForm } from '@reformer/core';
|
|
457
486
|
import { Checkbox, FormField } from '@reformer/ui-kit';
|
|
458
487
|
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
488
|
+
const model = createModel<{ accept: boolean }>({ accept: false });
|
|
489
|
+
const schema = {
|
|
490
|
+
children: [
|
|
491
|
+
{ value: model.$.accept, component: Checkbox, componentProps: { label: 'Принять' } },
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
const form = createForm<{ accept: boolean }>({ model, schema });
|
|
462
495
|
|
|
463
496
|
<FormField control={form.accept} testId="accept" />;
|
|
464
497
|
```
|
|
@@ -534,13 +567,19 @@ const LOAN_TYPES = [
|
|
|
534
567
|
В составе формы:
|
|
535
568
|
|
|
536
569
|
```tsx
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
570
|
+
import { createModel, createForm } from '@reformer/core';
|
|
571
|
+
|
|
572
|
+
const model = createModel<{ loanType: string }>({ loanType: 'consumer' });
|
|
573
|
+
const schema = {
|
|
574
|
+
children: [
|
|
575
|
+
{
|
|
576
|
+
value: model.$.loanType,
|
|
577
|
+
component: RadioGroup,
|
|
578
|
+
componentProps: { options: LOAN_TYPES },
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
};
|
|
582
|
+
const form = createForm<{ loanType: string }>({ model, schema });
|
|
544
583
|
|
|
545
584
|
<FormField control={form.loanType} testId="loan-type" />;
|
|
546
585
|
```
|
|
@@ -562,17 +601,29 @@ const form = createForm<FormSchema<{ loanType: string }>>({
|
|
|
562
601
|
данных:
|
|
563
602
|
|
|
564
603
|
- **Inline**: `options={[…]}` — массив `{ value, label, group? }`.
|
|
565
|
-
- **Resource**: `resource={{ type, load }}` — асинхронная загрузка
|
|
604
|
+
- **Resource**: `resource={{ type, load }}` — асинхронная загрузка со стратегией `type`:
|
|
605
|
+
- `static` — один `load({})` при маунте, без поиска (снимок);
|
|
606
|
+
- `preload` — грузит всё сразу, поиск фильтрует опции **на клиенте**;
|
|
607
|
+
- `partial` — **серверные** поиск (`load({ search })` с debounce ~300 мс) и
|
|
608
|
+
пагинация (`load({ page })` по мере прокрутки списка до `totalCount`).
|
|
609
|
+
|
|
610
|
+
Для `preload`/`partial` в дропдауне появляется поле поиска.
|
|
566
611
|
|
|
567
612
|
### API
|
|
568
613
|
|
|
569
614
|
```typescript
|
|
570
615
|
interface ResourceConfig<T> {
|
|
616
|
+
/** Стратегия загрузки. Если не задана — трактуется как `static`. */
|
|
571
617
|
type: 'static' | 'preload' | 'partial';
|
|
572
|
-
load: (params?:
|
|
618
|
+
load: (params?: {
|
|
619
|
+
search?: string; // серверная фильтрация (partial)
|
|
620
|
+
page?: number; // 1-based, пагинация (partial)
|
|
621
|
+
pageSize?: number;
|
|
622
|
+
}) => Promise<{
|
|
573
623
|
items: Array<{ id: string | number; label: string; value: T; group?: string }>;
|
|
574
|
-
totalCount: number;
|
|
624
|
+
totalCount: number; // общее число опций — для пагинации (partial)
|
|
575
625
|
}>;
|
|
626
|
+
pageSize?: number; // размер страницы для partial (по умолчанию 20)
|
|
576
627
|
}
|
|
577
628
|
|
|
578
629
|
interface SelectProps<T> {
|
|
@@ -593,7 +644,7 @@ interface SelectProps<T> {
|
|
|
593
644
|
| Prop | Тип | Default | Описание |
|
|
594
645
|
| ------------- | --------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
595
646
|
| `options` | `Array<{value,label,group?}>` | — | Inline-варианты. `value` приводится к строке. `group` опционально — варианты с одинаковым `group` объединяются в `SelectGroup` с `SelectLabel`. |
|
|
596
|
-
| `resource` | `ResourceConfig<T>` | — | Асинхронный
|
|
647
|
+
| `resource` | `ResourceConfig<T>` | — | Асинхронный источник со стратегией `type` (`static`/`preload`/`partial`). Во время первичной загрузки `Select` показывает `Loading...` и блокируется; при пагинации (`partial`) внизу списка — `Loading more...`. |
|
|
597
648
|
| `value` | `string \| null` | `null` | Выбранное значение (всегда строка из `option.value`). |
|
|
598
649
|
| `onChange` | `(value: string \| null) => void` | — | Срабатывает при выборе. При нажатии на крестик (`clearable`) приходит `null`. |
|
|
599
650
|
| `placeholder` | `string` | `'Select an option...'` | Подсказка в триггере. |
|
|
@@ -634,10 +685,10 @@ import { Select } from '@reformer/ui-kit';
|
|
|
634
685
|
/>;
|
|
635
686
|
```
|
|
636
687
|
|
|
637
|
-
Async `resource` (
|
|
688
|
+
Async `resource`, стратегия `preload` (грузим всё, поиск на клиенте):
|
|
638
689
|
|
|
639
690
|
```tsx
|
|
640
|
-
import { Select, type ResourceConfig } from '@reformer/ui-kit
|
|
691
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit';
|
|
641
692
|
|
|
642
693
|
const banksResource: ResourceConfig<string> = {
|
|
643
694
|
type: 'preload',
|
|
@@ -654,6 +705,26 @@ const banksResource: ResourceConfig<string> = {
|
|
|
654
705
|
<Select value={bankId} onChange={setBankId} resource={banksResource} />;
|
|
655
706
|
```
|
|
656
707
|
|
|
708
|
+
Стратегия `partial` (серверные поиск + пагинация больших списков):
|
|
709
|
+
|
|
710
|
+
```tsx
|
|
711
|
+
const usersResource: ResourceConfig<string> = {
|
|
712
|
+
type: 'partial',
|
|
713
|
+
pageSize: 20,
|
|
714
|
+
load: async ({ search = '', page = 1, pageSize = 20 } = {}) => {
|
|
715
|
+
const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
|
|
716
|
+
const { rows, total }: { rows: Array<{ id: number; name: string }>; total: number } =
|
|
717
|
+
await res.json();
|
|
718
|
+
return {
|
|
719
|
+
items: rows.map((u) => ({ id: u.id, value: String(u.id), label: u.name })),
|
|
720
|
+
totalCount: total, // Select догружает страницы, пока items.length < totalCount
|
|
721
|
+
};
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
<Select value={userId} onChange={setUserId} resource={usersResource} clearable />;
|
|
726
|
+
```
|
|
727
|
+
|
|
657
728
|
Grouped options:
|
|
658
729
|
|
|
659
730
|
```tsx
|
|
@@ -687,18 +758,25 @@ Grouped options:
|
|
|
687
758
|
В составе формы:
|
|
688
759
|
|
|
689
760
|
```tsx
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
761
|
+
import { createModel, createForm } from '@reformer/core';
|
|
762
|
+
|
|
763
|
+
const model = createModel<{ city: string }>({ city: '' });
|
|
764
|
+
const schema = {
|
|
765
|
+
children: [
|
|
766
|
+
{
|
|
767
|
+
value: model.$.city,
|
|
768
|
+
component: Select,
|
|
769
|
+
componentProps: {
|
|
770
|
+
placeholder: 'Город',
|
|
771
|
+
options: [
|
|
772
|
+
{ value: 'msk', label: 'Москва' },
|
|
773
|
+
{ value: 'spb', label: 'Санкт-Петербург' },
|
|
774
|
+
],
|
|
775
|
+
},
|
|
699
776
|
},
|
|
700
|
-
|
|
701
|
-
}
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
const form = createForm<{ city: string }>({ model, schema });
|
|
702
780
|
|
|
703
781
|
<FormField control={form.city} testId="city" />;
|
|
704
782
|
```
|
|
@@ -722,7 +800,6 @@ const form = createForm<FormSchema<{ city: string }>>({
|
|
|
722
800
|
- [02-text-fields.md](02-text-fields.md) — `Input`, `InputMask`, `InputPassword`, `Textarea`.
|
|
723
801
|
- [05-form-field-integration.md](05-form-field-integration.md) — `FormField` распознаёт `Checkbox` и не дублирует label.
|
|
724
802
|
- [06-troubleshooting.md](06-troubleshooting.md) — «Select не показывает options», «options vs resource», «onBlur не срабатывает на Select/RadioGroup».
|
|
725
|
-
- Эталон: `credit-application-schema.ts` (monorepo example) — большой пример с `Select` и `Checkbox` в реальной форме.
|
|
726
803
|
|
|
727
804
|
## 16. Button
|
|
728
805
|
|
|
@@ -1043,35 +1120,40 @@ interface FormFieldProps {
|
|
|
1043
1120
|
|
|
1044
1121
|
```tsx
|
|
1045
1122
|
import { useMemo } from 'react';
|
|
1046
|
-
import {
|
|
1123
|
+
import { createModel, createForm } from '@reformer/core';
|
|
1047
1124
|
import { Button, FormField, Input, Select } from '@reformer/ui-kit';
|
|
1048
1125
|
|
|
1049
|
-
|
|
1126
|
+
type RegistrationForm = {
|
|
1050
1127
|
email: string;
|
|
1051
1128
|
country: string;
|
|
1052
|
-
}
|
|
1129
|
+
};
|
|
1053
1130
|
|
|
1054
1131
|
function RegistrationPage() {
|
|
1055
|
-
const form = useMemo(
|
|
1056
|
-
()
|
|
1057
|
-
|
|
1058
|
-
|
|
1132
|
+
const form = useMemo(() => {
|
|
1133
|
+
const model = createModel<RegistrationForm>({ email: '', country: '' });
|
|
1134
|
+
const schema = {
|
|
1135
|
+
children: [
|
|
1136
|
+
{
|
|
1137
|
+
value: model.$.email,
|
|
1059
1138
|
component: Input,
|
|
1060
|
-
componentProps: { label: 'Email', placeholder: 'you@example.com' },
|
|
1139
|
+
componentProps: { label: 'Email', placeholder: 'you@example.com', testId: 'email' },
|
|
1061
1140
|
},
|
|
1062
|
-
|
|
1141
|
+
{
|
|
1142
|
+
value: model.$.country,
|
|
1063
1143
|
component: Select,
|
|
1064
1144
|
componentProps: {
|
|
1065
1145
|
label: 'Страна',
|
|
1146
|
+
testId: 'country',
|
|
1066
1147
|
options: [
|
|
1067
1148
|
{ value: 'ru', label: 'Россия' },
|
|
1068
1149
|
{ value: 'by', label: 'Беларусь' },
|
|
1069
1150
|
],
|
|
1070
1151
|
},
|
|
1071
1152
|
},
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1153
|
+
],
|
|
1154
|
+
};
|
|
1155
|
+
return createForm<RegistrationForm>({ model, schema });
|
|
1156
|
+
}, []);
|
|
1075
1157
|
|
|
1076
1158
|
return (
|
|
1077
1159
|
<form className="space-y-4">
|
|
@@ -1094,37 +1176,37 @@ function RegistrationPage() {
|
|
|
1094
1176
|
|
|
1095
1177
|
```tsx
|
|
1096
1178
|
import { useMemo } from 'react';
|
|
1179
|
+
import { createForm } from '@reformer/core';
|
|
1097
1180
|
import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
|
|
1098
|
-
import { FormField,
|
|
1099
|
-
import {
|
|
1181
|
+
import { FormField, Input, Section } from '@reformer/ui-kit';
|
|
1182
|
+
import { createCreditApplicationModel } from './schemas/model';
|
|
1100
1183
|
|
|
1101
1184
|
function CreditApplicationPage() {
|
|
1102
|
-
const form = useMemo(() =>
|
|
1103
|
-
|
|
1104
|
-
()
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1185
|
+
const { form, schema } = useMemo(() => {
|
|
1186
|
+
// M1: модель — источник истины; листья схемы ссылаются на её сигналы.
|
|
1187
|
+
const model = createCreditApplicationModel();
|
|
1188
|
+
const schema = createRenderSchema<CreditApplication>(() => ({
|
|
1189
|
+
component: Section,
|
|
1190
|
+
componentProps: { title: 'Заявка', className: 'space-y-4' },
|
|
1191
|
+
children: [
|
|
1192
|
+
{ value: model.$.email, component: Input, componentProps: { testId: 'email' } },
|
|
1193
|
+
{ value: model.$.phone, component: Input, componentProps: { testId: 'phone' } },
|
|
1194
|
+
{ value: model.$.amount, component: Input, componentProps: { testId: 'amount' } },
|
|
1195
|
+
],
|
|
1196
|
+
}));
|
|
1197
|
+
const form = createForm<CreditApplication>({ model, schema });
|
|
1198
|
+
return { form, schema };
|
|
1199
|
+
}, []);
|
|
1116
1200
|
|
|
1117
1201
|
// settings.fieldWrapper применяется к каждому field-узлу автоматически.
|
|
1118
1202
|
return <FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />;
|
|
1119
1203
|
}
|
|
1120
1204
|
```
|
|
1121
1205
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
`testId` рендерер берёт из `componentProps.testId` поля schema:
|
|
1206
|
+
`testId` рендерер берёт из `componentProps.testId` листа schema:
|
|
1125
1207
|
|
|
1126
1208
|
```tsx
|
|
1127
|
-
{
|
|
1209
|
+
{ value: itemModel.$.bank, component: Input, componentProps: { testId: 'existingLoan-bank' } }
|
|
1128
1210
|
// → <FormField control={...} testId="existingLoan-bank" />
|
|
1129
1211
|
// → data-testid="field-existingLoan-bank", "input-existingLoan-bank", ...
|
|
1130
1212
|
```
|
|
@@ -1164,15 +1246,20 @@ import { InputMask } from '@reformer/ui-kit/input-mask';
|
|
|
1164
1246
|
верхний `Label`:
|
|
1165
1247
|
|
|
1166
1248
|
```tsx
|
|
1249
|
+
import { createModel, createForm } from '@reformer/core';
|
|
1167
1250
|
import { Checkbox, FormField } from '@reformer/ui-kit';
|
|
1168
1251
|
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1252
|
+
const model = createModel<{ accept: boolean }>({ accept: false });
|
|
1253
|
+
const schema = {
|
|
1254
|
+
children: [
|
|
1255
|
+
{
|
|
1256
|
+
value: model.$.accept,
|
|
1257
|
+
component: Checkbox,
|
|
1258
|
+
componentProps: { label: 'Принимаю условия' },
|
|
1259
|
+
},
|
|
1260
|
+
],
|
|
1261
|
+
};
|
|
1262
|
+
const form = createForm<{ accept: boolean }>({ model, schema });
|
|
1176
1263
|
|
|
1177
1264
|
<FormField control={form.accept} testId="accept" />;
|
|
1178
1265
|
// рендерится только Checkbox с label справа + error снизу.
|
|
@@ -1207,7 +1294,6 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
|
1207
1294
|
- [04-layout-and-buttons.md](04-layout-and-buttons.md) — `Button` для submit/prev/next.
|
|
1208
1295
|
- [06-troubleshooting.md](06-troubleshooting.md) — «label дублируется», «error не появляется», «FormField не подцепляет ошибки».
|
|
1209
1296
|
- CDK-хуки: [@reformer/cdk/form-field](../../../reformer-cdk/docs/llms/) (`FormField.Root`, `useFormFieldContext`).
|
|
1210
|
-
- Эталон: `CreditApplicationFormRenderer.tsx` (monorepo example) — `FormField` как `fieldWrapper` целой multi-step формы.
|
|
1211
1297
|
|
|
1212
1298
|
## 25. 1. `Input type="number"` возвращает строку, а не число (или `null`)
|
|
1213
1299
|
|
|
@@ -1323,14 +1409,17 @@ const MyLink = React.forwardRef<HTMLAnchorElement, { href: string; children: Rea
|
|
|
1323
1409
|
|
|
1324
1410
|
- Передан `checked` вместо `value` (`<Checkbox checked={...}>`) — пропа
|
|
1325
1411
|
`checked` нет, нужно `value`.
|
|
1326
|
-
- В
|
|
1327
|
-
отрендерится как `false`, и при `setValue(true)` без вмешательства
|
|
1328
|
-
re-render не произойдёт. Указывай `
|
|
1412
|
+
- В модели поле имеет тип `boolean`, но начальное значение `undefined` —
|
|
1413
|
+
компонент отрендерится как `false`, и при `setValue(true)` без вмешательства
|
|
1414
|
+
React re-render не произойдёт. Указывай `accept: false` явно в initial-значениях
|
|
1415
|
+
модели.
|
|
1329
1416
|
|
|
1330
1417
|
```typescript
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1418
|
+
const model = createModel<{ accept: boolean }>({ accept: false }); // false, не undefined!
|
|
1419
|
+
const schema = {
|
|
1420
|
+
children: [{ value: model.$.accept, component: Checkbox }],
|
|
1421
|
+
};
|
|
1422
|
+
const form = createForm<{ accept: boolean }>({ model, schema });
|
|
1334
1423
|
```
|
|
1335
1424
|
|
|
1336
1425
|
## 30. 6. `FormField` не подцепляет ошибки (`<error>` не появляется)
|
|
@@ -1347,13 +1436,18 @@ const form = createForm<FormSchema<{ accept: boolean }>>({
|
|
|
1347
1436
|
после `blur` или `markAsTouched`. Если submit-кнопка не вызывает
|
|
1348
1437
|
`form.markAsTouched()` — пользователь вообще не увидит ошибку.
|
|
1349
1438
|
|
|
1350
|
-
**Решение.** На submit
|
|
1439
|
+
**Решение.** На submit обязательно помечаем touched и валидируем модель по схеме
|
|
1440
|
+
(M1: `validateFormModel(model, schema)` — именно он прогоняет `validators` листьев
|
|
1441
|
+
и роутит ошибки в ноды):
|
|
1351
1442
|
|
|
1352
1443
|
```tsx
|
|
1444
|
+
import { validateFormModel } from '@reformer/core';
|
|
1445
|
+
|
|
1353
1446
|
const onSubmit = async () => {
|
|
1354
1447
|
form.markAsTouched();
|
|
1355
|
-
|
|
1356
|
-
|
|
1448
|
+
const res = await validateFormModel(model, schema);
|
|
1449
|
+
if (!res.valid) return;
|
|
1450
|
+
// ... отправка (значения — из model.get())
|
|
1357
1451
|
};
|
|
1358
1452
|
```
|
|
1359
1453
|
|
|
@@ -1465,14 +1559,14 @@ const hasValue = Boolean(value);
|
|
|
1465
1559
|
|
|
1466
1560
|
## 35. 11. JSON-renderer: `Select`-`options` хранятся в реестре, но в дропдауне пусто
|
|
1467
1561
|
|
|
1468
|
-
**Симптом.** Регистрируется
|
|
1562
|
+
**Симптом.** Регистрируется dataSource `LOAN_TYPES` через `reg.dataSource('LOAN_TYPES', list)`,
|
|
1469
1563
|
в JSON-схеме `componentProps: { options: '$LOAN_TYPES' }`, но опции пустые.
|
|
1470
1564
|
|
|
1471
1565
|
**Причина.** `Select` ждёт `options: Array<{value, label, group?}>`, а из
|
|
1472
1566
|
реестра приходит уже обработанная строкой ссылка `'$LOAN_TYPES'`. Нужен
|
|
1473
|
-
правильный синтаксис
|
|
1567
|
+
правильный синтаксис dataSource-ссылки в реестре.
|
|
1474
1568
|
|
|
1475
|
-
**Решение.** Проверь convention для
|
|
1569
|
+
**Решение.** Проверь convention для dataSource-ссылок в
|
|
1476
1570
|
[`renderer-json/03-registry.md`](../../../reformer-renderer-json/docs/llms/03-registry.md).
|
|
1477
1571
|
Внутри `Select` дальнейших магий нет — он просто читает `directOptions`
|
|
1478
1572
|
один в один.
|
|
@@ -1486,19 +1580,47 @@ const hasValue = Boolean(value);
|
|
|
1486
1580
|
## 37. Базовое использование
|
|
1487
1581
|
|
|
1488
1582
|
```tsx
|
|
1489
|
-
import { useRef } from 'react';
|
|
1583
|
+
import { useMemo, useRef, type FC } from 'react';
|
|
1490
1584
|
import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
|
|
1491
|
-
import
|
|
1492
|
-
import type {
|
|
1585
|
+
import { FormField, Input, Checkbox } from '@reformer/ui-kit';
|
|
1586
|
+
import type { FormWizardHandle, FormWizardConfig } from '@reformer/cdk/form-wizard';
|
|
1587
|
+
import { createModel, createForm, validateFormModel, type FormProxy } from '@reformer/core';
|
|
1588
|
+
import { required, email, minLength } from '@reformer/core/validators';
|
|
1493
1589
|
|
|
1494
1590
|
// Используйте `type`, не `interface`, для structural-совместимости с
|
|
1495
|
-
//
|
|
1591
|
+
// constraint `T extends Record<string, any>` внутри FormWizard generic'а.
|
|
1496
1592
|
type MyForm = {
|
|
1497
1593
|
email: string;
|
|
1498
1594
|
password: string;
|
|
1499
1595
|
confirmation: boolean;
|
|
1500
1596
|
};
|
|
1501
1597
|
|
|
1598
|
+
// M1: модель — источник истины значений; листья схемы ссылаются на её сигналы.
|
|
1599
|
+
const model = createModel<MyForm>({ email: '', password: '', confirmation: false });
|
|
1600
|
+
const schema = {
|
|
1601
|
+
children: [
|
|
1602
|
+
{
|
|
1603
|
+
value: model.$.email,
|
|
1604
|
+
component: Input,
|
|
1605
|
+
componentProps: { label: 'Email', testId: 'email' },
|
|
1606
|
+
validators: [required(), email()],
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
value: model.$.password,
|
|
1610
|
+
component: Input,
|
|
1611
|
+
componentProps: { label: 'Пароль', testId: 'password' },
|
|
1612
|
+
validators: [required(), minLength(8)],
|
|
1613
|
+
},
|
|
1614
|
+
{
|
|
1615
|
+
value: model.$.confirmation,
|
|
1616
|
+
component: Checkbox,
|
|
1617
|
+
componentProps: { label: 'Подтверждаю' },
|
|
1618
|
+
validators: [required()],
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
};
|
|
1622
|
+
const form = createForm<MyForm>({ model, schema });
|
|
1623
|
+
|
|
1502
1624
|
const Step1: FC<{ control: FormProxy<MyForm> }> = ({ control }) => (
|
|
1503
1625
|
<FormField control={control.email} />
|
|
1504
1626
|
);
|
|
@@ -1513,25 +1635,20 @@ const steps: FormWizardStep<MyForm>[] = [
|
|
|
1513
1635
|
{ number: 3, title: 'Готово', icon: '✓', body: <ConfirmationView /> },
|
|
1514
1636
|
];
|
|
1515
1637
|
|
|
1516
|
-
// ⚠️ КРИТИЧНО: `
|
|
1517
|
-
//
|
|
1518
|
-
//
|
|
1519
|
-
//
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1638
|
+
// ⚠️ КРИТИЧНО: `config` это **FormWizardConfig** — объект с ДВУМЯ колбэками
|
|
1639
|
+
// (`validateStep`, `validateAll`), НЕ схемы/массивы валидаторов. Каждый колбэк
|
|
1640
|
+
// возвращает `boolean | Promise<boolean>`: `true` = валидно, идём дальше.
|
|
1641
|
+
// Если колбэк не задан — соответствующий шаг/submit считается валидным (no-op).
|
|
1642
|
+
// Канон M1 — валидировать per-step/полностью через validateFormModel(model, ...).
|
|
1643
|
+
const config: FormWizardConfig = {
|
|
1644
|
+
// step 1-based. Провалидируй нужный шаг (собери под-схему шага и прогони
|
|
1645
|
+
// validateFormModel), верни boolean.
|
|
1646
|
+
validateStep: async (step) => {
|
|
1647
|
+
const stepSchema = { children: [schema.children[step - 1]] };
|
|
1648
|
+
const res = await validateFormModel(model, stepSchema);
|
|
1649
|
+
return res.valid;
|
|
1528
1650
|
},
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const fullValidation: ValidationSchemaFn<MyForm> = (path) => {
|
|
1532
|
-
STEP_VALIDATIONS[1](path);
|
|
1533
|
-
STEP_VALIDATIONS[2](path);
|
|
1534
|
-
required(path.confirmation);
|
|
1651
|
+
validateAll: async () => (await validateFormModel(model, schema)).valid,
|
|
1535
1652
|
};
|
|
1536
1653
|
|
|
1537
1654
|
// ref типизируется явно типом формы; constraint `T extends Record<string, any>`
|
|
@@ -1540,21 +1657,26 @@ const navRef = useRef<FormWizardHandle<MyForm>>(null);
|
|
|
1540
1657
|
|
|
1541
1658
|
// ВАЖНО: prop-level `onSubmit` имеет signature `() => void | Promise<void>` —
|
|
1542
1659
|
// БЕЗ аргумента values. Это by-design (см. FormWizardActionsProps в @reformer/cdk).
|
|
1543
|
-
// Чтобы получить values — читай их из
|
|
1660
|
+
// Чтобы получить values — читай их из модели внутри handler:
|
|
1544
1661
|
const handleSubmit = async () => {
|
|
1545
|
-
const values =
|
|
1662
|
+
const values = model.get();
|
|
1546
1663
|
await api.submit(values);
|
|
1547
1664
|
};
|
|
1548
1665
|
|
|
1549
1666
|
<FormWizard
|
|
1550
1667
|
ref={navRef}
|
|
1551
1668
|
form={form}
|
|
1552
|
-
config={
|
|
1669
|
+
config={config}
|
|
1553
1670
|
steps={steps}
|
|
1554
1671
|
onSubmit={handleSubmit}
|
|
1555
1672
|
/>;
|
|
1556
1673
|
```
|
|
1557
1674
|
|
|
1675
|
+
> **`config` не привязан к типу формы.** `FormWizardConfig` — это `{ validateStep?, validateAll? }`,
|
|
1676
|
+
> оба колбэка возвращают `boolean | Promise<boolean>`. Канон M1 — прогонять
|
|
1677
|
+
> `validateFormModel(model, schema)` (именно он исполняет `validators` листьев;
|
|
1678
|
+
> `form.validate()` по нодам схемные правила НЕ запустит).
|
|
1679
|
+
|
|
1558
1680
|
### Альтернатива — imperative submit с values
|
|
1559
1681
|
|
|
1560
1682
|
`navRef.current?.submit(callback)` — отдельный API, ПРИНИМАЕТ `(values: T) =>` callback.
|
|
@@ -1585,8 +1707,14 @@ const handleSaveAndExit = async () => {
|
|
|
1585
1707
|
|
|
1586
1708
|
## 39. RenderNode body (renderer-react / renderer-json)
|
|
1587
1709
|
|
|
1710
|
+
M1: схема без аргумента `path` — листья ссылаются на сигналы модели
|
|
1711
|
+
(`value: model.$.x`), а не на `path.x`:
|
|
1712
|
+
|
|
1588
1713
|
```tsx
|
|
1589
|
-
|
|
1714
|
+
import { createRenderSchema } from '@reformer/renderer-react';
|
|
1715
|
+
import { Box, Input } from '@reformer/ui-kit';
|
|
1716
|
+
|
|
1717
|
+
const renderSchema = createRenderSchema<CreditApplication>(() => ({
|
|
1590
1718
|
selector: 'wizard',
|
|
1591
1719
|
component: FormWizard,
|
|
1592
1720
|
componentProps: {
|
|
@@ -1601,12 +1729,15 @@ const renderSchema = (path) => ({
|
|
|
1601
1729
|
body: {
|
|
1602
1730
|
component: Box,
|
|
1603
1731
|
componentProps: { className: 'space-y-4' },
|
|
1604
|
-
children: [
|
|
1732
|
+
children: [
|
|
1733
|
+
{ value: model.$.loanAmount, component: Input },
|
|
1734
|
+
{ value: model.$.loanTerm, component: Input },
|
|
1735
|
+
],
|
|
1605
1736
|
},
|
|
1606
1737
|
},
|
|
1607
1738
|
],
|
|
1608
1739
|
},
|
|
1609
|
-
});
|
|
1740
|
+
}));
|
|
1610
1741
|
```
|
|
1611
1742
|
|
|
1612
1743
|
ui-kit FormWizard детектирует RenderNode (объект с `.component` без React-element-маркера) и оборачивает в `RenderNodeComponent` с `form={form}`.
|
|
@@ -1685,11 +1816,22 @@ const wizardRef = useRef<FormWizardHandle<MyForm>>(null);
|
|
|
1685
1816
|
|
|
1686
1817
|
<FormWizard ref={wizardRef} ... />
|
|
1687
1818
|
|
|
1688
|
-
// Программная
|
|
1689
|
-
wizardRef.current?.goToStep(2);
|
|
1690
|
-
wizardRef.current?.
|
|
1819
|
+
// Программная навигация и submit (см. FormWizardHandle<T> в @reformer/cdk):
|
|
1820
|
+
wizardRef.current?.goToStep(2); // boolean: false, если предыдущий шаг не завершён
|
|
1821
|
+
await wizardRef.current?.goToNextStep(); // валидирует текущий шаг, затем переходит
|
|
1822
|
+
wizardRef.current?.goToPreviousStep();
|
|
1823
|
+
await wizardRef.current?.validateCurrentStep(); // Promise<boolean>
|
|
1824
|
+
|
|
1825
|
+
// submit принимает callback (values: T) => R | Promise<R>; возвращает R | null
|
|
1826
|
+
// (null — не прошла validateAll):
|
|
1827
|
+
const result = await wizardRef.current?.submit((values) => api.submit(values));
|
|
1691
1828
|
```
|
|
1692
1829
|
|
|
1830
|
+
Доступные поля/методы `FormWizardHandle<T>`: `form`, `currentStep`,
|
|
1831
|
+
`completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`,
|
|
1832
|
+
`validateCurrentStep()`, `goToNextStep()`, `goToPreviousStep()`,
|
|
1833
|
+
`goToStep(step)`, `submit(cb)`.
|
|
1834
|
+
|
|
1693
1835
|
## 43. Базовое использование (TS-flow)
|
|
1694
1836
|
|
|
1695
1837
|
```tsx
|
|
@@ -1737,34 +1879,42 @@ const PropertyForm: FC<{ control: FormProxy<Property> }> = ({ control }) => (
|
|
|
1737
1879
|
|
|
1738
1880
|
## 44. Renderer-react RenderSchema
|
|
1739
1881
|
|
|
1740
|
-
|
|
1882
|
+
M1: схема без аргумента `path` (`createRenderSchema<T>(() => ...)`). `control`
|
|
1883
|
+
ссылается на массив модели (`model.<arrayField>` — `ModelArray<T>`); `FormArraySection`
|
|
1884
|
+
резолвит его в `ArrayNode` внутри.
|
|
1741
1885
|
|
|
1742
1886
|
```tsx
|
|
1743
|
-
|
|
1887
|
+
import { createRenderSchema } from '@reformer/renderer-react';
|
|
1888
|
+
import { FormArraySection } from '@reformer/ui-kit/form-array';
|
|
1889
|
+
|
|
1890
|
+
const renderSchema = createRenderSchema<CreditApplication>(() => ({
|
|
1744
1891
|
selector: 'properties-section',
|
|
1745
1892
|
component: FormArraySection,
|
|
1746
1893
|
componentProps: {
|
|
1747
|
-
control:
|
|
1894
|
+
control: model.properties, // ModelArray → резолвится в ArrayNode
|
|
1748
1895
|
itemComponent: PropertyForm, // FC напрямую
|
|
1749
1896
|
title: 'Имущество',
|
|
1750
1897
|
addButtonLabel: '+ Добавить имущество',
|
|
1898
|
+
initialValue: createBlankProperty(),
|
|
1751
1899
|
},
|
|
1752
|
-
});
|
|
1900
|
+
}));
|
|
1753
1901
|
```
|
|
1754
1902
|
|
|
1755
|
-
ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
|
|
1903
|
+
ui-kit FormArraySection маркирован `__selfManagedChildren = true` — родитель-renderer пробрасывает `form` без рекурсии.
|
|
1904
|
+
|
|
1905
|
+
> Альтернатива — нативный array-узел движка `{ array: model.properties, initialValue, item: (im) => ({ children: [ { value: im.$.field, component } ] }) }`.
|
|
1756
1906
|
|
|
1757
1907
|
## 45. JSON (renderer-json)
|
|
1758
1908
|
|
|
1759
1909
|
Два варианта `itemComponent`:
|
|
1760
1910
|
|
|
1761
|
-
### Вариант 1: registry-name (FC зарегистрирован
|
|
1911
|
+
### Вариант 1: registry-name (FC зарегистрирован через reg.component)
|
|
1762
1912
|
|
|
1763
1913
|
```ts
|
|
1764
1914
|
// registry.ts
|
|
1765
1915
|
defineRegistry((reg) => {
|
|
1766
|
-
reg.
|
|
1767
|
-
reg.
|
|
1916
|
+
reg.component('FormArraySection', FormArraySection);
|
|
1917
|
+
reg.component('PropertyForm', PropertyForm);
|
|
1768
1918
|
});
|
|
1769
1919
|
```
|
|
1770
1920
|
|
|
@@ -1817,20 +1967,25 @@ defineRegistry((reg) => {
|
|
|
1817
1967
|
|
|
1818
1968
|
## 46. Props (полный список)
|
|
1819
1969
|
|
|
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
|
-
| `
|
|
1970
|
+
| Prop | Type | Default | Описание |
|
|
1971
|
+
| -------------------- | ------------------------------------------------------------ | -------------------------------- | --------------------------------------------------------- |
|
|
1972
|
+
| `control` | `FormArrayProxy<T> \| ArrayNode<T> \| undefined` | required | Массив для управления (в RenderSchema — `FieldPathNode`) |
|
|
1973
|
+
| `itemComponent` | `ComponentType<{ control: FormProxy<T> }>` | required | FC для рендера каждого item |
|
|
1974
|
+
| `title` | `string` | — | Заголовок секции (h3) |
|
|
1975
|
+
| `itemLabel` | `string \| (control: FormProxy<T>, index: number) => string` | — | Метка над каждым item |
|
|
1976
|
+
| `addButtonLabel` | `string` | `'+ Добавить'` | Текст кнопки добавления |
|
|
1977
|
+
| `removeButtonLabel` | `string` | `'Удалить'` | Текст кнопки удаления |
|
|
1978
|
+
| `emptyMessage` | `string` | — | Сообщение при пустом массиве |
|
|
1979
|
+
| `emptyMessageHint` | `string` | — | Подсказка под emptyMessage |
|
|
1980
|
+
| `hasItems` | `boolean` | — | `false` → секция полностью скрыта |
|
|
1981
|
+
| `initialValue` | `Partial<T>` | — | Plain-leaf значения для новых items |
|
|
1982
|
+
| `showRemoveOnSingle` | `boolean` | `false` | Показывать «Удалить» при одном item |
|
|
1983
|
+
| `reorderable` | `boolean` | `false` | Показывать кнопки ↑/↓ для перестановки элементов |
|
|
1984
|
+
| `maxItems` | `number` | — | Максимум items (AddButton скрывается при достижении) |
|
|
1985
|
+
| `className` | `string` | `'space-y-3 mt-2'` | Класс `<section>`-обёртки |
|
|
1986
|
+
| `cardClassName` | `string` | `'mb-4 p-4 bg-white rounded border'` | Класс card-обёртки каждого item |
|
|
1987
|
+
| `form` | `FormProxy<unknown>` | авто-инъекция | Проброс `form` (RenderNodeComponent через `__selfManagedChildren`) |
|
|
1988
|
+
| `fieldWrapper` | `ComponentType<FieldWrapperProps>` | авто-инъекция | Field wrapper для дочерних полей (по умолчанию — от родителя) |
|
|
1834
1989
|
|
|
1835
1990
|
## 47. Critical: `initialValue` — PLAIN LEAVES ONLY
|
|
1836
1991
|
|
|
@@ -1846,10 +2001,13 @@ FieldConfig попадает в значение поля → Textarea ренд
|
|
|
1846
2001
|
|
|
1847
2002
|
## 48. Schema-driven (canonical pattern)
|
|
1848
2003
|
|
|
1849
|
-
|
|
2004
|
+
Канон M1: `createModel` → схема, где лист = `{ value: model.$.field, component,
|
|
2005
|
+
componentProps, validators }` → `createForm({ model, schema })`. Объяви InputMask
|
|
2006
|
+
как `component` листа; передай `mask` в `componentProps`:
|
|
1850
2007
|
|
|
1851
2008
|
```ts
|
|
1852
|
-
import {
|
|
2009
|
+
import { createModel, createForm } from '@reformer/core';
|
|
2010
|
+
import { required, pattern } from '@reformer/core/validators';
|
|
1853
2011
|
import { InputMask } from '@reformer/ui-kit';
|
|
1854
2012
|
|
|
1855
2013
|
type ContactForm = {
|
|
@@ -1859,30 +2017,41 @@ type ContactForm = {
|
|
|
1859
2017
|
snils: string;
|
|
1860
2018
|
};
|
|
1861
2019
|
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2020
|
+
const model = createModel<ContactForm>({ phone: '', passport: '', inn: '', snils: '' });
|
|
2021
|
+
|
|
2022
|
+
const schema = {
|
|
2023
|
+
children: [
|
|
2024
|
+
{
|
|
2025
|
+
value: model.$.phone,
|
|
1866
2026
|
component: InputMask,
|
|
1867
|
-
componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99' },
|
|
2027
|
+
componentProps: { label: 'Телефон', mask: '+7 (999) 999-99-99', testId: 'phone' },
|
|
2028
|
+
validators: [
|
|
2029
|
+
required({ message: 'Телефон обязателен' }),
|
|
2030
|
+
pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message: 'Неверный формат телефона' }),
|
|
2031
|
+
],
|
|
1868
2032
|
},
|
|
1869
|
-
|
|
1870
|
-
value:
|
|
2033
|
+
{
|
|
2034
|
+
value: model.$.passport,
|
|
1871
2035
|
component: InputMask,
|
|
1872
|
-
componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999' },
|
|
2036
|
+
componentProps: { label: 'Серия и номер паспорта', mask: '9999 999999', testId: 'passport' },
|
|
2037
|
+
validators: [required()],
|
|
1873
2038
|
},
|
|
1874
|
-
|
|
1875
|
-
value:
|
|
2039
|
+
{
|
|
2040
|
+
value: model.$.inn,
|
|
1876
2041
|
component: InputMask,
|
|
1877
|
-
componentProps: { label: 'ИНН', mask: '999999999999' },
|
|
2042
|
+
componentProps: { label: 'ИНН', mask: '999999999999', testId: 'inn' },
|
|
2043
|
+
validators: [required(), pattern(/^\d{12}$/, { message: 'ИНН должен содержать 12 цифр' })],
|
|
1878
2044
|
},
|
|
1879
|
-
|
|
1880
|
-
value:
|
|
2045
|
+
{
|
|
2046
|
+
value: model.$.snils,
|
|
1881
2047
|
component: InputMask,
|
|
1882
|
-
componentProps: { label: 'СНИЛС', mask: '999-999-999 99' },
|
|
2048
|
+
componentProps: { label: 'СНИЛС', mask: '999-999-999 99', testId: 'snils' },
|
|
2049
|
+
validators: [required()],
|
|
1883
2050
|
},
|
|
1884
|
-
|
|
1885
|
-
}
|
|
2051
|
+
],
|
|
2052
|
+
};
|
|
2053
|
+
|
|
2054
|
+
const form = createForm<ContactForm>({ model, schema });
|
|
1886
2055
|
```
|
|
1887
2056
|
|
|
1888
2057
|
Render как обычно через `FormField`:
|
|
@@ -1915,24 +2084,30 @@ import { FormField } from '@reformer/ui-kit';
|
|
|
1915
2084
|
## 50. Validation
|
|
1916
2085
|
|
|
1917
2086
|
`InputMask` пишет в значение **то, что ввёл пользователь** (с literal-символами маски).
|
|
1918
|
-
|
|
2087
|
+
Валидаторы — фабрики из `@reformer/core/validators` — кладутся в массив `validators`
|
|
2088
|
+
листа схемы (см. схему выше), а не в отдельную path-функцию. Прогоняются через
|
|
2089
|
+
`validateFormModel(model, schema)`:
|
|
1919
2090
|
|
|
1920
|
-
- `required(
|
|
1921
|
-
- `minLength(
|
|
1922
|
-
- `pattern(
|
|
2091
|
+
- `required({ message })` — на пустоту;
|
|
2092
|
+
- `minLength(18)` — для проверки длины с literal-символами (телефон ровно 18 символов);
|
|
2093
|
+
- `pattern(/^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$/, { message })` — точный формат.
|
|
1923
2094
|
|
|
1924
2095
|
```ts
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2096
|
+
import { validateFormModel } from '@reformer/core';
|
|
2097
|
+
|
|
2098
|
+
// schema — из блока Schema-driven выше (валидаторы уже на листьях phone/inn/…).
|
|
2099
|
+
const onSubmit = async () => {
|
|
2100
|
+
form.markAsTouched();
|
|
2101
|
+
const res = await validateFormModel(model, schema);
|
|
2102
|
+
if (!res.valid) return;
|
|
2103
|
+
await api.submit(model.get());
|
|
1933
2104
|
};
|
|
1934
2105
|
```
|
|
1935
2106
|
|
|
2107
|
+
Cross-field правила (например, «доп. телефон отличается от основного») — это
|
|
2108
|
+
именованные `ModelValidator<value, scope, root>` в том же массиве `validators`
|
|
2109
|
+
(читают корень формы через третий аргумент).
|
|
2110
|
+
|
|
1936
2111
|
## 51. Advanced — strict mask через FormField + children slot
|
|
1937
2112
|
|
|
1938
2113
|
Если нужна автоматическая вставка literal-символов (true input-mask), используй
|
|
@@ -2013,13 +2188,15 @@ ErrorComponent={() => <p>Ошибка загрузки</p>}
|
|
|
2013
2188
|
|
|
2014
2189
|
Внутри RenderSchema (статус подставляется через `patchProps`)
|
|
2015
2190
|
```tsx
|
|
2191
|
+
import { createRenderSchema } from '@reformer/renderer-react';
|
|
2016
2192
|
import { AsyncBoundary } from '@reformer/ui-kit';
|
|
2017
2193
|
|
|
2018
|
-
|
|
2194
|
+
// M1: функция схемы не принимает аргументов — привязка к данным идёт через сигналы модели.
|
|
2195
|
+
const schema = createRenderSchema(() => ({
|
|
2019
2196
|
selector: 'data-boundary',
|
|
2020
2197
|
component: AsyncBoundary,
|
|
2021
2198
|
componentProps: { status: 'loading', LoadingComponent: Spinner },
|
|
2022
|
-
children: [
|
|
2199
|
+
children: [{ value: model.$.email, component: Input }],
|
|
2023
2200
|
}));
|
|
2024
2201
|
// позже: schema.node('data-boundary').patchProps({ status: 'ready' });
|
|
2025
2202
|
```
|
|
@@ -2077,26 +2254,30 @@ export function Box({ className, children }: BoxProps): ReactNode
|
|
|
2077
2254
|
|
|
2078
2255
|
**Examples:**
|
|
2079
2256
|
|
|
2080
|
-
Вертикальный список полей в RenderSchema
|
|
2257
|
+
Вертикальный список полей в RenderSchema (M1: лист = `value` + `component`)
|
|
2081
2258
|
```typescript
|
|
2259
|
+
import { Box, Input, InputPassword } from '@reformer/ui-kit';
|
|
2260
|
+
|
|
2082
2261
|
{
|
|
2083
2262
|
component: Box,
|
|
2084
2263
|
componentProps: { className: 'flex flex-col gap-4' },
|
|
2085
2264
|
children: [
|
|
2086
|
-
{
|
|
2087
|
-
{
|
|
2265
|
+
{ value: model.$.email, component: Input },
|
|
2266
|
+
{ value: model.$.password, component: InputPassword },
|
|
2088
2267
|
],
|
|
2089
2268
|
}
|
|
2090
2269
|
```
|
|
2091
2270
|
|
|
2092
2271
|
Двухколоночная сетка
|
|
2093
2272
|
```typescript
|
|
2273
|
+
import { Box, Input } from '@reformer/ui-kit';
|
|
2274
|
+
|
|
2094
2275
|
{
|
|
2095
2276
|
component: Box,
|
|
2096
2277
|
componentProps: { className: 'grid grid-cols-2 gap-4' },
|
|
2097
2278
|
children: [
|
|
2098
|
-
{
|
|
2099
|
-
{
|
|
2279
|
+
{ value: model.$.firstName, component: Input },
|
|
2280
|
+
{ value: model.$.lastName, component: Input },
|
|
2100
2281
|
],
|
|
2101
2282
|
}
|
|
2102
2283
|
```
|
|
@@ -2285,8 +2466,8 @@ Collapsible - сворачиваемая секция формы.
|
|
|
2285
2466
|
|
|
2286
2467
|
Заголовок-кнопка переключает видимость `children`. Состояние локальное
|
|
2287
2468
|
(`useState`), внешний control пока не поддерживается — для управляемого
|
|
2288
|
-
варианта используй `
|
|
2289
|
-
обычного `Box`.
|
|
2469
|
+
извне варианта используй `createRenderSchema(...).node('selector').setHidden(true)`
|
|
2470
|
+
поверх обычного `Box`/`Section`.
|
|
2290
2471
|
|
|
2291
2472
|
**Signature:**
|
|
2292
2473
|
```typescript
|
|
@@ -2302,8 +2483,10 @@ export function Collapsible({
|
|
|
2302
2483
|
|
|
2303
2484
|
**Examples:**
|
|
2304
2485
|
|
|
2305
|
-
Свёрнута по умолчанию
|
|
2486
|
+
Свёрнута по умолчанию (M1: лист = `value` + `component`)
|
|
2306
2487
|
```typescript
|
|
2488
|
+
import { Collapsible, Textarea, Input } from '@reformer/ui-kit';
|
|
2489
|
+
|
|
2307
2490
|
{
|
|
2308
2491
|
component: Collapsible,
|
|
2309
2492
|
componentProps: {
|
|
@@ -2313,14 +2496,16 @@ className: 'border rounded p-4',
|
|
|
2313
2496
|
titleClassName: 'font-semibold w-full text-left',
|
|
2314
2497
|
},
|
|
2315
2498
|
children: [
|
|
2316
|
-
{
|
|
2317
|
-
{
|
|
2499
|
+
{ value: model.$.notes, component: Textarea },
|
|
2500
|
+
{ value: model.$.tags, component: Input },
|
|
2318
2501
|
],
|
|
2319
2502
|
}
|
|
2320
2503
|
```
|
|
2321
2504
|
|
|
2322
2505
|
Развёрнута, со специальным фоном контента
|
|
2323
2506
|
```typescript
|
|
2507
|
+
import { Collapsible, Textarea } from '@reformer/ui-kit';
|
|
2508
|
+
|
|
2324
2509
|
{
|
|
2325
2510
|
component: Collapsible,
|
|
2326
2511
|
componentProps: {
|
|
@@ -2329,7 +2514,7 @@ defaultOpen: true,
|
|
|
2329
2514
|
contentClassName: 'mt-2 bg-gray-50 p-3 rounded',
|
|
2330
2515
|
},
|
|
2331
2516
|
children: [
|
|
2332
|
-
{
|
|
2517
|
+
{ value: model.$.deliveryAddress, component: Textarea },
|
|
2333
2518
|
],
|
|
2334
2519
|
}
|
|
2335
2520
|
```
|
|
@@ -2366,6 +2551,10 @@ _Source: src/components/ui/collapsible.tsx_
|
|
|
2366
2551
|
|
|
2367
2552
|
**Kind:** `function`
|
|
2368
2553
|
|
|
2554
|
+
Состояние ошибки — карточка с иконкой, заголовком, текстом ошибки и
|
|
2555
|
+
опциональной кнопкой повтора. Типовое применение — показать вместо формы,
|
|
2556
|
+
когда не удалось загрузить данные заявки.
|
|
2557
|
+
|
|
2369
2558
|
**Signature:**
|
|
2370
2559
|
```typescript
|
|
2371
2560
|
export function ErrorState({
|
|
@@ -2377,6 +2566,21 @@ export function ErrorState({
|
|
|
2377
2566
|
}: ErrorStateProps): ReactNode
|
|
2378
2567
|
```
|
|
2379
2568
|
|
|
2569
|
+
**Parameters:**
|
|
2570
|
+
- `props` — - Пропсы: `error` (обязателен), `title`, `onRetry`, `retryLabel`, `className`.
|
|
2571
|
+
|
|
2572
|
+
**Returns:** Разметку блока ошибки.
|
|
2573
|
+
|
|
2574
|
+
**Examples:**
|
|
2575
|
+
|
|
2576
|
+
Ошибка загрузки с повтором
|
|
2577
|
+
```tsx
|
|
2578
|
+
const { error } = useLoadCreditApplication(form, '1');
|
|
2579
|
+
if (error) {
|
|
2580
|
+
return <ErrorState error={error} onRetry={() => window.location.reload()} />;
|
|
2581
|
+
}
|
|
2582
|
+
```
|
|
2583
|
+
|
|
2380
2584
|
_Source: src/components/state/error-state.tsx_
|
|
2381
2585
|
|
|
2382
2586
|
### ExampleCard
|
|
@@ -2468,6 +2672,20 @@ _Source: src/components/ui/example-card.tsx_
|
|
|
2468
2672
|
|
|
2469
2673
|
**Kind:** `function`
|
|
2470
2674
|
|
|
2675
|
+
Готовая UI-секция для динамического массива форм — стилизованная обёртка поверх
|
|
2676
|
+
headless-compound `@reformer/cdk/form-array`. Рендерит заголовок, кнопку
|
|
2677
|
+
«Добавить», карточку с меткой на каждый элемент (плюс опциональные кнопки
|
|
2678
|
+
удаления и перестановки ↑/↓) и сообщение пустого состояния.
|
|
2679
|
+
|
|
2680
|
+
`control` принимает `FormArrayProxy<T>` или уже-резолвленный
|
|
2681
|
+
`ArrayNode<T>`/`ModelArrayNode<T>`. `itemComponent` — единственная форма
|
|
2682
|
+
рендера элемента: `ComponentType<{ control: FormProxy<T> }>` (тот же контракт,
|
|
2683
|
+
что у шага {@link FormWizardStep}). Внутри `RenderNodeComponent` проп `form`
|
|
2684
|
+
инъектится автоматически (маркер `__selfManagedChildren`).
|
|
2685
|
+
|
|
2686
|
+
Проп `hasItems` удобен для toggle-чекбоксов («У меня есть имущество»): при
|
|
2687
|
+
`false` секция скрывается целиком.
|
|
2688
|
+
|
|
2471
2689
|
**Signature:**
|
|
2472
2690
|
```typescript
|
|
2473
2691
|
export function FormArraySection<T extends object>({
|
|
@@ -2489,12 +2707,38 @@ export function FormArraySection<T extends object>({
|
|
|
2489
2707
|
}: FormArraySectionProps<T>): ReactNode
|
|
2490
2708
|
```
|
|
2491
2709
|
|
|
2710
|
+
**Examples:**
|
|
2711
|
+
|
|
2712
|
+
Массив «Имущество» под чекбоксом-переключателем
|
|
2713
|
+
```tsx
|
|
2714
|
+
import { FormArraySection } from '@reformer/ui-kit/form-array';
|
|
2715
|
+
|
|
2716
|
+
function AdditionalInfo({ control }: { control: FormProxy<CreditApplication> }) {
|
|
2717
|
+
const hasProperty = useFormControlValue(control.hasProperty) as boolean;
|
|
2718
|
+
return (
|
|
2719
|
+
<FormArraySection
|
|
2720
|
+
title="Имущество"
|
|
2721
|
+
control={control.properties}
|
|
2722
|
+
itemComponent={PropertyForm}
|
|
2723
|
+
itemLabel="Имущество"
|
|
2724
|
+
addButtonLabel="+ Добавить имущество"
|
|
2725
|
+
emptyMessage='Нажмите "Добавить имущество" для добавления информации'
|
|
2726
|
+
hasItems={hasProperty}
|
|
2727
|
+
initialValue={createBlankProperty()}
|
|
2728
|
+
reorderable
|
|
2729
|
+
/>
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
```
|
|
2733
|
+
|
|
2492
2734
|
_Source: src/components/form-array/form-array-section.tsx_
|
|
2493
2735
|
|
|
2494
2736
|
### FormArraySectionProps
|
|
2495
2737
|
|
|
2496
2738
|
**Kind:** `interface`
|
|
2497
2739
|
|
|
2740
|
+
Пропсы {@link FormArraySection}.
|
|
2741
|
+
|
|
2498
2742
|
**Signature:**
|
|
2499
2743
|
```typescript
|
|
2500
2744
|
export interface FormArraySectionProps<T extends object> {
|
|
@@ -2594,22 +2838,31 @@ export const FormField
|
|
|
2594
2838
|
|
|
2595
2839
|
**Examples:**
|
|
2596
2840
|
|
|
2597
|
-
Standalone в обычной форме
|
|
2841
|
+
Standalone в обычной форме (M1: `createModel` + `createForm({ model, schema })`)
|
|
2598
2842
|
```tsx
|
|
2599
2843
|
import { useMemo } from 'react';
|
|
2600
|
-
import {
|
|
2844
|
+
import { createModel, createForm } from '@reformer/core';
|
|
2845
|
+
import { required } from '@reformer/core/validators';
|
|
2601
2846
|
import { Button, FormField, Input } from '@reformer/ui-kit';
|
|
2602
2847
|
|
|
2603
2848
|
function RegistrationPage() {
|
|
2604
|
-
const form = useMemo(
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2849
|
+
const { form } = useMemo(() => {
|
|
2850
|
+
const model = createModel<{ email: string }>({ email: '' });
|
|
2851
|
+
const schema = {
|
|
2852
|
+
children: [
|
|
2853
|
+
{
|
|
2854
|
+
value: model.$.email,
|
|
2855
|
+
component: Input,
|
|
2856
|
+
componentProps: { label: 'Email', type: 'email', testId: 'email' },
|
|
2857
|
+
validators: [required({ message: 'Email обязателен' })],
|
|
2858
|
+
},
|
|
2859
|
+
],
|
|
2860
|
+
};
|
|
2861
|
+
return { form: createForm<{ email: string }>({ model, schema }) };
|
|
2862
|
+
}, []);
|
|
2610
2863
|
return (
|
|
2611
2864
|
<form>
|
|
2612
|
-
<FormField control={form.email}
|
|
2865
|
+
<FormField control={form.email} />
|
|
2613
2866
|
<Button type="submit">OK</Button>
|
|
2614
2867
|
</form>
|
|
2615
2868
|
);
|
|
@@ -2619,11 +2872,15 @@ return (
|
|
|
2619
2872
|
В качестве `fieldWrapper` для FormRenderer
|
|
2620
2873
|
```tsx
|
|
2621
2874
|
import { FormRenderer, createRenderSchema } from '@reformer/renderer-react';
|
|
2622
|
-
import { FormField } from '@reformer/ui-kit';
|
|
2875
|
+
import { Box, FormField, Input } from '@reformer/ui-kit';
|
|
2623
2876
|
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2877
|
+
// M1: узлы ссылаются на компоненты-значения, листья — на сигналы модели.
|
|
2878
|
+
const schema = createRenderSchema<MyForm>(() => ({
|
|
2879
|
+
component: Box,
|
|
2880
|
+
children: [
|
|
2881
|
+
{ value: model.$.email, component: Input },
|
|
2882
|
+
{ value: model.$.phone, component: Input },
|
|
2883
|
+
],
|
|
2627
2884
|
}));
|
|
2628
2885
|
|
|
2629
2886
|
<FormRenderer render={schema} settings={{ fieldWrapper: FormField }} />
|
|
@@ -2670,39 +2927,140 @@ _Source: src/components/ui/form-field.tsx_
|
|
|
2670
2927
|
|
|
2671
2928
|
**Kind:** `const`
|
|
2672
2929
|
|
|
2930
|
+
Готовая многошаговая форма (multi-step wizard) — стилизованная обёртка поверх
|
|
2931
|
+
headless-compound `@reformer/cdk/form-wizard`. Собирает Indicator, тело шагов,
|
|
2932
|
+
Actions (Назад / Далее / Отправить) и Progress в единый layout, работает с
|
|
2933
|
+
одной {@link FormProxy} и декларативным списком {@link FormWizardStep}.
|
|
2934
|
+
|
|
2935
|
+
Один компонент покрывает TS-flow, renderer-react и renderer-json за счёт
|
|
2936
|
+
полиморфного {@link FormWizardStepBody}. Валидация по шагам и submit-валидация
|
|
2937
|
+
задаются через `config` (`{ validateStep, validateAll }`, обычно из
|
|
2938
|
+
`validateFormModel`). Императивный доступ (submit/навигация снаружи дерева) —
|
|
2939
|
+
через `ref` типа `FormWizardHandle<T>`.
|
|
2940
|
+
|
|
2941
|
+
Экспонирует compound-слоты `FormWizard.Indicator` / `.Step` / `.Actions` /
|
|
2942
|
+
`.Progress` для кастомной раскладки.
|
|
2943
|
+
|
|
2673
2944
|
**Signature:**
|
|
2674
2945
|
```typescript
|
|
2675
2946
|
const FormWizard
|
|
2676
2947
|
```
|
|
2677
2948
|
|
|
2949
|
+
**Examples:**
|
|
2950
|
+
|
|
2951
|
+
Кредитная заявка с 3 шагами и внешним submit
|
|
2952
|
+
```tsx
|
|
2953
|
+
import { useMemo, useRef } from 'react';
|
|
2954
|
+
import { FormWizard, type FormWizardStep } from '@reformer/ui-kit/form-wizard';
|
|
2955
|
+
import type { FormWizardHandle } from '@reformer/cdk/form-wizard';
|
|
2956
|
+
|
|
2957
|
+
const STEPS: FormWizardStep<CreditApplication>[] = [
|
|
2958
|
+
{ number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
|
|
2959
|
+
{ number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
|
|
2960
|
+
{ number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
|
|
2961
|
+
];
|
|
2962
|
+
|
|
2963
|
+
function CreditForm() {
|
|
2964
|
+
const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
|
|
2965
|
+
const { form, model } = useMemo(() => createCreditForm(), []);
|
|
2966
|
+
const config = useMemo(() => makeValidationConfig(model), [model]);
|
|
2967
|
+
|
|
2968
|
+
const onSubmit = () =>
|
|
2969
|
+
navRef.current?.submit((values) => api.submit(values));
|
|
2970
|
+
|
|
2971
|
+
return (
|
|
2972
|
+
<FormWizard
|
|
2973
|
+
ref={navRef}
|
|
2974
|
+
form={form}
|
|
2975
|
+
config={config}
|
|
2976
|
+
steps={STEPS}
|
|
2977
|
+
onSubmit={onSubmit}
|
|
2978
|
+
/>
|
|
2979
|
+
);
|
|
2980
|
+
}
|
|
2981
|
+
```
|
|
2982
|
+
|
|
2983
|
+
**See also:**
|
|
2984
|
+
- {@link FormWizardStep} — форма элемента `steps`.
|
|
2985
|
+
- {@link StepIndicator}, {@link FormWizardActions}, {@link FormWizardProgress} — слоты layout'а.
|
|
2986
|
+
|
|
2678
2987
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2679
2988
|
|
|
2680
2989
|
### FormWizardActions
|
|
2681
2990
|
|
|
2682
2991
|
**Kind:** `const`
|
|
2683
2992
|
|
|
2993
|
+
Кнопки навигации wizard'а: «Назад» / «Далее →» / «Отправить». На первом шаге
|
|
2994
|
+
скрывает «Назад», на последнем показывает «Отправить» вместо «Далее». Во время
|
|
2995
|
+
валидации/отправки показывает промежуточные подписи и блокирует кнопку.
|
|
2996
|
+
|
|
2997
|
+
Рендерится из headless-слота `<FormWizard.Actions>` через render-prop, поэтому
|
|
2998
|
+
`prev`/`next`/`submit`/флаги приходят автоматически. Готовый {@link FormWizard}
|
|
2999
|
+
уже подключает этот компонент — использовать напрямую нужно только для кастомной
|
|
3000
|
+
раскладки.
|
|
3001
|
+
|
|
2684
3002
|
**Signature:**
|
|
2685
3003
|
```typescript
|
|
2686
3004
|
export const FormWizardActions: FC<FormWizardActionsProps>
|
|
2687
3005
|
```
|
|
2688
3006
|
|
|
3007
|
+
**Examples:**
|
|
3008
|
+
|
|
3009
|
+
Кастомные подписи в слоте Actions
|
|
3010
|
+
```tsx
|
|
3011
|
+
<FormWizard.Actions onSubmit={onSubmit}>
|
|
3012
|
+
{(actions) => (
|
|
3013
|
+
<FormWizardActions
|
|
3014
|
+
{...actions}
|
|
3015
|
+
submitLabel="Оформить заявку"
|
|
3016
|
+
className="mt-8"
|
|
3017
|
+
/>
|
|
3018
|
+
)}
|
|
3019
|
+
</FormWizard.Actions>
|
|
3020
|
+
```
|
|
3021
|
+
|
|
2689
3022
|
_Source: src/components/form-wizard/form-wizard-actions.tsx_
|
|
2690
3023
|
|
|
2691
3024
|
### FormWizardProgress
|
|
2692
3025
|
|
|
2693
3026
|
**Kind:** `const`
|
|
2694
3027
|
|
|
3028
|
+
Текстовый индикатор прогресса wizard'а — по умолчанию рендерит
|
|
3029
|
+
«Шаг N из M • X% завершено». Формат строки переопределяется пропом `format`.
|
|
3030
|
+
|
|
3031
|
+
Рендерится из headless-слота `<FormWizard.Progress>` через render-prop
|
|
3032
|
+
(`current`/`total`/`percent` приходят автоматически). Готовый {@link FormWizard}
|
|
3033
|
+
уже подключает этот компонент; напрямую нужен только для кастомной раскладки.
|
|
3034
|
+
|
|
2695
3035
|
**Signature:**
|
|
2696
3036
|
```typescript
|
|
2697
3037
|
export const FormWizardProgress: FC<FormWizardProgressProps>
|
|
2698
3038
|
```
|
|
2699
3039
|
|
|
3040
|
+
**Examples:**
|
|
3041
|
+
|
|
3042
|
+
Свой формат строки прогресса
|
|
3043
|
+
```tsx
|
|
3044
|
+
<FormWizard.Progress>
|
|
3045
|
+
{(progress) => (
|
|
3046
|
+
<FormWizardProgress
|
|
3047
|
+
{...progress}
|
|
3048
|
+
format={({ current, total }) => `${current} / ${total}`}
|
|
3049
|
+
/>
|
|
3050
|
+
)}
|
|
3051
|
+
</FormWizard.Progress>
|
|
3052
|
+
```
|
|
3053
|
+
|
|
2700
3054
|
_Source: src/components/form-wizard/form-wizard-progress.tsx_
|
|
2701
3055
|
|
|
2702
3056
|
### FormWizardProps
|
|
2703
3057
|
|
|
2704
3058
|
**Kind:** `interface`
|
|
2705
3059
|
|
|
3060
|
+
Пропсы {@link FormWizard}. Расширяют headless-пропсы из
|
|
3061
|
+
`@reformer/cdk/form-wizard` (`form`, `config`, `onStepChange`, …), добавляя
|
|
3062
|
+
декларативный `steps` и колбэк `onSubmit`.
|
|
3063
|
+
|
|
2706
3064
|
**Signature:**
|
|
2707
3065
|
```typescript
|
|
2708
3066
|
export interface FormWizardProps<
|
|
@@ -2713,43 +3071,71 @@ export interface FormWizardProps<
|
|
|
2713
3071
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2714
3072
|
T extends Record<string, any>,
|
|
2715
3073
|
> extends FormWizardHeadlessProps<T> {
|
|
3074
|
+
/** Внешний CSS-класс корневого контейнера. */
|
|
2716
3075
|
className?: string;
|
|
3076
|
+
/** Декларативный список шагов (см. {@link FormWizardStep}). Порядок = порядок навигации. */
|
|
2717
3077
|
steps: FormWizardStep<T>[];
|
|
3078
|
+
/** Колбэк отправки формы на последнем шаге; вызывается после успешной валидации. */
|
|
2718
3079
|
onSubmit: HeadlessFormWizardActionsProps['onSubmit'];
|
|
2719
3080
|
}
|
|
2720
3081
|
```
|
|
2721
3082
|
|
|
3083
|
+
**See also:**
|
|
3084
|
+
- {@link FormWizardStep} — форма элемента `steps`.
|
|
3085
|
+
- — императивный handle через `ref` (submit/навигация).
|
|
3086
|
+
|
|
2722
3087
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2723
3088
|
|
|
2724
3089
|
### FormWizardStep
|
|
2725
3090
|
|
|
2726
3091
|
**Kind:** `interface`
|
|
2727
3092
|
|
|
3093
|
+
Описание одного шага {@link FormWizard}: порядковый номер, заголовок и иконка
|
|
3094
|
+
для индикатора, плюс полиморфное тело {@link FormWizardStepBody}.
|
|
3095
|
+
|
|
3096
|
+
Массив `FormWizardStep<T>[]` передаётся в проп `steps`. Порядок и `number`
|
|
3097
|
+
задают последовательность навигации; `number` должен быть 1-based и уникальным.
|
|
3098
|
+
|
|
2728
3099
|
**Signature:**
|
|
2729
3100
|
```typescript
|
|
2730
3101
|
export interface FormWizardStep<T> {
|
|
2731
|
-
/**
|
|
3102
|
+
/** Порядковый номер шага (1-based). Уникальный, задаёт порядок навигации. */
|
|
2732
3103
|
number: number;
|
|
2733
|
-
/**
|
|
3104
|
+
/** Заголовок шага, показывается в {@link StepIndicator}. */
|
|
2734
3105
|
title: string;
|
|
2735
|
-
/**
|
|
3106
|
+
/** Иконка шага (эмодзи или строка). Передаётся в headless Indicator. */
|
|
2736
3107
|
icon?: string;
|
|
2737
|
-
/**
|
|
3108
|
+
/** Тело шага — FC | ReactNode | RenderNode<T> (см. {@link FormWizardStepBody}). */
|
|
2738
3109
|
body: FormWizardStepBody<T>;
|
|
2739
3110
|
}
|
|
2740
3111
|
```
|
|
2741
3112
|
|
|
3113
|
+
**Examples:**
|
|
3114
|
+
|
|
3115
|
+
Массив шагов кредитной заявки
|
|
3116
|
+
```tsx
|
|
3117
|
+
const STEPS: FormWizardStep<CreditApplication>[] = [
|
|
3118
|
+
{ number: 1, title: 'Кредит', icon: '💰', body: BasicInfoForm },
|
|
3119
|
+
{ number: 2, title: 'Данные', icon: '👤', body: PersonalInfoForm },
|
|
3120
|
+
{ number: 3, title: 'Подтверждение', icon: '✓', body: ConfirmationForm },
|
|
3121
|
+
];
|
|
3122
|
+
```
|
|
3123
|
+
|
|
2742
3124
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2743
3125
|
|
|
2744
3126
|
### FormWizardStepBody
|
|
2745
3127
|
|
|
2746
3128
|
**Kind:** `type`
|
|
2747
3129
|
|
|
2748
|
-
Полиморфное тело
|
|
3130
|
+
Полиморфное тело шага {@link FormWizardStep}. Один и тот же {@link FormWizard}
|
|
3131
|
+
покрывает TS-flow, renderer-react и renderer-json за счёт трёх допустимых форм
|
|
3132
|
+
`body`, дискриминация которых выполняется в рантайме по типу значения:
|
|
2749
3133
|
|
|
2750
|
-
-
|
|
2751
|
-
|
|
2752
|
-
-
|
|
3134
|
+
- `ComponentType<{ control: FormProxy<T> }>` — React-компонент; получает
|
|
3135
|
+
`control={form}` (корневой {@link FormProxy}) и сам обращается к нужным полям.
|
|
3136
|
+
- `ReactNode` — готовый JSX или статический контент шага (текст, число и т.п.).
|
|
3137
|
+
- `RenderNode<T>` — RenderSchema-поддерево; рендерится через `RenderNodeComponent`
|
|
3138
|
+
для интеграции с `@reformer/renderer-react`.
|
|
2753
3139
|
|
|
2754
3140
|
**Signature:**
|
|
2755
3141
|
```typescript
|
|
@@ -2759,6 +3145,16 @@ export type FormWizardStepBody<T> =
|
|
|
2759
3145
|
| RenderNode<T>;
|
|
2760
3146
|
```
|
|
2761
3147
|
|
|
3148
|
+
**Examples:**
|
|
3149
|
+
|
|
3150
|
+
Компонент шага получает control
|
|
3151
|
+
```tsx
|
|
3152
|
+
function BasicInfoForm({ control }: { control: FormProxy<CreditApplication> }) {
|
|
3153
|
+
return <FormField control={control.loanAmount} testId="loanAmount" />;
|
|
3154
|
+
}
|
|
3155
|
+
const body: FormWizardStepBody<CreditApplication> = BasicInfoForm;
|
|
3156
|
+
```
|
|
3157
|
+
|
|
2762
3158
|
_Source: src/components/form-wizard/form-wizard.tsx_
|
|
2763
3159
|
|
|
2764
3160
|
### Input
|
|
@@ -2797,6 +3193,27 @@ placeholder="Возраст"
|
|
|
2797
3193
|
/>
|
|
2798
3194
|
```
|
|
2799
3195
|
|
|
3196
|
+
Привязка к полю формы через `useFormControl`
|
|
3197
|
+
```tsx
|
|
3198
|
+
import { useFormControl, type FieldNode } from '@reformer/core';
|
|
3199
|
+
import { Input } from '@reformer/ui-kit';
|
|
3200
|
+
|
|
3201
|
+
function EmailField({ control }: { control: FieldNode<string> }) {
|
|
3202
|
+
const { value, disabled, errors, shouldShowError } = useFormControl(control);
|
|
3203
|
+
return (
|
|
3204
|
+
<Input
|
|
3205
|
+
type="email"
|
|
3206
|
+
value={value}
|
|
3207
|
+
disabled={disabled}
|
|
3208
|
+
onChange={(v) => control.setValue((v ?? '') as string)}
|
|
3209
|
+
onBlur={() => control.markAsTouched()}
|
|
3210
|
+
aria-invalid={shouldShowError}
|
|
3211
|
+
placeholder={shouldShowError ? errors[0]?.message : 'you@example.com'}
|
|
3212
|
+
/>
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
```
|
|
3216
|
+
|
|
2800
3217
|
_Source: src/components/ui/input.tsx_
|
|
2801
3218
|
|
|
2802
3219
|
### InputMask
|
|
@@ -2991,6 +3408,9 @@ _Source: src/components/ui/input.tsx_
|
|
|
2991
3408
|
|
|
2992
3409
|
**Kind:** `function`
|
|
2993
3410
|
|
|
3411
|
+
Состояние загрузки — центрированный спиннер с заголовком и подзаголовком.
|
|
3412
|
+
Типовое применение — показать вместо формы, пока грузятся данные заявки.
|
|
3413
|
+
|
|
2994
3414
|
**Signature:**
|
|
2995
3415
|
```typescript
|
|
2996
3416
|
export function LoadingState({
|
|
@@ -3000,6 +3420,21 @@ export function LoadingState({
|
|
|
3000
3420
|
}: LoadingStateProps): ReactNode
|
|
3001
3421
|
```
|
|
3002
3422
|
|
|
3423
|
+
**Parameters:**
|
|
3424
|
+
- `props` — - Пропсы: `title`, `subtitle`, `className` (все опциональны).
|
|
3425
|
+
|
|
3426
|
+
**Returns:** Разметку блока загрузки.
|
|
3427
|
+
|
|
3428
|
+
**Examples:**
|
|
3429
|
+
|
|
3430
|
+
Загрузка перед рендером формы
|
|
3431
|
+
```tsx
|
|
3432
|
+
const { isLoading } = useLoadCreditApplication(form, '1');
|
|
3433
|
+
if (isLoading) {
|
|
3434
|
+
return <LoadingState />;
|
|
3435
|
+
}
|
|
3436
|
+
```
|
|
3437
|
+
|
|
3003
3438
|
_Source: src/components/state/loading-state.tsx_
|
|
3004
3439
|
|
|
3005
3440
|
### RadioGroup
|
|
@@ -3093,6 +3528,26 @@ export interface RadioOption {
|
|
|
3093
3528
|
|
|
3094
3529
|
_Source: src/components/ui/radio-group.tsx_
|
|
3095
3530
|
|
|
3531
|
+
### ResourceConfig
|
|
3532
|
+
|
|
3533
|
+
**Kind:** `interface`
|
|
3534
|
+
|
|
3535
|
+
Конфигурация асинхронного источника опций для {@link Select}.
|
|
3536
|
+
|
|
3537
|
+
**Signature:**
|
|
3538
|
+
```typescript
|
|
3539
|
+
export interface ResourceConfig<T> {
|
|
3540
|
+
/** Стратегия загрузки. По умолчанию (если не задана) трактуется как `'static'`. */
|
|
3541
|
+
type: ResourceStrategy;
|
|
3542
|
+
/** Функция загрузки опций. Должна вернуть `{ items, totalCount }`. */
|
|
3543
|
+
load: (params?: ResourceLoadParams) => Promise<ResourceResult<T>>;
|
|
3544
|
+
/** Размер страницы для стратегии `partial`. По умолчанию 20. */
|
|
3545
|
+
pageSize?: number;
|
|
3546
|
+
}
|
|
3547
|
+
```
|
|
3548
|
+
|
|
3549
|
+
_Source: src/components/ui/select-resource.ts_
|
|
3550
|
+
|
|
3096
3551
|
### Section
|
|
3097
3552
|
|
|
3098
3553
|
**Kind:** `function`
|
|
@@ -3115,8 +3570,10 @@ export function Section({
|
|
|
3115
3570
|
|
|
3116
3571
|
**Examples:**
|
|
3117
3572
|
|
|
3118
|
-
Заголовок h2 + сетка из двух колонок
|
|
3573
|
+
Заголовок h2 + сетка из двух колонок (M1: лист = `value` + `component`)
|
|
3119
3574
|
```typescript
|
|
3575
|
+
import { Section, Input } from '@reformer/ui-kit';
|
|
3576
|
+
|
|
3120
3577
|
{
|
|
3121
3578
|
component: Section,
|
|
3122
3579
|
componentProps: {
|
|
@@ -3126,20 +3583,22 @@ titleClassName: 'text-xl font-bold',
|
|
|
3126
3583
|
className: 'grid grid-cols-2 gap-4',
|
|
3127
3584
|
},
|
|
3128
3585
|
children: [
|
|
3129
|
-
{
|
|
3130
|
-
{
|
|
3586
|
+
{ value: model.$.firstName, component: Input },
|
|
3587
|
+
{ value: model.$.lastName, component: Input },
|
|
3131
3588
|
],
|
|
3132
3589
|
}
|
|
3133
3590
|
```
|
|
3134
3591
|
|
|
3135
3592
|
Section без заголовка (только обёртка)
|
|
3136
3593
|
```typescript
|
|
3594
|
+
import { Section, Input } from '@reformer/ui-kit';
|
|
3595
|
+
|
|
3137
3596
|
{
|
|
3138
3597
|
component: Section,
|
|
3139
3598
|
componentProps: { className: 'space-y-4 mt-4' },
|
|
3140
3599
|
children: [
|
|
3141
|
-
{
|
|
3142
|
-
{
|
|
3600
|
+
{ value: model.$.address, component: Input },
|
|
3601
|
+
{ value: model.$.city, component: Input },
|
|
3143
3602
|
],
|
|
3144
3603
|
}
|
|
3145
3604
|
```
|
|
@@ -3174,9 +3633,17 @@ _Source: src/components/ui/section.tsx_
|
|
|
3174
3633
|
|
|
3175
3634
|
**Kind:** `const`
|
|
3176
3635
|
|
|
3177
|
-
Выпадающий список на `@radix-ui/react-select`.
|
|
3178
|
-
|
|
3179
|
-
|
|
3636
|
+
Выпадающий список на `@radix-ui/react-select`. Два источника данных: inline
|
|
3637
|
+
`options` и асинхронный `resource` со стратегией загрузки
|
|
3638
|
+
({@link ResourceConfig.type}):
|
|
3639
|
+
|
|
3640
|
+
- `static` — один `load({})` при маунте, без поиска;
|
|
3641
|
+
- `preload` — грузит всё сразу, поиск фильтрует опции на клиенте;
|
|
3642
|
+
- `partial` — серверные поиск (`load({ search })` с debounce) и пагинация
|
|
3643
|
+
(`load({ page })` по мере скролла до `totalCount`).
|
|
3644
|
+
|
|
3645
|
+
Для `preload`/`partial` в дропдауне показывается поле поиска. При
|
|
3646
|
+
`clearable=true` рядом со значением есть крестик сброса в `null`.
|
|
3180
3647
|
|
|
3181
3648
|
**Signature:**
|
|
3182
3649
|
```typescript
|
|
@@ -3196,24 +3663,47 @@ placeholder="Тип кредита"
|
|
|
3196
3663
|
options={[
|
|
3197
3664
|
{ value: 'consumer', label: 'Потребительский' },
|
|
3198
3665
|
{ value: 'mortgage', label: 'Ипотека' },
|
|
3199
|
-
{ value: 'auto', label: 'Авто' },
|
|
3200
3666
|
]}
|
|
3201
3667
|
/>
|
|
3202
3668
|
```
|
|
3203
3669
|
|
|
3204
|
-
|
|
3670
|
+
Серверные поиск и пагинация (resource type='partial')
|
|
3205
3671
|
```tsx
|
|
3206
|
-
import { Select } from '@reformer/ui-kit';
|
|
3672
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit';
|
|
3673
|
+
|
|
3674
|
+
const users: ResourceConfig<string> = {
|
|
3675
|
+
type: 'partial',
|
|
3676
|
+
pageSize: 20,
|
|
3677
|
+
load: async ({ search = '', page = 1, pageSize = 20 }) => {
|
|
3678
|
+
const res = await fetch(`/api/users?q=${search}&page=${page}&size=${pageSize}`);
|
|
3679
|
+
const { rows, total } = await res.json();
|
|
3680
|
+
return {
|
|
3681
|
+
items: rows.map((u) => ({ id: u.id, label: u.name, value: u.id })),
|
|
3682
|
+
totalCount: total,
|
|
3683
|
+
};
|
|
3684
|
+
},
|
|
3685
|
+
};
|
|
3207
3686
|
|
|
3208
|
-
<Select
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
{
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3687
|
+
<Select value={userId} onChange={setUserId} resource={users} clearable />
|
|
3688
|
+
```
|
|
3689
|
+
|
|
3690
|
+
Предзагрузка с клиентским поиском (resource type='preload')
|
|
3691
|
+
```tsx
|
|
3692
|
+
import { Select, type ResourceConfig } from '@reformer/ui-kit';
|
|
3693
|
+
|
|
3694
|
+
const countries: ResourceConfig<string> = {
|
|
3695
|
+
type: 'preload',
|
|
3696
|
+
load: async () => {
|
|
3697
|
+
const res = await fetch('/api/countries');
|
|
3698
|
+
const items = await res.json();
|
|
3699
|
+
return {
|
|
3700
|
+
items: items.map((c) => ({ id: c.id, label: c.name, value: c.code })),
|
|
3701
|
+
totalCount: items.length,
|
|
3702
|
+
};
|
|
3703
|
+
},
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3706
|
+
<Select value={country} onChange={setCountry} resource={countries} />
|
|
3217
3707
|
```
|
|
3218
3708
|
|
|
3219
3709
|
_Source: src/components/ui/select.tsx_
|
|
@@ -3222,18 +3712,29 @@ _Source: src/components/ui/select.tsx_
|
|
|
3222
3712
|
|
|
3223
3713
|
**Kind:** `function`
|
|
3224
3714
|
|
|
3225
|
-
|
|
3715
|
+
Дропдаун-контent {@link Select} (попап со списком). Обёртка над
|
|
3226
3716
|
`Radix.Select.Content` с порталом, скролл-кнопками сверху/снизу и
|
|
3227
3717
|
`position='popper'` по умолчанию (растягивается под ширину триггера).
|
|
3228
3718
|
|
|
3719
|
+
Опционально принимает `header` (нескроллящаяся шапка, например поле поиска)
|
|
3720
|
+
и `onViewportScroll` (для infinite-scroll — вешается на скроллящийся
|
|
3721
|
+
viewport со списком опций).
|
|
3722
|
+
|
|
3229
3723
|
**Signature:**
|
|
3230
3724
|
```typescript
|
|
3231
3725
|
function SelectContent({
|
|
3232
3726
|
className,
|
|
3233
3727
|
children,
|
|
3234
3728
|
position = 'popper',
|
|
3729
|
+
header,
|
|
3730
|
+
onViewportScroll,
|
|
3235
3731
|
...props
|
|
3236
|
-
}: React.ComponentProps<typeof SelectPrimitive.Content>
|
|
3732
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content> & {
|
|
3733
|
+
/** Нескроллящаяся шапка над списком (например, поле поиска). */
|
|
3734
|
+
header?: React.ReactNode;
|
|
3735
|
+
/** Обработчик прокрутки viewport-а со списком (для infinite-scroll). */
|
|
3736
|
+
onViewportScroll?: React.UIEventHandler<HTMLDivElement>;
|
|
3737
|
+
})
|
|
3237
3738
|
```
|
|
3238
3739
|
|
|
3239
3740
|
**Examples:**
|
|
@@ -3571,11 +4072,33 @@ _Source: src/components/ui/select.tsx_
|
|
|
3571
4072
|
|
|
3572
4073
|
**Kind:** `const`
|
|
3573
4074
|
|
|
4075
|
+
Визуальная цепочка шагов wizard'а — иконки, заголовки и соединительные линии
|
|
4076
|
+
с подсветкой текущего / завершённого шага. Клик (и Enter) по доступному шагу
|
|
4077
|
+
вызывает `goToStep`; недоступные шаги приглушены и не кликабельны. Разметка
|
|
4078
|
+
доступна для скринридеров (`role="navigation"`, `aria-current="step"`,
|
|
4079
|
+
настраиваемые aria-метки).
|
|
4080
|
+
|
|
4081
|
+
Рендерится из headless-слота `<FormWizard.Indicator>` через render-prop
|
|
4082
|
+
(`steps` со статусами и `goToStep` приходят автоматически). Готовый
|
|
4083
|
+
{@link FormWizard} уже подключает этот компонент; напрямую нужен только для
|
|
4084
|
+
кастомной раскладки.
|
|
4085
|
+
|
|
3574
4086
|
**Signature:**
|
|
3575
4087
|
```typescript
|
|
3576
4088
|
export const StepIndicator: FC<StepIndicatorProps>
|
|
3577
4089
|
```
|
|
3578
4090
|
|
|
4091
|
+
**Examples:**
|
|
4092
|
+
|
|
4093
|
+
Индикатор с кастомной aria-меткой контейнера
|
|
4094
|
+
```tsx
|
|
4095
|
+
<FormWizard.Indicator steps={steps}>
|
|
4096
|
+
{(indicator) => (
|
|
4097
|
+
<StepIndicator {...indicator} navAriaLabel="Этапы заявки" className="mb-8" />
|
|
4098
|
+
)}
|
|
4099
|
+
</FormWizard.Indicator>
|
|
4100
|
+
```
|
|
4101
|
+
|
|
3579
4102
|
_Source: src/components/form-wizard/step-indicator.tsx_
|
|
3580
4103
|
|
|
3581
4104
|
### Textarea
|