Интеграция
Примеры кода
Готовые примеры для HTML, React, Vue и Svelte с реальным API v3.
HTML + реальная интеграция (JS API v3)
Полный пример с проверкой согласия (152-ФЗ), inline error banner, сбросом капчи после успеха/ошибки.
index.html — форма
<form id="contact-form" novalidate>
<input type="text" name="name" placeholder="Имя" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Сообщение"></textarea>
<!-- Согласие на обработку ПД (152-ФЗ) -->
<label>
<input type="checkbox" id="consent" name="consent" required />
Даю согласие на обработку персональных данных
</label>
<div id="captcha-container"></div>
<!-- Inline error banner -->
<div id="form-error" hidden>
<span id="form-error-text"></span>
</div>
<button type="submit" disabled>Отправить</button>
</form>
<script src="https://app.form-hook.com/sdk/v3/loader.min.js" async defer></script> index.html — скрипт интеграции
var widgetId = null;
var submitBtn = document.querySelector('#contact-form button[type="submit"]');
var consentEl = document.getElementById('consent');
var captchaSolved = false;
function updateSubmitState() {
if (!submitBtn) return;
submitBtn.disabled = !(captchaSolved && consentEl && consentEl.checked);
}
if (consentEl) {
consentEl.addEventListener('change', updateSubmitState);
}
// Коды ошибок SDK v3
function userFriendlyError(err) {
if (!err) return 'Произошла ошибка, попробуйте позже';
switch (err.code) {
case 'CAPTCHA_ANSWER_INVALID': return 'Капча не принята, попробуйте ещё раз.';
case 'CAPTCHA_NOT_SOLVED': return 'Пожалуйста, подтвердите, что вы не робот.';
case 'SUBMIT_RATE_LIMITED': return 'Слишком много попыток, подождите немного.';
case 'SUBMIT_VALIDATION': return 'Проверьте правильность заполненных полей.';
case 'SUBMIT_NETWORK': return 'Не удалось связаться с сервером.';
case 'SUBMIT_IN_FLIGHT': return 'Отправка уже идёт, подождите…';
case 'WIDGET_LOAD_TIMEOUT': return 'Капча не загрузилась, обновите страницу.';
case 'CAPTCHA_SITE_DISABLED': return 'Сайт временно отключён, попробуйте позже.';
default: return 'Произошла ошибка, попробуйте позже';
}
}
var errorBanner = document.getElementById('form-error');
var errorText = document.getElementById('form-error-text');
function showError(msg) { errorText.textContent = msg; errorBanner.hidden = false; }
function hideError() { errorBanner.hidden = true; errorText.textContent = ''; }
// Инициализация SDK — используем formhook.ready(), не onloadFormhook
window.formhook.ready(function() {
widgetId = window.formhook.render('#captcha-container', {
sitekey: 'ВАШ_SITE_KEY',
locale: 'ru',
theme: 'light',
callback: function() {
captchaSolved = true;
updateSubmitState();
},
'expired-callback': function() {
captchaSolved = false;
updateSubmitState();
},
'error-callback': function(err) {
console.error('captcha error:', err.code, err.message);
captchaSolved = false;
updateSubmitState();
},
});
});
document.getElementById('contact-form').addEventListener('submit', async function(e) {
e.preventDefault();
hideError();
if (!consentEl.checked) {
showError('Пожалуйста, дайте согласие на обработку персональных данных.');
return;
}
if (!widgetId || !window.formhook.isSolved(widgetId)) {
showError('Пожалуйста, решите пример с капчей перед отправкой.');
return;
}
var btn = this.querySelector('button[type="submit"]');
var originalText = btn.textContent;
btn.disabled = true;
btn.textContent = 'Отправка...';
try {
// Удаляем consent из данных перед отправкой
var formData = new FormData(this);
formData.delete('consent');
var data = Object.fromEntries(formData.entries());
await window.formhook.submit(widgetId, data);
this.reset();
hideError();
// Показать success (например, модалка)
document.getElementById('success-modal').style.display = 'flex';
// Сброс после успеха
window.formhook.reset(widgetId);
captchaSolved = false;
updateSubmitState();
btn.textContent = originalText;
} catch (err) {
console.error(err.code, err.message);
showError(userFriendlyError(err));
btn.textContent = originalText;
// Сброс капчи при ошибке капчи
var isCaptchaError = err.code === 'CAPTCHA_ANSWER_INVALID'
|| err.code === 'CAPTCHA_NOT_SOLVED';
if (isCaptchaError) {
window.formhook.reset(widgetId);
captchaSolved = false;
}
updateSubmitState();
}
}); HTML (декларативный режим — минимальный)
Если не нужна кастомная логика — loader делает всё за вас.
html
<form
data-formhook
data-sitekey="ВАШ_SITE_KEY"
data-success-redirect="/thanks">
<input name="name" placeholder="Имя" required />
<input name="email" type="email" placeholder="Email" required />
<div class="formhook-captcha"></div>
<button type="submit">Отправить</button>
</form>
<script src="https://app.form-hook.com/sdk/v3/loader.min.js" async defer></script> React
tsx
import { useState, useEffect, useRef } from 'react';
// Loader подключён в index.html или через next/script
declare global {
interface Window {
formhook: {
ready: (cb: () => void) => void;
render: (el: string | Element, opts: Record<string, unknown>) => string;
isSolved: (id: string) => boolean;
submit: (id: string, data: Record<string, string>) => Promise<{ id: string }>;
reset: (id: string) => void;
remove: (id: string) => void;
};
}
}
export function ContactForm() {
const [form, setForm] = useState({ name: '', email: '', message: '' });
const [status, setStatus] = useState<'idle' | 'sending' | 'success' | 'error'>('idle');
const [errorMsg, setErrorMsg] = useState('');
const [solved, setSolved] = useState(false);
const widgetId = useRef<string | null>(null);
useEffect(() => {
window.formhook.ready(() => {
widgetId.current = window.formhook.render('#captcha', {
sitekey: 'ВАШ_SITE_KEY',
locale: 'ru',
theme: 'auto',
callback: () => setSolved(true),
'expired-callback': () => setSolved(false),
'error-callback': (e: { code: string }) => {
console.error(e.code);
setSolved(false);
},
});
});
return () => {
if (widgetId.current) window.formhook.remove(widgetId.current);
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!widgetId.current || !window.formhook.isSolved(widgetId.current)) {
setErrorMsg('Решите капчу');
return;
}
setStatus('sending');
try {
await window.formhook.submit(widgetId.current, form);
setStatus('success');
window.formhook.reset(widgetId.current);
setSolved(false);
} catch (err: any) {
setErrorMsg(err.message);
setStatus('error');
if (err.code === 'CAPTCHA_ANSWER_INVALID' || err.code === 'CAPTCHA_NOT_SOLVED') {
window.formhook.reset(widgetId.current);
setSolved(false);
}
}
};
if (status === 'success') return <p>Заявка отправлена!</p>;
return (
<form onSubmit={handleSubmit}>
<input value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))}
placeholder="Имя" required />
<input value={form.email} onChange={e => setForm(p => ({ ...p, email: e.target.value }))}
type="email" placeholder="Email" required />
<textarea value={form.message} onChange={e => setForm(p => ({ ...p, message: e.target.value }))}
placeholder="Сообщение" />
<div id="captcha" />
{status === 'error' && <p>{errorMsg}</p>}
<button type="submit" disabled={!solved || status === 'sending'}>
{status === 'sending' ? 'Отправка...' : 'Отправить'}
</button>
</form>
);
} Vue 3
vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const form = ref({ name: '', email: '', message: '' });
const status = ref<'idle' | 'sending' | 'success' | 'error'>('idle');
const errorMsg = ref('');
const solved = ref(false);
let widgetId: string | null = null;
onMounted(() => {
(window as any).formhook.ready(() => {
widgetId = (window as any).formhook.render('#captcha', {
sitekey: 'ВАШ_SITE_KEY',
locale: 'ru',
theme: 'auto',
callback: () => { solved.value = true; },
'expired-callback': () => { solved.value = false; },
'error-callback': (e: { code: string }) => { solved.value = false; },
});
});
});
onUnmounted(() => {
if (widgetId) (window as any).formhook.remove(widgetId);
});
async function handleSubmit() {
if (!widgetId || !(window as any).formhook.isSolved(widgetId)) {
errorMsg.value = 'Решите капчу';
return;
}
status.value = 'sending';
try {
await (window as any).formhook.submit(widgetId, form.value);
status.value = 'success';
(window as any).formhook.reset(widgetId);
solved.value = false;
} catch (err: any) {
errorMsg.value = err.message;
status.value = 'error';
if (err.code === 'CAPTCHA_ANSWER_INVALID' || err.code === 'CAPTCHA_NOT_SOLVED') {
(window as any).formhook.reset(widgetId);
solved.value = false;
}
}
}
</script>
<template>
<p v-if="status === 'success'">Заявка отправлена!</p>
<form v-else @submit.prevent="handleSubmit">
<input v-model="form.name" placeholder="Имя" required />
<input v-model="form.email" type="email" placeholder="Email" required />
<textarea v-model="form.message" placeholder="Сообщение" />
<div id="captcha" />
<p v-if="status === 'error'">{{ errorMsg }}</p>
<button type="submit" :disabled="!solved || status === 'sending'">
{{ status === 'sending' ? 'Отправка...' : 'Отправить' }}
</button>
</form>
</template> Svelte
svelte
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
let form = { name: '', email: '', message: '' };
let status: 'idle' | 'sending' | 'success' | 'error' = 'idle';
let errorMsg = '';
let solved = false;
let widgetId: string | null = null;
onMount(() => {
(window as any).formhook.ready(() => {
widgetId = (window as any).formhook.render('#captcha', {
sitekey: 'ВАШ_SITE_KEY',
locale: 'ru',
theme: 'auto',
callback: () => { solved = true; },
'expired-callback': () => { solved = false; },
'error-callback': () => { solved = false; },
});
});
});
onDestroy(() => {
if (widgetId) (window as any).formhook.remove(widgetId);
});
async function handleSubmit() {
if (!widgetId || !(window as any).formhook.isSolved(widgetId)) {
errorMsg = 'Решите капчу'; return;
}
status = 'sending';
try {
await (window as any).formhook.submit(widgetId, form);
status = 'success';
(window as any).formhook.reset(widgetId);
solved = false;
} catch (err: any) {
errorMsg = err.message;
status = 'error';
if (err.code === 'CAPTCHA_ANSWER_INVALID' || err.code === 'CAPTCHA_NOT_SOLVED') {
(window as any).formhook.reset(widgetId);
solved = false;
}
}
}
</script>
{#if status === 'success'}
<p>Заявка отправлена!</p>
{:else}
<form on:submit|preventDefault={handleSubmit}>
<input bind:value={form.name} placeholder="Имя" required />
<input bind:value={form.email} type="email" placeholder="Email" required />
<textarea bind:value={form.message} placeholder="Сообщение" />
<div id="captcha" />
{#if status === 'error'}<p>{errorMsg}</p>{/if}
<button type="submit" disabled={!solved || status === 'sending'}>
{status === 'sending' ? 'Отправка...' : 'Отправить'}
</button>
</form>
{/if}