Интеграция n8n для обработки форм контактов. Часть 3: Обновление фронтенда

Модификация HTML формы, добавление JavaScript обработки и CSS стилей для интеграции с n8n workflow.

Интеграция n8n для обработки форм контактов. Часть 3: Обновление фронтенда

Введение

В предыдущих статьях мы настроили n8n workflow для обработки форм контактов. Теперь пришло время обновить фронтенд — сделать форму интерактивной, добавить AJAX отправку, улучшить пользовательский опыт и обеспечить обратную связь.

Хороший UX формы — это не только красивые поля, но и понятная обратная связь. — Принцип веб-разработки

Текущее состояние формы

Предположим, у нас есть базовая HTML форма в content/contact.md:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<form method="post" class="card" style="max-width: 72ch;">
  <div class="field">
    <label for="name">Имя</label>
    <input id="name" name="name" required />
  </div>
  
  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
  </div>
  
  <div class="field">
    <label for="topic">Тема</label>
    <select id="topic" name="topic" required>
      <option value="">Выберите тему</option>
      <option value="general">Общий вопрос</option>
      <option value="technical">Техническая поддержка</option>
      <option value="partnership">Сотрудничество</option>
    </select>
  </div>
  
  <div class="field">
    <label for="message">Сообщение</label>
    <textarea id="message" name="message" rows="5" required></textarea>
  </div>
  
  <div class="field">
    <label>
      <input type="checkbox" name="agreement" required />
      Согласен на обработку персональных данных
    </label>
  </div>
  
  <button class="btn btn-primary" type="submit">
    Отправить
  </button>
</form>

Шаг 1: Добавление honeypot поля

Honeypot поле защищает от спам-ботов — оно скрыто от пользователей, но заполняется ботами.

Добавление honeypot поля html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<form id="contactForm" method="post" class="card" style="max-width: 72ch;">
  <!-- Honeypot поле (скрытое для пользователей, но видимое ботам) -->
  <div style="display: none;">
    <label for="website">Website</label>
    <input id="website" name="website" autocomplete="off" />
  </div>

  <!-- Остальные поля формы остаются без изменений -->
  <div class="field">
    <label for="name">Имя</label>
    <input id="name" name="name" autocomplete="name" required />
    <div class="error-message" id="nameError"></div>
  </div>

  <!-- ... остальные поля с контейнерами для ошибок ... -->

Шаг 2: Добавление контейнеров для сообщений

Добавим контейнеры для отображения состояния отправки:

Контейнеры для сообщений html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- Индикатор загрузки -->
<div id="loadingIndicator" style="display: none; margin: 16px 0;">
  <div style="display: flex; align-items: center; gap: 8px;">
    <div class="spinner"></div>
    <span>Отправка...</span>
  </div>
</div>

<!-- Сообщение об успехе -->
<div id="successMessage" style="display: none;" class="alert alert-success">
  ✅ Заявка успешно отправлена! Ответим в течение 1-2 рабочих дней.
</div>

<!-- Сообщение об ошибке -->
<div id="errorMessage" style="display: none;" class="alert alert-error">
  ❌ Произошла ошибка. Пожалуйста, проверьте форму и попробуйте снова.
</div>

<div style="margin-top: 16px;">
  <button class="btn btn-primary" type="submit" id="submitBtn">
    Отправить
  </button>
</div>

Шаг 3: JavaScript обработка формы

Создан отдельный файл с полной реализацией обработки формы.

Структура файла form-validation.js:

  1. Базовая валидация данных формы
  2. Валидация в реальном времени
  3. Автосохранение черновика
  4. Основная функция обработки формы
  5. Интеграция с существующим main.js

Полный код обработки формы доступен в файле: form-validation.js

Интеграция с существующим main.js

Для интеграции с сайтом файл подключен в assets/js/main.js:

Интеграция в main.js javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Импорт функции обработки формы
import { initContactForm } from '/static/code-examples/form-validation.js';

// Обновление функции init()
function init() {
  setupCopyButtons();
  setupScrollAnimations();
  setupSmoothAnchors();
  initContactForm(); // <-- Добавить эту строку
}

Шаг 4: CSS стили

Добавим стили в assets/css/main.css:

Стили для формы css
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/* Form styles */
.error-message {
  color: #dc2626;
  font-size: 0.875rem;
  margin-top: 4px;
  min-height: 20px;
}

.field {
  margin-bottom: 1.5rem;
  position: relative;
}

.field label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: #374151;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  font-size: 1rem;
  transition: border-color 0.15s ease-in-out;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.field input.error,
.field select.error,
.field textarea.error {
  border-color: #dc2626;
}

/* Alert styles */
.alert {
  padding: 1rem;
  border-radius: 0.5rem;
  margin: 1rem 0;
  animation: fadeIn 0.3s ease-in-out;
}

.alert-success {
  background-color: #d1fae5;
  color: #065f46;
  border: 1px solid #a7f3d0;
}

.alert-error {
  background-color: #fee2e2;
  color: #991b1b;
  border: 1px solid #fecaca;
}

/* Spinner animation */
.spinner {
  width: 20px;
  height: 20px;
  border: 2px solid rgba(0, 0, 0, 0.1);
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Button styles */
.btn {
  padding: 0.75rem 1.5rem;
  border-radius: 0.375rem;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.15s ease-in-out;
  border: none;
}

.btn-primary {
  background-color: #3b82f6;
  color: white;
}

.btn-primary:hover:not(:disabled) {
  background-color: #2563eb;
}

.btn-primary:disabled {
  background-color: #93c5fd;
  cursor: not-allowed;
}

/* Form container */
.card {
  background: white;
  border-radius: 0.75rem;
  padding: 2rem;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
              0 2px 4px -1px rgba(0, 0, 0, 0.06);
  max-width: 72ch;
  margin: 0 auto;
}

Шаг 5: Улучшения UX

Валидация в реальном времени

Продолжим реализацию функции setupRealTimeValidation и привяжем её к полям формы:

setupRealTimeValidation: завершённая версия javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function setupRealTimeValidation(form) {
  const emailRegex = /^[^@]+@[^@]+\.[^@]+$/;

  function validateField(field, value) {
    const errors = {};

    switch (field) {
      case 'name':
        if (!value.trim()) errors.name = "Имя обязательно";
        break;
      case 'email':
        if (!value.trim()) {
          errors.email = "Email обязателен";
        } else if (!emailRegex.test(value)) {
          errors.email = "Введите корректный email";
        }
        break;
      case 'topic':
        if (!value.trim()) errors.topic = "Выберите тему";
        break;
      case 'message':
        if (!value.trim()) errors.message = "Сообщение обязательно";
        break;
    }

    return errors;
  }

  function showErrors(errors) {
    // Сброс предыдущих ошибок
    ['name', 'email', 'topic', 'message'].forEach((field) => {
      const input = form.querySelector(`[name="${field}"]`);
      const errorContainer = form.querySelector(`#${field}Error`);
      if (!input || !errorContainer) return;

      if (errors[field]) {
        input.classList.add('error');
        errorContainer.textContent = errors[field];
      } else {
        input.classList.remove('error');
        errorContainer.textContent = '';
      }
    });
  }

  function handleInput(event) {
    const target = event.target;
    const field = target.name;
    if (!field) return;

    const value = target.type === 'checkbox'
      ? target.checked
      : target.value;

    const errors = validateField(field, value);
    showErrors(errors);
  }

  form.addEventListener('input', handleInput);
  form.addEventListener('change', handleInput);
}

Такой подход даёт «мгновенную обратную связь» пользователю: ошибки появляются сразу при вводе, а не только после отправки формы. Это снижает фрустрацию и повышает конверсию.

Шаг 6: Автосохранение черновика

Частая проблема длинных форм — потеря введённых данных при перезагрузке страницы или случайном закрытии вкладки. Решить это можно с помощью простого механизма автосохранения на localStorage.

Автосохранение формы в localStorage javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const FORM_STORAGE_KEY = 'contactFormDraft';

function saveDraft(form) {
  const data = {
    name: form.name.value,
    email: form.email.value,
    topic: form.topic.value,
    message: form.message.value,
    agreement: form.agreement.checked,
  };

  try {
    localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify(data));
  } catch (e) {
    // В продакшене можно добавить логгирование
    console.warn('Не удалось сохранить черновик формы', e);
  }
}

function loadDraft(form) {
  try {
    const raw = localStorage.getItem(FORM_STORAGE_KEY);
    if (!raw) return;

    const data = JSON.parse(raw);
    if (data.name) form.name.value = data.name;
    if (data.email) form.email.value = data.email;
    if (data.topic) form.topic.value = data.topic;
    if (data.message) form.message.value = data.message;
    if (typeof data.agreement === 'boolean') {
      form.agreement.checked = data.agreement;
    }
  } catch (e) {
    console.warn('Не удалось загрузить черновик формы', e);
  }
}

function clearDraft() {
  try {
    localStorage.removeItem(FORM_STORAGE_KEY);
  } catch (e) {
    console.warn('Не удалось очистить черновик формы', e);
  }
}

function setupAutosave(form) {
  // Восстанавливаем черновик при загрузке страницы
  loadDraft(form);

  // Сохраняем при вводе данных
  const handler = () => saveDraft(form);
  form.addEventListener('input', handler);
  form.addEventListener('change', handler);

  // Очищаем черновик при успешной отправке
  form.addEventListener('form:success', clearDraft);
}

Мы используем кастомное событие form:success, которое будет сгенерировано в основной функции отправки формы. Это позволяет изолировать автосохранение от логики отправки.

Связь с workflow из Часть 2

Обрати внимание, что:

  • URL N8N_WEBHOOK_URL должен совпадать с Production URL webhook узла в n8n (а не тестовым /webhook-test/...).
  • Формат payload соответствует полям, которые мы использовали в узле Edit Fields в части 2:
    • name, email, topic, message, agreement, website.
  • Ответы обрабатываются в соответствии с узлами Success Response и Error Response:
    • при успехе {"success": true, "message": "..."},
    • при ошибке {"success": false, "error": "...", "field": "..."}.

Это обеспечивает полное соответствие между фронтендом и логикой в n8n.

Типичные ошибки и отладка

Ошибка: CORS / блокировка браузером

  • Проверь, что в узле Webhook в n8n включены корректные CORS-заголовки:
    • Access-Control-Allow-Origin — домен твоего сайта (или * для тестов)
    • Access-Control-Allow-Headers — минимум Content-Type

Ошибка: TypeError: Failed to fetch

Возможные причины:

  • Неверный N8N_WEBHOOK_URL (опечатка, тестовый URL вместо продакшн)
  • n8n недоступен (упал инстанс, проблемы с сетью)
  • SSL-проблемы (невалидный сертификат)

Проверка:

  • Выполни curl запрос к webhook URL
  • Открой Execution History в n8n и посмотри, доходит ли запрос

Ошибка: Клиент «не видит» детализацию ошибок валидации

  • Убедись, что в Error Response узле n8n возвращаются поля error и field
  • В коде фронтенда обработай data.field и отобрази сообщение в соответствующем контейнере #fieldError

Заключение

В этой части мы:

  1. Обновили HTML-форму контактов, добавив:
  • honeypot-поле для защиты от ботов,
  • контейнеры для сообщений об ошибках и статусах.
  1. Подключили JavaScript-обработку:
  • валидация в реальном времени,
  • AJAX-отправка данных на n8n webhook,
  • обработка успешного ответа и ошибок валидации с сервера.

В сумме это превращает простую статическую форму в полноценный фронтенд-клиент к n8n workflow из части 2, с хорошим UX и минимальными требованиями к backend-инфраструктуре.

Что дальше?

В следующей части мы сосредоточимся на безопасности и тестировании: разберём продвинутые техники защиты от спама, rate limiting на уровне n8n и внешнего прокси, а также систематические сценарии тестирования всей цепочки «форма → n8n → email/Telegram».


*Это третья статья из серии по интеграции n8n.