@reformer/cdk 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/FormArray-CBT-1kKN.js +120 -0
- package/dist/FormWizard-DLDm4FJM.js +311 -0
- package/dist/Slot-YDt2BEtP.js +27 -0
- package/dist/components/form-array/FormArray.d.ts +223 -0
- package/dist/components/form-array/FormArrayAddButton.d.ts +6 -0
- package/dist/components/form-array/FormArrayContext.d.ts +137 -0
- package/dist/components/form-array/FormArrayCount.d.ts +17 -0
- package/dist/components/form-array/FormArrayEmpty.d.ts +22 -0
- package/dist/components/form-array/FormArrayItemIndex.d.ts +24 -0
- package/dist/components/form-array/FormArrayList.d.ts +26 -0
- package/dist/components/form-array/FormArrayRemoveButton.d.ts +26 -0
- package/dist/components/form-array/index.d.ts +13 -0
- package/dist/components/form-array/types.d.ts +77 -0
- package/dist/components/form-array/useFormArray.d.ts +95 -0
- package/dist/components/form-field/FormField.d.ts +107 -0
- package/dist/components/form-field/FormFieldContext.d.ts +56 -0
- package/dist/components/form-field/FormFieldControl.d.ts +35 -0
- package/dist/components/form-field/FormFieldDescription.d.ts +30 -0
- package/dist/components/form-field/FormFieldError.d.ts +36 -0
- package/dist/components/form-field/FormFieldLabel.d.ts +35 -0
- package/dist/components/form-field/FormFieldRoot.d.ts +32 -0
- package/dist/components/form-field/index.d.ts +10 -0
- package/dist/components/form-field/types.d.ts +114 -0
- package/dist/components/form-field/useFormField.d.ts +111 -0
- package/dist/components/form-wizard/FormWizard.d.ts +47 -0
- package/dist/components/form-wizard/FormWizardActions.d.ts +98 -0
- package/dist/components/form-wizard/FormWizardContext.d.ts +84 -0
- package/dist/components/form-wizard/FormWizardIndicator.d.ts +118 -0
- package/dist/components/form-wizard/FormWizardNext.d.ts +35 -0
- package/dist/components/form-wizard/FormWizardPrev.d.ts +35 -0
- package/dist/components/form-wizard/FormWizardProgress.d.ts +83 -0
- package/dist/components/form-wizard/FormWizardStep.d.ts +55 -0
- package/dist/components/form-wizard/FormWizardSubmit.d.ts +43 -0
- package/dist/components/form-wizard/Slot.d.ts +20 -0
- package/dist/components/form-wizard/Step.d.ts +24 -0
- package/dist/components/form-wizard/index.d.ts +21 -0
- package/dist/components/form-wizard/types.d.ts +108 -0
- package/dist/form-array.d.ts +2 -0
- package/dist/form-array.js +15 -0
- package/dist/form-field.d.ts +2 -0
- package/dist/form-field.js +12 -0
- package/dist/form-wizard.d.ts +2 -0
- package/dist/form-wizard.js +22 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +33 -0
- package/dist/useFormField-DV396Bxa.js +232 -0
- package/llms.txt +3294 -0
- package/package.json +90 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,3294 @@
|
|
|
1
|
+
# ReFormer CDK - LLM Integration Guide
|
|
2
|
+
# AUTO-GENERATED. Edit docs/llms/*.md or JSDoc in src/ and run npm run generate:llms.
|
|
3
|
+
|
|
4
|
+
> Headless UI components for @reformer/core - form arrays, multi-step wizards, and more
|
|
5
|
+
> Package: @reformer/cdk • Version: 1.0.0-beta.2
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
- 01-overview.md — Overview
|
|
9
|
+
- 02-form-array.md — FormArray
|
|
10
|
+
- 03-form-navigation.md — FormWizard
|
|
11
|
+
- 04-form-field.md — FormField
|
|
12
|
+
- 05-recipes.md — Advanced Recipes
|
|
13
|
+
- 06-troubleshooting.md — Troubleshooting / FAQ
|
|
14
|
+
- API Reference (auto-generated from JSDoc)
|
|
15
|
+
|
|
16
|
+
## 1. Key Concepts
|
|
17
|
+
|
|
18
|
+
- **Headless**: No default UI or styles - you build the interface
|
|
19
|
+
- **Compound Components**: Composable, declarative API
|
|
20
|
+
- **Render Props**: Children as function for full control
|
|
21
|
+
- **Context-based**: State shared via React Context
|
|
22
|
+
|
|
23
|
+
## 2. Components
|
|
24
|
+
|
|
25
|
+
| Component | Purpose |
|
|
26
|
+
| ------------ | -------------------------- |
|
|
27
|
+
| `FormArray` | Manage dynamic form arrays |
|
|
28
|
+
| `FormWizard` | Multi-step form wizard |
|
|
29
|
+
|
|
30
|
+
## 3. Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @reformer/cdk @reformer/core
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 4. Import Patterns
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// All components
|
|
40
|
+
import { FormArray, FormWizard } from '@reformer/cdk';
|
|
41
|
+
|
|
42
|
+
// Tree-shaking (recommended)
|
|
43
|
+
import { FormArray, useFormArray } from '@reformer/cdk/form-array';
|
|
44
|
+
import { FormWizard, useFormWizard } from '@reformer/cdk/form-wizard';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 5. Basic Usage
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { FormArray } from '@reformer/cdk/form-array';
|
|
51
|
+
|
|
52
|
+
<FormArray.Root control={form.items}>
|
|
53
|
+
<FormArray.Empty>
|
|
54
|
+
<p>No items added</p>
|
|
55
|
+
</FormArray.Empty>
|
|
56
|
+
|
|
57
|
+
<FormArray.List>
|
|
58
|
+
{({ control, index, remove }) => (
|
|
59
|
+
<div key={control.id}>
|
|
60
|
+
<h4>Item #{index + 1}</h4>
|
|
61
|
+
<ItemForm control={control} />
|
|
62
|
+
<button onClick={remove}>Remove</button>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</FormArray.List>
|
|
66
|
+
|
|
67
|
+
<FormArray.AddButton>Add Item</FormArray.AddButton>
|
|
68
|
+
</FormArray.Root>;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 6. Sub-components
|
|
72
|
+
|
|
73
|
+
| Component | Props | Purpose |
|
|
74
|
+
| ------------------------ | ------------------------------- | ---------------------------------- |
|
|
75
|
+
| `FormArray.Root` | `control: ArrayNode<T>` | Context provider |
|
|
76
|
+
| `FormArray.List` | `children: (item) => ReactNode` | Iterates items (render props) |
|
|
77
|
+
| `FormArray.AddButton` | `initialValue?: Partial<T>` | Adds new item |
|
|
78
|
+
| `FormArray.RemoveButton` | - | Removes current item (inside List) |
|
|
79
|
+
| `FormArray.Empty` | `children: ReactNode` | Shows when array is empty |
|
|
80
|
+
| `FormArray.Count` | `render?: (count) => ReactNode` | Displays item count |
|
|
81
|
+
| `FormArray.ItemIndex` | `render?: (index) => ReactNode` | Displays current index |
|
|
82
|
+
|
|
83
|
+
## 7. List Render Props
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface FormArrayItemRenderProps<T> {
|
|
87
|
+
control: FormProxy<T>; // Form control for item
|
|
88
|
+
index: number; // Zero-based index
|
|
89
|
+
id: string | number; // Unique key
|
|
90
|
+
remove: () => void; // Remove this item
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 8. External Control via Ref
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { useRef } from 'react';
|
|
98
|
+
import { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';
|
|
99
|
+
|
|
100
|
+
const arrayRef = useRef<FormArrayHandle<ItemType>>(null);
|
|
101
|
+
|
|
102
|
+
// Control from outside
|
|
103
|
+
arrayRef.current?.add({ name: 'New' });
|
|
104
|
+
arrayRef.current?.removeAt(0);
|
|
105
|
+
arrayRef.current?.clear();
|
|
106
|
+
|
|
107
|
+
<FormArray.Root ref={arrayRef} control={form.items}>
|
|
108
|
+
...
|
|
109
|
+
</FormArray.Root>;
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 9. FormArrayHandle API
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
interface FormArrayHandle<T> {
|
|
116
|
+
add: (value?: Partial<T>) => void;
|
|
117
|
+
clear: () => void;
|
|
118
|
+
insert: (index: number, value?: Partial<T>) => void;
|
|
119
|
+
removeAt: (index: number) => void;
|
|
120
|
+
length: number;
|
|
121
|
+
isEmpty: boolean;
|
|
122
|
+
at: (index: number) => FormProxy<T> | undefined;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 10. useFormArray Hook
|
|
127
|
+
|
|
128
|
+
For full customization without compound components:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useFormArray } from '@reformer/cdk/form-array';
|
|
132
|
+
|
|
133
|
+
function CustomList() {
|
|
134
|
+
const { items, add, isEmpty, length } = useFormArray(form.items);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div>
|
|
138
|
+
<span>Total: {length}</span>
|
|
139
|
+
{items.map(({ control, id, remove }) => (
|
|
140
|
+
<div key={id}>
|
|
141
|
+
<ItemForm control={control} />
|
|
142
|
+
<button onClick={remove}>X</button>
|
|
143
|
+
</div>
|
|
144
|
+
))}
|
|
145
|
+
{isEmpty && <p>Empty</p>}
|
|
146
|
+
<button onClick={() => add()}>Add</button>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 11. Basic Usage
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { FormWizard } from '@reformer/cdk/form-wizard';
|
|
156
|
+
|
|
157
|
+
const config = {
|
|
158
|
+
stepValidations: {
|
|
159
|
+
1: step1Schema,
|
|
160
|
+
2: step2Schema,
|
|
161
|
+
},
|
|
162
|
+
fullValidation: fullFormSchema,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
<FormWizard form={form} config={config}>
|
|
166
|
+
<FormWizard.Step component={Step1Form} control={form} />
|
|
167
|
+
<FormWizard.Step component={Step2Form} control={form} />
|
|
168
|
+
|
|
169
|
+
<FormWizard.Actions onSubmit={handleSubmit}>
|
|
170
|
+
{({ prev, next, submit, isFirstStep, isLastStep }) => (
|
|
171
|
+
<div>
|
|
172
|
+
{!isFirstStep && <button {...prev}>Back</button>}
|
|
173
|
+
{!isLastStep ? <button {...next}>Next</button> : <button {...submit}>Submit</button>}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</FormWizard.Actions>
|
|
177
|
+
</FormWizard>;
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 12. Sub-components
|
|
181
|
+
|
|
182
|
+
| Component | Purpose |
|
|
183
|
+
| ---------------------- | ------------------------------------------ |
|
|
184
|
+
| `FormWizard` | Root provider |
|
|
185
|
+
| `FormWizard.Step` | Renders component when step is current |
|
|
186
|
+
| `FormWizard.Indicator` | Headless step indicator (render props) |
|
|
187
|
+
| `FormWizard.Actions` | Headless navigation buttons (render props) |
|
|
188
|
+
| `FormWizard.Progress` | Headless progress display (render props) |
|
|
189
|
+
|
|
190
|
+
## 13. FormWizard.Indicator
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
<FormWizard.Indicator steps={STEPS}>
|
|
194
|
+
{({ steps, goToStep, currentStep }) => (
|
|
195
|
+
<nav>
|
|
196
|
+
{steps.map((step) => (
|
|
197
|
+
<button
|
|
198
|
+
key={step.number}
|
|
199
|
+
onClick={() => goToStep(step.number)}
|
|
200
|
+
disabled={!step.canNavigate}
|
|
201
|
+
aria-current={step.isCurrent ? 'step' : undefined}
|
|
202
|
+
>
|
|
203
|
+
{step.isCompleted ? '✓' : step.number} {step.title}
|
|
204
|
+
</button>
|
|
205
|
+
))}
|
|
206
|
+
</nav>
|
|
207
|
+
)}
|
|
208
|
+
</FormWizard.Indicator>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Step Definition
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
interface FormWizardIndicatorStep {
|
|
215
|
+
number: number; // 1-based step number
|
|
216
|
+
title: string;
|
|
217
|
+
icon?: string;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Render Props
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface FormWizardIndicatorRenderProps {
|
|
225
|
+
steps: FormWizardIndicatorStepWithState[];
|
|
226
|
+
goToStep: (step: number) => boolean;
|
|
227
|
+
currentStep: number;
|
|
228
|
+
totalSteps: number;
|
|
229
|
+
completedSteps: number[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
interface FormWizardIndicatorStepWithState {
|
|
233
|
+
number: number;
|
|
234
|
+
title: string;
|
|
235
|
+
icon?: string;
|
|
236
|
+
isCurrent: boolean;
|
|
237
|
+
isCompleted: boolean;
|
|
238
|
+
canNavigate: boolean;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 14. FormWizard.Actions
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
<FormWizard.Actions onSubmit={handleSubmit}>
|
|
246
|
+
{({ prev, next, submit, isFirstStep, isLastStep, isValidating }) => (
|
|
247
|
+
<div>
|
|
248
|
+
{!isFirstStep && (
|
|
249
|
+
<button onClick={prev.onClick} disabled={prev.disabled}>
|
|
250
|
+
Back
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
253
|
+
{!isLastStep ? (
|
|
254
|
+
<button onClick={next.onClick} disabled={next.disabled}>
|
|
255
|
+
{isValidating ? 'Validating...' : 'Next'}
|
|
256
|
+
</button>
|
|
257
|
+
) : (
|
|
258
|
+
<button onClick={submit.onClick} disabled={submit.disabled}>
|
|
259
|
+
{submit.isSubmitting ? 'Submitting...' : 'Submit'}
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</FormWizard.Actions>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Render Props
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
interface FormWizardActionsRenderProps {
|
|
271
|
+
prev: { onClick: () => void; disabled: boolean };
|
|
272
|
+
next: { onClick: () => void; disabled: boolean };
|
|
273
|
+
submit: { onClick: () => void; disabled: boolean; isSubmitting: boolean };
|
|
274
|
+
isFirstStep: boolean;
|
|
275
|
+
isLastStep: boolean;
|
|
276
|
+
isValidating: boolean;
|
|
277
|
+
isSubmitting: boolean;
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## 15. FormWizard.Progress
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
<FormWizard.Progress>
|
|
285
|
+
{({ current, total, percent }) => (
|
|
286
|
+
<div>
|
|
287
|
+
Step {current} of {total} ({percent}%)
|
|
288
|
+
<div style={{ width: `${percent}%` }} />
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</FormWizard.Progress>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Render Props
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
interface FormWizardProgressRenderProps {
|
|
298
|
+
current: number;
|
|
299
|
+
total: number;
|
|
300
|
+
percent: number;
|
|
301
|
+
completedCount: number;
|
|
302
|
+
isFirstStep: boolean;
|
|
303
|
+
isLastStep: boolean;
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## 16. External Control via Ref
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
const navRef = useRef<FormWizardHandle<FormType>>(null);
|
|
311
|
+
|
|
312
|
+
// Programmatic navigation
|
|
313
|
+
navRef.current?.goToStep(2);
|
|
314
|
+
navRef.current?.goToNextStep();
|
|
315
|
+
navRef.current?.goToPreviousStep();
|
|
316
|
+
|
|
317
|
+
// Submit with validation
|
|
318
|
+
const result = await navRef.current?.submit(async (values) => {
|
|
319
|
+
return api.submit(values);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
<FormWizard ref={navRef} form={form} config={config}>
|
|
323
|
+
...
|
|
324
|
+
</FormWizard>;
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## 17. Configuration
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
interface FormWizardConfig<T> {
|
|
331
|
+
stepValidations: Record<number, ValidationSchemaFn<T>>;
|
|
332
|
+
fullValidation: ValidationSchemaFn<T>;
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Validation happens automatically:
|
|
337
|
+
|
|
338
|
+
- On `next.onClick`: validates current step
|
|
339
|
+
- On `submit.onClick`: validates entire form
|
|
340
|
+
|
|
341
|
+
## 18. Purpose
|
|
342
|
+
|
|
343
|
+
- Дать минимальный «скелет» поля: label + control + description + error.
|
|
344
|
+
- Не навязывать стилей — каждый sub-компонент рендерит обычные HTML-элементы (или `Slot` через `asChild`).
|
|
345
|
+
- Подписаться на `FieldNode` ровно один раз в `Root` и раздать состояние детям через React Context.
|
|
346
|
+
- Гарантировать корректные ARIA-атрибуты в нетривиальных сценариях (multi-error, отсутствие label, async pending).
|
|
347
|
+
|
|
348
|
+
В отличие от `FormField` из `@reformer/ui-kit`, который рендерит готовый layout (label сверху, ошибка снизу), `FormField` из `@reformer/cdk` ничего не рендерит сверх минимально необходимого и нужен, когда требуется собственный layout.
|
|
349
|
+
|
|
350
|
+
## 19. Components
|
|
351
|
+
|
|
352
|
+
| Component | Purpose | Notes |
|
|
353
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
354
|
+
| `FormField.Root` | Context provider; принимает `control: FieldNode<T>` и опциональный `id`/`hasDescription`. | Подписывается на `useFormControl(control)` один раз. Без `Root` дети бросают исключение. |
|
|
355
|
+
| `FormField.Label` | `<label>` с автоматическим `htmlFor`. Текст по умолчанию из `componentProps.label`. Required-индикатор `*` добавляется при `required`. | Возвращает `null`, если нет ни `componentProps.label`, ни `children`. Используйте `forceRender` чтобы рендерить пустой label. |
|
|
356
|
+
| `FormField.Control` | Auto-renders `control.component` со всеми пропсами и a11y-атрибутами. С `asChild`/`children` — вмёрживает a11y-атрибуты в произвольный дочерний элемент через `Slot`. | Auto-mode прокидывает `componentProps`, `value`, `disabled`, `onChange`, `onBlur`. |
|
|
357
|
+
| `FormField.Error` | `<p role="alert">` с `errors[0].message`. Поддерживает `multi`, `render`, кастомные `children`. | Не рендерится, пока `shouldShowError === false` (поле не touched / нет ошибок). |
|
|
358
|
+
| `FormField.Description` | `<p>` с стабильным `id={ids.descriptionId}` для `aria-describedby`. | Чтобы `Control` автоматически прописал `aria-describedby`, передайте `hasDescription` в `Root`. |
|
|
359
|
+
| `useFormFieldContext<T>()` | Хук для произвольных дочерних компонентов, которым нужен `control`, `value`, `errors`, `ids`, `componentProps`. | Бросает `Error`, если вызван вне `FormField.Root`. |
|
|
360
|
+
| `useFormField(control, id?)` | Standalone hook без compound API. Возвращает `labelProps`, `controlProps`, `errorProps`, `descriptionProps`, `state`, `actions`, `ids`. | Удобен, когда нужен полный контроль над DOM-структурой и пропсами. |
|
|
361
|
+
|
|
362
|
+
## 20. Examples
|
|
363
|
+
|
|
364
|
+
### Базовый сценарий — auto-render всех частей
|
|
365
|
+
|
|
366
|
+
Минимум кода: `Label` и `Control` сами берут текст / компонент из конфига поля, `Error` прячется до touch.
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
import { FormField } from '@reformer/cdk/form-field';
|
|
370
|
+
|
|
371
|
+
function EmailField({ control }: { control: typeof form.email }) {
|
|
372
|
+
return (
|
|
373
|
+
<FormField.Root control={control}>
|
|
374
|
+
<FormField.Label />
|
|
375
|
+
<FormField.Control />
|
|
376
|
+
<FormField.Error />
|
|
377
|
+
</FormField.Root>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
`Label` рендерит текст из `componentProps.label`, `Control` — компонент, заданный через `component:` в схеме формы (`Input`, `InputPassword`, `Select`...).
|
|
383
|
+
|
|
384
|
+
### Custom layout — обёртки и стилизация
|
|
385
|
+
|
|
386
|
+
Когда требуется горизонтальный layout, иконка слева и helper-текст:
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
<FormField.Root control={form.email} hasDescription>
|
|
390
|
+
<div className="grid grid-cols-[120px_1fr] items-start gap-3">
|
|
391
|
+
<FormField.Label className="pt-2 text-sm font-medium text-gray-700" />
|
|
392
|
+
|
|
393
|
+
<div className="space-y-1">
|
|
394
|
+
<FormField.Control asChild>
|
|
395
|
+
<Input type="email" leftIcon={<MailIcon />} className="w-full" />
|
|
396
|
+
</FormField.Control>
|
|
397
|
+
<FormField.Description className="text-xs text-gray-500">
|
|
398
|
+
Мы не передаём email третьим сторонам.
|
|
399
|
+
</FormField.Description>
|
|
400
|
+
<FormField.Error className="text-xs text-red-600" />
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</FormField.Root>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Передача `hasDescription` обязательна для того, чтобы `Control` прописал `aria-describedby={descriptionId}`.
|
|
407
|
+
|
|
408
|
+
### Async-валидация с pending-индикатором
|
|
409
|
+
|
|
410
|
+
Состояние асинхронной валидации (`pending`) доступно через `useFormFieldContext()`. Удобно показать спиннер или disabled у submit-кнопки.
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
import { FormField, useFormFieldContext } from '@reformer/cdk/form-field';
|
|
414
|
+
|
|
415
|
+
function PendingDot() {
|
|
416
|
+
const { pending } = useFormFieldContext();
|
|
417
|
+
if (!pending) return null;
|
|
418
|
+
return <Spinner size="sm" aria-label="Проверяем..." />;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
<FormField.Root control={form.username}>
|
|
422
|
+
<div className="flex items-center gap-2">
|
|
423
|
+
<FormField.Label />
|
|
424
|
+
<PendingDot />
|
|
425
|
+
</div>
|
|
426
|
+
<FormField.Control />
|
|
427
|
+
<FormField.Error multi className="text-xs text-red-600" />
|
|
428
|
+
</FormField.Root>;
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`multi` рендерит все ошибки из `errors[]` (например, async-валидатор может вернуть и «слишком короткое имя», и «уже занято»). Первая ошибка получит `id={errorId}` для `aria-errormessage`.
|
|
432
|
+
|
|
433
|
+
### Интеграция с готовым `FormField` из `@reformer/ui-kit`
|
|
434
|
+
|
|
435
|
+
`@reformer/ui-kit` экспортирует свой `FormField`, который собран на этих compound-блоках. В большинстве форм его достаточно — без необходимости опускаться на уровень CDK:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
import { FormField } from '@reformer/ui-kit';
|
|
439
|
+
|
|
440
|
+
<form>
|
|
441
|
+
<FormField control={form.username} className="mb-4" />
|
|
442
|
+
<FormField control={form.email} className="mb-4" />
|
|
443
|
+
</form>;
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Если нужен один кастомный кейс среди типовых — комбинируйте: `FormField` из ui-kit для большинства полей и `FormField.Root` из cdk для нестандартного:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
import { FormField } from '@reformer/ui-kit';
|
|
450
|
+
import { FormField as FieldRoot } from '@reformer/cdk/form-field';
|
|
451
|
+
|
|
452
|
+
<>
|
|
453
|
+
<FormField control={form.email} />
|
|
454
|
+
<FieldRoot.Root control={form.captcha}>
|
|
455
|
+
<FieldRoot.Label />
|
|
456
|
+
<div className="flex items-center gap-2">
|
|
457
|
+
<FieldRoot.Control asChild>
|
|
458
|
+
<Input className="flex-1" />
|
|
459
|
+
</FieldRoot.Control>
|
|
460
|
+
<CaptchaImage />
|
|
461
|
+
</div>
|
|
462
|
+
<FieldRoot.Error />
|
|
463
|
+
</FieldRoot.Root>
|
|
464
|
+
</>;
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## 21. Anti-patterns
|
|
468
|
+
|
|
469
|
+
- **Использовать `FormField.Label` / `Error` / `Control` без `FormField.Root`.** Каждый дочерний компонент вызывает `useFormFieldContext()` и бросает: `FormField.* components must be used within <FormField.Root>`.
|
|
470
|
+
- **Подписываться на `useFormControl(control)` рядом с `FormField.Root`.** `Root` уже подписан — лишняя подписка приведёт к двойному ререндеру. Используйте `useFormFieldContext()` для доступа к состоянию.
|
|
471
|
+
- **Передавать `id` руками в `Control` / `Label`.** ID назначаются автоматически из `useId()`. Если нужен предсказуемый ID для тестов, передайте `id="my-field"` в `Root` — все потомки получат `control-my-field`, `label-my-field`, …
|
|
472
|
+
- **Забывать `hasDescription` при наличии `FormField.Description`.** Без флага `Control` не пропишет `aria-describedby={descriptionId}`, и screen reader не зачитает helper-текст.
|
|
473
|
+
- **Двойное рендерание ошибки (`Error` + ручной `<p>`).** `FormField.Error` уже подписан на `errors`/`shouldShowError`. Если нужен кастомный layout — используйте `render` prop, а не дублируйте.
|
|
474
|
+
- **Применять `asChild` к компоненту, который не пробрасывает `ref`/`...props`.** `Slot` объединяет пропсы и ref в дочерний элемент; если потомок их не принимает, `aria-*`-атрибуты потеряются.
|
|
475
|
+
|
|
476
|
+
## 22. Troubleshooting
|
|
477
|
+
|
|
478
|
+
- **`Error: FormField.* components must be used within <FormField.Root>`.** Проверьте, что вызов `FormField.Label` / `Control` / `Error` обёрнут в `FormField.Root` и компонент не рендерится в портале выше провайдера.
|
|
479
|
+
- **`Label` ничего не показывает.** В схеме поля нет `componentProps.label`. Вариант: задайте `label` в схеме, или передайте `children` в `FormField.Label`, или поставьте `forceRender`.
|
|
480
|
+
- **`Control` рендерит «голый» `<input>` без стилей.** Auto-mode рендерит `control.component` — убедитесь, что в схеме указан компонент (`component: Input`). Иначе используйте `asChild` + свой компонент.
|
|
481
|
+
- **`aria-describedby` пустой при наличии `Description`.** Не передан `hasDescription` в `Root`. Это не «магический» флаг — без него `Control` не знает, что description есть в дереве.
|
|
482
|
+
- **`FormField.Error` не появляется при наличии ошибки.** Поле не помечено как touched. Используйте `form.markAsTouched()` или `control.markAsTouched()` перед сабмитом, либо настройте `revalidateWhen` чтобы помечать touched по `change`.
|
|
483
|
+
- **При async-валидации индикатор моргает.** `pending` переключается на каждый `setValue`. Дебаунсьте источник или добавьте задержку перед показом спиннера (например, `useDeferredValue`).
|
|
484
|
+
- **Дубликаты `id` в DOM.** Несколько `FormField.Root` с одинаковым явным `id`. Опустите `id` (тогда работает `useId()`) или дайте уникальные значения.
|
|
485
|
+
|
|
486
|
+
## 23. See also
|
|
487
|
+
|
|
488
|
+
- [01-overview.md](01-overview.md) — общее введение в `@reformer/cdk`.
|
|
489
|
+
- [02-form-array.md](02-form-array.md), [03-form-navigation.md](03-form-navigation.md) — соседние compound-компоненты.
|
|
490
|
+
- [05-recipes.md](05-recipes.md) — продвинутые паттерны (включая собственный wrapper над FormField).
|
|
491
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — типичные ошибки FormField/FormArray/FormWizard.
|
|
492
|
+
- `RegistrationForm.tsx` (monorepo example) — `FormField` из ui-kit вокруг каждого поля.
|
|
493
|
+
- `CreditApplicationForm` (monorepo example) — поля внутри multi-step формы.
|
|
494
|
+
- [src/components/form-field/](../../src/components/form-field/) — исходники compound-блоков и `useFormField`.
|
|
495
|
+
|
|
496
|
+
## 24. Nested FormArray
|
|
497
|
+
|
|
498
|
+
### Problem
|
|
499
|
+
|
|
500
|
+
В одной форме нужно несколько уровней вложенности массивов: например, `properties[]` (имущество клиента) и внутри каждого — `coOwners[]` (совладельцы). Контролы вложенных массивов — это `ArrayNode<T>` внутри `FormProxy<Property>`, и стандартного `FormArray.Root + List` достаточно, чтобы рендерить любую глубину.
|
|
501
|
+
|
|
502
|
+
### Solution
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
import { FormArray } from '@reformer/cdk/form-array';
|
|
506
|
+
import { FormField } from '@reformer/ui-kit';
|
|
507
|
+
|
|
508
|
+
<FormArray.Root control={form.properties}>
|
|
509
|
+
<FormArray.List className="space-y-4">
|
|
510
|
+
{({ control: property, index, remove }) => (
|
|
511
|
+
<fieldset className="border rounded p-4">
|
|
512
|
+
<legend className="flex justify-between w-full">
|
|
513
|
+
<span>Имущество #{index + 1}</span>
|
|
514
|
+
<button type="button" onClick={remove}>
|
|
515
|
+
×
|
|
516
|
+
</button>
|
|
517
|
+
</legend>
|
|
518
|
+
|
|
519
|
+
<FormField control={property.address} />
|
|
520
|
+
<FormField control={property.estimatedValue} />
|
|
521
|
+
|
|
522
|
+
<h4 className="mt-4 mb-2">Совладельцы</h4>
|
|
523
|
+
<FormArray.Root control={property.coOwners}>
|
|
524
|
+
<FormArray.List className="space-y-2">
|
|
525
|
+
{({ control: owner, index: ownerIdx, remove: removeOwner }) => (
|
|
526
|
+
<div className="flex gap-2">
|
|
527
|
+
<FormField control={owner.fullName} className="flex-1" />
|
|
528
|
+
<FormField control={owner.share} className="w-24" />
|
|
529
|
+
<button type="button" onClick={removeOwner}>
|
|
530
|
+
×
|
|
531
|
+
</button>
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
534
|
+
</FormArray.List>
|
|
535
|
+
<FormArray.AddButton className="btn-secondary mt-2">
|
|
536
|
+
+ Добавить совладельца
|
|
537
|
+
</FormArray.AddButton>
|
|
538
|
+
</FormArray.Root>
|
|
539
|
+
</fieldset>
|
|
540
|
+
)}
|
|
541
|
+
</FormArray.List>
|
|
542
|
+
<FormArray.AddButton className="btn-primary">+ Добавить имущество</FormArray.AddButton>
|
|
543
|
+
</FormArray.Root>;
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Notes
|
|
547
|
+
|
|
548
|
+
- Каждый `FormArray.Root` создаёт собственный `FormArrayContext`. Внутренний `useFormArrayContext()` (например, в `AddButton`) видит ближайший провайдер — в примере выше `coOwners`, не `properties`.
|
|
549
|
+
- `FormArray.List` мемоизирует элементы по `length` массива (см. `useFormArray`); смена порядка/количества внутреннего массива не дёргает внешний `List`.
|
|
550
|
+
- `useFormArrayItemContext()` внутри вложенного `List` вернёт **внутренний** item (`coOwner`), не внешний. Если нужен индекс внешнего элемента, забирайте его в замыкании render-функции (`index`, `property`).
|
|
551
|
+
- Рендер-проп `FormArray.List` стабилен по identity: вынесите тяжёлые шаги в отдельный компонент, чтобы получить React.memo-эффект.
|
|
552
|
+
|
|
553
|
+
## 25. Custom AddButton
|
|
554
|
+
|
|
555
|
+
### Problem
|
|
556
|
+
|
|
557
|
+
`FormArray.AddButton` рендерит обычный `<button>` (или Slot через `asChild`). Иногда нужно совсем другое UI: dropdown с выбором типа, drag-drop файлов, шаблоны заготовок. Самый чистый путь — обойти compound-компонент и вызвать `useFormArrayContext()` напрямую.
|
|
558
|
+
|
|
559
|
+
### Solution
|
|
560
|
+
|
|
561
|
+
```tsx
|
|
562
|
+
import { useFormArrayContext } from '@reformer/cdk/form-array';
|
|
563
|
+
import { Menu } from '@/ui';
|
|
564
|
+
|
|
565
|
+
function AddPropertyMenu() {
|
|
566
|
+
const { add } = useFormArrayContext<Property>();
|
|
567
|
+
|
|
568
|
+
return (
|
|
569
|
+
<Menu>
|
|
570
|
+
<Menu.Trigger className="btn-primary">+ Добавить имущество ▾</Menu.Trigger>
|
|
571
|
+
<Menu.Content>
|
|
572
|
+
<Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>
|
|
573
|
+
<Menu.Item onSelect={() => add({ type: 'house', estimatedValue: 0 })}>Дом</Menu.Item>
|
|
574
|
+
<Menu.Item onSelect={() => add({ type: 'commercial' })}>Коммерческое</Menu.Item>
|
|
575
|
+
</Menu.Content>
|
|
576
|
+
</Menu>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
<FormArray.Root control={form.properties}>
|
|
581
|
+
<FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>
|
|
582
|
+
<AddPropertyMenu />
|
|
583
|
+
</FormArray.Root>;
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Notes
|
|
587
|
+
|
|
588
|
+
- Хук работает только внутри `FormArray.Root` — снаружи бросает `Error: FormArray.* components must be used within FormArray.Root`.
|
|
589
|
+
- `add(value?: Partial<T>)` пропускает значение в `control.push(value)` — не задавать поля можно, ReFormer возьмёт значения по умолчанию из схемы.
|
|
590
|
+
- Если кастомный триггер живёт **снаружи** `Root` (например, в шапке страницы), используйте ref-handle: `useRef<FormArrayHandle<T>>` + `arrayRef.current?.add(...)`.
|
|
591
|
+
- Для batch-добавления нескольких элементов вызывайте `add` в цикле — каждый push триггерит ререндер `List` (через `length`-зависимость в `useFormArray`).
|
|
592
|
+
|
|
593
|
+
## 26. Conditional / dynamic step count in FormWizard
|
|
594
|
+
|
|
595
|
+
### Problem
|
|
596
|
+
|
|
597
|
+
`FormWizard` считает количество шагов через `Children.forEach` по `FormWizard.Step` детям (см. `FormWizard.tsx`). Если шаг условный (например, «верификация документов» нужна только для суммы > 1 000 000), достаточно условного рендера: `{showVerification && <FormWizard.Step ... />}`. `totalSteps` пересчитается, индикатор сожмётся.
|
|
598
|
+
|
|
599
|
+
### Solution
|
|
600
|
+
|
|
601
|
+
```tsx
|
|
602
|
+
import { useFormControl } from '@reformer/core';
|
|
603
|
+
import { FormWizard } from '@reformer/cdk/form-wizard';
|
|
604
|
+
|
|
605
|
+
function CreditWizard({ form, navConfig }: Props) {
|
|
606
|
+
const { value: amount } = useFormControl(form.amount);
|
|
607
|
+
const needsVerification = amount > 1_000_000;
|
|
608
|
+
|
|
609
|
+
// Шаг 4 нужен только для крупных сумм
|
|
610
|
+
const stepValidations = useMemo(
|
|
611
|
+
() => ({
|
|
612
|
+
1: amountValidation,
|
|
613
|
+
2: personalValidation,
|
|
614
|
+
3: contactValidation,
|
|
615
|
+
...(needsVerification && { 4: verificationValidation }),
|
|
616
|
+
// последний шаг — confirmation, его номер сдвигается автоматически
|
|
617
|
+
[needsVerification ? 5 : 4]: confirmationValidation,
|
|
618
|
+
}),
|
|
619
|
+
[needsVerification]
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<FormWizard form={form} config={{ stepValidations, fullValidation: full }}>
|
|
624
|
+
<FormWizard.Step component={AmountForm} control={form} />
|
|
625
|
+
<FormWizard.Step component={PersonalForm} control={form} />
|
|
626
|
+
<FormWizard.Step component={ContactForm} control={form} />
|
|
627
|
+
{needsVerification && <FormWizard.Step component={VerificationForm} control={form} />}
|
|
628
|
+
<FormWizard.Step component={ConfirmationForm} control={form} />
|
|
629
|
+
|
|
630
|
+
<FormWizard.Actions onSubmit={handleSubmit}>
|
|
631
|
+
{({ prev, next, submit, isLastStep }) => (
|
|
632
|
+
<div>
|
|
633
|
+
<button {...prev}>Назад</button>
|
|
634
|
+
{isLastStep ? (
|
|
635
|
+
<button {...submit}>Подтвердить</button>
|
|
636
|
+
) : (
|
|
637
|
+
<button {...next}>Далее</button>
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
)}
|
|
641
|
+
</FormWizard.Actions>
|
|
642
|
+
</FormWizard>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Notes
|
|
648
|
+
|
|
649
|
+
- **Номера шагов сдвигаются** при включении/выключении: `stepValidations[4]` будет либо `verificationValidation`, либо `confirmationValidation` в зависимости от `needsVerification`. Держите словарь валидаций в `useMemo` от того же флага.
|
|
650
|
+
- `currentStep` в state `FormWizard` хранится как число. Если флаг изменился пока пользователь стоит на шаге 4 (где раньше была верификация, а теперь подтверждение), он останется на том же номере — но рендер увидит уже другой `Step`. В критичных кейсах вызывайте `navRef.current?.goToStep(1)` после переключения.
|
|
651
|
+
- `completedSteps` — массив номеров; при изменении общей нумерации логика «можно ли перейти на шаг N» (`step === 1 || completedSteps.includes(step - 1)`) может отметить шаг как доступный без валидации. Скиньте `completedSteps` через перемонтирование `FormWizard` (key={needsVerification}) если важна строгость.
|
|
652
|
+
- Children-формы рендерятся условно `_stepIndex === currentStep` (см. `FormWizardStep.tsx`); неактивные `Step` возвращают `null`, состояние их полей живёт в `form` независимо от рендера.
|
|
653
|
+
|
|
654
|
+
## 27. Externally-controlled wizard via `useRef<FormWizardHandle>`
|
|
655
|
+
|
|
656
|
+
### Problem
|
|
657
|
+
|
|
658
|
+
Кнопка «Сохранить и выйти» лежит вне `FormWizard` (в шапке страницы), нужен программный submit. Аналогично — переход на шаг по клику в стороннем breadcrumb или после ответа из API.
|
|
659
|
+
|
|
660
|
+
### Solution
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
import { useRef } from 'react';
|
|
664
|
+
import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';
|
|
665
|
+
|
|
666
|
+
function Page({ form, config }: Props) {
|
|
667
|
+
const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
|
|
668
|
+
|
|
669
|
+
const handleSaveAndExit = async () => {
|
|
670
|
+
// submit с полной валидацией; null если форма невалидна
|
|
671
|
+
const result = await navRef.current?.submit(async (values) => {
|
|
672
|
+
return api.saveDraft(values);
|
|
673
|
+
});
|
|
674
|
+
if (result) router.push('/dashboard');
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const jumpToContacts = () => {
|
|
678
|
+
const ok = navRef.current?.goToStep(3);
|
|
679
|
+
if (!ok) toast('Сначала заполните предыдущие шаги');
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<>
|
|
684
|
+
<header className="flex justify-between p-4 border-b">
|
|
685
|
+
<button onClick={jumpToContacts}>Перейти к контактам</button>
|
|
686
|
+
<button onClick={handleSaveAndExit}>Сохранить и выйти</button>
|
|
687
|
+
</header>
|
|
688
|
+
|
|
689
|
+
<FormWizard ref={navRef} form={form} config={config}>
|
|
690
|
+
<FormWizard.Step component={Step1} control={form} />
|
|
691
|
+
<FormWizard.Step component={Step2} control={form} />
|
|
692
|
+
<FormWizard.Step component={Step3} control={form} />
|
|
693
|
+
</FormWizard>
|
|
694
|
+
</>
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Notes
|
|
700
|
+
|
|
701
|
+
- `FormWizardHandle` exposes: `currentStep`, `completedSteps`, `goToNextStep` (с валидацией), `goToPreviousStep`, `goToStep` (boolean — true если переход разрешён), `submit`, `validateCurrentStep`, `isFirstStep`, `isLastStep`, `isValidating`, `form`.
|
|
702
|
+
- `submit(onSubmit)` сначала прогоняет `config.fullValidation`, затем `form.markAsTouched()` если invalid, иначе делегирует в `form.submit(onSubmit, { skipValidation: true })`. Возвращает `R | null`. `null` — форма не прошла валидацию.
|
|
703
|
+
- `goToStep(n)` возвращает `false`, если `n > totalSteps`, `n < 1`, или предыдущий шаг (`n - 1`) не в `completedSteps` (исключение — сам шаг 1). Используйте `await goToNextStep()` чтобы пройти вперёд с валидацией.
|
|
704
|
+
- Ref становится `null` пока компонент не смонтировался. Все вызовы — `navRef.current?.method()` с optional chaining, либо проверка `if (!navRef.current) return`.
|
|
705
|
+
- Эталон: `CreditApplicationForm.tsx` (monorepo example) — `submit` через ref + центральная кнопка отправки.
|
|
706
|
+
|
|
707
|
+
## 28. See also
|
|
708
|
+
|
|
709
|
+
- [02-form-array.md](02-form-array.md) — основы FormArray (compound API, ref-handle).
|
|
710
|
+
- [03-form-navigation.md](03-form-navigation.md) — основы FormWizard (Indicator, Actions, Progress).
|
|
711
|
+
- [04-form-field.md](04-form-field.md) — компоновка одного поля.
|
|
712
|
+
- [06-troubleshooting.md](06-troubleshooting.md) — типичные ошибки и пути их обхода.
|
|
713
|
+
|
|
714
|
+
## 29. `FormArray.AddButton` не появляется на странице
|
|
715
|
+
|
|
716
|
+
**Причина.** `FormArray.AddButton` не самостоятельный элемент — он рендерит кнопку только внутри `FormArray.Root`. Если он вынесен наружу или `Root` не отрендерился (например, под условным `if`), кнопки не будет.
|
|
717
|
+
|
|
718
|
+
**Решение.** Поместите `AddButton` в дерево `Root` либо вызывайте `useFormArrayContext().add()` из своей кнопки (см. [05-recipes.md → Custom AddButton](05-recipes.md)).
|
|
719
|
+
|
|
720
|
+
```tsx
|
|
721
|
+
<FormArray.Root control={form.items}>
|
|
722
|
+
<FormArray.List>{renderItem}</FormArray.List>
|
|
723
|
+
<FormArray.AddButton>+ Добавить</FormArray.AddButton>
|
|
724
|
+
</FormArray.Root>
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## 30. `FormArray.List` ререндерит весь массив при изменении одного элемента
|
|
728
|
+
|
|
729
|
+
**Причина.** Render-функция массива возвращает inline JSX, который не мемоизирован. Изменение поля у элемента триггерит ререндер `FormArray.Root` (через `useFormControl(arrayNode)`), а List-children получают новые ссылки.
|
|
730
|
+
|
|
731
|
+
**Решение.** Вынесите рендер элемента в отдельный компонент, обёрнутый в `React.memo`. Передавайте `control` (стабильный по identity на протяжении жизни элемента) и `id` для key.
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
const Item = React.memo(({ control }: { control: FormProxy<Property> }) => (
|
|
735
|
+
<PropertyForm control={control} />
|
|
736
|
+
));
|
|
737
|
+
|
|
738
|
+
<FormArray.List>{({ control, id }) => <Item key={id} control={control} />}</FormArray.List>;
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
`useFormArray` мемоизирует `items` по длине массива (см. `useFormArray.ts`), поэтому смена значения внутри элемента не пересоздаёт `items`-массив.
|
|
742
|
+
|
|
743
|
+
## 31. `FormWizardHandle.goToStep` возвращает `false`
|
|
744
|
+
|
|
745
|
+
**Причина.** `goToStep(n)` пускает только если `n === 1` или `n - 1` уже в `completedSteps` (см. `FormWizard.tsx:goToStep`). Это защита от пропуска валидации. Также `false` возвращается при `n < 1` или `n > totalSteps`.
|
|
746
|
+
|
|
747
|
+
**Решение.**
|
|
748
|
+
|
|
749
|
+
- Для последовательного перехода вперёд используйте `await navRef.current?.goToNextStep()` — он сам валидирует и помечает шаг completed.
|
|
750
|
+
- Для произвольного «прыжка» предварительно отметьте предыдущий шаг как completed через `goToNextStep()` или вручную (через свой `useState` поверх).
|
|
751
|
+
- Проверьте `n` в диапазоне `[1; totalSteps]` (totalSteps = количество `FormWizard.Step` детей).
|
|
752
|
+
|
|
753
|
+
## 32. Шаг без `stepValidations[N]` пропускается без проверки
|
|
754
|
+
|
|
755
|
+
**Причина.** `validateCurrentStep` сначала смотрит `config.stepValidations[currentStep]`. Если схемы нет — выводит `console.warn` и возвращает `true` (шаг считается валидным).
|
|
756
|
+
|
|
757
|
+
**Решение.** Добавьте схему для каждого шага в `stepValidations`. Если шаг чисто информационный (например, «Подтверждение»), используйте схему-no-op: `() => ({})` (или импортируйте noop-validation из вашего набора).
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
const config: FormWizardConfig<MyForm> = {
|
|
761
|
+
stepValidations: {
|
|
762
|
+
1: amountSchema,
|
|
763
|
+
2: personalSchema,
|
|
764
|
+
3: () => ({}), // подтверждение, нет полей для валидации
|
|
765
|
+
},
|
|
766
|
+
fullValidation: fullSchema,
|
|
767
|
+
};
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
## 33. `useFormArrayContext` бросает исключение
|
|
771
|
+
|
|
772
|
+
**Сообщение.** `Error: FormArray.* components must be used within FormArray.Root or RenderSchema FormArray`.
|
|
773
|
+
|
|
774
|
+
**Причина.** Хук вызван вне дерева `FormArray.Root`. Часто — в компоненте, который рендерится через портал (Modal, Tooltip), либо рядом с `Root`, а не внутри.
|
|
775
|
+
|
|
776
|
+
**Решение.** Поместите потребителя внутрь `FormArray.Root`. Для портала — оберните потребителя ещё одним `Root` с тем же `control`, либо используйте ref-handle `FormArrayHandle` снаружи.
|
|
777
|
+
|
|
778
|
+
## 34. `useFormFieldContext` бросает исключение
|
|
779
|
+
|
|
780
|
+
**Сообщение.** `Error: FormField.* components must be used within <FormField.Root>`.
|
|
781
|
+
|
|
782
|
+
**Причина.** Тот же сценарий: `FormField.Label` / `Control` / `Error` без обёрнутого `FormField.Root`. Также возникает, если `Root` рендерит `null` (например, когда поле скрыто `enableWhen`) — Label/Error всё равно бросят, если рендерятся параллельно.
|
|
783
|
+
|
|
784
|
+
**Решение.** Перепроверьте дерево; если поле условное — выносите всю секцию в `if (!visible) return null;` снаружи `FormField.Root`.
|
|
785
|
+
|
|
786
|
+
## 35. `FormField.Error` не показывает ошибку, хотя `errors.length > 0`
|
|
787
|
+
|
|
788
|
+
**Причина.** Поле не помечено как touched, `shouldShowError === false`. По умолчанию ReFormer показывает ошибки только после blur/submit.
|
|
789
|
+
|
|
790
|
+
**Решение.**
|
|
791
|
+
|
|
792
|
+
- На сабмите формы вызовите `form.markAsTouched()` перед `await form.validate()`.
|
|
793
|
+
- Для немедленной валидации поля используйте `revalidateWhen({ when: 'change' })` behavior, либо вручную `control.markAsTouched()` в `onChange`.
|
|
794
|
+
|
|
795
|
+
```tsx
|
|
796
|
+
const handleSubmit = async () => {
|
|
797
|
+
form.markAsTouched();
|
|
798
|
+
await form.validate();
|
|
799
|
+
if (form.valid.value) {
|
|
800
|
+
/* submit */
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
## 36. `FormField.Description` есть в DOM, но `aria-describedby` пустой
|
|
806
|
+
|
|
807
|
+
**Причина.** В `FormField.Root` не передан проп `hasDescription`. Без него `Control` не вписывает `descriptionId` в `aria-describedby` (это сделано осознанно — чтобы избежать двойного рендера от динамической регистрации).
|
|
808
|
+
|
|
809
|
+
**Решение.** Установите `hasDescription` в `Root` явно.
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
<FormField.Root control={form.email} hasDescription>
|
|
813
|
+
<FormField.Label />
|
|
814
|
+
<FormField.Control />
|
|
815
|
+
<FormField.Description>Hint</FormField.Description>
|
|
816
|
+
</FormField.Root>
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
## 37. Multi-step submit срабатывает раньше валидации последнего шага
|
|
820
|
+
|
|
821
|
+
**Причина.** `FormWizard.Actions` диспатчит `submit` сразу при `onClick`. Если пользователь нажал Submit, не пройдя валидацию последнего шага через Next, `goToNextStep` не вызывался — но `submit` всё равно запустит `config.fullValidation` поверх всей формы. Однако touched-флаги ставятся только на полях с ошибками, и пользователю может быть неочевидно где именно проблема.
|
|
822
|
+
|
|
823
|
+
**Решение.** В кастомном submit-обработчике сначала вызывайте `validateCurrentStep`, затем `submit`. Или используйте `FormWizard.Actions`-render, где `submit.disabled === true` пока поля не валидны (Actions сам отключает кнопку при `isValidating`).
|
|
824
|
+
|
|
825
|
+
```tsx
|
|
826
|
+
const handleSubmit = async () => {
|
|
827
|
+
const stepOk = await navRef.current?.validateCurrentStep();
|
|
828
|
+
if (!stepOk) return;
|
|
829
|
+
await navRef.current?.submit(api.submit);
|
|
830
|
+
};
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
## 38. `FormArray.List` теряет focus или ререндерит inputs при `add`/`removeAt`
|
|
834
|
+
|
|
835
|
+
**Причина.** Используется `index` в качестве React-key. После `removeAt(0)` индексы сдвигаются, React переиспользует DOM-узлы для других данных — фокус «переезжает».
|
|
836
|
+
|
|
837
|
+
**Решение.** Используйте `id` из item-render-props, который ReFormer присваивает каждому элементу при `push`/`insert`:
|
|
838
|
+
|
|
839
|
+
```tsx
|
|
840
|
+
<FormArray.List>
|
|
841
|
+
{({ control, id, remove }) => (
|
|
842
|
+
<div key={id}>
|
|
843
|
+
{' '}
|
|
844
|
+
{/* ✓ стабильный id */}
|
|
845
|
+
<ItemForm control={control} />
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
848
|
+
</FormArray.List>
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
Сам `FormArray.List` уже использует `item.id` для ключа `<FormArrayItemContext.Provider key={item.id}>` — но если внутренний контейнер тоже задаёт key, поставьте `id`, а не `index`.
|
|
852
|
+
|
|
853
|
+
## 39. `FormWizard` показывает «No validation schema for step N» в консоли
|
|
854
|
+
|
|
855
|
+
**Причина.** `validateCurrentStep` warn'ит, если `config.stepValidations[currentStep]` отсутствует.
|
|
856
|
+
|
|
857
|
+
**Решение.** Добавьте schema для шага N или, если шаг намеренно без валидации, передайте noop: `[N]: () => ({})`.
|
|
858
|
+
|
|
859
|
+
## 40. Ref `FormArrayHandle.length` / `isEmpty` не обновляется
|
|
860
|
+
|
|
861
|
+
**Причина.** Эти поля snapshot'ятся в `useImperativeHandle` от `arrayState` и пересоздаются при каждом ререндере. Если вы храните `arrayRef.current` в замыкании или в `useEffect` без зависимостей, можете прочитать устаревшее значение.
|
|
862
|
+
|
|
863
|
+
**Решение.** Читайте `arrayRef.current.length` непосредственно в обработчике, не кешируйте. Для реактивной длины снаружи используйте `useFormControl(form.items).length` — это даст подписку.
|
|
864
|
+
|
|
865
|
+
## 41. See also
|
|
866
|
+
|
|
867
|
+
- [01-overview.md](01-overview.md), [04-form-field.md](04-form-field.md), [05-recipes.md](05-recipes.md).
|
|
868
|
+
- [@reformer/core troubleshooting](../../../reformer/docs/llms/) — общие проблемы с валидацией и behaviors.
|
|
869
|
+
|
|
870
|
+
## 42. API Reference
|
|
871
|
+
|
|
872
|
+
_Auto-generated from JSDoc on public exports._
|
|
873
|
+
|
|
874
|
+
### FormArray
|
|
875
|
+
|
|
876
|
+
**Kind:** `const`
|
|
877
|
+
|
|
878
|
+
FormArray - Headless compound component for managing form arrays
|
|
879
|
+
|
|
880
|
+
Provides complete flexibility for building array UI while handling
|
|
881
|
+
all the form array state and actions internally.
|
|
882
|
+
|
|
883
|
+
#### Features
|
|
884
|
+
- **Headless** - complete freedom in building UI
|
|
885
|
+
- **Compound Components** - declarative API via nested components
|
|
886
|
+
- **External Control** - control from outside via ref (useImperativeHandle)
|
|
887
|
+
- **Type Safe** - full TypeScript support
|
|
888
|
+
|
|
889
|
+
#### Sub-components
|
|
890
|
+
- `FormArray.Root` - context provider, accepts ref for external control
|
|
891
|
+
- `FormArray.List` - iterates over array items
|
|
892
|
+
- `FormArray.AddButton` - button to add item
|
|
893
|
+
- `FormArray.RemoveButton` - button to remove item (inside List)
|
|
894
|
+
- `FormArray.Empty` - content for empty state
|
|
895
|
+
- `FormArray.Count` - display item count
|
|
896
|
+
- `FormArray.ItemIndex` - display item index (inside List)
|
|
897
|
+
|
|
898
|
+
#### FormArrayHandle API (ref)
|
|
899
|
+
- `add(value?)` - add item to the end
|
|
900
|
+
- `insert(index, value?)` - insert item at position
|
|
901
|
+
- `removeAt(index)` - remove item by index
|
|
902
|
+
- `clear()` - clear array
|
|
903
|
+
- `at(index)` - get item control by index
|
|
904
|
+
- `length` - current item count
|
|
905
|
+
- `isEmpty` - empty array flag
|
|
906
|
+
|
|
907
|
+
**Signature:**
|
|
908
|
+
```typescript
|
|
909
|
+
export const FormArray
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**Examples:**
|
|
913
|
+
|
|
914
|
+
Basic usage
|
|
915
|
+
```tsx
|
|
916
|
+
<FormArray.Root control={form.properties}>
|
|
917
|
+
<h3>Properties (<FormArray.Count />)</h3>
|
|
918
|
+
|
|
919
|
+
<FormArray.Empty>
|
|
920
|
+
<p className="text-gray-500">No properties added</p>
|
|
921
|
+
</FormArray.Empty>
|
|
922
|
+
|
|
923
|
+
<FormArray.List className="space-y-4">
|
|
924
|
+
{({ control }) => (
|
|
925
|
+
<div className="p-4 border rounded">
|
|
926
|
+
<div className="flex justify-between mb-2">
|
|
927
|
+
<h4>Property #<FormArray.ItemIndex /></h4>
|
|
928
|
+
<FormArray.RemoveButton className="text-red-500">
|
|
929
|
+
Remove
|
|
930
|
+
</FormArray.RemoveButton>
|
|
931
|
+
</div>
|
|
932
|
+
<PropertyForm control={control} />
|
|
933
|
+
</div>
|
|
934
|
+
)}
|
|
935
|
+
</FormArray.List>
|
|
936
|
+
|
|
937
|
+
<FormArray.AddButton className="mt-4 btn-primary">
|
|
938
|
+
+ Add Property
|
|
939
|
+
</FormArray.AddButton>
|
|
940
|
+
</FormArray.Root>
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
External control via ref
|
|
944
|
+
```tsx
|
|
945
|
+
import { useRef } from 'react';
|
|
946
|
+
import { FormArray, FormArrayHandle } from '@reformer/cdk/form-array';
|
|
947
|
+
|
|
948
|
+
function PropertiesManager() {
|
|
949
|
+
const arrayRef = useRef<FormArrayHandle<Property>>(null);
|
|
950
|
+
|
|
951
|
+
// Programmatic control from outside
|
|
952
|
+
const handleAddApartment = () => {
|
|
953
|
+
arrayRef.current?.add({ type: 'apartment', estimatedValue: 0 });
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
const handleClearAll = () => {
|
|
957
|
+
if (confirm('Delete all items?')) {
|
|
958
|
+
arrayRef.current?.clear();
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const handleRemoveFirst = () => {
|
|
963
|
+
if (arrayRef.current && arrayRef.current.length > 0) {
|
|
964
|
+
arrayRef.current.removeAt(0);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const handleInsertAtStart = () => {
|
|
969
|
+
arrayRef.current?.insert(0, { type: 'house' });
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
return (
|
|
973
|
+
<div>
|
|
974
|
+
<div className="toolbar">
|
|
975
|
+
<button onClick={handleAddApartment}>+ Apartment</button>
|
|
976
|
+
<button onClick={handleInsertAtStart}>Insert at start</button>
|
|
977
|
+
<button onClick={handleRemoveFirst}>Remove first</button>
|
|
978
|
+
<button onClick={handleClearAll}>Clear all</button>
|
|
979
|
+
</div>
|
|
980
|
+
|
|
981
|
+
<FormArray.Root ref={arrayRef} control={form.properties}>
|
|
982
|
+
<FormArray.List>
|
|
983
|
+
{({ control }) => <PropertyForm control={control} />}
|
|
984
|
+
</FormArray.List>
|
|
985
|
+
</FormArray.Root>
|
|
986
|
+
</div>
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
Using useFormArray hook for full customization
|
|
992
|
+
```tsx
|
|
993
|
+
import { useFormArray } from '@reformer/cdk/form-array';
|
|
994
|
+
|
|
995
|
+
function CustomArrayUI() {
|
|
996
|
+
const { items, add, isEmpty, length } = useFormArray(form.properties);
|
|
997
|
+
|
|
998
|
+
return (
|
|
999
|
+
<div>
|
|
1000
|
+
<span>Total: {length}</span>
|
|
1001
|
+
{items.map(({ control, id, remove }) => (
|
|
1002
|
+
<CustomCard key={id} onDelete={remove}>
|
|
1003
|
+
<PropertyForm control={control} />
|
|
1004
|
+
</CustomCard>
|
|
1005
|
+
))}
|
|
1006
|
+
{isEmpty && <EmptyState />}
|
|
1007
|
+
<button onClick={() => add()}>Add</button>
|
|
1008
|
+
</div>
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
_Source: src/components/form-array/FormArray.tsx_
|
|
1014
|
+
|
|
1015
|
+
### FormArrayAddButton
|
|
1016
|
+
|
|
1017
|
+
**Kind:** `const`
|
|
1018
|
+
|
|
1019
|
+
**Signature:**
|
|
1020
|
+
```typescript
|
|
1021
|
+
export const FormArrayAddButton
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
_Source: src/components/form-array/FormArrayAddButton.tsx_
|
|
1025
|
+
|
|
1026
|
+
### FormArrayAddButtonProps
|
|
1027
|
+
|
|
1028
|
+
**Kind:** `interface`
|
|
1029
|
+
|
|
1030
|
+
Props for FormArray.AddButton component
|
|
1031
|
+
|
|
1032
|
+
Generic `T` — тип элемента массива. По умолчанию `FormFields` (широкий) —
|
|
1033
|
+
для совместимости. Для type-safe initialValue передавайте generic явно
|
|
1034
|
+
(`<FormArray.AddButton<PropertyItem> ...>`) либо проксируйте через
|
|
1035
|
+
`FormArraySection<T>` из `@reformer/ui-kit`.
|
|
1036
|
+
|
|
1037
|
+
**Signature:**
|
|
1038
|
+
```typescript
|
|
1039
|
+
export interface FormArrayAddButtonProps<T extends FormFields = FormFields> extends Omit<
|
|
1040
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
1041
|
+
'onClick'
|
|
1042
|
+
> {
|
|
1043
|
+
/** Initial value for the new item */
|
|
1044
|
+
initialValue?: Partial<T>;
|
|
1045
|
+
/** Custom render function for the button */
|
|
1046
|
+
asChild?: boolean;
|
|
1047
|
+
}
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
_Source: src/components/form-array/types.ts_
|
|
1051
|
+
|
|
1052
|
+
### FormArrayContext
|
|
1053
|
+
|
|
1054
|
+
**Kind:** `const`
|
|
1055
|
+
|
|
1056
|
+
React context, который снабжает дочерние компоненты `FormArray` (List, AddButton, …)
|
|
1057
|
+
текущим `ArrayNode` и хелперами. Создаётся `FormArray.Root`. Читать через {@link useFormArrayContext}.
|
|
1058
|
+
|
|
1059
|
+
**Signature:**
|
|
1060
|
+
```typescript
|
|
1061
|
+
export const FormArrayContext
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
**Examples:**
|
|
1065
|
+
|
|
1066
|
+
```tsx
|
|
1067
|
+
import { FormArrayContext } from '@reformer/cdk/form-array';
|
|
1068
|
+
|
|
1069
|
+
function MyConsumer() {
|
|
1070
|
+
const ctx = useContext(FormArrayContext);
|
|
1071
|
+
return <span>items: {ctx?.items.length}</span>;
|
|
1072
|
+
}
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
_Source: src/components/form-array/FormArrayContext.tsx_
|
|
1076
|
+
|
|
1077
|
+
### FormArrayContextValue
|
|
1078
|
+
|
|
1079
|
+
**Kind:** `interface`
|
|
1080
|
+
|
|
1081
|
+
Контекст уровня массива
|
|
1082
|
+
|
|
1083
|
+
**Signature:**
|
|
1084
|
+
```typescript
|
|
1085
|
+
export interface FormArrayContextValue<T extends FormFields = FormFields> {
|
|
1086
|
+
/** Массив элементов с контролами и действиями */
|
|
1087
|
+
items: FormArrayItem<T>[];
|
|
1088
|
+
/** Текущая длина массива */
|
|
1089
|
+
length: number;
|
|
1090
|
+
/** Пустой ли массив */
|
|
1091
|
+
isEmpty: boolean;
|
|
1092
|
+
/** Добавить новый элемент в конец */
|
|
1093
|
+
add: (value?: Partial<T>) => void;
|
|
1094
|
+
/** Удалить все элементы */
|
|
1095
|
+
clear: () => void;
|
|
1096
|
+
/** Вставить элемент на указанную позицию */
|
|
1097
|
+
insert: (index: number, value?: Partial<T>) => void;
|
|
1098
|
+
/** Оригинальный ArrayNode */
|
|
1099
|
+
control: ArrayNode<T>;
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
_Source: src/components/form-array/FormArrayContext.tsx_
|
|
1104
|
+
|
|
1105
|
+
### FormArrayCount
|
|
1106
|
+
|
|
1107
|
+
**Kind:** `function`
|
|
1108
|
+
|
|
1109
|
+
FormArray.Count - Displays the number of items in the array
|
|
1110
|
+
|
|
1111
|
+
**Signature:**
|
|
1112
|
+
```typescript
|
|
1113
|
+
export function FormArrayCount({ render }: FormArrayCountProps)
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
**Examples:**
|
|
1117
|
+
|
|
1118
|
+
Basic usage
|
|
1119
|
+
```tsx
|
|
1120
|
+
<h3>Items (<FormArray.Count />)</h3>
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
With custom render
|
|
1124
|
+
```tsx
|
|
1125
|
+
<FormArray.Count render={(count) => (
|
|
1126
|
+
count === 0 ? 'No items' : `${count} item${count > 1 ? 's' : ''}`
|
|
1127
|
+
)} />
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
_Source: src/components/form-array/FormArrayCount.tsx_
|
|
1131
|
+
|
|
1132
|
+
### FormArrayCountProps
|
|
1133
|
+
|
|
1134
|
+
**Kind:** `interface`
|
|
1135
|
+
|
|
1136
|
+
Props for FormArray.Count component
|
|
1137
|
+
|
|
1138
|
+
**Signature:**
|
|
1139
|
+
```typescript
|
|
1140
|
+
export interface FormArrayCountProps {
|
|
1141
|
+
/** Custom render function for the count */
|
|
1142
|
+
render?: (count: number) => ReactNode;
|
|
1143
|
+
}
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
_Source: src/components/form-array/types.ts_
|
|
1147
|
+
|
|
1148
|
+
### FormArrayEmpty
|
|
1149
|
+
|
|
1150
|
+
**Kind:** `function`
|
|
1151
|
+
|
|
1152
|
+
FormArray.Empty - Renders children only when array is empty
|
|
1153
|
+
|
|
1154
|
+
**Signature:**
|
|
1155
|
+
```typescript
|
|
1156
|
+
export function FormArrayEmpty({ children }: FormArrayEmptyProps)
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
**Examples:**
|
|
1160
|
+
|
|
1161
|
+
Basic usage
|
|
1162
|
+
```tsx
|
|
1163
|
+
<FormArray.Empty>
|
|
1164
|
+
<p className="text-gray-500">No items added yet</p>
|
|
1165
|
+
</FormArray.Empty>
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
With call to action
|
|
1169
|
+
```tsx
|
|
1170
|
+
<FormArray.Empty>
|
|
1171
|
+
<div className="text-center p-8">
|
|
1172
|
+
<p>No properties</p>
|
|
1173
|
+
<FormArray.AddButton>Add your first property</FormArray.AddButton>
|
|
1174
|
+
</div>
|
|
1175
|
+
</FormArray.Empty>
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
_Source: src/components/form-array/FormArrayEmpty.tsx_
|
|
1179
|
+
|
|
1180
|
+
### FormArrayEmptyProps
|
|
1181
|
+
|
|
1182
|
+
**Kind:** `interface`
|
|
1183
|
+
|
|
1184
|
+
Props for FormArray.Empty component
|
|
1185
|
+
|
|
1186
|
+
**Signature:**
|
|
1187
|
+
```typescript
|
|
1188
|
+
export interface FormArrayEmptyProps {
|
|
1189
|
+
/** Content to show when array is empty */
|
|
1190
|
+
children: ReactNode;
|
|
1191
|
+
}
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
_Source: src/components/form-array/types.ts_
|
|
1195
|
+
|
|
1196
|
+
### FormArrayHandle
|
|
1197
|
+
|
|
1198
|
+
**Kind:** `interface`
|
|
1199
|
+
|
|
1200
|
+
Handle exposed via ref for external control of {@link FormArray}.
|
|
1201
|
+
|
|
1202
|
+
Имперо-API для случаев, когда триггер находится вне дерева `FormArray.Root`
|
|
1203
|
+
(тулбар страницы, диалог подтверждения, async-эффект). Получают через
|
|
1204
|
+
`useRef<FormArrayHandle<T>>(null)` и передают в `<FormArray.Root ref={...}>`.
|
|
1205
|
+
|
|
1206
|
+
Свойства `length` / `isEmpty` — снимок на момент рендера. Реактивную длину
|
|
1207
|
+
для условного UI снаружи берите через `useFormControl(arrayNode).length`.
|
|
1208
|
+
|
|
1209
|
+
**Signature:**
|
|
1210
|
+
```typescript
|
|
1211
|
+
export interface FormArrayHandle<T extends FormFields> {
|
|
1212
|
+
/** Add a new item to the end of the array */
|
|
1213
|
+
add: (value?: Partial<T>) => void;
|
|
1214
|
+
/** Remove all items from the array */
|
|
1215
|
+
clear: () => void;
|
|
1216
|
+
/** Insert a new item at a specific index */
|
|
1217
|
+
insert: (index: number, value?: Partial<T>) => void;
|
|
1218
|
+
/** Remove item at specific index */
|
|
1219
|
+
removeAt: (index: number) => void;
|
|
1220
|
+
/** Current number of items */
|
|
1221
|
+
length: number;
|
|
1222
|
+
/** Whether the array is empty */
|
|
1223
|
+
isEmpty: boolean;
|
|
1224
|
+
/** Get item control at specific index */
|
|
1225
|
+
at: (index: number) => FormProxy<T> | undefined;
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
**Examples:**
|
|
1230
|
+
|
|
1231
|
+
Тулбар «Добавить / Очистить» поверх массива
|
|
1232
|
+
```tsx
|
|
1233
|
+
import { useRef } from 'react';
|
|
1234
|
+
import { FormArray, type FormArrayHandle } from '@reformer/cdk/form-array';
|
|
1235
|
+
|
|
1236
|
+
function PropertiesEditor({ form }: Props) {
|
|
1237
|
+
const arrayRef = useRef<FormArrayHandle<Property>>(null);
|
|
1238
|
+
|
|
1239
|
+
return (
|
|
1240
|
+
<>
|
|
1241
|
+
<div className="toolbar">
|
|
1242
|
+
<button onClick={() => arrayRef.current?.add({ type: 'apartment' })}>
|
|
1243
|
+
+ Квартира
|
|
1244
|
+
</button>
|
|
1245
|
+
<button onClick={() => arrayRef.current?.add({ type: 'house' })}>
|
|
1246
|
+
+ Дом
|
|
1247
|
+
</button>
|
|
1248
|
+
<button onClick={() => confirm('Удалить всё?') && arrayRef.current?.clear()}>
|
|
1249
|
+
Очистить
|
|
1250
|
+
</button>
|
|
1251
|
+
</div>
|
|
1252
|
+
<FormArray.Root ref={arrayRef} control={form.properties}>
|
|
1253
|
+
<FormArray.List>{({ control }) => <PropertyForm control={control} />}</FormArray.List>
|
|
1254
|
+
</FormArray.Root>
|
|
1255
|
+
</>
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
Импорт массива из API: insert + at для проверки дублей
|
|
1261
|
+
```tsx
|
|
1262
|
+
const arrayRef = useRef<FormArrayHandle<Contact>>(null);
|
|
1263
|
+
|
|
1264
|
+
async function importFromCSV(rows: Contact[]) {
|
|
1265
|
+
for (const row of rows) {
|
|
1266
|
+
// skip duplicates by email
|
|
1267
|
+
const existing = Array.from({ length: arrayRef.current?.length ?? 0 })
|
|
1268
|
+
.map((_, i) => arrayRef.current?.at(i)?.getValue());
|
|
1269
|
+
if (existing.some((c) => c?.email === row.email)) continue;
|
|
1270
|
+
arrayRef.current?.insert(0, row); // добавляем в начало
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
_Source: src/components/form-array/FormArray.tsx_
|
|
1276
|
+
|
|
1277
|
+
### FormArrayItem
|
|
1278
|
+
|
|
1279
|
+
**Kind:** `interface`
|
|
1280
|
+
|
|
1281
|
+
Represents a single item in a form array with its control, index, and actions
|
|
1282
|
+
|
|
1283
|
+
**Signature:**
|
|
1284
|
+
```typescript
|
|
1285
|
+
export interface FormArrayItem<T extends FormFields> {
|
|
1286
|
+
/** The form control for this item */
|
|
1287
|
+
control: FormProxy<T>;
|
|
1288
|
+
/** Zero-based index of the item in the array */
|
|
1289
|
+
index: number;
|
|
1290
|
+
/** Unique identifier for React key (uses internal id or falls back to index) */
|
|
1291
|
+
id: string | number;
|
|
1292
|
+
/** Remove this item from the array */
|
|
1293
|
+
remove: () => void;
|
|
1294
|
+
}
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
_Source: src/components/form-array/useFormArray.ts_
|
|
1298
|
+
|
|
1299
|
+
### FormArrayItemContext
|
|
1300
|
+
|
|
1301
|
+
**Kind:** `const`
|
|
1302
|
+
|
|
1303
|
+
React context, видимый внутри `FormArray.List` для одного элемента массива.
|
|
1304
|
+
Содержит `index`, `path` и `remove()`. Читать через {@link useFormArrayItemContext}.
|
|
1305
|
+
|
|
1306
|
+
**Signature:**
|
|
1307
|
+
```typescript
|
|
1308
|
+
export const FormArrayItemContext
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
**Examples:**
|
|
1312
|
+
|
|
1313
|
+
```tsx
|
|
1314
|
+
import { FormArrayItemContext } from '@reformer/cdk/form-array';
|
|
1315
|
+
|
|
1316
|
+
function CurrentIndex() {
|
|
1317
|
+
const item = useContext(FormArrayItemContext);
|
|
1318
|
+
return <small>#{item?.index}</small>;
|
|
1319
|
+
}
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
_Source: src/components/form-array/FormArrayContext.tsx_
|
|
1323
|
+
|
|
1324
|
+
### FormArrayItemContextValue
|
|
1325
|
+
|
|
1326
|
+
**Kind:** `interface`
|
|
1327
|
+
|
|
1328
|
+
Контекст уровня элемента массива
|
|
1329
|
+
|
|
1330
|
+
**Signature:**
|
|
1331
|
+
```typescript
|
|
1332
|
+
export interface FormArrayItemContextValue<T extends FormFields = FormFields> {
|
|
1333
|
+
/** Контрол для данного элемента */
|
|
1334
|
+
control: FormProxy<T>;
|
|
1335
|
+
/** Индекс элемента (0-based) */
|
|
1336
|
+
index: number;
|
|
1337
|
+
/** Уникальный идентификатор для React key */
|
|
1338
|
+
id: string | number;
|
|
1339
|
+
/** Удалить этот элемент из массива */
|
|
1340
|
+
remove: () => void;
|
|
1341
|
+
}
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
_Source: src/components/form-array/FormArrayContext.tsx_
|
|
1345
|
+
|
|
1346
|
+
### FormArrayItemIndex
|
|
1347
|
+
|
|
1348
|
+
**Kind:** `function`
|
|
1349
|
+
|
|
1350
|
+
FormArray.ItemIndex - Displays the index of current item (must be inside FormArray.List)
|
|
1351
|
+
|
|
1352
|
+
**Signature:**
|
|
1353
|
+
```typescript
|
|
1354
|
+
export function FormArrayItemIndex({ render }: FormArrayItemIndexProps)
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
**Examples:**
|
|
1358
|
+
|
|
1359
|
+
Basic usage (1-based display)
|
|
1360
|
+
```tsx
|
|
1361
|
+
<FormArray.List>
|
|
1362
|
+
{() => (
|
|
1363
|
+
<h4>Item #<FormArray.ItemIndex render={(i) => i + 1} /></h4>
|
|
1364
|
+
)}
|
|
1365
|
+
</FormArray.List>
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
Zero-based index
|
|
1369
|
+
```tsx
|
|
1370
|
+
<FormArray.ItemIndex /> // Renders 0, 1, 2, ...
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
Custom render
|
|
1374
|
+
```tsx
|
|
1375
|
+
<FormArray.ItemIndex render={(index) => `Position: ${index + 1}`} />
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
_Source: src/components/form-array/FormArrayItemIndex.tsx_
|
|
1379
|
+
|
|
1380
|
+
### FormArrayItemIndexProps
|
|
1381
|
+
|
|
1382
|
+
**Kind:** `interface`
|
|
1383
|
+
|
|
1384
|
+
Props for FormArray.ItemIndex component
|
|
1385
|
+
|
|
1386
|
+
**Signature:**
|
|
1387
|
+
```typescript
|
|
1388
|
+
export interface FormArrayItemIndexProps {
|
|
1389
|
+
/** Custom render function for the index (receives 0-based index) */
|
|
1390
|
+
render?: (index: number) => ReactNode;
|
|
1391
|
+
}
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
_Source: src/components/form-array/types.ts_
|
|
1395
|
+
|
|
1396
|
+
### FormArrayItemRenderProps
|
|
1397
|
+
|
|
1398
|
+
**Kind:** `interface`
|
|
1399
|
+
|
|
1400
|
+
Props passed to the render function in FormArray.List
|
|
1401
|
+
|
|
1402
|
+
**Signature:**
|
|
1403
|
+
```typescript
|
|
1404
|
+
export interface FormArrayItemRenderProps<T extends FormFields> {
|
|
1405
|
+
/** The form control for this item */
|
|
1406
|
+
control: FormProxy<T>;
|
|
1407
|
+
/** Zero-based index of the item */
|
|
1408
|
+
index: number;
|
|
1409
|
+
/** Unique identifier for React key */
|
|
1410
|
+
id: string | number;
|
|
1411
|
+
/** Remove this item from the array */
|
|
1412
|
+
remove: () => void;
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
_Source: src/components/form-array/types.ts_
|
|
1417
|
+
|
|
1418
|
+
### FormArrayList
|
|
1419
|
+
|
|
1420
|
+
**Kind:** `function`
|
|
1421
|
+
|
|
1422
|
+
FormArray.List - Iterates over array items and provides item context
|
|
1423
|
+
|
|
1424
|
+
**Signature:**
|
|
1425
|
+
```typescript
|
|
1426
|
+
export function FormArrayList<T extends FormFields>({
|
|
1427
|
+
children,
|
|
1428
|
+
className,
|
|
1429
|
+
as = 'div',
|
|
1430
|
+
}: FormArrayListProps<T>)
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
**Examples:**
|
|
1434
|
+
|
|
1435
|
+
Basic usage
|
|
1436
|
+
```tsx
|
|
1437
|
+
<FormArray.List>
|
|
1438
|
+
{({ control, index, remove }) => (
|
|
1439
|
+
<div>
|
|
1440
|
+
<span>Item #{index + 1}</span>
|
|
1441
|
+
<button onClick={remove}>Remove</button>
|
|
1442
|
+
<ItemForm control={control} />
|
|
1443
|
+
</div>
|
|
1444
|
+
)}
|
|
1445
|
+
</FormArray.List>
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
With custom container
|
|
1449
|
+
```tsx
|
|
1450
|
+
<FormArray.List className="space-y-4" as="ul">
|
|
1451
|
+
{(item) => <li><ItemForm control={item.control} /></li>}
|
|
1452
|
+
</FormArray.List>
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
_Source: src/components/form-array/FormArrayList.tsx_
|
|
1456
|
+
|
|
1457
|
+
### FormArrayListProps
|
|
1458
|
+
|
|
1459
|
+
**Kind:** `interface`
|
|
1460
|
+
|
|
1461
|
+
Props for FormArray.List component
|
|
1462
|
+
|
|
1463
|
+
**Signature:**
|
|
1464
|
+
```typescript
|
|
1465
|
+
export interface FormArrayListProps<T extends FormFields> {
|
|
1466
|
+
/** Render function for each item */
|
|
1467
|
+
children: (item: FormArrayItemRenderProps<T>) => ReactNode;
|
|
1468
|
+
/** Optional className for the list container */
|
|
1469
|
+
className?: string;
|
|
1470
|
+
/** Optional element type for the container (default: 'div') */
|
|
1471
|
+
as?: ElementType;
|
|
1472
|
+
}
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
_Source: src/components/form-array/types.ts_
|
|
1476
|
+
|
|
1477
|
+
### FormArrayRemoveButton
|
|
1478
|
+
|
|
1479
|
+
**Kind:** `const`
|
|
1480
|
+
|
|
1481
|
+
FormArray.RemoveButton - Button to remove current item (must be inside FormArray.List)
|
|
1482
|
+
|
|
1483
|
+
**Signature:**
|
|
1484
|
+
```typescript
|
|
1485
|
+
export const FormArrayRemoveButton
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
**Examples:**
|
|
1489
|
+
|
|
1490
|
+
Basic usage
|
|
1491
|
+
```tsx
|
|
1492
|
+
<FormArray.List>
|
|
1493
|
+
{({ control }) => (
|
|
1494
|
+
<div>
|
|
1495
|
+
<ItemForm control={control} />
|
|
1496
|
+
<FormArray.RemoveButton className="text-red-500">
|
|
1497
|
+
Remove
|
|
1498
|
+
</FormArray.RemoveButton>
|
|
1499
|
+
</div>
|
|
1500
|
+
)}
|
|
1501
|
+
</FormArray.List>
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
With custom button (asChild)
|
|
1505
|
+
```tsx
|
|
1506
|
+
<FormArray.RemoveButton asChild>
|
|
1507
|
+
<IconButton icon="trash" aria-label="Remove" />
|
|
1508
|
+
</FormArray.RemoveButton>
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
_Source: src/components/form-array/FormArrayRemoveButton.tsx_
|
|
1512
|
+
|
|
1513
|
+
### FormArrayRemoveButtonProps
|
|
1514
|
+
|
|
1515
|
+
**Kind:** `interface`
|
|
1516
|
+
|
|
1517
|
+
Props for FormArray.RemoveButton component
|
|
1518
|
+
|
|
1519
|
+
**Signature:**
|
|
1520
|
+
```typescript
|
|
1521
|
+
export interface FormArrayRemoveButtonProps extends Omit<
|
|
1522
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
1523
|
+
'onClick'
|
|
1524
|
+
> {
|
|
1525
|
+
/** Custom render function for the button */
|
|
1526
|
+
asChild?: boolean;
|
|
1527
|
+
}
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
_Source: src/components/form-array/types.ts_
|
|
1531
|
+
|
|
1532
|
+
### FormArrayRootProps
|
|
1533
|
+
|
|
1534
|
+
**Kind:** `interface`
|
|
1535
|
+
|
|
1536
|
+
Props for FormArray.Root component
|
|
1537
|
+
|
|
1538
|
+
**Signature:**
|
|
1539
|
+
```typescript
|
|
1540
|
+
export interface FormArrayRootProps<T extends FormFields> {
|
|
1541
|
+
/** The ArrayNode control from the form */
|
|
1542
|
+
control: ArrayNode<T>;
|
|
1543
|
+
/** Child components */
|
|
1544
|
+
children: ReactNode;
|
|
1545
|
+
}
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
_Source: src/components/form-array/types.ts_
|
|
1549
|
+
|
|
1550
|
+
### FormField
|
|
1551
|
+
|
|
1552
|
+
**Kind:** `const`
|
|
1553
|
+
|
|
1554
|
+
FormField - Headless compound component for accessible form field anatomy.
|
|
1555
|
+
|
|
1556
|
+
Provides complete freedom in building field UI while handling all accessible
|
|
1557
|
+
ID wiring (htmlFor, aria-labelledby, aria-describedby, aria-errormessage)
|
|
1558
|
+
and field state management internally.
|
|
1559
|
+
|
|
1560
|
+
#### Features
|
|
1561
|
+
- **Headless** — complete freedom in building UI, no styles imposed
|
|
1562
|
+
- **Compound Components** — declarative API via nested components
|
|
1563
|
+
- **Accessible by default** — all ARIA relationships wired automatically
|
|
1564
|
+
- **Type Safe** — full TypeScript support with generics
|
|
1565
|
+
|
|
1566
|
+
#### Sub-components
|
|
1567
|
+
- `FormField.Root` — context provider, accepts `control` and optional `id`
|
|
1568
|
+
- `FormField.Label` — `<label>` with automatic `htmlFor` and required indicator
|
|
1569
|
+
- `FormField.Control` — auto-renders `control.component` or wraps custom children
|
|
1570
|
+
- `FormField.Error` — error message with `role="alert"`, supports multi-error
|
|
1571
|
+
- `FormField.Description` — helper text with stable `id` for `aria-describedby`
|
|
1572
|
+
|
|
1573
|
+
**Signature:**
|
|
1574
|
+
```typescript
|
|
1575
|
+
export const FormField
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
**Examples:**
|
|
1579
|
+
|
|
1580
|
+
Minimal (auto-renders everything from field config)
|
|
1581
|
+
```tsx
|
|
1582
|
+
<FormField.Root control={control.email}>
|
|
1583
|
+
<FormField.Label />
|
|
1584
|
+
<FormField.Control />
|
|
1585
|
+
<FormField.Error />
|
|
1586
|
+
</FormField.Root>
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
Full control with custom styling
|
|
1590
|
+
```tsx
|
|
1591
|
+
<FormField.Root control={control.email} hasDescription>
|
|
1592
|
+
<div className="space-y-1">
|
|
1593
|
+
<FormField.Label className="text-sm font-medium text-gray-700" />
|
|
1594
|
+
|
|
1595
|
+
<FormField.Control asChild>
|
|
1596
|
+
<Input type="email" className="border rounded-md px-3 py-2 w-full" />
|
|
1597
|
+
</FormField.Control>
|
|
1598
|
+
|
|
1599
|
+
<FormField.Description className="text-xs text-gray-500">
|
|
1600
|
+
We'll never share your email.
|
|
1601
|
+
</FormField.Description>
|
|
1602
|
+
|
|
1603
|
+
<FormField.Error className="text-xs text-red-600" />
|
|
1604
|
+
</div>
|
|
1605
|
+
</FormField.Root>
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
Multiple errors with custom rendering
|
|
1609
|
+
```tsx
|
|
1610
|
+
<FormField.Root control={control.password}>
|
|
1611
|
+
<FormField.Label />
|
|
1612
|
+
<FormField.Control />
|
|
1613
|
+
<FormField.Error
|
|
1614
|
+
multi
|
|
1615
|
+
render={(err) => (
|
|
1616
|
+
<span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>
|
|
1617
|
+
{err.message}
|
|
1618
|
+
</span>
|
|
1619
|
+
)}
|
|
1620
|
+
/>
|
|
1621
|
+
</FormField.Root>
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
Using useFormField hook for full customization
|
|
1625
|
+
```tsx
|
|
1626
|
+
import { useFormField } from '@reformer/cdk/form-field';
|
|
1627
|
+
|
|
1628
|
+
function EmailField({ control }: { control: FieldNode<string> }) {
|
|
1629
|
+
const { labelProps, controlProps, errorProps, state, ids } = useFormField(control);
|
|
1630
|
+
|
|
1631
|
+
return (
|
|
1632
|
+
<div>
|
|
1633
|
+
<label {...labelProps}>{state.label}</label>
|
|
1634
|
+
<Input
|
|
1635
|
+
{...controlProps}
|
|
1636
|
+
aria-describedby={ids.descriptionId}
|
|
1637
|
+
type="email"
|
|
1638
|
+
/>
|
|
1639
|
+
<p id={ids.descriptionId} className="text-xs text-gray-500">
|
|
1640
|
+
Helper text
|
|
1641
|
+
</p>
|
|
1642
|
+
{state.shouldShowError && (
|
|
1643
|
+
<p {...errorProps} className="text-xs text-red-600">{state.error}</p>
|
|
1644
|
+
)}
|
|
1645
|
+
</div>
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
_Source: src/components/form-field/FormField.tsx_
|
|
1651
|
+
|
|
1652
|
+
### FormFieldContext
|
|
1653
|
+
|
|
1654
|
+
**Kind:** `const`
|
|
1655
|
+
|
|
1656
|
+
React context, который снабжает дочерние компоненты `FormField` (Label, Error,
|
|
1657
|
+
Hint, Control) текущим контролом. Создаётся `FormField.Root`. Читать через
|
|
1658
|
+
{@link useFormFieldContext}.
|
|
1659
|
+
|
|
1660
|
+
**Signature:**
|
|
1661
|
+
```typescript
|
|
1662
|
+
export const FormFieldContext
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
**Examples:**
|
|
1666
|
+
|
|
1667
|
+
```tsx
|
|
1668
|
+
import { FormFieldContext } from '@reformer/cdk/form-field';
|
|
1669
|
+
|
|
1670
|
+
function CurrentValue() {
|
|
1671
|
+
const ctx = useContext(FormFieldContext);
|
|
1672
|
+
return <pre>{JSON.stringify(ctx?.control.value)}</pre>;
|
|
1673
|
+
}
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
_Source: src/components/form-field/FormFieldContext.tsx_
|
|
1677
|
+
|
|
1678
|
+
### FormFieldContextValue
|
|
1679
|
+
|
|
1680
|
+
**Kind:** `interface`
|
|
1681
|
+
|
|
1682
|
+
Context value provided by FormField.Root
|
|
1683
|
+
|
|
1684
|
+
**Signature:**
|
|
1685
|
+
```typescript
|
|
1686
|
+
export interface FormFieldContextValue<T extends FormValue = FormValue> {
|
|
1687
|
+
// ─── Field state ──────────────────────────────────────────────────────────
|
|
1688
|
+
value: T;
|
|
1689
|
+
errors: ValidationError[];
|
|
1690
|
+
pending: boolean;
|
|
1691
|
+
disabled: boolean;
|
|
1692
|
+
valid: boolean;
|
|
1693
|
+
invalid: boolean;
|
|
1694
|
+
touched: boolean;
|
|
1695
|
+
shouldShowError: boolean;
|
|
1696
|
+
/** First error message, only set when shouldShowError is true */
|
|
1697
|
+
error: string | undefined;
|
|
1698
|
+
// ─── Derived from componentProps ──────────────────────────────────────────
|
|
1699
|
+
label: string | undefined;
|
|
1700
|
+
required: boolean;
|
|
1701
|
+
/** Full componentProps bag */
|
|
1702
|
+
componentProps: Record<string, unknown>;
|
|
1703
|
+
// ─── The control itself ───────────────────────────────────────────────────
|
|
1704
|
+
control: FieldNode<T>;
|
|
1705
|
+
// ─── Accessible IDs ───────────────────────────────────────────────────────
|
|
1706
|
+
ids: FormFieldIds;
|
|
1707
|
+
/** Whether a FormField.Description is present (drives aria-describedby on Control) */
|
|
1708
|
+
hasDescription: boolean;
|
|
1709
|
+
}
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
_Source: src/components/form-field/types.ts_
|
|
1713
|
+
|
|
1714
|
+
### FormFieldControl
|
|
1715
|
+
|
|
1716
|
+
**Kind:** `const`
|
|
1717
|
+
|
|
1718
|
+
FormField.Control - Renders the interactive form control.
|
|
1719
|
+
|
|
1720
|
+
**Auto-render mode** (default): renders `control.component` with all necessary
|
|
1721
|
+
props pre-wired: `value`, `onChange`, `onBlur`, `disabled`, `aria-*` attributes,
|
|
1722
|
+
and all `componentProps` from the field config.
|
|
1723
|
+
|
|
1724
|
+
**Custom children mode** (`asChild` or `children`): merges accessible props
|
|
1725
|
+
into the provided child element via Slot, letting you use any custom component.
|
|
1726
|
+
|
|
1727
|
+
**Signature:**
|
|
1728
|
+
```typescript
|
|
1729
|
+
export const FormFieldControl
|
|
1730
|
+
```
|
|
1731
|
+
|
|
1732
|
+
**Examples:**
|
|
1733
|
+
|
|
1734
|
+
Auto-render (renders control.component)
|
|
1735
|
+
```tsx
|
|
1736
|
+
<FormField.Root control={control.email}>
|
|
1737
|
+
<FormField.Label />
|
|
1738
|
+
<FormField.Control />
|
|
1739
|
+
</FormField.Root>
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
Custom input with asChild (merges aria-* into your element)
|
|
1743
|
+
```tsx
|
|
1744
|
+
<FormField.Control asChild>
|
|
1745
|
+
<MyInput type="email" className="custom-input" />
|
|
1746
|
+
</FormField.Control>
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
Custom children (same as asChild)
|
|
1750
|
+
```tsx
|
|
1751
|
+
<FormField.Control>
|
|
1752
|
+
<MyInput type="email" />
|
|
1753
|
+
</FormField.Control>
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
_Source: src/components/form-field/FormFieldControl.tsx_
|
|
1757
|
+
|
|
1758
|
+
### FormFieldControlProps
|
|
1759
|
+
|
|
1760
|
+
**Kind:** `interface`
|
|
1761
|
+
|
|
1762
|
+
Props for FormField.Control
|
|
1763
|
+
|
|
1764
|
+
**Signature:**
|
|
1765
|
+
```typescript
|
|
1766
|
+
export interface FormFieldControlProps extends Omit<
|
|
1767
|
+
HTMLAttributes<HTMLElement>,
|
|
1768
|
+
'id' | 'onChange' | 'onBlur'
|
|
1769
|
+
> {
|
|
1770
|
+
/**
|
|
1771
|
+
* Merge accessible props into a custom child element instead of
|
|
1772
|
+
* auto-rendering control.component.
|
|
1773
|
+
*/
|
|
1774
|
+
asChild?: boolean;
|
|
1775
|
+
/**
|
|
1776
|
+
* Custom children. When provided, control.component is NOT auto-rendered.
|
|
1777
|
+
* Accessible props are merged into the child via Slot.
|
|
1778
|
+
*/
|
|
1779
|
+
children?: ReactNode;
|
|
1780
|
+
}
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
_Source: src/components/form-field/types.ts_
|
|
1784
|
+
|
|
1785
|
+
### FormFieldDescription
|
|
1786
|
+
|
|
1787
|
+
**Kind:** `const`
|
|
1788
|
+
|
|
1789
|
+
FormField.Description - Helper text for the form field.
|
|
1790
|
+
|
|
1791
|
+
Renders with a stable `id` (descriptionId) that can be wired to
|
|
1792
|
+
`aria-describedby` on the control. To enable automatic wiring, pass
|
|
1793
|
+
`hasDescription` to the parent `FormField.Root`.
|
|
1794
|
+
|
|
1795
|
+
**Signature:**
|
|
1796
|
+
```typescript
|
|
1797
|
+
export const FormFieldDescription
|
|
1798
|
+
```
|
|
1799
|
+
|
|
1800
|
+
**Examples:**
|
|
1801
|
+
|
|
1802
|
+
```tsx
|
|
1803
|
+
<FormField.Root control={control.email} hasDescription>
|
|
1804
|
+
<FormField.Label />
|
|
1805
|
+
<FormField.Control />
|
|
1806
|
+
<FormField.Description className="text-xs text-gray-500">
|
|
1807
|
+
We'll never share your email with anyone.
|
|
1808
|
+
</FormField.Description>
|
|
1809
|
+
<FormField.Error />
|
|
1810
|
+
</FormField.Root>
|
|
1811
|
+
```
|
|
1812
|
+
|
|
1813
|
+
With custom element (asChild)
|
|
1814
|
+
```tsx
|
|
1815
|
+
<FormField.Description asChild>
|
|
1816
|
+
<Tooltip content="More info">
|
|
1817
|
+
<InfoIcon />
|
|
1818
|
+
</Tooltip>
|
|
1819
|
+
</FormField.Description>
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
_Source: src/components/form-field/FormFieldDescription.tsx_
|
|
1823
|
+
|
|
1824
|
+
### FormFieldDescriptionProps
|
|
1825
|
+
|
|
1826
|
+
**Kind:** `interface`
|
|
1827
|
+
|
|
1828
|
+
Props for FormField.Description
|
|
1829
|
+
|
|
1830
|
+
**Signature:**
|
|
1831
|
+
```typescript
|
|
1832
|
+
export interface FormFieldDescriptionProps extends Omit<
|
|
1833
|
+
HTMLAttributes<HTMLParagraphElement>,
|
|
1834
|
+
'id'
|
|
1835
|
+
> {
|
|
1836
|
+
asChild?: boolean;
|
|
1837
|
+
children: ReactNode;
|
|
1838
|
+
}
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
_Source: src/components/form-field/types.ts_
|
|
1842
|
+
|
|
1843
|
+
### FormFieldError
|
|
1844
|
+
|
|
1845
|
+
**Kind:** `const`
|
|
1846
|
+
|
|
1847
|
+
FormField.Error - Displays validation error message(s).
|
|
1848
|
+
|
|
1849
|
+
Renders nothing when `shouldShowError` is false (field not touched or no errors).
|
|
1850
|
+
The first error paragraph receives `id={ids.errorId}` for `aria-errormessage` wiring.
|
|
1851
|
+
|
|
1852
|
+
**Signature:**
|
|
1853
|
+
```typescript
|
|
1854
|
+
export const FormFieldError
|
|
1855
|
+
```
|
|
1856
|
+
|
|
1857
|
+
**Examples:**
|
|
1858
|
+
|
|
1859
|
+
Single error (default)
|
|
1860
|
+
```tsx
|
|
1861
|
+
<FormField.Error className="text-xs text-red-600" />
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
All errors
|
|
1865
|
+
```tsx
|
|
1866
|
+
<FormField.Error multi className="text-xs text-red-600" />
|
|
1867
|
+
```
|
|
1868
|
+
|
|
1869
|
+
Custom render per error
|
|
1870
|
+
```tsx
|
|
1871
|
+
<FormField.Error
|
|
1872
|
+
render={(err) => (
|
|
1873
|
+
<span className={err.severity === 'warning' ? 'text-yellow-600' : 'text-red-600'}>
|
|
1874
|
+
{err.message}
|
|
1875
|
+
</span>
|
|
1876
|
+
)}
|
|
1877
|
+
/>
|
|
1878
|
+
```
|
|
1879
|
+
|
|
1880
|
+
Custom error content
|
|
1881
|
+
```tsx
|
|
1882
|
+
<FormField.Error className="text-xs">
|
|
1883
|
+
This field is required
|
|
1884
|
+
</FormField.Error>
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
_Source: src/components/form-field/FormFieldError.tsx_
|
|
1888
|
+
|
|
1889
|
+
### FormFieldErrorProps
|
|
1890
|
+
|
|
1891
|
+
**Kind:** `interface`
|
|
1892
|
+
|
|
1893
|
+
Props for FormField.Error
|
|
1894
|
+
|
|
1895
|
+
**Signature:**
|
|
1896
|
+
```typescript
|
|
1897
|
+
export interface FormFieldErrorProps extends Omit<
|
|
1898
|
+
HTMLAttributes<HTMLParagraphElement>,
|
|
1899
|
+
'id' | 'role'
|
|
1900
|
+
> {
|
|
1901
|
+
asChild?: boolean;
|
|
1902
|
+
/**
|
|
1903
|
+
* When true, renders all errors instead of only the first.
|
|
1904
|
+
* @default false
|
|
1905
|
+
*/
|
|
1906
|
+
multi?: boolean;
|
|
1907
|
+
/**
|
|
1908
|
+
* Custom render function per error. When provided, multi is implied true.
|
|
1909
|
+
*/
|
|
1910
|
+
render?: (error: ValidationError, index: number) => ReactNode;
|
|
1911
|
+
/**
|
|
1912
|
+
* Override error content. Defaults to errors[0].message.
|
|
1913
|
+
*/
|
|
1914
|
+
children?: ReactNode;
|
|
1915
|
+
}
|
|
1916
|
+
```
|
|
1917
|
+
|
|
1918
|
+
_Source: src/components/form-field/types.ts_
|
|
1919
|
+
|
|
1920
|
+
### FormFieldIds
|
|
1921
|
+
|
|
1922
|
+
**Kind:** `interface`
|
|
1923
|
+
|
|
1924
|
+
Stable IDs for all accessible elements of a form field
|
|
1925
|
+
|
|
1926
|
+
**Signature:**
|
|
1927
|
+
```typescript
|
|
1928
|
+
export interface FormFieldIds {
|
|
1929
|
+
/** ID placed on the interactive control element (<input>, etc.) */
|
|
1930
|
+
controlId: string;
|
|
1931
|
+
/** ID placed on the <label> element */
|
|
1932
|
+
labelId: string;
|
|
1933
|
+
/** ID placed on the description paragraph */
|
|
1934
|
+
descriptionId: string;
|
|
1935
|
+
/** ID placed on the first error paragraph */
|
|
1936
|
+
errorId: string;
|
|
1937
|
+
}
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
_Source: src/components/form-field/types.ts_
|
|
1941
|
+
|
|
1942
|
+
### FormFieldLabel
|
|
1943
|
+
|
|
1944
|
+
**Kind:** `const`
|
|
1945
|
+
|
|
1946
|
+
FormField.Label - Accessible label for the form field.
|
|
1947
|
+
|
|
1948
|
+
Automatically wires `htmlFor` to the control ID and `id` to the label ID
|
|
1949
|
+
so that `aria-labelledby` on FormField.Control works correctly.
|
|
1950
|
+
|
|
1951
|
+
The label text defaults to `componentProps.label` from the field config.
|
|
1952
|
+
Pass `children` to override or enrich the label content.
|
|
1953
|
+
|
|
1954
|
+
A required indicator `*` is appended automatically when `componentProps.required` is set.
|
|
1955
|
+
|
|
1956
|
+
**Signature:**
|
|
1957
|
+
```typescript
|
|
1958
|
+
export const FormFieldLabel
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
**Examples:**
|
|
1962
|
+
|
|
1963
|
+
Auto label from field config
|
|
1964
|
+
```tsx
|
|
1965
|
+
<FormField.Root control={control.email}>
|
|
1966
|
+
<FormField.Label /> {/* renders componentProps.label *\/}
|
|
1967
|
+
<FormField.Control />
|
|
1968
|
+
</FormField.Root>
|
|
1969
|
+
```
|
|
1970
|
+
|
|
1971
|
+
Custom label content
|
|
1972
|
+
```tsx
|
|
1973
|
+
<FormField.Label className="font-semibold">
|
|
1974
|
+
Email Address <span className="text-gray-400">(optional)</span>
|
|
1975
|
+
</FormField.Label>
|
|
1976
|
+
```
|
|
1977
|
+
|
|
1978
|
+
With custom element (asChild)
|
|
1979
|
+
```tsx
|
|
1980
|
+
<FormField.Label asChild>
|
|
1981
|
+
<Typography variant="label">{label}</Typography>
|
|
1982
|
+
</FormField.Label>
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
_Source: src/components/form-field/FormFieldLabel.tsx_
|
|
1986
|
+
|
|
1987
|
+
### FormFieldLabelProps
|
|
1988
|
+
|
|
1989
|
+
**Kind:** `interface`
|
|
1990
|
+
|
|
1991
|
+
Props for FormField.Label
|
|
1992
|
+
|
|
1993
|
+
**Signature:**
|
|
1994
|
+
```typescript
|
|
1995
|
+
export interface FormFieldLabelProps extends Omit<
|
|
1996
|
+
LabelHTMLAttributes<HTMLLabelElement>,
|
|
1997
|
+
'htmlFor'
|
|
1998
|
+
> {
|
|
1999
|
+
/** Render as child element via Slot (merges accessible props into the child) */
|
|
2000
|
+
asChild?: boolean;
|
|
2001
|
+
/**
|
|
2002
|
+
* Override label text. Defaults to componentProps.label.
|
|
2003
|
+
* Pass children explicitly when you need custom content inside the label.
|
|
2004
|
+
*/
|
|
2005
|
+
children?: ReactNode;
|
|
2006
|
+
/**
|
|
2007
|
+
* If true, always renders even when no label text is available.
|
|
2008
|
+
* Useful when the consumer provides children.
|
|
2009
|
+
* @default false
|
|
2010
|
+
*/
|
|
2011
|
+
forceRender?: boolean;
|
|
2012
|
+
}
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
_Source: src/components/form-field/types.ts_
|
|
2016
|
+
|
|
2017
|
+
### FormFieldRoot
|
|
2018
|
+
|
|
2019
|
+
**Kind:** `function`
|
|
2020
|
+
|
|
2021
|
+
FormField.Root - Context provider for form field compound component.
|
|
2022
|
+
|
|
2023
|
+
Computes stable accessible IDs (controlId, labelId, descriptionId, errorId)
|
|
2024
|
+
and provides all field state to child FormField.* components.
|
|
2025
|
+
|
|
2026
|
+
**Signature:**
|
|
2027
|
+
```typescript
|
|
2028
|
+
function FormFieldRoot<T extends FormValue>({
|
|
2029
|
+
control,
|
|
2030
|
+
children,
|
|
2031
|
+
id,
|
|
2032
|
+
hasDescription = false,
|
|
2033
|
+
}: FormFieldRootProps<T>)
|
|
2034
|
+
```
|
|
2035
|
+
|
|
2036
|
+
**Examples:**
|
|
2037
|
+
|
|
2038
|
+
Minimal usage
|
|
2039
|
+
```tsx
|
|
2040
|
+
<FormField.Root control={control.email}>
|
|
2041
|
+
<FormField.Label />
|
|
2042
|
+
<FormField.Control />
|
|
2043
|
+
<FormField.Error />
|
|
2044
|
+
</FormField.Root>
|
|
2045
|
+
```
|
|
2046
|
+
|
|
2047
|
+
With description (pass hasDescription to auto-wire aria-describedby)
|
|
2048
|
+
```tsx
|
|
2049
|
+
<FormField.Root control={control.email} hasDescription>
|
|
2050
|
+
<FormField.Label />
|
|
2051
|
+
<FormField.Control />
|
|
2052
|
+
<FormField.Description>Helper text</FormField.Description>
|
|
2053
|
+
<FormField.Error />
|
|
2054
|
+
</FormField.Root>
|
|
2055
|
+
```
|
|
2056
|
+
|
|
2057
|
+
_Source: src/components/form-field/FormFieldRoot.tsx_
|
|
2058
|
+
|
|
2059
|
+
### FormFieldRootProps
|
|
2060
|
+
|
|
2061
|
+
**Kind:** `interface`
|
|
2062
|
+
|
|
2063
|
+
Props for FormField.Root
|
|
2064
|
+
|
|
2065
|
+
**Signature:**
|
|
2066
|
+
```typescript
|
|
2067
|
+
export interface FormFieldRootProps<T extends FormValue = FormValue> {
|
|
2068
|
+
/** The FieldNode control from the form */
|
|
2069
|
+
control: FieldNode<T>;
|
|
2070
|
+
children: ReactNode;
|
|
2071
|
+
/** Explicit id prefix; if omitted, useId() is used */
|
|
2072
|
+
id?: string;
|
|
2073
|
+
/**
|
|
2074
|
+
* Set to true when the field has a description element so that
|
|
2075
|
+
* FormField.Control automatically wires aria-describedby.
|
|
2076
|
+
* Avoids the double-render caused by dynamic description registration.
|
|
2077
|
+
* @default false
|
|
2078
|
+
*/
|
|
2079
|
+
hasDescription?: boolean;
|
|
2080
|
+
}
|
|
2081
|
+
```
|
|
2082
|
+
|
|
2083
|
+
_Source: src/components/form-field/types.ts_
|
|
2084
|
+
|
|
2085
|
+
### FormWizard
|
|
2086
|
+
|
|
2087
|
+
**Kind:** `const`
|
|
2088
|
+
|
|
2089
|
+
Headless multi-step wizard. Compound-компонент: `FormWizard` + `FormWizard.Step`,
|
|
2090
|
+
`FormWizard.Actions`, `FormWizard.Indicator`, `FormWizard.Progress`,
|
|
2091
|
+
`FormWizard.Prev`, `FormWizard.Next`, `FormWizard.Submit`.
|
|
2092
|
+
|
|
2093
|
+
**Signature:**
|
|
2094
|
+
```typescript
|
|
2095
|
+
export const FormWizard
|
|
2096
|
+
```
|
|
2097
|
+
|
|
2098
|
+
**Examples:**
|
|
2099
|
+
|
|
2100
|
+
```tsx
|
|
2101
|
+
import { FormWizard } from '@reformer/cdk/form-wizard';
|
|
2102
|
+
|
|
2103
|
+
<FormWizard form={form} steps={[{ name: 'profile' }, { name: 'review' }]}>
|
|
2104
|
+
<FormWizard.Step name="profile"><ProfileFields /></FormWizard.Step>
|
|
2105
|
+
<FormWizard.Step name="review"><Review /></FormWizard.Step>
|
|
2106
|
+
<FormWizard.Actions>
|
|
2107
|
+
{({ prev, next, submit, isLastStep }) => (
|
|
2108
|
+
<div>
|
|
2109
|
+
<button {...prev}>Prev</button>
|
|
2110
|
+
{isLastStep ? <button {...submit}>Submit</button> : <button {...next}>Next</button>}
|
|
2111
|
+
</div>
|
|
2112
|
+
)}
|
|
2113
|
+
</FormWizard.Actions>
|
|
2114
|
+
</FormWizard>
|
|
2115
|
+
```
|
|
2116
|
+
|
|
2117
|
+
**See also:**
|
|
2118
|
+
- [docs/llms/03-form-navigation.md](../../../docs/llms/03-form-navigation.md)
|
|
2119
|
+
|
|
2120
|
+
_Source: src/components/form-wizard/FormWizard.tsx_
|
|
2121
|
+
|
|
2122
|
+
### FormWizardActions
|
|
2123
|
+
|
|
2124
|
+
**Kind:** `function`
|
|
2125
|
+
|
|
2126
|
+
`FormWizard.Actions` — контейнер кнопок навигации мастера. Поддерживает два режима:
|
|
2127
|
+
compound-children (`FormWizard.Prev`/`Next`/`Submit`) и render-props.
|
|
2128
|
+
|
|
2129
|
+
Render-props получают: `prev`, `next`, `submit` (все с `onClick`/`disabled`),
|
|
2130
|
+
`isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`.
|
|
2131
|
+
|
|
2132
|
+
**Signature:**
|
|
2133
|
+
```typescript
|
|
2134
|
+
export function FormWizardActions({
|
|
2135
|
+
onSubmit,
|
|
2136
|
+
children,
|
|
2137
|
+
className,
|
|
2138
|
+
style,
|
|
2139
|
+
}: FormWizardActionsProps)
|
|
2140
|
+
```
|
|
2141
|
+
|
|
2142
|
+
**Examples:**
|
|
2143
|
+
|
|
2144
|
+
Compound mode
|
|
2145
|
+
```tsx
|
|
2146
|
+
<FormWizard.Actions onSubmit={handleSubmit}>
|
|
2147
|
+
<FormWizard.Prev>Back</FormWizard.Prev>
|
|
2148
|
+
<FormWizard.Next>Next</FormWizard.Next>
|
|
2149
|
+
<FormWizard.Submit loadingText="Submitting...">Submit</FormWizard.Submit>
|
|
2150
|
+
</FormWizard.Actions>
|
|
2151
|
+
```
|
|
2152
|
+
|
|
2153
|
+
Render-props mode
|
|
2154
|
+
```tsx
|
|
2155
|
+
<FormWizard.Actions onSubmit={handleSubmit}>
|
|
2156
|
+
{({ prev, next, submit, isFirstStep, isLastStep }) => (
|
|
2157
|
+
<div className="flex justify-between">
|
|
2158
|
+
{!isFirstStep && <button onClick={prev.onClick} disabled={prev.disabled}>Back</button>}
|
|
2159
|
+
{isLastStep
|
|
2160
|
+
? <button onClick={submit.onClick} disabled={submit.disabled}>{submit.isSubmitting ? '…' : 'Submit'}</button>
|
|
2161
|
+
: <button onClick={next.onClick} disabled={next.disabled}>Next</button>}
|
|
2162
|
+
</div>
|
|
2163
|
+
)}
|
|
2164
|
+
</FormWizard.Actions>
|
|
2165
|
+
```
|
|
2166
|
+
|
|
2167
|
+
_Source: src/components/form-wizard/FormWizardActions.tsx_
|
|
2168
|
+
|
|
2169
|
+
### FormWizardActionsProps
|
|
2170
|
+
|
|
2171
|
+
**Kind:** `interface`
|
|
2172
|
+
|
|
2173
|
+
Props for FormWizard.Actions component
|
|
2174
|
+
|
|
2175
|
+
**Signature:**
|
|
2176
|
+
```typescript
|
|
2177
|
+
export interface FormWizardActionsProps {
|
|
2178
|
+
/** Submit handler (called on last step) */
|
|
2179
|
+
onSubmit?: () => void | Promise<void>;
|
|
2180
|
+
/** Children: render function (headless) or ReactNode (compound components) */
|
|
2181
|
+
children: ReactNode | RenderFunction;
|
|
2182
|
+
/** Optional className for wrapper (compound mode only) */
|
|
2183
|
+
className?: string;
|
|
2184
|
+
/** Optional style for wrapper (compound mode only) */
|
|
2185
|
+
style?: CSSProperties;
|
|
2186
|
+
}
|
|
2187
|
+
```
|
|
2188
|
+
|
|
2189
|
+
_Source: src/components/form-wizard/FormWizardActions.tsx_
|
|
2190
|
+
|
|
2191
|
+
### FormWizardActionsRenderProps
|
|
2192
|
+
|
|
2193
|
+
**Kind:** `interface`
|
|
2194
|
+
|
|
2195
|
+
Render props passed to children function
|
|
2196
|
+
|
|
2197
|
+
**Signature:**
|
|
2198
|
+
```typescript
|
|
2199
|
+
export interface FormWizardActionsRenderProps {
|
|
2200
|
+
/** Props for the "Previous" button */
|
|
2201
|
+
prev: FormWizardButtonProps;
|
|
2202
|
+
/** Props for the "Next" button */
|
|
2203
|
+
next: FormWizardButtonProps;
|
|
2204
|
+
/** Props for the "Submit" button */
|
|
2205
|
+
submit: FormWizardSubmitProps;
|
|
2206
|
+
/** Whether current step is the first step */
|
|
2207
|
+
isFirstStep: boolean;
|
|
2208
|
+
/** Whether current step is the last step */
|
|
2209
|
+
isLastStep: boolean;
|
|
2210
|
+
/** Whether validation is in progress */
|
|
2211
|
+
isValidating: boolean;
|
|
2212
|
+
/** Whether form is submitting */
|
|
2213
|
+
isSubmitting: boolean;
|
|
2214
|
+
}
|
|
2215
|
+
```
|
|
2216
|
+
|
|
2217
|
+
_Source: src/components/form-wizard/FormWizardActions.tsx_
|
|
2218
|
+
|
|
2219
|
+
### FormWizardButtonProps
|
|
2220
|
+
|
|
2221
|
+
**Kind:** `interface`
|
|
2222
|
+
|
|
2223
|
+
Props for a navigation button (prev/next)
|
|
2224
|
+
|
|
2225
|
+
**Signature:**
|
|
2226
|
+
```typescript
|
|
2227
|
+
export interface FormWizardButtonProps {
|
|
2228
|
+
/** Click handler */
|
|
2229
|
+
onClick: () => void;
|
|
2230
|
+
/** Whether the button is disabled */
|
|
2231
|
+
disabled: boolean;
|
|
2232
|
+
}
|
|
2233
|
+
```
|
|
2234
|
+
|
|
2235
|
+
_Source: src/components/form-wizard/FormWizardActions.tsx_
|
|
2236
|
+
|
|
2237
|
+
### FormWizardConfig
|
|
2238
|
+
|
|
2239
|
+
**Kind:** `interface`
|
|
2240
|
+
|
|
2241
|
+
Configuration for multi-step form navigation
|
|
2242
|
+
Note: totalSteps is inferred from children count
|
|
2243
|
+
|
|
2244
|
+
**Signature:**
|
|
2245
|
+
```typescript
|
|
2246
|
+
export interface FormWizardConfig<T extends Record<string, any>> {
|
|
2247
|
+
/** Validation schemas per step (1-based indexing) */
|
|
2248
|
+
stepValidations: Record<number, ValidationSchemaFn<T>>;
|
|
2249
|
+
|
|
2250
|
+
/** Full validation schema for submit */
|
|
2251
|
+
fullValidation: ValidationSchemaFn<T>;
|
|
2252
|
+
}
|
|
2253
|
+
```
|
|
2254
|
+
|
|
2255
|
+
_Source: src/components/form-wizard/types.ts_
|
|
2256
|
+
|
|
2257
|
+
### FormWizardContext
|
|
2258
|
+
|
|
2259
|
+
**Kind:** `const`
|
|
2260
|
+
|
|
2261
|
+
React context, который снабжает дочерние компоненты `FormWizard` (Step,
|
|
2262
|
+
Actions, Indicator, Progress) текущим состоянием мастера. Создаётся
|
|
2263
|
+
`FormWizard`. Читать через `useFormWizard()`.
|
|
2264
|
+
|
|
2265
|
+
**Signature:**
|
|
2266
|
+
```typescript
|
|
2267
|
+
export const FormWizardContext
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
**Examples:**
|
|
2271
|
+
|
|
2272
|
+
```tsx
|
|
2273
|
+
import { FormWizardContext } from '@reformer/cdk/form-wizard';
|
|
2274
|
+
|
|
2275
|
+
function CurrentStep() {
|
|
2276
|
+
const ctx = useContext(FormWizardContext);
|
|
2277
|
+
return <span>step {ctx?.currentStep}</span>;
|
|
2278
|
+
}
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
_Source: src/components/form-wizard/FormWizardContext.tsx_
|
|
2282
|
+
|
|
2283
|
+
### FormWizardContextValue
|
|
2284
|
+
|
|
2285
|
+
**Kind:** `interface`
|
|
2286
|
+
|
|
2287
|
+
Context value for FormWizard
|
|
2288
|
+
Shares navigation state and methods with child components
|
|
2289
|
+
|
|
2290
|
+
**Signature:**
|
|
2291
|
+
```typescript
|
|
2292
|
+
export interface FormWizardContextValue<T extends Record<string, any>> {
|
|
2293
|
+
// ============================================================================
|
|
2294
|
+
// State
|
|
2295
|
+
// ============================================================================
|
|
2296
|
+
|
|
2297
|
+
/** Current step (1-based) */
|
|
2298
|
+
currentStep: number;
|
|
2299
|
+
|
|
2300
|
+
/** Total number of steps */
|
|
2301
|
+
totalSteps: number;
|
|
2302
|
+
|
|
2303
|
+
/** Completed steps */
|
|
2304
|
+
completedSteps: number[];
|
|
2305
|
+
|
|
2306
|
+
/** Is this the first step */
|
|
2307
|
+
isFirstStep: boolean;
|
|
2308
|
+
|
|
2309
|
+
/** Is this the last step */
|
|
2310
|
+
isLastStep: boolean;
|
|
2311
|
+
|
|
2312
|
+
/** Is validation in progress */
|
|
2313
|
+
isValidating: boolean;
|
|
2314
|
+
|
|
2315
|
+
/** Is form submitting */
|
|
2316
|
+
isSubmitting: boolean;
|
|
2317
|
+
|
|
2318
|
+
/** Form instance */
|
|
2319
|
+
form: FormProxy<T>;
|
|
2320
|
+
|
|
2321
|
+
// ============================================================================
|
|
2322
|
+
// Navigation Methods
|
|
2323
|
+
// ============================================================================
|
|
2324
|
+
|
|
2325
|
+
/** Go to next step (with validation) */
|
|
2326
|
+
goToNextStep: () => Promise<boolean>;
|
|
2327
|
+
|
|
2328
|
+
/** Go to previous step */
|
|
2329
|
+
goToPreviousStep: () => void;
|
|
2330
|
+
|
|
2331
|
+
/** Go to specific step */
|
|
2332
|
+
goToStep: (step: number) => boolean;
|
|
2333
|
+
}
|
|
2334
|
+
```
|
|
2335
|
+
|
|
2336
|
+
_Source: src/components/form-wizard/FormWizardContext.tsx_
|
|
2337
|
+
|
|
2338
|
+
### FormWizardHandle
|
|
2339
|
+
|
|
2340
|
+
**Kind:** `interface`
|
|
2341
|
+
|
|
2342
|
+
Handle для внешнего управления {@link FormWizard} через `useRef`.
|
|
2343
|
+
|
|
2344
|
+
Используется, когда submit/навигация инициируется снаружи дерева Wizard:
|
|
2345
|
+
шапка страницы, breadcrumbs, side-effect от API. Получают через
|
|
2346
|
+
`useRef<FormWizardHandle<T>>(null)` и передают в `<FormWizard ref={...}>`.
|
|
2347
|
+
|
|
2348
|
+
- `goToNextStep()` / `submit()` запускают валидацию текущего шага / всей
|
|
2349
|
+
формы соответственно.
|
|
2350
|
+
- `goToStep(n)` возвращает `false`, если предыдущий шаг не в `completedSteps`
|
|
2351
|
+
(защита от пропуска валидации) либо `n` вне диапазона `[1; totalSteps]`.
|
|
2352
|
+
- `submit()` возвращает `R | null`. `null` — форма не прошла `fullValidation`.
|
|
2353
|
+
|
|
2354
|
+
**Signature:**
|
|
2355
|
+
```typescript
|
|
2356
|
+
export interface FormWizardHandle<T extends Record<string, any>> {
|
|
2357
|
+
/** Form instance — используется в RenderBehaviorFn для доступа к форме через ref */
|
|
2358
|
+
form: FormProxy<T>;
|
|
2359
|
+
|
|
2360
|
+
/** Current step (1-based) */
|
|
2361
|
+
currentStep: number;
|
|
2362
|
+
|
|
2363
|
+
/** Completed steps */
|
|
2364
|
+
completedSteps: number[];
|
|
2365
|
+
|
|
2366
|
+
/** Validate current step */
|
|
2367
|
+
validateCurrentStep: () => Promise<boolean>;
|
|
2368
|
+
|
|
2369
|
+
/** Go to next step (with validation) */
|
|
2370
|
+
goToNextStep: () => Promise<boolean>;
|
|
2371
|
+
|
|
2372
|
+
/** Go to previous step */
|
|
2373
|
+
goToPreviousStep: () => void;
|
|
2374
|
+
|
|
2375
|
+
/** Go to specific step */
|
|
2376
|
+
goToStep: (step: number) => boolean;
|
|
2377
|
+
|
|
2378
|
+
/** Submit form (with full validation) */
|
|
2379
|
+
submit: <R>(onSubmit: (values: T) => Promise<R> | R) => Promise<R | null>;
|
|
2380
|
+
|
|
2381
|
+
/** Is this the first step */
|
|
2382
|
+
isFirstStep: boolean;
|
|
2383
|
+
|
|
2384
|
+
/** Is this the last step */
|
|
2385
|
+
isLastStep: boolean;
|
|
2386
|
+
|
|
2387
|
+
/** Is validation in progress */
|
|
2388
|
+
isValidating: boolean;
|
|
2389
|
+
}
|
|
2390
|
+
```
|
|
2391
|
+
|
|
2392
|
+
**Examples:**
|
|
2393
|
+
|
|
2394
|
+
«Сохранить и выйти» поверх wizard
|
|
2395
|
+
```tsx
|
|
2396
|
+
import { useRef } from 'react';
|
|
2397
|
+
import { FormWizard, type FormWizardHandle } from '@reformer/cdk/form-wizard';
|
|
2398
|
+
|
|
2399
|
+
function Page({ form, config }: Props) {
|
|
2400
|
+
const navRef = useRef<FormWizardHandle<CreditApplication>>(null);
|
|
2401
|
+
|
|
2402
|
+
const handleSaveAndExit = async () => {
|
|
2403
|
+
const saved = await navRef.current?.submit((values) => api.saveDraft(values));
|
|
2404
|
+
if (saved) router.push('/dashboard');
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
return (
|
|
2408
|
+
<>
|
|
2409
|
+
<header>
|
|
2410
|
+
<button onClick={handleSaveAndExit}>Сохранить и выйти</button>
|
|
2411
|
+
</header>
|
|
2412
|
+
<FormWizard ref={navRef} form={form} config={config}>
|
|
2413
|
+
<FormWizard.Step component={Step1} control={form} />
|
|
2414
|
+
<FormWizard.Step component={Step2} control={form} />
|
|
2415
|
+
</FormWizard>
|
|
2416
|
+
</>
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
```
|
|
2420
|
+
|
|
2421
|
+
Программный переход на шаг с проверкой доступности
|
|
2422
|
+
```tsx
|
|
2423
|
+
const handleClickContacts = () => {
|
|
2424
|
+
const ok = navRef.current?.goToStep(3);
|
|
2425
|
+
if (!ok) toast('Сначала заполните предыдущие шаги');
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
// Или с явной валидацией текущего шага:
|
|
2429
|
+
const moveOn = async () => {
|
|
2430
|
+
const valid = await navRef.current?.validateCurrentStep();
|
|
2431
|
+
if (!valid) return;
|
|
2432
|
+
await navRef.current?.goToNextStep();
|
|
2433
|
+
};
|
|
2434
|
+
```
|
|
2435
|
+
|
|
2436
|
+
_Source: src/components/form-wizard/types.ts_
|
|
2437
|
+
|
|
2438
|
+
### FormWizardIndicator
|
|
2439
|
+
|
|
2440
|
+
**Kind:** `function`
|
|
2441
|
+
|
|
2442
|
+
FormWizard.Indicator - Headless component for step indicator
|
|
2443
|
+
|
|
2444
|
+
Provides step data with state for building custom step indicators.
|
|
2445
|
+
No default UI - you build exactly what you need.
|
|
2446
|
+
|
|
2447
|
+
#### Render Props
|
|
2448
|
+
- `steps` - array of steps with state (`isCurrent`, `isCompleted`, `canNavigate`)
|
|
2449
|
+
- `goToStep` - function to navigate to a step
|
|
2450
|
+
- `currentStep` - current step number
|
|
2451
|
+
- `totalSteps` - total number of steps
|
|
2452
|
+
- `completedSteps` - array of completed step numbers
|
|
2453
|
+
|
|
2454
|
+
**Signature:**
|
|
2455
|
+
```typescript
|
|
2456
|
+
export function FormWizardIndicator({ steps, children }: FormWizardIndicatorProps)
|
|
2457
|
+
```
|
|
2458
|
+
|
|
2459
|
+
**Examples:**
|
|
2460
|
+
|
|
2461
|
+
Basic stepper
|
|
2462
|
+
```tsx
|
|
2463
|
+
<FormWizard.Indicator steps={STEPS}>
|
|
2464
|
+
{({ steps, goToStep }) => (
|
|
2465
|
+
<nav className="flex gap-2">
|
|
2466
|
+
{steps.map((step) => (
|
|
2467
|
+
<button
|
|
2468
|
+
key={step.number}
|
|
2469
|
+
onClick={() => goToStep(step.number)}
|
|
2470
|
+
disabled={!step.canNavigate}
|
|
2471
|
+
className={cn(
|
|
2472
|
+
'px-4 py-2 rounded',
|
|
2473
|
+
step.isCurrent && 'bg-blue-500 text-white',
|
|
2474
|
+
step.isCompleted && 'bg-green-100',
|
|
2475
|
+
!step.canNavigate && 'opacity-50 cursor-not-allowed'
|
|
2476
|
+
)}
|
|
2477
|
+
>
|
|
2478
|
+
{step.icon} {step.title}
|
|
2479
|
+
</button>
|
|
2480
|
+
))}
|
|
2481
|
+
</nav>
|
|
2482
|
+
)}
|
|
2483
|
+
</FormWizard.Indicator>
|
|
2484
|
+
```
|
|
2485
|
+
|
|
2486
|
+
With progress line
|
|
2487
|
+
```tsx
|
|
2488
|
+
<FormWizard.Indicator steps={STEPS}>
|
|
2489
|
+
{({ steps, goToStep }) => (
|
|
2490
|
+
<div className="flex items-center">
|
|
2491
|
+
{steps.map((step, index) => (
|
|
2492
|
+
<Fragment key={step.number}>
|
|
2493
|
+
<StepCircle
|
|
2494
|
+
active={step.isCurrent}
|
|
2495
|
+
completed={step.isCompleted}
|
|
2496
|
+
onClick={() => step.canNavigate && goToStep(step.number)}
|
|
2497
|
+
>
|
|
2498
|
+
{step.isCompleted ? '✓' : step.number}
|
|
2499
|
+
</StepCircle>
|
|
2500
|
+
{index < steps.length - 1 && (
|
|
2501
|
+
<StepLine completed={step.isCompleted} />
|
|
2502
|
+
)}
|
|
2503
|
+
</Fragment>
|
|
2504
|
+
))}
|
|
2505
|
+
</div>
|
|
2506
|
+
)}
|
|
2507
|
+
</FormWizard.Indicator>
|
|
2508
|
+
```
|
|
2509
|
+
|
|
2510
|
+
_Source: src/components/form-wizard/FormWizardIndicator.tsx_
|
|
2511
|
+
|
|
2512
|
+
### FormWizardIndicatorProps
|
|
2513
|
+
|
|
2514
|
+
**Kind:** `interface`
|
|
2515
|
+
|
|
2516
|
+
Props for FormWizard.Indicator component
|
|
2517
|
+
|
|
2518
|
+
**Signature:**
|
|
2519
|
+
```typescript
|
|
2520
|
+
export interface FormWizardIndicatorProps {
|
|
2521
|
+
/** Step definitions */
|
|
2522
|
+
steps: FormWizardIndicatorStep[];
|
|
2523
|
+
/** Render function for custom UI */
|
|
2524
|
+
children: (props: FormWizardIndicatorRenderProps) => ReactNode;
|
|
2525
|
+
}
|
|
2526
|
+
```
|
|
2527
|
+
|
|
2528
|
+
_Source: src/components/form-wizard/FormWizardIndicator.tsx_
|
|
2529
|
+
|
|
2530
|
+
### FormWizardIndicatorRenderProps
|
|
2531
|
+
|
|
2532
|
+
**Kind:** `interface`
|
|
2533
|
+
|
|
2534
|
+
Render props passed to children function
|
|
2535
|
+
|
|
2536
|
+
**Signature:**
|
|
2537
|
+
```typescript
|
|
2538
|
+
export interface FormWizardIndicatorRenderProps {
|
|
2539
|
+
/** Steps with their current state */
|
|
2540
|
+
steps: FormWizardIndicatorStepWithState[];
|
|
2541
|
+
/** Navigate to a specific step */
|
|
2542
|
+
goToStep: (step: number) => boolean;
|
|
2543
|
+
/** Current step number */
|
|
2544
|
+
currentStep: number;
|
|
2545
|
+
/** Total number of steps */
|
|
2546
|
+
totalSteps: number;
|
|
2547
|
+
/** Completed step numbers */
|
|
2548
|
+
completedSteps: number[];
|
|
2549
|
+
}
|
|
2550
|
+
```
|
|
2551
|
+
|
|
2552
|
+
_Source: src/components/form-wizard/FormWizardIndicator.tsx_
|
|
2553
|
+
|
|
2554
|
+
### FormWizardIndicatorStep
|
|
2555
|
+
|
|
2556
|
+
**Kind:** `interface`
|
|
2557
|
+
|
|
2558
|
+
Step definition for the indicator
|
|
2559
|
+
|
|
2560
|
+
**Signature:**
|
|
2561
|
+
```typescript
|
|
2562
|
+
export interface FormWizardIndicatorStep {
|
|
2563
|
+
/** Step number (1-based) */
|
|
2564
|
+
number: number;
|
|
2565
|
+
/** Step title/label */
|
|
2566
|
+
title: string;
|
|
2567
|
+
/** Optional icon */
|
|
2568
|
+
icon?: string;
|
|
2569
|
+
/** Component to render for this step, or a ReactNode (pre-rendered element) */
|
|
2570
|
+
component?:
|
|
2571
|
+
| ComponentType<
|
|
2572
|
+
{
|
|
2573
|
+
control: FormProxy<unknown>;
|
|
2574
|
+
} & Record<string, unknown>
|
|
2575
|
+
>
|
|
2576
|
+
| ReactNode;
|
|
2577
|
+
}
|
|
2578
|
+
```
|
|
2579
|
+
|
|
2580
|
+
_Source: src/components/form-wizard/FormWizardIndicator.tsx_
|
|
2581
|
+
|
|
2582
|
+
### FormWizardIndicatorStepWithState
|
|
2583
|
+
|
|
2584
|
+
**Kind:** `interface`
|
|
2585
|
+
|
|
2586
|
+
Enriched step with state information
|
|
2587
|
+
|
|
2588
|
+
**Signature:**
|
|
2589
|
+
```typescript
|
|
2590
|
+
export interface FormWizardIndicatorStepWithState extends FormWizardIndicatorStep {
|
|
2591
|
+
/** Whether this is the current step */
|
|
2592
|
+
isCurrent: boolean;
|
|
2593
|
+
/** Whether this step is completed */
|
|
2594
|
+
isCompleted: boolean;
|
|
2595
|
+
/** Whether user can navigate to this step */
|
|
2596
|
+
canNavigate: boolean;
|
|
2597
|
+
}
|
|
2598
|
+
```
|
|
2599
|
+
|
|
2600
|
+
_Source: src/components/form-wizard/FormWizardIndicator.tsx_
|
|
2601
|
+
|
|
2602
|
+
### FormWizardProgress
|
|
2603
|
+
|
|
2604
|
+
**Kind:** `function`
|
|
2605
|
+
|
|
2606
|
+
FormWizard.Progress - Headless component for progress display
|
|
2607
|
+
|
|
2608
|
+
Provides progress data for building custom progress indicators.
|
|
2609
|
+
No default UI - you build exactly what you need.
|
|
2610
|
+
|
|
2611
|
+
#### Render Props
|
|
2612
|
+
- `current` - current step number
|
|
2613
|
+
- `total` - total number of steps
|
|
2614
|
+
- `percent` - completion percentage (0-100)
|
|
2615
|
+
- `completedCount` - number of completed steps
|
|
2616
|
+
- `isFirstStep` - whether on first step
|
|
2617
|
+
- `isLastStep` - whether on last step
|
|
2618
|
+
|
|
2619
|
+
**Signature:**
|
|
2620
|
+
```typescript
|
|
2621
|
+
export function FormWizardProgress({ children }: FormWizardProgressProps)
|
|
2622
|
+
```
|
|
2623
|
+
|
|
2624
|
+
**Examples:**
|
|
2625
|
+
|
|
2626
|
+
Simple text progress
|
|
2627
|
+
```tsx
|
|
2628
|
+
<FormWizard.Progress>
|
|
2629
|
+
{({ current, total, percent }) => (
|
|
2630
|
+
<div className="text-sm text-gray-600">
|
|
2631
|
+
Step {current} of {total} ({percent}% complete)
|
|
2632
|
+
</div>
|
|
2633
|
+
)}
|
|
2634
|
+
</FormWizard.Progress>
|
|
2635
|
+
```
|
|
2636
|
+
|
|
2637
|
+
Progress bar
|
|
2638
|
+
```tsx
|
|
2639
|
+
<FormWizard.Progress>
|
|
2640
|
+
{({ percent, current, total }) => (
|
|
2641
|
+
<div className="space-y-2">
|
|
2642
|
+
<div className="flex justify-between text-sm">
|
|
2643
|
+
<span>Step {current}/{total}</span>
|
|
2644
|
+
<span>{percent}%</span>
|
|
2645
|
+
</div>
|
|
2646
|
+
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
2647
|
+
<div
|
|
2648
|
+
className="h-full bg-blue-500 transition-all"
|
|
2649
|
+
style={{ width: `${percent}%` }}
|
|
2650
|
+
/>
|
|
2651
|
+
</div>
|
|
2652
|
+
</div>
|
|
2653
|
+
)}
|
|
2654
|
+
</FormWizard.Progress>
|
|
2655
|
+
```
|
|
2656
|
+
|
|
2657
|
+
Circular progress
|
|
2658
|
+
```tsx
|
|
2659
|
+
<FormWizard.Progress>
|
|
2660
|
+
{({ percent }) => (
|
|
2661
|
+
<CircularProgress value={percent} />
|
|
2662
|
+
)}
|
|
2663
|
+
</FormWizard.Progress>
|
|
2664
|
+
```
|
|
2665
|
+
|
|
2666
|
+
_Source: src/components/form-wizard/FormWizardProgress.tsx_
|
|
2667
|
+
|
|
2668
|
+
### FormWizardProgressProps
|
|
2669
|
+
|
|
2670
|
+
**Kind:** `interface`
|
|
2671
|
+
|
|
2672
|
+
Props for FormWizard.Progress component
|
|
2673
|
+
|
|
2674
|
+
**Signature:**
|
|
2675
|
+
```typescript
|
|
2676
|
+
export interface FormWizardProgressProps {
|
|
2677
|
+
/** Render function for custom UI */
|
|
2678
|
+
children: (props: FormWizardProgressRenderProps) => ReactNode;
|
|
2679
|
+
}
|
|
2680
|
+
```
|
|
2681
|
+
|
|
2682
|
+
_Source: src/components/form-wizard/FormWizardProgress.tsx_
|
|
2683
|
+
|
|
2684
|
+
### FormWizardProgressRenderProps
|
|
2685
|
+
|
|
2686
|
+
**Kind:** `interface`
|
|
2687
|
+
|
|
2688
|
+
Render props passed to children function
|
|
2689
|
+
|
|
2690
|
+
**Signature:**
|
|
2691
|
+
```typescript
|
|
2692
|
+
export interface FormWizardProgressRenderProps {
|
|
2693
|
+
/** Current step number (1-based) */
|
|
2694
|
+
current: number;
|
|
2695
|
+
/** Total number of steps */
|
|
2696
|
+
total: number;
|
|
2697
|
+
/** Completion percentage (0-100) */
|
|
2698
|
+
percent: number;
|
|
2699
|
+
/** Number of completed steps */
|
|
2700
|
+
completedCount: number;
|
|
2701
|
+
/** Whether on first step */
|
|
2702
|
+
isFirstStep: boolean;
|
|
2703
|
+
/** Whether on last step */
|
|
2704
|
+
isLastStep: boolean;
|
|
2705
|
+
}
|
|
2706
|
+
```
|
|
2707
|
+
|
|
2708
|
+
_Source: src/components/form-wizard/FormWizardProgress.tsx_
|
|
2709
|
+
|
|
2710
|
+
### FormWizardProps
|
|
2711
|
+
|
|
2712
|
+
**Kind:** `interface`
|
|
2713
|
+
|
|
2714
|
+
Props for FormWizard component
|
|
2715
|
+
|
|
2716
|
+
**Signature:**
|
|
2717
|
+
```typescript
|
|
2718
|
+
export interface FormWizardProps<T extends Record<string, any>> {
|
|
2719
|
+
/** Form instance */
|
|
2720
|
+
form: FormProxy<T>;
|
|
2721
|
+
|
|
2722
|
+
/** Step configuration (validation schemas) */
|
|
2723
|
+
config: FormWizardConfig<T>;
|
|
2724
|
+
|
|
2725
|
+
/** Children (Step components, Indicator, Actions, Progress, or any ReactNode) */
|
|
2726
|
+
children?: ReactNode;
|
|
2727
|
+
|
|
2728
|
+
/** Callback when step changes */
|
|
2729
|
+
onStepChange?: (step: number) => void;
|
|
2730
|
+
|
|
2731
|
+
/** Scroll to top on step change */
|
|
2732
|
+
scrollToTop?: boolean;
|
|
2733
|
+
}
|
|
2734
|
+
```
|
|
2735
|
+
|
|
2736
|
+
_Source: src/components/form-wizard/types.ts_
|
|
2737
|
+
|
|
2738
|
+
### FormWizardStep
|
|
2739
|
+
|
|
2740
|
+
**Kind:** `function`
|
|
2741
|
+
|
|
2742
|
+
FormWizard.Step - renders a step component when it's the current step
|
|
2743
|
+
|
|
2744
|
+
**Signature:**
|
|
2745
|
+
```typescript
|
|
2746
|
+
export function FormWizardStep<T extends Record<string, any>>({
|
|
2747
|
+
component: Component,
|
|
2748
|
+
control,
|
|
2749
|
+
children,
|
|
2750
|
+
_stepIndex,
|
|
2751
|
+
...restProps
|
|
2752
|
+
}: FormWizardStepInternalProps<T>)
|
|
2753
|
+
```
|
|
2754
|
+
|
|
2755
|
+
**Examples:**
|
|
2756
|
+
|
|
2757
|
+
Component-based (legacy)
|
|
2758
|
+
```tsx
|
|
2759
|
+
<FormWizard ref={navRef} form={form} config={config}>
|
|
2760
|
+
<FormWizard.Step component={Step1} control={form} />
|
|
2761
|
+
<FormWizard.Step component={Step2} control={form} extraProp="value" />
|
|
2762
|
+
</FormWizard>
|
|
2763
|
+
```
|
|
2764
|
+
|
|
2765
|
+
Children-based (new)
|
|
2766
|
+
```tsx
|
|
2767
|
+
<FormWizard form={form} config={config}>
|
|
2768
|
+
<FormWizard.Step>
|
|
2769
|
+
<RenderNodeComponent node={step1Content} ... />
|
|
2770
|
+
</FormWizard.Step>
|
|
2771
|
+
<FormWizard.Step>
|
|
2772
|
+
<RenderNodeComponent node={step2Content} ... />
|
|
2773
|
+
</FormWizard.Step>
|
|
2774
|
+
</FormWizard>
|
|
2775
|
+
```
|
|
2776
|
+
|
|
2777
|
+
_Source: src/components/form-wizard/FormWizardStep.tsx_
|
|
2778
|
+
|
|
2779
|
+
### FormWizardStepProps
|
|
2780
|
+
|
|
2781
|
+
**Kind:** `interface`
|
|
2782
|
+
|
|
2783
|
+
Props for FormWizard.Step component
|
|
2784
|
+
|
|
2785
|
+
Supports two usage patterns:
|
|
2786
|
+
1. Component-based: `<FormWizard.Step component={Step1} control={form} />`
|
|
2787
|
+
2. Children-based: `<FormWizard.Step>{children}</FormWizard.Step>`
|
|
2788
|
+
|
|
2789
|
+
**Signature:**
|
|
2790
|
+
```typescript
|
|
2791
|
+
export interface FormWizardStepProps<T extends Record<string, any>> {
|
|
2792
|
+
/** Component to render for this step (legacy API) */
|
|
2793
|
+
component?: ComponentType<{ control: FormProxy<T> } & Record<string, unknown>>;
|
|
2794
|
+
|
|
2795
|
+
/** Form control to pass to the component (legacy API) */
|
|
2796
|
+
control?: FormProxy<T>;
|
|
2797
|
+
|
|
2798
|
+
/** Children to render (new API - for use with selector-based wizard) */
|
|
2799
|
+
children?: ReactNode;
|
|
2800
|
+
|
|
2801
|
+
/** Any additional props to pass to the component */
|
|
2802
|
+
[key: string]: unknown;
|
|
2803
|
+
}
|
|
2804
|
+
```
|
|
2805
|
+
|
|
2806
|
+
_Source: src/components/form-wizard/FormWizardStep.tsx_
|
|
2807
|
+
|
|
2808
|
+
### FormWizardSubmitProps
|
|
2809
|
+
|
|
2810
|
+
**Kind:** `interface`
|
|
2811
|
+
|
|
2812
|
+
Props for FormWizard.Submit component
|
|
2813
|
+
|
|
2814
|
+
**Signature:**
|
|
2815
|
+
```typescript
|
|
2816
|
+
export interface FormWizardSubmitProps extends Omit<
|
|
2817
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
2818
|
+
'onClick'
|
|
2819
|
+
> {
|
|
2820
|
+
/** Button content */
|
|
2821
|
+
children: ReactNode;
|
|
2822
|
+
/** Render as child element (merge props into child) */
|
|
2823
|
+
asChild?: boolean;
|
|
2824
|
+
/** Additional disabled state (merged with automatic via OR) */
|
|
2825
|
+
disabled?: boolean;
|
|
2826
|
+
/** Content to show during submission (replaces children) */
|
|
2827
|
+
loadingText?: ReactNode;
|
|
2828
|
+
}
|
|
2829
|
+
```
|
|
2830
|
+
|
|
2831
|
+
_Source: src/components/form-wizard/FormWizardSubmit.tsx_
|
|
2832
|
+
|
|
2833
|
+
### useFormArray
|
|
2834
|
+
|
|
2835
|
+
**Kind:** `function`
|
|
2836
|
+
|
|
2837
|
+
Headless hook for managing form arrays
|
|
2838
|
+
|
|
2839
|
+
Provides reactive state and actions for form array manipulation
|
|
2840
|
+
without any UI - perfect for building custom array interfaces.
|
|
2841
|
+
|
|
2842
|
+
**Signature:**
|
|
2843
|
+
```typescript
|
|
2844
|
+
export function useFormArray<T extends FormFields>(control: ArrayNode<T>): UseFormArrayReturn<T>
|
|
2845
|
+
```
|
|
2846
|
+
|
|
2847
|
+
**Examples:**
|
|
2848
|
+
|
|
2849
|
+
Basic usage
|
|
2850
|
+
```tsx
|
|
2851
|
+
function PropertyList() {
|
|
2852
|
+
const { items, add, isEmpty } = useFormArray(form.properties);
|
|
2853
|
+
|
|
2854
|
+
return (
|
|
2855
|
+
<div>
|
|
2856
|
+
{items.map(({ control, index, remove, id }) => (
|
|
2857
|
+
<div key={id}>
|
|
2858
|
+
<span>Property #{index + 1}</span>
|
|
2859
|
+
<button onClick={remove}>Remove</button>
|
|
2860
|
+
<PropertyForm control={control} />
|
|
2861
|
+
</div>
|
|
2862
|
+
))}
|
|
2863
|
+
{isEmpty && <p>No properties added</p>}
|
|
2864
|
+
<button onClick={() => add()}>Add Property</button>
|
|
2865
|
+
</div>
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2868
|
+
```
|
|
2869
|
+
|
|
2870
|
+
With initial values + clear / insert
|
|
2871
|
+
```tsx
|
|
2872
|
+
function PropertyToolbar() {
|
|
2873
|
+
const { add, clear, insert, length } = useFormArray(form.properties);
|
|
2874
|
+
|
|
2875
|
+
return (
|
|
2876
|
+
<div className="flex gap-2">
|
|
2877
|
+
<button onClick={() => add({ type: 'apartment', estimatedValue: 0 })}>
|
|
2878
|
+
+ Квартира
|
|
2879
|
+
</button>
|
|
2880
|
+
<button onClick={() => insert(0, { type: 'house' })}>
|
|
2881
|
+
+ Дом (в начало)
|
|
2882
|
+
</button>
|
|
2883
|
+
<button onClick={clear} disabled={length === 0}>Очистить</button>
|
|
2884
|
+
<span>{length} шт.</span>
|
|
2885
|
+
</div>
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2888
|
+
```
|
|
2889
|
+
|
|
2890
|
+
Кастомный AddButton снаружи compound API (drop-down)
|
|
2891
|
+
```tsx
|
|
2892
|
+
import { useFormArrayContext } from '@reformer/cdk/form-array';
|
|
2893
|
+
|
|
2894
|
+
function AddPropertyMenu() {
|
|
2895
|
+
const { add } = useFormArrayContext<Property>();
|
|
2896
|
+
return (
|
|
2897
|
+
<Menu>
|
|
2898
|
+
<Menu.Trigger>+ Добавить ▾</Menu.Trigger>
|
|
2899
|
+
<Menu.Item onSelect={() => add({ type: 'apartment' })}>Квартира</Menu.Item>
|
|
2900
|
+
<Menu.Item onSelect={() => add({ type: 'house' })}>Дом</Menu.Item>
|
|
2901
|
+
</Menu>
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
```
|
|
2905
|
+
|
|
2906
|
+
_Source: src/components/form-array/useFormArray.ts_
|
|
2907
|
+
|
|
2908
|
+
### useFormArrayContext
|
|
2909
|
+
|
|
2910
|
+
**Kind:** `function`
|
|
2911
|
+
|
|
2912
|
+
Хук для доступа к контексту `FormArray`. Бросает исключение, если вызван вне
|
|
2913
|
+
`FormArray.Root` или эквивалентного провайдера.
|
|
2914
|
+
|
|
2915
|
+
**Signature:**
|
|
2916
|
+
```typescript
|
|
2917
|
+
export function useFormArrayContext<T extends FormFields = FormFields>(): FormArrayContextValue<T>
|
|
2918
|
+
```
|
|
2919
|
+
|
|
2920
|
+
**Returns:** Текущий {@link FormArrayContextValue}.
|
|
2921
|
+
|
|
2922
|
+
**Examples:**
|
|
2923
|
+
|
|
2924
|
+
Кастомный AddButton с predefined значением
|
|
2925
|
+
```tsx
|
|
2926
|
+
import { useFormArrayContext } from '@reformer/cdk/form-array';
|
|
2927
|
+
|
|
2928
|
+
function AddDraftButton() {
|
|
2929
|
+
const { add } = useFormArrayContext<Item>();
|
|
2930
|
+
return (
|
|
2931
|
+
<button onClick={() => add({ status: 'draft', createdAt: Date.now() })}>
|
|
2932
|
+
+ Add Draft
|
|
2933
|
+
</button>
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
```
|
|
2937
|
+
|
|
2938
|
+
Счётчик и условный empty-state из произвольного места дерева
|
|
2939
|
+
```tsx
|
|
2940
|
+
function ItemsBadge() {
|
|
2941
|
+
const { length, isEmpty } = useFormArrayContext();
|
|
2942
|
+
if (isEmpty) return <span className="text-gray-400">Нет элементов</span>;
|
|
2943
|
+
return <span className="badge">{length}</span>;
|
|
2944
|
+
}
|
|
2945
|
+
```
|
|
2946
|
+
|
|
2947
|
+
_Source: src/components/form-array/FormArrayContext.tsx_
|
|
2948
|
+
|
|
2949
|
+
### useFormArrayItemContext
|
|
2950
|
+
|
|
2951
|
+
**Kind:** `function`
|
|
2952
|
+
|
|
2953
|
+
Хук для доступа к контексту текущего элемента внутри `FormArray.List`.
|
|
2954
|
+
|
|
2955
|
+
**Signature:**
|
|
2956
|
+
```typescript
|
|
2957
|
+
export function useFormArrayItemContext<
|
|
2958
|
+
T extends FormFields = FormFields,
|
|
2959
|
+
>(): FormArrayItemContextValue<T>
|
|
2960
|
+
```
|
|
2961
|
+
|
|
2962
|
+
**Returns:** Текущий {@link FormArrayItemContextValue} (`index`, `path`, `remove`).
|
|
2963
|
+
|
|
2964
|
+
**Examples:**
|
|
2965
|
+
|
|
2966
|
+
Кнопка удаления текущего элемента
|
|
2967
|
+
```tsx
|
|
2968
|
+
import { useFormArrayItemContext } from '@reformer/cdk/form-array';
|
|
2969
|
+
|
|
2970
|
+
function ItemRemoveButton() {
|
|
2971
|
+
const { remove } = useFormArrayItemContext();
|
|
2972
|
+
return <button onClick={remove}>×</button>;
|
|
2973
|
+
}
|
|
2974
|
+
```
|
|
2975
|
+
|
|
2976
|
+
Доступ к control + index для условной валидации
|
|
2977
|
+
```tsx
|
|
2978
|
+
function ItemHeader() {
|
|
2979
|
+
const { control, index } = useFormArrayItemContext<Property>();
|
|
2980
|
+
const { value: type } = useFormControl(control.type);
|
|
2981
|
+
return (
|
|
2982
|
+
<h4>
|
|
2983
|
+
#{index + 1} — {type === 'house' ? 'Дом' : 'Квартира'}
|
|
2984
|
+
</h4>
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
```
|
|
2988
|
+
|
|
2989
|
+
_Source: src/components/form-array/FormArrayContext.tsx_
|
|
2990
|
+
|
|
2991
|
+
### UseFormArrayReturn
|
|
2992
|
+
|
|
2993
|
+
**Kind:** `interface`
|
|
2994
|
+
|
|
2995
|
+
Return type for useFormArray hook
|
|
2996
|
+
|
|
2997
|
+
**Signature:**
|
|
2998
|
+
```typescript
|
|
2999
|
+
export interface UseFormArrayReturn<T extends FormFields> {
|
|
3000
|
+
/** Array of items with their controls and actions */
|
|
3001
|
+
items: FormArrayItem<T>[];
|
|
3002
|
+
/** Current number of items in the array */
|
|
3003
|
+
length: number;
|
|
3004
|
+
/** Whether the array is empty */
|
|
3005
|
+
isEmpty: boolean;
|
|
3006
|
+
/** Add a new item to the end of the array */
|
|
3007
|
+
add: (value?: Partial<T>) => void;
|
|
3008
|
+
/** Remove all items from the array */
|
|
3009
|
+
clear: () => void;
|
|
3010
|
+
/** Insert a new item at a specific index */
|
|
3011
|
+
insert: (index: number, value?: Partial<T>) => void;
|
|
3012
|
+
}
|
|
3013
|
+
```
|
|
3014
|
+
|
|
3015
|
+
_Source: src/components/form-array/useFormArray.ts_
|
|
3016
|
+
|
|
3017
|
+
### useFormField
|
|
3018
|
+
|
|
3019
|
+
**Kind:** `function`
|
|
3020
|
+
|
|
3021
|
+
Primary hook for building accessible form fields.
|
|
3022
|
+
|
|
3023
|
+
Returns partitioned prop collections and structured state that you can spread
|
|
3024
|
+
directly onto your own elements. No prescribed DOM structure.
|
|
3025
|
+
|
|
3026
|
+
**Signature:**
|
|
3027
|
+
```typescript
|
|
3028
|
+
export function useFormField<T extends FormValue>(
|
|
3029
|
+
control: FieldNode<T>,
|
|
3030
|
+
id?: string
|
|
3031
|
+
): UseFormFieldReturn<T>
|
|
3032
|
+
```
|
|
3033
|
+
|
|
3034
|
+
**Examples:**
|
|
3035
|
+
|
|
3036
|
+
Basic usage
|
|
3037
|
+
```tsx
|
|
3038
|
+
function EmailField({ control }: { control: FieldNode<string> }) {
|
|
3039
|
+
const { labelProps, controlProps, errorProps, state } = useFormField(control);
|
|
3040
|
+
|
|
3041
|
+
return (
|
|
3042
|
+
<div>
|
|
3043
|
+
<label {...labelProps}>{state.label}</label>
|
|
3044
|
+
<input {...controlProps} type="email" />
|
|
3045
|
+
{state.shouldShowError && (
|
|
3046
|
+
<p {...errorProps}>{state.error}</p>
|
|
3047
|
+
)}
|
|
3048
|
+
</div>
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
```
|
|
3052
|
+
|
|
3053
|
+
With description (manual aria-describedby wiring)
|
|
3054
|
+
```tsx
|
|
3055
|
+
const { labelProps, controlProps, errorProps, descriptionProps, state, ids } =
|
|
3056
|
+
useFormField(control);
|
|
3057
|
+
|
|
3058
|
+
const enrichedControlProps = {
|
|
3059
|
+
...controlProps,
|
|
3060
|
+
'aria-describedby': [
|
|
3061
|
+
ids.descriptionId,
|
|
3062
|
+
state.shouldShowError ? ids.errorId : null,
|
|
3063
|
+
].filter(Boolean).join(' ') || undefined,
|
|
3064
|
+
};
|
|
3065
|
+
```
|
|
3066
|
+
|
|
3067
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3068
|
+
|
|
3069
|
+
### useFormFieldContext
|
|
3070
|
+
|
|
3071
|
+
**Kind:** `function`
|
|
3072
|
+
|
|
3073
|
+
Хук для доступа к контексту `FormField`. Бросает исключение, если вызван
|
|
3074
|
+
вне `FormField.Root`.
|
|
3075
|
+
|
|
3076
|
+
**Signature:**
|
|
3077
|
+
```typescript
|
|
3078
|
+
export function useFormFieldContext<T extends FormValue = FormValue>(): FormFieldContextValue<T>
|
|
3079
|
+
```
|
|
3080
|
+
|
|
3081
|
+
**Returns:** Текущий {@link FormFieldContextValue}.
|
|
3082
|
+
|
|
3083
|
+
**Examples:**
|
|
3084
|
+
|
|
3085
|
+
Счётчик символов рядом с label
|
|
3086
|
+
```tsx
|
|
3087
|
+
import { useFormFieldContext } from '@reformer/cdk/form-field';
|
|
3088
|
+
|
|
3089
|
+
function CharCount() {
|
|
3090
|
+
const { control } = useFormFieldContext<string>();
|
|
3091
|
+
return <small>{control.value.length} chars</small>;
|
|
3092
|
+
}
|
|
3093
|
+
```
|
|
3094
|
+
|
|
3095
|
+
Async pending-индикатор и required-астериск
|
|
3096
|
+
```tsx
|
|
3097
|
+
function PendingBadge() {
|
|
3098
|
+
const { pending, required, error } = useFormFieldContext();
|
|
3099
|
+
if (pending) return <Spinner size="xs" aria-label="Проверяем..." />;
|
|
3100
|
+
if (error) return <span className="text-red-600 text-xs">!</span>;
|
|
3101
|
+
if (required) return <span className="text-gray-400">*</span>;
|
|
3102
|
+
return null;
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
<FormField.Root control={form.username}>
|
|
3106
|
+
<div className="flex items-center gap-2">
|
|
3107
|
+
<FormField.Label />
|
|
3108
|
+
<PendingBadge />
|
|
3109
|
+
</div>
|
|
3110
|
+
<FormField.Control />
|
|
3111
|
+
<FormField.Error />
|
|
3112
|
+
</FormField.Root>
|
|
3113
|
+
```
|
|
3114
|
+
|
|
3115
|
+
_Source: src/components/form-field/FormFieldContext.tsx_
|
|
3116
|
+
|
|
3117
|
+
### UseFormFieldControlProps
|
|
3118
|
+
|
|
3119
|
+
**Kind:** `interface`
|
|
3120
|
+
|
|
3121
|
+
Props to spread onto the interactive control element
|
|
3122
|
+
|
|
3123
|
+
**Signature:**
|
|
3124
|
+
```typescript
|
|
3125
|
+
export interface UseFormFieldControlProps {
|
|
3126
|
+
id: string;
|
|
3127
|
+
disabled: boolean;
|
|
3128
|
+
'aria-labelledby': string;
|
|
3129
|
+
'aria-invalid': true | undefined;
|
|
3130
|
+
'aria-errormessage': string | undefined;
|
|
3131
|
+
'aria-required': true | undefined;
|
|
3132
|
+
/** Direct-value onChange compatible with ReFormer field components */
|
|
3133
|
+
onChange: (value: unknown) => void;
|
|
3134
|
+
onBlur: () => void;
|
|
3135
|
+
}
|
|
3136
|
+
```
|
|
3137
|
+
|
|
3138
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3139
|
+
|
|
3140
|
+
### UseFormFieldDescriptionProps
|
|
3141
|
+
|
|
3142
|
+
**Kind:** `interface`
|
|
3143
|
+
|
|
3144
|
+
Props to spread onto a description paragraph
|
|
3145
|
+
|
|
3146
|
+
**Signature:**
|
|
3147
|
+
```typescript
|
|
3148
|
+
export interface UseFormFieldDescriptionProps {
|
|
3149
|
+
id: string;
|
|
3150
|
+
}
|
|
3151
|
+
```
|
|
3152
|
+
|
|
3153
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3154
|
+
|
|
3155
|
+
### UseFormFieldErrorProps
|
|
3156
|
+
|
|
3157
|
+
**Kind:** `interface`
|
|
3158
|
+
|
|
3159
|
+
Props to spread onto an error paragraph
|
|
3160
|
+
|
|
3161
|
+
**Signature:**
|
|
3162
|
+
```typescript
|
|
3163
|
+
export interface UseFormFieldErrorProps {
|
|
3164
|
+
id: string;
|
|
3165
|
+
role: 'alert';
|
|
3166
|
+
}
|
|
3167
|
+
```
|
|
3168
|
+
|
|
3169
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3170
|
+
|
|
3171
|
+
### UseFormFieldLabelProps
|
|
3172
|
+
|
|
3173
|
+
**Kind:** `interface`
|
|
3174
|
+
|
|
3175
|
+
Props to spread onto a <label> element
|
|
3176
|
+
|
|
3177
|
+
**Signature:**
|
|
3178
|
+
```typescript
|
|
3179
|
+
export interface UseFormFieldLabelProps {
|
|
3180
|
+
id: string;
|
|
3181
|
+
htmlFor: string;
|
|
3182
|
+
}
|
|
3183
|
+
```
|
|
3184
|
+
|
|
3185
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3186
|
+
|
|
3187
|
+
### UseFormFieldReturn
|
|
3188
|
+
|
|
3189
|
+
**Kind:** `interface`
|
|
3190
|
+
|
|
3191
|
+
Return type of useFormField hook
|
|
3192
|
+
|
|
3193
|
+
**Signature:**
|
|
3194
|
+
```typescript
|
|
3195
|
+
export interface UseFormFieldReturn<T extends FormValue = FormValue> {
|
|
3196
|
+
/** Spread onto <label> */
|
|
3197
|
+
labelProps: UseFormFieldLabelProps;
|
|
3198
|
+
/** Spread onto the interactive control; includes value */
|
|
3199
|
+
controlProps: UseFormFieldControlProps & { value: T };
|
|
3200
|
+
/** Spread onto the first error paragraph */
|
|
3201
|
+
errorProps: UseFormFieldErrorProps;
|
|
3202
|
+
/** Spread onto the description paragraph */
|
|
3203
|
+
descriptionProps: UseFormFieldDescriptionProps;
|
|
3204
|
+
/** Structured field state */
|
|
3205
|
+
state: UseFormFieldState<T>;
|
|
3206
|
+
/** Field actions */
|
|
3207
|
+
actions: {
|
|
3208
|
+
setValue: (value: T) => void;
|
|
3209
|
+
markAsTouched: () => void;
|
|
3210
|
+
markAsUntouched: () => void;
|
|
3211
|
+
reset: (value?: T) => void;
|
|
3212
|
+
};
|
|
3213
|
+
/** Raw IDs for manual wiring (e.g. aria-describedby) */
|
|
3214
|
+
ids: FormFieldIds;
|
|
3215
|
+
}
|
|
3216
|
+
```
|
|
3217
|
+
|
|
3218
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3219
|
+
|
|
3220
|
+
### UseFormFieldState
|
|
3221
|
+
|
|
3222
|
+
**Kind:** `interface`
|
|
3223
|
+
|
|
3224
|
+
Field state returned by useFormField
|
|
3225
|
+
|
|
3226
|
+
**Signature:**
|
|
3227
|
+
```typescript
|
|
3228
|
+
export interface UseFormFieldState<T extends FormValue = FormValue> {
|
|
3229
|
+
value: T;
|
|
3230
|
+
errors: ValidationError[];
|
|
3231
|
+
/** First error message, only set when shouldShowError is true */
|
|
3232
|
+
error: string | undefined;
|
|
3233
|
+
isPending: boolean;
|
|
3234
|
+
isDisabled: boolean;
|
|
3235
|
+
isValid: boolean;
|
|
3236
|
+
isInvalid: boolean;
|
|
3237
|
+
isTouched: boolean;
|
|
3238
|
+
shouldShowError: boolean;
|
|
3239
|
+
label: string | undefined;
|
|
3240
|
+
required: boolean;
|
|
3241
|
+
/** Full componentProps bag from FieldNode config */
|
|
3242
|
+
componentProps: Record<string, unknown>;
|
|
3243
|
+
}
|
|
3244
|
+
```
|
|
3245
|
+
|
|
3246
|
+
_Source: src/components/form-field/useFormField.ts_
|
|
3247
|
+
|
|
3248
|
+
### useFormWizard
|
|
3249
|
+
|
|
3250
|
+
**Kind:** `function`
|
|
3251
|
+
|
|
3252
|
+
Хук для доступа к контексту {@link FormWizard} из любого потомка.
|
|
3253
|
+
|
|
3254
|
+
Возвращает текущее состояние мастера (`currentStep`, `totalSteps`,
|
|
3255
|
+
`completedSteps`, `isFirstStep`, `isLastStep`, `isValidating`, `isSubmitting`,
|
|
3256
|
+
`form`) и методы навигации (`goToNextStep`, `goToPreviousStep`, `goToStep`).
|
|
3257
|
+
Бросает исключение, если вызван вне `<FormWizard>`.
|
|
3258
|
+
|
|
3259
|
+
Для внешнего управления (вне дерева Wizard) используйте
|
|
3260
|
+
{@link FormWizardHandle} через `useRef`.
|
|
3261
|
+
|
|
3262
|
+
**Signature:**
|
|
3263
|
+
```typescript
|
|
3264
|
+
export function useFormWizard<T extends Record<string, any>>(): FormWizardContextValue<T>
|
|
3265
|
+
```
|
|
3266
|
+
|
|
3267
|
+
**Returns:** Текущий {@link FormWizardContextValue}.
|
|
3268
|
+
|
|
3269
|
+
**Examples:**
|
|
3270
|
+
|
|
3271
|
+
Минимальное использование внутри custom-step
|
|
3272
|
+
```tsx
|
|
3273
|
+
function MyStepComponent() {
|
|
3274
|
+
const { currentStep, isLastStep } = useFormWizard();
|
|
3275
|
+
return <p>Шаг {currentStep}{isLastStep && ' — последний'}</p>;
|
|
3276
|
+
}
|
|
3277
|
+
```
|
|
3278
|
+
|
|
3279
|
+
Условный рендер кнопки на основе isValidating + completedSteps
|
|
3280
|
+
```tsx
|
|
3281
|
+
function ProgressBadge() {
|
|
3282
|
+
const { currentStep, totalSteps, completedSteps, isValidating } =
|
|
3283
|
+
useFormWizard<CreditApplication>();
|
|
3284
|
+
|
|
3285
|
+
if (isValidating) return <span>Проверяем шаг {currentStep}...</span>;
|
|
3286
|
+
return (
|
|
3287
|
+
<span>
|
|
3288
|
+
Завершено {completedSteps.length} из {totalSteps}
|
|
3289
|
+
</span>
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
```
|
|
3293
|
+
|
|
3294
|
+
_Source: src/components/form-wizard/FormWizardContext.tsx_
|