@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 CHANGED
@@ -1,24 +1,1405 @@
1
- # React Form Constructor
1
+ # 🎨 React Form Constructor
2
2
 
3
- > Минималистичный конструктор форм для React на базе **react-hook-form**, с удобной декларацией полей, валидацией и масками ввода.
3
+ > Мощный и гибкий конструктор форм для React с двумя подходами: **JSX-based** для полного контроля и **JSON-based** для скорости разработки. Интегрирует **react-hook-form**, **react-number-format** и поддерживает полную кастомизацию.
4
4
 
5
- ## Зачем это нужно
5
+ ## Особенности
6
6
 
7
- Когда форма состоит из набора однотипных полей, удобнее описывать её **данными**, а не JSX-шаблонами. Библиотека помогает:
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
- ## Метод 1: Provider (рекомендуется)
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