Типичная боль: менеджер просит добавить поле «Удобное время для звонка» в форму обратной связи. Разработчик правит шаблон, обновляет валидацию, тестирует на staging — уходит 4 часа. Такие правки случаются еженедельно, отвлекая от основной разработки. В итоге компания теряет до 16 часов в месяц только на доработки форм. При этом в типовом проекте с десятком форм подобные изменения превращаются в бесконечный цикл согласований и деплоев.
Готовый конструктор форм с drag-and-drop устраняет эту проблему на уровне архитектуры. Мы разработали решение, которое позволяет нетехническим сотрудникам создавать и изменять формы через интуитивный интерфейс. Конструктор состоит из трёх независимых компонентов: Builder (редактор на React), Renderer (компонент отображения) и Backend (API для хранения схем и ответов). Схема формы — JSON-схема, которая интерпретируется рендерером. Это даёт полную гибкость без изменения кода при добавлении нового типа поля. Ниже — пример такой схемы.
{ "id": "uuid-v4", "title": "Заявка на обратный звонок", "description": "Мы перезвоним в течение 30 минут", "settings": { "submit_label": "Отправить заявку", "success_message": "Спасибо! Мы свяжемся с вами.", "redirect_url": null, "notify_emails": ["[email protected]"], "allow_multiple_submissions": false }, "fields": [ { "id": "field_1", "type": "text", "label": "Имя", "placeholder": "Введите ваше имя", "required": true, "validation": { "min_length": 2, "max_length": 100 } }, { "id": "field_2", "type": "phone", "label": "Телефон", "required": true, "validation": { "pattern": "^\\+?[\\d\\s\\-\\(\\)]{7,20}$" } }, { "id": "field_3", "type": "select", "label": "Удобное время звонка", "required": false, "options": [ { "value": "morning", "label": "9:00 – 12:00" }, { "value": "afternoon", "label": "12:00 – 17:00" }, { "value": "evening", "label": "17:00 – 20:00" } ] }, { "id": "field_4", "type": "conditional_group", "condition": { "field": "field_3", "operator": "equals", "value": "evening" }, "fields": [ { "id": "field_4_1", "type": "checkbox", "label": "Подтверждаю, что звонок после 17:00 мне удобен", "required": true } ] } ] } Почему JSON-схема?
JSON-схема — это единый источник истины для всех компонентов. Builder генерирует её, Renderer читает и отрисовывает, Backend валидирует и сохраняет ответы. Такая архитектура позволяет добавлять новые типы полей без изменения кода фронтенда или бэкенда — достаточно расширить схему. Это ключевое отличие от самописных решений, где каждое поле жёстко зашито в шаблон.
Проблемы, которые решаем
- Ручное кодирование каждой формы — уходит до 4 часов на одно изменение. Наш конструктор сокращает это до 30 минут на редактирование через интерфейс.
- Сложность поддержки кастомных полей — добавление нового типа требует изменения кода на всех уровнях. JSON-схема позволяет добавлять поля без участия разработчика.
- Отсутствие аналитики — без конструктора сложно отслеживать конверсию. Мы встраиваем дашборд с распределением ответов, конверсией и брошенными сессиями.
Поддерживаемые типы полей
| Тип | Описание |
|---|---|
text |
Однострочный текст |
textarea |
Многострочный текст |
email |
Email с встроенной валидацией |
phone |
Телефон с маской |
number |
Число с min/max/step |
select |
Выпадающий список |
multiselect |
Выбор нескольких значений |
radio |
Радиокнопки |
checkbox |
Один чекбокс (согласие) |
checkbox_group |
Группа чекбоксов |
date |
Дата |
date_range |
Диапазон дат |
file |
Загрузка файла |
rating |
Оценка звёздочками (1–5) |
scale |
Шкала (NPS, 0–10) |
heading |
Заголовок (не поле ввода) |
paragraph |
Текстовый блок |
divider |
Разделитель |
conditional_group |
Группа с условием отображения |
Какую библиотеку выбрать для drag-and-drop?
Для реализации drag-and-drop в редакторе мы используем библиотеку @dnd-kit. Она современнее, чем react-beautiful-dnd: поддерживает более сложные сценарии — например, перетаскивание между контейнерами, сортировку с анимацией и работу с сенсорными экранами. При этом она легче и быстрее.
import { DndContext, closestCenter, DragEndEvent } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; function FormBuilder({ schema, onChange }: BuilderProps) { const [fields, setFields] = useState(schema.fields); function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (active.id !== over?.id) { setFields((items) => { const oldIndex = items.findIndex((i) => i.id === active.id); const newIndex = items.findIndex((i) => i.id === over!.id); const reordered = arrayMove(items, oldIndex, newIndex); onChange({ ...schema, fields: reordered }); return reordered; }); } } return ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <SortableContext items={fields.map(f => f.id)} strategy={verticalListSortingStrategy}> {fields.map(field => ( <SortableFieldCard key={field.id} field={field} onEdit={(updated) => updateField(field.id, updated)} onDelete={() => removeField(field.id)} /> ))} </SortableContext> </DndContext> ); } Рендеринг и валидация
Рендерер работает с той же JSON-схемой. Для валидации используем React Hook Form — библиотеку, которая минимизирует ререндеры и упрощает правила проверки.
import { useForm } from 'react-hook-form'; function FormRenderer({ schema, onSubmit }: RendererProps) { const { register, handleSubmit, watch, formState: { errors } } = useForm(); return ( <form onSubmit={handleSubmit(onSubmit)}> {schema.fields.map(field => ( <FormField key={field.id} field={field} register={register} errors={errors} watch={watch} /> ))} <button type="submit">{schema.settings.submit_label}</button> </form> ); } function FormField({ field, register, errors, watch }) { // Условная логика: показывать поле только если условие выполнено if (field.condition) { const watchValue = watch(field.condition.field); const conditionMet = evaluateCondition(watchValue, field.condition); if (!conditionMet) return null; } const rules = buildValidationRules(field); switch (field.type) { case 'text': case 'email': case 'phone': return ( <div> <label>{field.label}{field.required && ' *'}</label> <input {...register(field.id, rules)} placeholder={field.placeholder} /> {errors[field.id] && <span>{errors[field.id].message}</span>} </div> ); case 'select': return ( <div> <label>{field.label}</label> <select {...register(field.id, rules)}> <option value="">Выберите...</option> {field.options.map(opt => ( <option key={opt.value} value={opt.value}>{opt.label}</option> ))} </select> </div> ); // ... другие типы } } Как хранить данные и анализировать ответы?
Хранение схем и ответов организовано на PostgreSQL. Схема формы — JSONB, ответы — тоже JSONB с GIN-индексом для быстрого поиска.
CREATE TABLE forms ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, slug VARCHAR(100) UNIQUE, schema JSONB NOT NULL, is_active BOOLEAN DEFAULT TRUE, created_by INTEGER, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE form_submissions ( id BIGSERIAL PRIMARY KEY, form_id UUID REFERENCES forms(id), data JSONB NOT NULL, metadata JSONB DEFAULT '{}', submitted_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_submissions_form ON form_submissions (form_id, submitted_at DESC); CREATE INDEX idx_submissions_data ON form_submissions USING gin(data); -- Аналитика: распределение ответов для поля SELECT data->>'field_3' AS answer, COUNT(*) AS count, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS percent FROM form_submissions WHERE form_id = $1 AND data ? 'field_3' GROUP BY data->>'field_3' ORDER BY count DESC; Обработка отправки выполняется через единый API-эндпоинт:
// POST /api/forms/{slug}/submit async function submitForm(req: Request, res: Response) { const form = await getFormBySlug(req.params.slug); if (!form || !form.is_active) return res.status(404).json({ error: 'Form not found' }); const schema = form.schema; const errors = validateSubmission(schema.fields, req.body); if (Object.keys(errors).length) { return res.status(422).json({ errors }); } const submission = await saveSubmission(form.id, req.body, { ip: req.ip, user_agent: req.headers['user-agent'], referer: req.headers.referer, }); // Уведомления if (schema.settings.notify_emails?.length) { await sendNotificationEmail(form, submission); } if (schema.settings.webhook_url) { await triggerWebhook(schema.settings.webhook_url, submission); } return res.json({ success: true, message: schema.settings.success_message, redirect: schema.settings.redirect_url, }); } Типичные ошибки при реализации
- N+1 запросы при загрузке списка форм — решаем через eager loading или кэширование.
- Проблемы гидратации на сервере — формы с условиями могут рендериться некорректно при SSR. Используем Suspense и постепенную загрузку.
- Сложность валидации кастомных полей — JSON-схема с правилами упрощает поддержку.
- Отсутствие защиты от повторной отправки — внедряем CSRF-токены и уникальные ключи.
Процесс работы
- Аналитика — изучаем требования, типы форм, интеграции.
- Проектирование — разрабатываем JSON-схему и прототип интерфейса.
- Реализация — пишем Builder, Renderer, Backend, подключаем аналитику.
- Тестирование — проверяем корректность рендеринга, валидации, отправок.
- Деплой — разворачиваем на сервере, настраиваем мониторинг.
Сроки реализации
| Версия | Время |
|---|---|
| Базовая (текст, email, select, checkbox, без условной логики) | 10–12 рабочих дней |
| Полная (все типы, conditional logic, файлы, аналитика, webhook, iframe) | 16–22 рабочих дня |
Что входит в работу
- Готовый React-компонент редактора с палитрой полей
- API для хранения схем и ответов
- Страница аналитики с графиками и выгрузкой
- Интеграция с почтой (уведомления) и вебхуками
- Документация по API и настройке
- Обучение администраторов (1–2 часа)
- Гарантия 12 месяцев на разработанное решение
В отличие от самописного решения, где каждое изменение требует 4 часов работы разработчика, наш конструктор позволяет изменить форму за 30 минут через интерфейс. Это даёт существенную экономию бюджета — до 95% времени на создание и изменение форм. Оценим ваш проект за 1 час: свяжитесь с нами, чтобы обсудить детали и получить коммерческое предложение. Закажите разработку конструктора форм под ключ уже сегодня.







