@arturton/react-form-constructor 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1393 -12
- package/dist/index.d.mts +61 -15
- package/dist/index.d.ts +61 -15
- package/dist/index.js +337 -68
- package/dist/index.mjs +339 -70
- package/package.json +1 -4
package/README.md
CHANGED
|
@@ -1,24 +1,1405 @@
|
|
|
1
|
-
# React Form Constructor
|
|
1
|
+
# 🎨 React Form Constructor
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Мощный и гибкий конструктор форм для React с двумя подходами: **JSX-based** для полного контроля и **JSON-based** для скорости разработки. Интегрирует **react-hook-form**, **react-number-format** и поддерживает полную кастомизацию.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## ✨ Особенности
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- ✅ **Два подхода**: выбери тот, что подходит для твоего случая
|
|
8
|
+
- 🎯 **Валидация**: встроенная, кастомная, асинхронная
|
|
9
|
+
- 🔢 **Маски ввода**: телефоны, карты, форматированные числа
|
|
10
|
+
- 🎨 **Полная кастомизация**: стили, классы, кастомные компоненты
|
|
11
|
+
- ⚡ **Производительность**: оптимизирована для больших форм
|
|
12
|
+
- 📦 **Современный стек**: React 18+, TypeScript, ESM/CJS
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
- подключить валидацию `react-hook-form` без лишнего кода;
|
|
11
|
-
- использовать маски ввода через `react-number-format`;
|
|
12
|
-
- гибко стилизовать форму через классы;
|
|
13
|
-
- при необходимости перейти на «ручной» рендер через `children`.
|
|
14
|
-
|
|
15
|
-
## Установка
|
|
14
|
+
## 📦 Установка
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
17
|
npm install @arturton/react-form-constructor
|
|
19
18
|
```
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
### Требования
|
|
21
|
+
|
|
22
|
+
- React `^18` или `^19`
|
|
23
|
+
- react-hook-form `^7`
|
|
24
|
+
- react-number-format `^5`
|
|
25
|
+
|
|
26
|
+
## 🚀 Два подхода к созданию форм
|
|
27
|
+
|
|
28
|
+
### Подход 1️⃣: JSX-based (FormProvider)
|
|
29
|
+
|
|
30
|
+
**Когда использовать**: Сложные формы с кастомной разметкой, нестандартными элементами управления, специфичными требованиями к макету.
|
|
31
|
+
|
|
32
|
+
**Преимущества**:
|
|
33
|
+
|
|
34
|
+
- 🎨 Полный контроль над разметкой
|
|
35
|
+
- 🔧 Гибкость в позиционировании элементов
|
|
36
|
+
- 🎯 Комбинируй с любыми React компонентами
|
|
37
|
+
- 📖 Читаемая иерархия JSX
|
|
38
|
+
|
|
39
|
+
**Пример**:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import {
|
|
43
|
+
FormProvider,
|
|
44
|
+
FormInputLayout,
|
|
45
|
+
FormLabel,
|
|
46
|
+
FormInput,
|
|
47
|
+
FormError,
|
|
48
|
+
FormButton,
|
|
49
|
+
} from "react-form-constructor";
|
|
50
|
+
|
|
51
|
+
type LoginForm = {
|
|
52
|
+
email: string;
|
|
53
|
+
password: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function LoginForm() {
|
|
57
|
+
return (
|
|
58
|
+
<FormProvider<LoginForm>
|
|
59
|
+
funSubmit={(data) => console.log(data)}
|
|
60
|
+
className="max-w-md mx-auto p-6 bg-white rounded-lg shadow"
|
|
61
|
+
>
|
|
62
|
+
<div className="mb-6">
|
|
63
|
+
<FormInputLayout
|
|
64
|
+
name="email"
|
|
65
|
+
required="Email обязателен"
|
|
66
|
+
pattern={{
|
|
67
|
+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
68
|
+
message: "Введите корректный email",
|
|
69
|
+
}}
|
|
70
|
+
className="mb-4"
|
|
71
|
+
>
|
|
72
|
+
<FormLabel className="block text-sm font-semibold mb-2">
|
|
73
|
+
Email адрес
|
|
74
|
+
</FormLabel>
|
|
75
|
+
<FormInput
|
|
76
|
+
type="email"
|
|
77
|
+
placeholder="вы@example.com"
|
|
78
|
+
className="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
79
|
+
classNameError="border-red-500 bg-red-50"
|
|
80
|
+
/>
|
|
81
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
82
|
+
</FormInputLayout>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="mb-6">
|
|
86
|
+
<FormInputLayout
|
|
87
|
+
name="password"
|
|
88
|
+
required="Пароль обязателен"
|
|
89
|
+
minLength={{ value: 6, message: "Минимум 6 символов" }}
|
|
90
|
+
className="mb-4"
|
|
91
|
+
>
|
|
92
|
+
<FormLabel className="block text-sm font-semibold mb-2">
|
|
93
|
+
Пароль
|
|
94
|
+
</FormLabel>
|
|
95
|
+
<FormPasswordInput
|
|
96
|
+
placeholder="••••••••"
|
|
97
|
+
className="w-full"
|
|
98
|
+
inputClassName="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
99
|
+
classNameError="border-red-500 bg-red-50"
|
|
100
|
+
/>
|
|
101
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
102
|
+
</FormInputLayout>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<FormButton
|
|
106
|
+
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition font-semibold"
|
|
107
|
+
disabledError
|
|
108
|
+
>
|
|
109
|
+
Вход
|
|
110
|
+
</FormButton>
|
|
111
|
+
</FormProvider>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### Подход 2️⃣: JSON-based (FormLayout)
|
|
119
|
+
|
|
120
|
+
**Когда использовать**: Быстрое прототипирование, стандартные формы, когда скорость разработки важнее максимальной гибкости.
|
|
121
|
+
|
|
122
|
+
**Преимущества**:
|
|
123
|
+
|
|
124
|
+
- ⚡ Быстрое создание форм (70% экономия кода)
|
|
125
|
+
- 📊 Конфиг отдельно от компонента
|
|
126
|
+
- ♻️ Легко переиспользовать конфигурации
|
|
127
|
+
- 🎯 Меньше шаблонного кода
|
|
128
|
+
|
|
129
|
+
**Пример**:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { FormLayout, type FormField } from "react-form-constructor";
|
|
133
|
+
|
|
134
|
+
type LoginForm = {
|
|
135
|
+
email: string;
|
|
136
|
+
password: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const loginFields: FormField<LoginForm>[] = [
|
|
140
|
+
{
|
|
141
|
+
key: "email",
|
|
142
|
+
label: "Email адрес",
|
|
143
|
+
type: "email",
|
|
144
|
+
placeholder: "вы@example.com",
|
|
145
|
+
required: "Email обязателен",
|
|
146
|
+
pattern: {
|
|
147
|
+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
148
|
+
message: "Введите корректный email",
|
|
149
|
+
},
|
|
150
|
+
inputClass:
|
|
151
|
+
"w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
152
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
153
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: "password",
|
|
157
|
+
label: "Пароль",
|
|
158
|
+
type: "password",
|
|
159
|
+
placeholder: "••••••••",
|
|
160
|
+
required: "Пароль обязателен",
|
|
161
|
+
minLength: { value: 6, message: "Минимум 6 символов" },
|
|
162
|
+
inputClass:
|
|
163
|
+
"w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500",
|
|
164
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
165
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
export function LoginForm() {
|
|
170
|
+
return (
|
|
171
|
+
<FormLayout<LoginForm>
|
|
172
|
+
formData={loginFields}
|
|
173
|
+
funSubmit={(data) => console.log(data)}
|
|
174
|
+
formClass="max-w-md mx-auto p-6 bg-white rounded-lg shadow"
|
|
175
|
+
containerClass="space-y-6"
|
|
176
|
+
buttonClass="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition font-semibold"
|
|
177
|
+
buttonName="Вход"
|
|
178
|
+
disabledOnError
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 📚 Компоненты FormProvider
|
|
187
|
+
|
|
188
|
+
### FormProvider
|
|
189
|
+
|
|
190
|
+
Контейнер для всей формы. Инициализирует `react-hook-form` и предоставляет контекст для всех полей.
|
|
191
|
+
|
|
192
|
+
**Props**:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
interface FormProviderProps<T extends object = any> {
|
|
196
|
+
// Обработчик отправки формы
|
|
197
|
+
funSubmit: (data: T) => void;
|
|
198
|
+
|
|
199
|
+
// Дочерние элементы
|
|
200
|
+
children: React.ReactNode;
|
|
201
|
+
|
|
202
|
+
// CSS класс для элемента <form>
|
|
203
|
+
className?: string;
|
|
204
|
+
|
|
205
|
+
// Получить доступ к методам react-hook-form
|
|
206
|
+
setFormApi?: (formMethods: any) => void;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Пример**:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<FormProvider<MyForm>
|
|
214
|
+
funSubmit={(data) => {
|
|
215
|
+
console.log("Отправка:", data);
|
|
216
|
+
// Отправить на сервер
|
|
217
|
+
}}
|
|
218
|
+
className="flex flex-col gap-4"
|
|
219
|
+
setFormApi={(methods) => {
|
|
220
|
+
console.log("Form API доступен:", methods.register, methods.errors);
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{/* Поля формы */}
|
|
224
|
+
</FormProvider>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### FormInputLayout
|
|
230
|
+
|
|
231
|
+
Контейнер для одного поля с поддержкой валидации. Обертка вокруг компонентов ввода.
|
|
232
|
+
|
|
233
|
+
**Props**:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
interface FormInputLayoutProps<T extends object = any> {
|
|
237
|
+
// Имя поля (ключ в типе T)
|
|
238
|
+
name: keyof T;
|
|
239
|
+
|
|
240
|
+
// Обязательное поле (сообщение об ошибке)
|
|
241
|
+
required?: string | boolean;
|
|
242
|
+
|
|
243
|
+
// Минимальная длина строки
|
|
244
|
+
minLength?: { value: number; message: string };
|
|
245
|
+
|
|
246
|
+
// Максимальная длина строки
|
|
247
|
+
maxLength?: { value: number; message: string };
|
|
248
|
+
|
|
249
|
+
// Проверка регулярным выражением
|
|
250
|
+
pattern?: { value: RegExp; message: string };
|
|
251
|
+
|
|
252
|
+
// Кастомная функция валидации
|
|
253
|
+
validate?: (value: any) => boolean | string;
|
|
254
|
+
|
|
255
|
+
// Асинхронная валидация
|
|
256
|
+
validateAsync?: (value: any) => Promise<boolean | string>;
|
|
257
|
+
|
|
258
|
+
// Конфиг маски ввода (для FormMaskedInput)
|
|
259
|
+
maska?: { required: string; format: string; mask: string };
|
|
260
|
+
|
|
261
|
+
// CSS класс контейнера
|
|
262
|
+
className?: string;
|
|
263
|
+
|
|
264
|
+
// Дочерние элементы (FormLabel, FormInput, FormError)
|
|
265
|
+
children: React.ReactNode;
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Пример**:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
<FormInputLayout<ProfileForm>
|
|
273
|
+
name="email"
|
|
274
|
+
required="Email обязателен"
|
|
275
|
+
pattern={{
|
|
276
|
+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
277
|
+
message: "Некорректный email",
|
|
278
|
+
}}
|
|
279
|
+
className="mb-4"
|
|
280
|
+
>
|
|
281
|
+
<FormLabel className="font-semibold">Email</FormLabel>
|
|
282
|
+
<FormInput placeholder="your@email.com" className="input" />
|
|
283
|
+
<FormError className="text-red-600 text-sm" />
|
|
284
|
+
</FormInputLayout>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### FormLabel
|
|
290
|
+
|
|
291
|
+
Компонент для рендера подписи поля.
|
|
292
|
+
|
|
293
|
+
**Props**:
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
interface FormLabelProps {
|
|
297
|
+
// CSS класс
|
|
298
|
+
className?: string;
|
|
299
|
+
|
|
300
|
+
// CSS класс при ошибке валидации
|
|
301
|
+
classNameError?: string;
|
|
302
|
+
|
|
303
|
+
// Содержимое
|
|
304
|
+
children: React.ReactNode;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Пример**:
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
<FormLabel className="block text-sm font-bold mb-2">Ваше имя</FormLabel>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### FormInput
|
|
317
|
+
|
|
318
|
+
Базовый текстовый ввод с поддержкой различных типов.
|
|
319
|
+
|
|
320
|
+
**Props**:
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
interface FormInputProps {
|
|
324
|
+
// Тип инпута: text, email, password, url, tel, search и т.д.
|
|
325
|
+
type?: string;
|
|
326
|
+
|
|
327
|
+
// Плейсхолдер
|
|
328
|
+
placeholder?: string;
|
|
329
|
+
|
|
330
|
+
// CSS класс инпута
|
|
331
|
+
className?: string;
|
|
332
|
+
|
|
333
|
+
// CSS класс при ошибке
|
|
334
|
+
classNameError?: string;
|
|
335
|
+
|
|
336
|
+
// Отключить поле
|
|
337
|
+
disabled?: boolean;
|
|
338
|
+
|
|
339
|
+
// Значение по умолчанию
|
|
340
|
+
defaultValue?: string;
|
|
341
|
+
|
|
342
|
+
// Стандартные HTML атрибуты
|
|
343
|
+
[key: string]: any;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Пример**:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
<FormInput
|
|
351
|
+
type="email"
|
|
352
|
+
placeholder="your@email.com"
|
|
353
|
+
className="w-full px-3 py-2 border rounded"
|
|
354
|
+
classNameError="border-red-500 bg-red-50"
|
|
355
|
+
/>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
### FormPasswordInput
|
|
361
|
+
|
|
362
|
+
Специализированный ввод для паролей с кнопкой показать/скрыть.
|
|
363
|
+
|
|
364
|
+
**Props**:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
interface FormPasswordInputProps {
|
|
368
|
+
// Плейсхолдер
|
|
369
|
+
placeholder?: string;
|
|
370
|
+
|
|
371
|
+
// CSS класс для контейнера
|
|
372
|
+
className?: string;
|
|
373
|
+
|
|
374
|
+
// CSS класс для инпута
|
|
375
|
+
inputClassName?: string;
|
|
376
|
+
|
|
377
|
+
// CSS класс при ошибке
|
|
378
|
+
classNameError?: string;
|
|
379
|
+
|
|
380
|
+
// React элемент для иконки видимости
|
|
381
|
+
visibleIcon?: React.ReactNode;
|
|
382
|
+
|
|
383
|
+
// React элемент для иконки скрытия
|
|
384
|
+
hiddenIcon?: React.ReactNode;
|
|
385
|
+
|
|
386
|
+
// CSS класс для иконки
|
|
387
|
+
iconClassName?: string;
|
|
388
|
+
|
|
389
|
+
// CSS класс для контейнера иконки
|
|
390
|
+
iconWrapperClassName?: string;
|
|
391
|
+
|
|
392
|
+
// Отключить поле
|
|
393
|
+
disabled?: boolean;
|
|
394
|
+
|
|
395
|
+
// Значение по умолчанию
|
|
396
|
+
defaultValue?: string;
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
**Пример**:
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
<FormPasswordInput
|
|
404
|
+
placeholder="Введите пароль"
|
|
405
|
+
className="flex items-center gap-2"
|
|
406
|
+
inputClassName="flex-1 px-3 py-2 border rounded"
|
|
407
|
+
iconClassName="w-5 h-5 text-gray-500 cursor-pointer"
|
|
408
|
+
visibleIcon={<EyeIcon />}
|
|
409
|
+
hiddenIcon={<EyeOffIcon />}
|
|
410
|
+
/>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### FormTextarea
|
|
416
|
+
|
|
417
|
+
Многострочный ввод текста.
|
|
418
|
+
|
|
419
|
+
**Props**:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
interface FormTextareaProps {
|
|
423
|
+
// Плейсхолдер
|
|
424
|
+
placeholder?: string;
|
|
425
|
+
|
|
426
|
+
// Количество строк
|
|
427
|
+
rows?: number;
|
|
428
|
+
|
|
429
|
+
// Количество столбцов
|
|
430
|
+
cols?: number;
|
|
431
|
+
|
|
432
|
+
// CSS класс
|
|
433
|
+
className?: string;
|
|
434
|
+
|
|
435
|
+
// CSS класс при ошибке
|
|
436
|
+
classNameError?: string;
|
|
437
|
+
|
|
438
|
+
// Отключить поле
|
|
439
|
+
disabled?: boolean;
|
|
440
|
+
|
|
441
|
+
// Значение по умолчанию
|
|
442
|
+
defaultValue?: string;
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Пример**:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
<FormTextarea
|
|
450
|
+
placeholder="Расскажите о себе..."
|
|
451
|
+
rows={5}
|
|
452
|
+
className="w-full px-3 py-2 border rounded resize-none"
|
|
453
|
+
/>
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
### FormMaskedInput
|
|
459
|
+
|
|
460
|
+
Ввод с форматированием и маской (телефон, карта, валюта и т.д.).
|
|
461
|
+
|
|
462
|
+
**Требует**: `maska` конфиг в `FormInputLayout`
|
|
463
|
+
|
|
464
|
+
**Props**:
|
|
465
|
+
|
|
466
|
+
```tsx
|
|
467
|
+
interface FormMaskedInputProps {
|
|
468
|
+
// Плейсхолдер
|
|
469
|
+
placeholder?: string;
|
|
470
|
+
|
|
471
|
+
// CSS класс
|
|
472
|
+
className?: string;
|
|
473
|
+
|
|
474
|
+
// CSS класс при ошибке
|
|
475
|
+
classNameError?: string;
|
|
476
|
+
|
|
477
|
+
// Отключить поле
|
|
478
|
+
disabled?: boolean;
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Пример**:
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
<FormInputLayout
|
|
486
|
+
name="phone"
|
|
487
|
+
maska={{
|
|
488
|
+
required: "Телефон обязателен",
|
|
489
|
+
format: "+7 (###) ###-##-##",
|
|
490
|
+
mask: "_",
|
|
491
|
+
}}
|
|
492
|
+
>
|
|
493
|
+
<FormLabel>Номер телефона</FormLabel>
|
|
494
|
+
<FormMaskedInput
|
|
495
|
+
placeholder="+7 (___) ___-__-__"
|
|
496
|
+
className="w-full px-3 py-2 border rounded"
|
|
497
|
+
/>
|
|
498
|
+
<FormError className="text-red-600 text-sm" />
|
|
499
|
+
</FormInputLayout>
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Доступные маски**:
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
// Телефон РФ
|
|
506
|
+
maska={{ format: "+7 (###) ###-##-##", mask: "_" }}
|
|
507
|
+
|
|
508
|
+
// Номер кредитной карты
|
|
509
|
+
maska={{ format: "#### #### #### ####", mask: "_" }}
|
|
510
|
+
|
|
511
|
+
// Дата
|
|
512
|
+
maska={{ format: "##/##/####", mask: "_" }}
|
|
513
|
+
|
|
514
|
+
// Процент
|
|
515
|
+
maska={{ format: "###%", mask: "_" }}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### FormSelect
|
|
521
|
+
|
|
522
|
+
Выпадающий список.
|
|
523
|
+
|
|
524
|
+
**Props**:
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
interface FormSelectProps {
|
|
528
|
+
// Список опций
|
|
529
|
+
options: Array<{
|
|
530
|
+
value: string | number;
|
|
531
|
+
label: string;
|
|
532
|
+
}>;
|
|
533
|
+
|
|
534
|
+
// Множественный выбор
|
|
535
|
+
multiple?: boolean;
|
|
536
|
+
|
|
537
|
+
// Плейсхолдер (пустой вариант)
|
|
538
|
+
placeholder?: string;
|
|
539
|
+
|
|
540
|
+
// CSS класс
|
|
541
|
+
className?: string;
|
|
542
|
+
|
|
543
|
+
// CSS класс при ошибке
|
|
544
|
+
classNameError?: string;
|
|
545
|
+
|
|
546
|
+
// Отключить поле
|
|
547
|
+
disabled?: boolean;
|
|
548
|
+
|
|
549
|
+
// Значение по умолчанию
|
|
550
|
+
defaultValue?: string | number | (string | number)[];
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Пример**:
|
|
555
|
+
|
|
556
|
+
```tsx
|
|
557
|
+
<FormInputLayout name="country">
|
|
558
|
+
<FormLabel>Страна</FormLabel>
|
|
559
|
+
<FormSelect
|
|
560
|
+
options={[
|
|
561
|
+
{ value: "ru", label: "Россия" },
|
|
562
|
+
{ value: "kz", label: "Казахстан" },
|
|
563
|
+
{ value: "us", label: "США" },
|
|
564
|
+
]}
|
|
565
|
+
placeholder="Выберите страну"
|
|
566
|
+
className="w-full px-3 py-2 border rounded"
|
|
567
|
+
/>
|
|
568
|
+
<FormError className="text-red-600 text-sm" />
|
|
569
|
+
</FormInputLayout>
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### FormNumber
|
|
575
|
+
|
|
576
|
+
Ввод чисел с контролем мин/макс значений.
|
|
577
|
+
|
|
578
|
+
**Props**:
|
|
579
|
+
|
|
580
|
+
```tsx
|
|
581
|
+
interface FormNumberProps {
|
|
582
|
+
// Минимальное значение
|
|
583
|
+
min?: number;
|
|
584
|
+
|
|
585
|
+
// Максимальное значение
|
|
586
|
+
max?: number;
|
|
587
|
+
|
|
588
|
+
// Шаг изменения
|
|
589
|
+
step?: number;
|
|
590
|
+
|
|
591
|
+
// Плейсхолдер
|
|
592
|
+
placeholder?: string;
|
|
593
|
+
|
|
594
|
+
// CSS класс
|
|
595
|
+
className?: string;
|
|
596
|
+
|
|
597
|
+
// CSS класс при ошибке
|
|
598
|
+
classNameError?: string;
|
|
599
|
+
|
|
600
|
+
// Отключить поле
|
|
601
|
+
disabled?: boolean;
|
|
602
|
+
|
|
603
|
+
// Значение по умолчанию
|
|
604
|
+
defaultValue?: number;
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**Пример**:
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
<FormInputLayout name="age">
|
|
612
|
+
<FormLabel>Возраст</FormLabel>
|
|
613
|
+
<FormNumber
|
|
614
|
+
min={18}
|
|
615
|
+
max={120}
|
|
616
|
+
step={1}
|
|
617
|
+
placeholder="Введите возраст"
|
|
618
|
+
className="w-full px-3 py-2 border rounded"
|
|
619
|
+
/>
|
|
620
|
+
<FormError className="text-red-600 text-sm" />
|
|
621
|
+
</FormInputLayout>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
### FormDate
|
|
627
|
+
|
|
628
|
+
Ввод даты/времени.
|
|
629
|
+
|
|
630
|
+
**Props**:
|
|
631
|
+
|
|
632
|
+
```tsx
|
|
633
|
+
interface FormDateProps {
|
|
634
|
+
// Тип: date, datetime-local, time, month, week
|
|
635
|
+
type?: "date" | "datetime-local" | "time" | "month" | "week";
|
|
636
|
+
|
|
637
|
+
// Минимальная дата (формат YYYY-MM-DD)
|
|
638
|
+
min?: string;
|
|
639
|
+
|
|
640
|
+
// Максимальная дата (формат YYYY-MM-DD)
|
|
641
|
+
max?: string;
|
|
642
|
+
|
|
643
|
+
// CSS класс
|
|
644
|
+
className?: string;
|
|
645
|
+
|
|
646
|
+
// CSS класс при ошибке
|
|
647
|
+
classNameError?: string;
|
|
648
|
+
|
|
649
|
+
// Отключить поле
|
|
650
|
+
disabled?: boolean;
|
|
651
|
+
|
|
652
|
+
// Значение по умолчанию
|
|
653
|
+
defaultValue?: string;
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Пример**:
|
|
658
|
+
|
|
659
|
+
```tsx
|
|
660
|
+
<FormInputLayout name="birthDate">
|
|
661
|
+
<FormLabel>Дата рождения</FormLabel>
|
|
662
|
+
<FormDate
|
|
663
|
+
type="date"
|
|
664
|
+
min="1900-01-01"
|
|
665
|
+
max={new Date().toISOString().split("T")[0]}
|
|
666
|
+
className="w-full px-3 py-2 border rounded"
|
|
667
|
+
/>
|
|
668
|
+
<FormError className="text-red-600 text-sm" />
|
|
669
|
+
</FormInputLayout>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
### FormRange
|
|
675
|
+
|
|
676
|
+
Ползунок для выбора числового значения.
|
|
677
|
+
|
|
678
|
+
**Props**:
|
|
679
|
+
|
|
680
|
+
```tsx
|
|
681
|
+
interface FormRangeProps {
|
|
682
|
+
// Тип: single (один ползунок) или double (два ползунка)
|
|
683
|
+
range?: "single" | "double";
|
|
684
|
+
|
|
685
|
+
// Минимальное значение
|
|
686
|
+
min?: number;
|
|
687
|
+
|
|
688
|
+
// Максимальное значение
|
|
689
|
+
max?: number;
|
|
690
|
+
|
|
691
|
+
// Шаг
|
|
692
|
+
step?: number;
|
|
693
|
+
|
|
694
|
+
// Показывать текущее значение
|
|
695
|
+
showValue?: boolean;
|
|
696
|
+
|
|
697
|
+
// CSS класс слайдера
|
|
698
|
+
className?: string;
|
|
699
|
+
|
|
700
|
+
// CSS класс контейнера
|
|
701
|
+
containerClassName?: string;
|
|
702
|
+
|
|
703
|
+
// CSS класс при ошибке
|
|
704
|
+
classNameError?: string;
|
|
705
|
+
|
|
706
|
+
// Отключить поле
|
|
707
|
+
disabled?: boolean;
|
|
708
|
+
|
|
709
|
+
// Значение по умолчанию
|
|
710
|
+
defaultValue?: number | [number, number];
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**Пример**:
|
|
715
|
+
|
|
716
|
+
```tsx
|
|
717
|
+
<FormInputLayout name="rating">
|
|
718
|
+
<FormLabel>Оцените (0-10)</FormLabel>
|
|
719
|
+
<FormRange
|
|
720
|
+
range="single"
|
|
721
|
+
min={0}
|
|
722
|
+
max={10}
|
|
723
|
+
step={1}
|
|
724
|
+
showValue
|
|
725
|
+
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
|
726
|
+
containerClassName="flex gap-4 items-center"
|
|
727
|
+
/>
|
|
728
|
+
<FormError className="text-red-600 text-sm" />
|
|
729
|
+
</FormInputLayout>
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
### FormFileInput
|
|
735
|
+
|
|
736
|
+
Загрузка файлов.
|
|
737
|
+
|
|
738
|
+
**Props**:
|
|
739
|
+
|
|
740
|
+
```tsx
|
|
741
|
+
interface FormFileInputProps {
|
|
742
|
+
// MIME типы или расширения: image/*, .pdf, .doc,docx и т.д.
|
|
743
|
+
accept?: string;
|
|
744
|
+
|
|
745
|
+
// Множественная загрузка
|
|
746
|
+
multiple?: boolean;
|
|
747
|
+
|
|
748
|
+
// CSS класс
|
|
749
|
+
className?: string;
|
|
750
|
+
|
|
751
|
+
// CSS класс при ошибке
|
|
752
|
+
classNameError?: string;
|
|
753
|
+
|
|
754
|
+
// Отключить поле
|
|
755
|
+
disabled?: boolean;
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**Пример**:
|
|
760
|
+
|
|
761
|
+
```tsx
|
|
762
|
+
<FormInputLayout name="avatar">
|
|
763
|
+
<FormLabel>Загрузить аватар</FormLabel>
|
|
764
|
+
<FormFileInput
|
|
765
|
+
accept="image/*"
|
|
766
|
+
className="w-full px-3 py-2 border rounded cursor-pointer"
|
|
767
|
+
/>
|
|
768
|
+
<FormError className="text-red-600 text-sm" />
|
|
769
|
+
</FormInputLayout>
|
|
770
|
+
|
|
771
|
+
<FormInputLayout name="documents">
|
|
772
|
+
<FormLabel>Загрузить документы</FormLabel>
|
|
773
|
+
<FormFileInput
|
|
774
|
+
accept=".pdf,.doc,.docx"
|
|
775
|
+
multiple
|
|
776
|
+
className="w-full px-3 py-2 border rounded cursor-pointer"
|
|
777
|
+
/>
|
|
778
|
+
<FormError className="text-red-600 text-sm" />
|
|
779
|
+
</FormInputLayout>
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
### FormCheckbox
|
|
785
|
+
|
|
786
|
+
Одиночный флажок.
|
|
787
|
+
|
|
788
|
+
**Props**:
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
interface FormCheckboxProps {
|
|
792
|
+
// Значение при отмеченном состоянии
|
|
793
|
+
value?: string | number | boolean;
|
|
794
|
+
|
|
795
|
+
// Отмечено ли по умолчанию
|
|
796
|
+
defaultChecked?: boolean;
|
|
797
|
+
|
|
798
|
+
// Отключить
|
|
799
|
+
disabled?: boolean;
|
|
800
|
+
|
|
801
|
+
// CSS класс
|
|
802
|
+
className?: string;
|
|
803
|
+
|
|
804
|
+
// CSS класс при ошибке
|
|
805
|
+
classNameError?: string;
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
**Пример**:
|
|
810
|
+
|
|
811
|
+
```tsx
|
|
812
|
+
<FormInputLayout name="terms">
|
|
813
|
+
<label className="flex items-center gap-3">
|
|
814
|
+
<FormCheckbox value={true} className="w-5 h-5 cursor-pointer" />
|
|
815
|
+
<span>Я согласен с условиями использования</span>
|
|
816
|
+
</label>
|
|
817
|
+
<FormError className="text-red-600 text-sm" />
|
|
818
|
+
</FormInputLayout>
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
### FormRadio
|
|
824
|
+
|
|
825
|
+
Кнопка-радио для выбора одного из вариантов.
|
|
826
|
+
|
|
827
|
+
**Props**:
|
|
828
|
+
|
|
829
|
+
```tsx
|
|
830
|
+
interface FormRadioProps {
|
|
831
|
+
// Значение опции
|
|
832
|
+
value: string | number;
|
|
833
|
+
|
|
834
|
+
// Выбрано ли по умолчанию
|
|
835
|
+
defaultChecked?: boolean;
|
|
836
|
+
|
|
837
|
+
// Отключить
|
|
838
|
+
disabled?: boolean;
|
|
839
|
+
|
|
840
|
+
// CSS класс
|
|
841
|
+
className?: string;
|
|
842
|
+
|
|
843
|
+
// CSS класс при ошибке
|
|
844
|
+
classNameError?: string;
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
**Пример**:
|
|
849
|
+
|
|
850
|
+
```tsx
|
|
851
|
+
<FormInputLayout name="gender">
|
|
852
|
+
<FormLabel>Пол</FormLabel>
|
|
853
|
+
<div className="flex gap-4">
|
|
854
|
+
<label className="flex items-center gap-2">
|
|
855
|
+
<FormRadio value="male" className="w-5 h-5 cursor-pointer" />
|
|
856
|
+
<span>Мужской</span>
|
|
857
|
+
</label>
|
|
858
|
+
<label className="flex items-center gap-2">
|
|
859
|
+
<FormRadio value="female" className="w-5 h-5 cursor-pointer" />
|
|
860
|
+
<span>Женский</span>
|
|
861
|
+
</label>
|
|
862
|
+
<label className="flex items-center gap-2">
|
|
863
|
+
<FormRadio value="other" className="w-5 h-5 cursor-pointer" />
|
|
864
|
+
<span>Другое</span>
|
|
865
|
+
</label>
|
|
866
|
+
</div>
|
|
867
|
+
<FormError className="text-red-600 text-sm" />
|
|
868
|
+
</FormInputLayout>
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
### FormError
|
|
874
|
+
|
|
875
|
+
Компонент для отображения сообщения об ошибке.
|
|
876
|
+
|
|
877
|
+
**Props**:
|
|
878
|
+
|
|
879
|
+
```tsx
|
|
880
|
+
interface FormErrorProps {
|
|
881
|
+
// CSS класс
|
|
882
|
+
className?: string;
|
|
883
|
+
|
|
884
|
+
// Кастомное сообщение (по умолчанию берется из валидации)
|
|
885
|
+
children?: React.ReactNode;
|
|
886
|
+
}
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
**Пример**:
|
|
890
|
+
|
|
891
|
+
```tsx
|
|
892
|
+
<FormError className="text-red-600 text-sm font-medium mt-1" />
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
### FormButton
|
|
898
|
+
|
|
899
|
+
Кнопка отправки формы.
|
|
900
|
+
|
|
901
|
+
**Props**:
|
|
902
|
+
|
|
903
|
+
```tsx
|
|
904
|
+
interface FormButtonProps {
|
|
905
|
+
// Текст кнопки
|
|
906
|
+
children: React.ReactNode;
|
|
907
|
+
|
|
908
|
+
// CSS класс
|
|
909
|
+
className?: string;
|
|
910
|
+
|
|
911
|
+
// Отключить кнопку, если есть ошибки валидации
|
|
912
|
+
disabledError?: boolean;
|
|
913
|
+
|
|
914
|
+
// Стандартные HTML атрибуты button
|
|
915
|
+
[key: string]: any;
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**Пример**:
|
|
920
|
+
|
|
921
|
+
```tsx
|
|
922
|
+
<FormButton
|
|
923
|
+
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 font-semibold transition disabled:opacity-50"
|
|
924
|
+
disabledError
|
|
925
|
+
>
|
|
926
|
+
Отправить
|
|
927
|
+
</FormButton>
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
## 📋 Компоненты FormLayout
|
|
933
|
+
|
|
934
|
+
### FormLayout
|
|
935
|
+
|
|
936
|
+
Компонент для рендера формы из JSON конфига. Автоматически регистрирует все поля и управляет их состоянием.
|
|
937
|
+
|
|
938
|
+
**Props**:
|
|
939
|
+
|
|
940
|
+
```tsx
|
|
941
|
+
interface FormLayoutProps<T extends object = any> {
|
|
942
|
+
// Массив конфигураций полей
|
|
943
|
+
formData: FormField<T>[];
|
|
944
|
+
|
|
945
|
+
// Обработчик отправки
|
|
946
|
+
funSubmit: (data: T) => void;
|
|
947
|
+
|
|
948
|
+
// Начальные значения формы
|
|
949
|
+
defaultValues?: T | Partial<T>;
|
|
950
|
+
|
|
951
|
+
// Обработчик ошибок валидации
|
|
952
|
+
onError?: (errors: any) => void;
|
|
953
|
+
|
|
954
|
+
// CSS класс для элемента <form>
|
|
955
|
+
formClass?: string;
|
|
956
|
+
|
|
957
|
+
// CSS класс для контейнера полей
|
|
958
|
+
containerClass?: string;
|
|
959
|
+
|
|
960
|
+
// CSS класс для кнопки отправки
|
|
961
|
+
buttonClass?: string;
|
|
962
|
+
|
|
963
|
+
// Текст кнопки отправки
|
|
964
|
+
buttonName?: string;
|
|
965
|
+
|
|
966
|
+
// Глобальный класс для всех лейблов
|
|
967
|
+
labelClass?: string;
|
|
968
|
+
|
|
969
|
+
// Глобальный класс для всех инпутов
|
|
970
|
+
inputClass?: string;
|
|
971
|
+
|
|
972
|
+
// Глобальный класс для всех сообщений об ошибках
|
|
973
|
+
errorClass?: string;
|
|
974
|
+
|
|
975
|
+
// Дополнительные пропсы для кнопки
|
|
976
|
+
submitButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
977
|
+
|
|
978
|
+
// Отключить кнопку, если есть ошибки
|
|
979
|
+
disabledOnError?: boolean;
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
### FormField
|
|
986
|
+
|
|
987
|
+
Конфиг для одного поля формы.
|
|
988
|
+
|
|
989
|
+
**Props**:
|
|
990
|
+
|
|
991
|
+
```tsx
|
|
992
|
+
interface FormField<T extends object = any> {
|
|
993
|
+
// ⭐ Обязательные
|
|
994
|
+
key: keyof T; // Ключ поля в типе T
|
|
995
|
+
type?: FormFieldType; // Тип: text, email, password, number, date, textarea, select, checkbox, radio, file, range, mask
|
|
996
|
+
|
|
997
|
+
// 🏷️ Лейбл и плейсхолдер
|
|
998
|
+
label?: string; // Текст лейбла
|
|
999
|
+
placeholder?: string; // Плейсхолдер
|
|
1000
|
+
|
|
1001
|
+
// ✅ Валидация
|
|
1002
|
+
required?: string | boolean; // Обязательное поле
|
|
1003
|
+
minLength?: { value: number; message: string }; // Мин. длина
|
|
1004
|
+
maxLength?: { value: number; message: string }; // Макс. длина
|
|
1005
|
+
pattern?: { value: RegExp; message: string }; // Регулярное выражение
|
|
1006
|
+
validate?: (value: any) => boolean | string; // Кастомная валидация
|
|
1007
|
+
validateAsync?: (value: any) => Promise<boolean | string>; // Асинхронная валидация
|
|
1008
|
+
|
|
1009
|
+
// 🔢 Для типов number, range
|
|
1010
|
+
min?: number; // Минимальное значение
|
|
1011
|
+
max?: number; // Максимальное значение
|
|
1012
|
+
step?: number; // Шаг
|
|
1013
|
+
|
|
1014
|
+
// 📋 Для типов select
|
|
1015
|
+
options?: Array<{ value: string | number; label: string }>; // Опции
|
|
1016
|
+
multiple?: boolean; // Множественный выбор
|
|
1017
|
+
|
|
1018
|
+
// 🔘 Для типа radio
|
|
1019
|
+
radioOptions?: Array<{ value: string | number; label: string }>; // Опции
|
|
1020
|
+
|
|
1021
|
+
// 📁 Для типа file
|
|
1022
|
+
accept?: string; // MIME типы/расширения
|
|
1023
|
+
|
|
1024
|
+
// 🎭 Для типа mask
|
|
1025
|
+
maska?: { required: string; format: string; mask: string }; // Конфиг маски
|
|
1026
|
+
|
|
1027
|
+
// 🎨 Стили
|
|
1028
|
+
containerClass?: string; // Класс контейнера поля
|
|
1029
|
+
labelClass?: string; // Класс лейбла
|
|
1030
|
+
inputClass?: string; // Класс инпута
|
|
1031
|
+
errorClass?: string; // Класс сообщения об ошибке
|
|
1032
|
+
classNameError?: string; // Класс инпута при ошибке
|
|
1033
|
+
|
|
1034
|
+
// 🔧 Прочие
|
|
1035
|
+
disabled?: boolean; // Отключить поле
|
|
1036
|
+
defaultChecked?: boolean; // Для checkbox/radio
|
|
1037
|
+
rows?: number; // Кол-во строк для textarea
|
|
1038
|
+
showValue?: boolean; // Показать значение для range
|
|
1039
|
+
render?: (props: any) => React.ReactNode; // Кастомный рендер
|
|
1040
|
+
}
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## 🎯 Примеры форм
|
|
1046
|
+
|
|
1047
|
+
### Пример 1: Регистрация (FormProvider)
|
|
1048
|
+
|
|
1049
|
+
```tsx
|
|
1050
|
+
import {
|
|
1051
|
+
FormProvider,
|
|
1052
|
+
FormInputLayout,
|
|
1053
|
+
FormLabel,
|
|
1054
|
+
FormInput,
|
|
1055
|
+
FormPasswordInput,
|
|
1056
|
+
FormCheckbox,
|
|
1057
|
+
FormError,
|
|
1058
|
+
FormButton,
|
|
1059
|
+
} from "react-form-constructor";
|
|
1060
|
+
|
|
1061
|
+
type SignUpForm = {
|
|
1062
|
+
username: string;
|
|
1063
|
+
email: string;
|
|
1064
|
+
password: string;
|
|
1065
|
+
confirmPassword: string;
|
|
1066
|
+
terms: boolean;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
export function SignUp() {
|
|
1070
|
+
return (
|
|
1071
|
+
<FormProvider<SignUpForm>
|
|
1072
|
+
funSubmit={(data) => {
|
|
1073
|
+
console.log("Регистрация:", data);
|
|
1074
|
+
}}
|
|
1075
|
+
className="max-w-2xl mx-auto p-8 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl shadow-lg"
|
|
1076
|
+
>
|
|
1077
|
+
<h1 className="text-3xl font-bold mb-8 text-gray-900">Создать аккаунт</h1>
|
|
1078
|
+
|
|
1079
|
+
<FormInputLayout
|
|
1080
|
+
name="username"
|
|
1081
|
+
required="Имя пользователя обязательно"
|
|
1082
|
+
minLength={{ value: 3, message: "Минимум 3 символа" }}
|
|
1083
|
+
maxLength={{ value: 20, message: "Максимум 20 символов" }}
|
|
1084
|
+
className="mb-6"
|
|
1085
|
+
>
|
|
1086
|
+
<FormLabel className="block text-sm font-semibold mb-2 text-gray-700">
|
|
1087
|
+
Имя пользователя
|
|
1088
|
+
</FormLabel>
|
|
1089
|
+
<FormInput
|
|
1090
|
+
placeholder="john_doe"
|
|
1091
|
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
1092
|
+
/>
|
|
1093
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1094
|
+
</FormInputLayout>
|
|
1095
|
+
|
|
1096
|
+
<FormInputLayout
|
|
1097
|
+
name="email"
|
|
1098
|
+
required="Email обязателен"
|
|
1099
|
+
pattern={{
|
|
1100
|
+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
1101
|
+
message: "Введите корректный email",
|
|
1102
|
+
}}
|
|
1103
|
+
className="mb-6"
|
|
1104
|
+
>
|
|
1105
|
+
<FormLabel className="block text-sm font-semibold mb-2 text-gray-700">
|
|
1106
|
+
Email
|
|
1107
|
+
</FormLabel>
|
|
1108
|
+
<FormInput
|
|
1109
|
+
type="email"
|
|
1110
|
+
placeholder="your@email.com"
|
|
1111
|
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
1112
|
+
/>
|
|
1113
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1114
|
+
</FormInputLayout>
|
|
1115
|
+
|
|
1116
|
+
<FormInputLayout
|
|
1117
|
+
name="password"
|
|
1118
|
+
required="Пароль обязателен"
|
|
1119
|
+
minLength={{ value: 8, message: "Минимум 8 символов" }}
|
|
1120
|
+
validate={(value) => {
|
|
1121
|
+
if (!/[A-Z]/.test(value)) return "Добавьте заглавную букву";
|
|
1122
|
+
if (!/[0-9]/.test(value)) return "Добавьте цифру";
|
|
1123
|
+
return true;
|
|
1124
|
+
}}
|
|
1125
|
+
className="mb-6"
|
|
1126
|
+
>
|
|
1127
|
+
<FormLabel className="block text-sm font-semibold mb-2 text-gray-700">
|
|
1128
|
+
Пароль
|
|
1129
|
+
</FormLabel>
|
|
1130
|
+
<FormPasswordInput
|
|
1131
|
+
placeholder="••••••••"
|
|
1132
|
+
className="flex items-center gap-2"
|
|
1133
|
+
inputClassName="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
|
1134
|
+
/>
|
|
1135
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
1136
|
+
Минимум 8 символов, заглавная буква и цифра
|
|
1137
|
+
</p>
|
|
1138
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1139
|
+
</FormInputLayout>
|
|
1140
|
+
|
|
1141
|
+
<FormInputLayout
|
|
1142
|
+
name="terms"
|
|
1143
|
+
required="Вы должны согласиться"
|
|
1144
|
+
className="mb-8"
|
|
1145
|
+
>
|
|
1146
|
+
<label className="flex items-start gap-3">
|
|
1147
|
+
<FormCheckbox value={true} className="w-5 h-5 mt-1 cursor-pointer" />
|
|
1148
|
+
<span className="text-sm text-gray-700">
|
|
1149
|
+
Я согласен с{" "}
|
|
1150
|
+
<a href="#" className="text-blue-600 hover:underline">
|
|
1151
|
+
условиями использования
|
|
1152
|
+
</a>{" "}
|
|
1153
|
+
и{" "}
|
|
1154
|
+
<a href="#" className="text-blue-600 hover:underline">
|
|
1155
|
+
политикой конфиденциальности
|
|
1156
|
+
</a>
|
|
1157
|
+
</span>
|
|
1158
|
+
</label>
|
|
1159
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1160
|
+
</FormInputLayout>
|
|
1161
|
+
|
|
1162
|
+
<FormButton
|
|
1163
|
+
className="w-full px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white font-bold rounded-lg hover:shadow-lg transition disabled:opacity-50"
|
|
1164
|
+
disabledError
|
|
1165
|
+
>
|
|
1166
|
+
Создать аккаунт
|
|
1167
|
+
</FormButton>
|
|
1168
|
+
</FormProvider>
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
### Пример 2: Профиль пользователя (FormLayout)
|
|
1174
|
+
|
|
1175
|
+
```tsx
|
|
1176
|
+
import { FormLayout, type FormField } from "react-form-constructor";
|
|
1177
|
+
|
|
1178
|
+
type UserProfile = {
|
|
1179
|
+
firstName: string;
|
|
1180
|
+
lastName: string;
|
|
1181
|
+
email: string;
|
|
1182
|
+
phone: string;
|
|
1183
|
+
country: string;
|
|
1184
|
+
bio: string;
|
|
1185
|
+
avatar: FileList;
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
const profileFields: FormField<UserProfile>[] = [
|
|
1189
|
+
{
|
|
1190
|
+
key: "firstName",
|
|
1191
|
+
label: "Имя",
|
|
1192
|
+
type: "text",
|
|
1193
|
+
placeholder: "Иван",
|
|
1194
|
+
required: "Имя обязательно",
|
|
1195
|
+
minLength: { value: 2, message: "Минимум 2 символа" },
|
|
1196
|
+
inputClass: "w-full px-4 py-2 border border-gray-300 rounded-lg",
|
|
1197
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1198
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
key: "lastName",
|
|
1202
|
+
label: "Фамилия",
|
|
1203
|
+
type: "text",
|
|
1204
|
+
placeholder: "Петров",
|
|
1205
|
+
required: "Фамилия обязательна",
|
|
1206
|
+
minLength: { value: 2, message: "Минимум 2 символа" },
|
|
1207
|
+
inputClass: "w-full px-4 py-2 border border-gray-300 rounded-lg",
|
|
1208
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1209
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1210
|
+
},
|
|
1211
|
+
{
|
|
1212
|
+
key: "email",
|
|
1213
|
+
label: "Email",
|
|
1214
|
+
type: "email",
|
|
1215
|
+
placeholder: "ivan@example.com",
|
|
1216
|
+
required: "Email обязателен",
|
|
1217
|
+
pattern: {
|
|
1218
|
+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
1219
|
+
message: "Некорректный email",
|
|
1220
|
+
},
|
|
1221
|
+
inputClass: "w-full px-4 py-2 border border-gray-300 rounded-lg",
|
|
1222
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1223
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
key: "phone",
|
|
1227
|
+
label: "Телефон",
|
|
1228
|
+
type: "mask",
|
|
1229
|
+
placeholder: "+7 (___) ___-__-__",
|
|
1230
|
+
maska: {
|
|
1231
|
+
required: "Телефон обязателен",
|
|
1232
|
+
format: "+7 (###) ###-##-##",
|
|
1233
|
+
mask: "_",
|
|
1234
|
+
},
|
|
1235
|
+
inputClass: "w-full px-4 py-2 border border-gray-300 rounded-lg",
|
|
1236
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1237
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
key: "country",
|
|
1241
|
+
label: "Страна",
|
|
1242
|
+
type: "select",
|
|
1243
|
+
placeholder: "Выберите страну",
|
|
1244
|
+
options: [
|
|
1245
|
+
{ value: "ru", label: "Россия" },
|
|
1246
|
+
{ value: "kz", label: "Казахстан" },
|
|
1247
|
+
{ value: "by", label: "Беларусь" },
|
|
1248
|
+
{ value: "ua", label: "Украина" },
|
|
1249
|
+
],
|
|
1250
|
+
inputClass: "w-full px-4 py-2 border border-gray-300 rounded-lg",
|
|
1251
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1252
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
key: "bio",
|
|
1256
|
+
label: "О себе",
|
|
1257
|
+
type: "textarea",
|
|
1258
|
+
placeholder: "Расскажите о себе...",
|
|
1259
|
+
maxLength: { value: 500, message: "Максимум 500 символов" },
|
|
1260
|
+
inputClass:
|
|
1261
|
+
"w-full px-4 py-2 border border-gray-300 rounded-lg resize-none",
|
|
1262
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1263
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1264
|
+
rows: 4,
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
key: "avatar",
|
|
1268
|
+
label: "Аватар",
|
|
1269
|
+
type: "file",
|
|
1270
|
+
accept: "image/*",
|
|
1271
|
+
inputClass: "w-full px-4 py-2 border border-gray-300 rounded-lg",
|
|
1272
|
+
labelClass: "block text-sm font-semibold mb-2",
|
|
1273
|
+
errorClass: "text-red-600 text-sm mt-1",
|
|
1274
|
+
},
|
|
1275
|
+
];
|
|
1276
|
+
|
|
1277
|
+
export function UserProfile() {
|
|
1278
|
+
return (
|
|
1279
|
+
<div className="max-w-2xl mx-auto p-8">
|
|
1280
|
+
<h1 className="text-3xl font-bold mb-8">Мой профиль</h1>
|
|
1281
|
+
|
|
1282
|
+
<FormLayout<UserProfile>
|
|
1283
|
+
formData={profileFields}
|
|
1284
|
+
funSubmit={(data) => {
|
|
1285
|
+
console.log("Обновление профиля:", data);
|
|
1286
|
+
}}
|
|
1287
|
+
containerClass="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"
|
|
1288
|
+
formClass="bg-white rounded-lg shadow p-8"
|
|
1289
|
+
buttonClass="w-full px-6 py-3 bg-blue-600 text-white font-bold rounded-lg hover:bg-blue-700 transition"
|
|
1290
|
+
buttonName="Сохранить изменения"
|
|
1291
|
+
/>
|
|
1292
|
+
</div>
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
---
|
|
1298
|
+
|
|
1299
|
+
## 🎓 Типичные сценарии
|
|
1300
|
+
|
|
1301
|
+
### Сценарий 1: Валидация пароля
|
|
1302
|
+
|
|
1303
|
+
```tsx
|
|
1304
|
+
<FormInputLayout
|
|
1305
|
+
name="password"
|
|
1306
|
+
required="Пароль обязателен"
|
|
1307
|
+
validate={(value) => {
|
|
1308
|
+
if (!/[A-Z]/.test(value)) return "Добавьте заглавную букву";
|
|
1309
|
+
if (!/[a-z]/.test(value)) return "Добавьте строчную букву";
|
|
1310
|
+
if (!/[0-9]/.test(value)) return "Добавьте цифру";
|
|
1311
|
+
if (!/[!@#$%^&*]/.test(value)) return "Добавьте спецсимвол (!@#$%^&*)";
|
|
1312
|
+
return true;
|
|
1313
|
+
}}
|
|
1314
|
+
>
|
|
1315
|
+
<FormLabel>Пароль</FormLabel>
|
|
1316
|
+
<FormPasswordInput
|
|
1317
|
+
placeholder="••••••••"
|
|
1318
|
+
className="w-full px-3 py-2 border rounded"
|
|
1319
|
+
/>
|
|
1320
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1321
|
+
</FormInputLayout>
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
### Сценарий 2: Асинхронная валидация
|
|
1325
|
+
|
|
1326
|
+
```tsx
|
|
1327
|
+
<FormInputLayout
|
|
1328
|
+
name="username"
|
|
1329
|
+
validateAsync={async (value) => {
|
|
1330
|
+
const response = await fetch(`/api/check-username?name=${value}`);
|
|
1331
|
+
const data = await response.json();
|
|
1332
|
+
return data.available ? true : "Имя пользователя занято";
|
|
1333
|
+
}}
|
|
1334
|
+
>
|
|
1335
|
+
<FormLabel>Имя пользователя</FormLabel>
|
|
1336
|
+
<FormInput
|
|
1337
|
+
placeholder="john_doe"
|
|
1338
|
+
className="w-full px-3 py-2 border rounded"
|
|
1339
|
+
/>
|
|
1340
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1341
|
+
</FormInputLayout>
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
### Сценарий 3: Условное отображение
|
|
1345
|
+
|
|
1346
|
+
```tsx
|
|
1347
|
+
const MyForm = () => {
|
|
1348
|
+
const [showExtraField, setShowExtraField] = useState(false);
|
|
1349
|
+
|
|
1350
|
+
return (
|
|
1351
|
+
<FormProvider<MyForm>
|
|
1352
|
+
funSubmit={(data) => console.log(data)}
|
|
1353
|
+
setFormApi={(methods) => {
|
|
1354
|
+
// Следить за изменениями поля
|
|
1355
|
+
const subscription = methods.watch((data) => {
|
|
1356
|
+
setShowExtraField(data.type === "other");
|
|
1357
|
+
});
|
|
1358
|
+
return () => subscription.unsubscribe();
|
|
1359
|
+
}}
|
|
1360
|
+
>
|
|
1361
|
+
<FormInputLayout name="type">
|
|
1362
|
+
<FormLabel>Выберите тип</FormLabel>
|
|
1363
|
+
<FormSelect
|
|
1364
|
+
options={[
|
|
1365
|
+
{ value: "personal", label: "Личное" },
|
|
1366
|
+
{ value: "business", label: "Бизнес" },
|
|
1367
|
+
{ value: "other", label: "Другое" },
|
|
1368
|
+
]}
|
|
1369
|
+
className="w-full px-3 py-2 border rounded"
|
|
1370
|
+
/>
|
|
1371
|
+
</FormInputLayout>
|
|
1372
|
+
|
|
1373
|
+
{showExtraField && (
|
|
1374
|
+
<FormInputLayout name="otherDescription">
|
|
1375
|
+
<FormLabel>Опишите тип</FormLabel>
|
|
1376
|
+
<FormTextarea
|
|
1377
|
+
placeholder="Описание..."
|
|
1378
|
+
className="w-full px-3 py-2 border rounded"
|
|
1379
|
+
/>
|
|
1380
|
+
<FormError className="text-red-600 text-sm mt-1" />
|
|
1381
|
+
</FormInputLayout>
|
|
1382
|
+
)}
|
|
1383
|
+
</FormProvider>
|
|
1384
|
+
);
|
|
1385
|
+
};
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
---
|
|
1389
|
+
|
|
1390
|
+
## 📖 Дополнительные ресурсы
|
|
1391
|
+
|
|
1392
|
+
- [react-hook-form документация](https://react-hook-form.com/)
|
|
1393
|
+
- [react-number-format документация](https://www.npmjs.com/package/react-number-format)
|
|
1394
|
+
|
|
1395
|
+
## 📄 Лицензия
|
|
1396
|
+
|
|
1397
|
+
MIT — см. файл [LICENSE](./LICENSE)
|
|
1398
|
+
|
|
1399
|
+
---
|
|
1400
|
+
|
|
1401
|
+
**Версия**: 0.1.5
|
|
1402
|
+
**Последнее обновление**: Январь 2026
|
|
22
1403
|
|
|
23
1404
|
Используйте `FormProvider` и набор компонентов для построения формы через `children`.
|
|
24
1405
|
|