Розробка конструктора форм (Drag-and-Drop Form Builder) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка конструктора форм (Drag-and-Drop Form Builder) на сайті
Складна
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розроблення конструктора форм (Drag-and-Drop Form Builder) на веб-сайту

Конструктор форм — інструмент, який дозволяє користувачам без навичок програмування створювати довільні форми: опитування, заявки, реєстрації, квізи. Ключові вимоги: гнучка схема полів, візуальний редактор з drag-and-drop, рендеринг форм на сайті і збір відповідей.

Архітектура

Система складається з трьох незалежних частин:

  1. Builder — React-компонент редактора форм (drag-and-drop)
  2. Renderer — React-компонент для відображення та заповнення формы
  3. Backend — API для зберігання схем, збору відповідей, аналітики

Схема форми — це JSON, який інтерпретується рендерером. Це дає повну гнучкість без змін коду при додаванні нового типу поля.

Структура схеми форми (JSON Schema)

{
  "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
        }
      ]
    }
  ]
}

Підтримувані типи полів

Тип Опис
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 Група з умовою відображення

Builder: компонент редактора

Використовується @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>
  );
}

Панель інструментів зліва — палітра типів полів. Перетаскування з палітри на холст додає нове поле в потрібне місце.

Renderer: рендеринг та валідація

Рендерер працює з тією ж 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>
      );
    // ... інші типи
  }
}

База даних

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,        -- { "field_1": "Іван", "field_2": "+79991234567" }
    metadata    JSONB DEFAULT '{}',   -- IP, user agent, UTM
    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);

JSONB для відповідей — правильний вибір: структура кожної форми унікальна, і фіксована схема таблиці не підходить. GIN-індекс дозволяє шукати по значеннях конкретних полів.

Обробка відправки форми

// 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,
  });
}

Аналітика відповідей

Для кожної форми — сторінка з агрегованою статистикою:

  • Кількість відповідей за днями (графік)
  • Для select / radio / checkbox_group — розподіл відповідей (pie chart)
  • Для числових полів — середнє, медіана, діапазон
  • Експорт всіх відповідей в CSV/Excel
-- Розподіл відповідей для поля select
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;

Строки реалізації

Конструктор з базовими типами полів (text, email, select, checkbox), без умовної логіки — 10–12 робочих днів. Повний набір типів, умовна логіка, файлові поля, аналітика відповідей, експорт в CSV, webhook, вбудовування через iframe — 16–22 робочі дні.