Введение
В предыдущих статьях мы настроили 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 поле защищает от спам-ботов — оно скрыто от пользователей, но заполняется ботами.
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: Добавление контейнеров для сообщений
Добавим контейнеры для отображения состояния отправки:
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:
- Базовая валидация данных формы
- Валидация в реальном времени
- Автосохранение черновика
- Основная функция обработки формы
- Интеграция с существующим main.js
Полный код обработки формы доступен в файле: form-validation.js
Интеграция с существующим main.js
Для интеграции с сайтом файл подключен в assets/js/main.js:
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:
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 и привяжем её к полям формы:
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.
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
Заключение
В этой части мы:
- Обновили HTML-форму контактов, добавив:
- honeypot-поле для защиты от ботов,
- контейнеры для сообщений об ошибках и статусах.
- Подключили JavaScript-обработку:
- валидация в реальном времени,
- AJAX-отправка данных на n8n webhook,
- обработка успешного ответа и ошибок валидации с сервера.
В сумме это превращает простую статическую форму в полноценный фронтенд-клиент к n8n workflow из части 2, с хорошим UX и минимальными требованиями к backend-инфраструктуре.
Что дальше?
В следующей части мы сосредоточимся на безопасности и тестировании: разберём продвинутые техники защиты от спама, rate limiting на уровне n8n и внешнего прокси, а также систематические сценарии тестирования всей цепочки «форма → n8n → email/Telegram».
*Это третья статья из серии по интеграции n8n.