Skip to content

Commit 702418d

Browse files
authored
docs: form integration (#15)
1 parent b9e1706 commit 702418d

File tree

10 files changed

+314
-32
lines changed

10 files changed

+314
-32
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20.9
1+
20.13.1

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
# 1.1.3
4+
5+
- docs: add "form integration" section
6+
37
# 1.1.2
48

59
- fix: 'loaded' event race condition when calling 'execute'

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ It allows for easy integration with hCaptcha in many modern web frameworks.
99
[Install](#install)
1010
| [Browser Compatibility](#browser-compatibility)
1111
| [Usage](#usage)
12+
| [Form Integration](#form-integration)
1213
| [Attributes](#attributes)
1314
| [Events](#events)
1415
| [Methods](#methods)
@@ -249,6 +250,54 @@ mainstream web frameworks such as: React, Preact, Vue.js, Angular, Stencil.js, e
249250
</script>
250251
```
251252

253+
## Form Integration
254+
255+
When using hCaptcha within forms, **you must manually control the verification flow** to ensure proper security. The hCaptcha component should be triggered on form submission, and the actual form submission should only happen after successful verification.
256+
257+
### Required Flow
258+
259+
1. **Prevent default form submission** - Use `preventDefault()` to stop immediate form submission
260+
2. **Execute hCaptcha verification** - Call `.execute()` method to trigger the challenge
261+
3. **Handle verification result** - Submit the form only when the `verified` event fires
262+
263+
### Minimalist Example
264+
265+
```html
266+
<form id="myForm">
267+
<input type="email" name="email" placeholder="Your email" required>
268+
<button type="submit">Submit</button>
269+
270+
<!-- hCaptcha component (can be invisible) -->
271+
<h-captcha id="hcaptchaEl"
272+
site-key="your-site-key"
273+
size="invisible"></h-captcha>
274+
</form>
275+
276+
<script>
277+
const form = document.getElementById('myForm');
278+
const hcaptchaEl = document.getElementById('hcaptchaEl');
279+
280+
// 1. Intercept form submission
281+
form.addEventListener('submit', (e) => {
282+
e.preventDefault(); // Stop default submission
283+
hcaptchaEl.execute(); // Trigger hCaptcha verification
284+
});
285+
286+
// 2. Handle successful verification
287+
hcaptchaEl.addEventListener('verified', (e) => {
288+
console.log("hCaptcha successfull verification", { token: e.token })
289+
// Now submit the form
290+
form.submit();
291+
});
292+
293+
// 3. Handle errors
294+
hcaptchaEl.addEventListener('error', (e) => {
295+
console.error('hCaptcha error:', e.error);
296+
// Show user-friendly error message
297+
});
298+
</script>
299+
```
300+
252301
## Attributes
253302

254303
The web component allows specifying attributes. These are split into two categories: render and api attributes.

examples/angular/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@angular/forms": "^17.3.0",
1818
"@angular/platform-browser": "^17.3.0",
1919
"@angular/platform-browser-dynamic": "^17.3.0",
20-
"@hcaptcha/vanilla-hcaptcha": "link:../../packages/vanilla-hcaptcha",
20+
"@hcaptcha/vanilla-hcaptcha": "workspace:*",
2121
"rxjs": "~7.8.0",
2222
"tslib": "^2.3.0",
2323
"zone.js": "~0.14.3"

examples/cdn/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"test": "echo Examples have no tests."
99
},
1010
"dependencies": {
11-
"@hcaptcha/vanilla-hcaptcha": "link:../../packages/vanilla-hcaptcha",
11+
"@hcaptcha/vanilla-hcaptcha": "workspace:*",
1212
"serve": "^13.0.2"
1313
}
1414
}

examples/cdn/vanilla.html

Lines changed: 250 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,277 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
56
<title>Demo hCaptcha Web Component - Vanilla</title>
7+
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
68
</head>
7-
<body>
8-
9-
<div>Open the console!</div>
9+
<body class="bg-gray-50 p-8 font-sans">
1010

1111
<!-- <script src="https://cdn.jsdelivr.net/npm/@hcaptcha/vanilla-hcaptcha" async defer></script>-->
1212
<script src="/node_modules/@hcaptcha/vanilla-hcaptcha/dist/index.min.js"></script>
1313

14-
<h-captcha id="signupCaptcha"
15-
site-key="781559eb-513a-4bae-8d29-d4af340e3624"
16-
host="example.com"
17-
size="normal"
18-
theme="dark"
19-
tabindex="0"></h-captcha>
14+
<!-- Page Header -->
15+
<div class="max-w-6xl mx-auto text-center mb-8">
16+
<h1 class="text-3xl font-bold text-gray-800 mb-2">hCaptcha Integration Demo</h1>
17+
<p class="text-gray-600 text-sm">Test different hCaptcha configurations and form submission</p>
18+
</div>
19+
20+
<!-- Main Content Grid -->
21+
<div class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-6">
22+
23+
<!-- Left Column: Configuration & Status -->
24+
<div class="space-y-6">
25+
26+
<!-- hCaptcha Configuration Section -->
27+
<div class="bg-white rounded-lg shadow-md p-6">
28+
<h2 class="text-lg font-semibold text-gray-800 mb-4">hCaptcha Configuration</h2>
29+
<div class="bg-gray-50 p-4 rounded-lg">
30+
<div class="space-y-2">
31+
<label class="text-sm font-medium text-gray-600">Size:</label>
32+
<div class="flex gap-4">
33+
<label class="flex items-center">
34+
<input type="radio" name="captchaSize" value="normal" class="mr-2" />
35+
<span class="text-sm">Normal</span>
36+
</label>
37+
<label class="flex items-center">
38+
<input type="radio" name="captchaSize" value="compact" class="mr-2" />
39+
<span class="text-sm">Compact</span>
40+
</label>
41+
<label class="flex items-center">
42+
<input type="radio" name="captchaSize" value="invisible" class="mr-2" checked />
43+
<span class="text-sm">Invisible</span>
44+
</label>
45+
</div>
46+
</div>
47+
48+
<!-- Manual Testing Buttons -->
49+
<div class="mt-4 pt-3 border-t border-gray-200">
50+
<label class="text-sm font-medium text-gray-600 mb-2 block">Manual Testing:</label>
51+
<div class="flex gap-2">
52+
<button onclick="executeHCaptcha()"
53+
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white text-xs font-medium py-2 px-3 rounded-md transition duration-200">
54+
EXECUTE
55+
</button>
56+
<button onclick="resetHCaptcha()"
57+
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white text-xs font-medium py-2 px-3 rounded-md transition duration-200">
58+
RESET
59+
</button>
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
65+
<!-- Status History Section -->
66+
<div id="formStatus" class="bg-white rounded-lg shadow-md p-6 space-y-2 hidden relative">
67+
<div class="flex justify-between items-center">
68+
<h4 class="text-lg font-semibold text-gray-800">Status History</h4>
69+
<button onclick="clearStatus()"
70+
class="text-gray-400 hover:text-gray-600 transition-colors duration-200 p-1 rounded"
71+
title="Clear status history">
72+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
73+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
74+
</svg>
75+
</button>
76+
</div>
77+
<div id="statusMessages" class="space-y-1 max-h-48 overflow-y-auto"></div>
78+
</div>
79+
</div>
80+
81+
<!-- Right Column: Contact Form -->
82+
<div>
83+
<div class="bg-white rounded-lg shadow-md p-6">
84+
<h3 class="text-lg font-semibold text-gray-800 mb-4">Contact Form</h3>
85+
86+
<form id="testForm" class="space-y-4">
87+
<div>
88+
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">Full Name:</label>
89+
<input type="text" id="name" name="name"
90+
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
91+
</div>
92+
93+
<div>
94+
<label for="message" class="block text-sm font-medium text-gray-700 mb-2">Message:</label>
95+
<textarea id="message" name="message" rows="4"
96+
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-vertical"></textarea>
97+
</div>
98+
99+
<div class="pt-2 border-t border-gray-200">
100+
<div class="mb-3">
101+
<h-captcha id="signupCaptcha"
102+
site-key="781559eb-513a-4bae-8d29-d4af340e3624"
103+
host="example.com"
104+
size="normal"
105+
theme="dark"
106+
recaptchacompat="false"
107+
tabindex="0"></h-captcha>
108+
</div>
109+
110+
<button onclick="executeHCaptcha()"
111+
class="h-captcha w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
112+
Submit Form
113+
</button>
114+
</div>
115+
</form>
116+
</div>
117+
</div>
118+
119+
</div>
20120

21-
<button onclick="executeHCaptcha()">EXECUTE</button>
22-
<button onclick="resetHCaptcha()">RESET</button>
23121

24122
<script>
25-
const signupCaptcha = document.getElementById('signupCaptcha');
123+
let signupCaptcha = document.getElementById('signupCaptcha');
124+
const testForm = document.getElementById('testForm');
125+
const formStatus = document.getElementById('formStatus');
126+
const sizeRadios = document.querySelectorAll('input[name="captchaSize"]');
26127

27128
signupCaptcha.setAttribute("jsapi", "https://js.hcaptcha.com/1/api.js");
28129

29-
signupCaptcha.addEventListener('loaded', () => {
30-
console.log('hCaptcha Component Loaded');
31-
// Safe to call `execute` once component is loaded
32-
// signupCaptcha.execute();
33-
});
34-
signupCaptcha.addEventListener('verified', (e) => {
35-
console.log('verified event', { token: e.token, eKey: e.eKey });
36-
});
37-
signupCaptcha.addEventListener('expired', () => {
38-
console.log('expired event');
130+
sizeRadios.forEach(radio => {
131+
radio.addEventListener('change', function() {
132+
if (this.checked) {
133+
updateCaptchaSize(this.value);
134+
}
135+
});
39136
});
40-
signupCaptcha.addEventListener('error', (e) => {
41-
console.log('error event', { error: e.error });
137+
138+
function attachHCaptchaEventListeners(captchaElement) {
139+
captchaElement.addEventListener('loaded', () => {
140+
showStatus('hCaptcha Component Loaded', 'info');
141+
showStatus('Ready for auto-triggering on form submit', 'info');
142+
});
143+
144+
captchaElement.addEventListener('verified', (e) => {
145+
showStatus('Auto-submitting form after hCaptcha verification...', 'info');
146+
147+
// Following is simulation of form submission.
148+
// You would usually call "formElement.submit()";
149+
150+
simulateFormSubmission(e.token);
151+
});
152+
153+
captchaElement.addEventListener('expired', () => {
154+
showStatus('hCaptcha expired', 'warning');
155+
showStatus('Please try submitting again.', 'warning');
156+
});
157+
158+
captchaElement.addEventListener('error', (e) => {
159+
showStatus(`hCaptcha error: ${e.error}`, 'error');
160+
showStatus('Please try again.', 'error');
161+
});
162+
}
163+
164+
function updateCaptchaSize(newSize) {
165+
// Store current attributes
166+
const currentAttributes = {};
167+
for (let i = 0; i < signupCaptcha.attributes.length; i++) {
168+
const attr = signupCaptcha.attributes[i];
169+
currentAttributes[attr.name] = attr.value;
170+
}
171+
172+
// Update size attribute
173+
currentAttributes.size = newSize;
174+
175+
// Get parent container
176+
const parentContainer = signupCaptcha.parentElement;
177+
178+
// Remove current element (triggers disconnectedCallback)
179+
signupCaptcha.remove();
180+
181+
// Create new h-captcha element
182+
const newCaptcha = document.createElement('h-captcha');
183+
184+
// Apply all attributes to new element
185+
Object.keys(currentAttributes).forEach(attrName => {
186+
newCaptcha.setAttribute(attrName, currentAttributes[attrName]);
187+
});
188+
189+
// Add new element to DOM (triggers connectedCallback and auto-render)
190+
parentContainer.appendChild(newCaptcha);
191+
192+
// Update global reference
193+
signupCaptcha = newCaptcha;
194+
195+
// Re-attach event listeners
196+
attachHCaptchaEventListeners(signupCaptcha);
197+
198+
showStatus(`hCaptcha re-rendered with size: ${newSize}`, 'success');
199+
}
200+
201+
const currentSize = signupCaptcha.getAttribute('size') || 'normal';
202+
document.querySelector(`input[value="${currentSize}"]`).checked = true;
203+
204+
// Attach initial event listeners
205+
attachHCaptchaEventListeners(signupCaptcha);
206+
207+
// Form submission triggers hCaptcha.
208+
testForm.addEventListener('submit', (e) => {
209+
// Prevent default to handle via hCaptcha flow.
210+
e.preventDefault();
211+
showStatus(`Form submission initiated...`, 'info');
42212
});
43213

214+
// Simulate actual form submission after hCaptcha success.
215+
function simulateFormSubmission(token) {
216+
const formData = new FormData(testForm);
217+
const formDataObj = Object.fromEntries(formData);
218+
const formDataStr = Object.keys(formDataObj).length > 0 ?
219+
Object.entries(formDataObj).map(([key, value]) => `${key}: "${value}"`).join(', ') :
220+
'No form data entered';
221+
showStatus('Form Data: ' + formDataStr, 'info');
222+
showStatus('hCaptcha Token: ' + token.substring(0, 20) + '...', 'info');
223+
showStatus('Form submitted successfully to server!', 'success');
224+
}
225+
44226
function executeHCaptcha() {
227+
showStatus('Executing hCaptcha...', 'info');
45228
signupCaptcha.execute();
46229
}
47230

48231
function resetHCaptcha() {
232+
showStatus('Resetting hCaptcha...', 'info');
49233
signupCaptcha.reset();
234+
showStatus('hCaptcha reset complete', 'info');
235+
}
236+
237+
function showStatus(message, type = 'info') {
238+
const statusMessages = document.getElementById('statusMessages');
239+
formStatus.classList.remove('hidden');
240+
const timestamp = new Date().toLocaleTimeString();
241+
const messageElement = document.createElement('div');
242+
messageElement.className = 'text-xs p-2 rounded border-l-4 block w-full';
243+
let icon = '';
244+
switch(type) {
245+
case 'success':
246+
messageElement.classList.add('bg-green-50', 'text-green-800', 'border-l-green-400');
247+
icon = '✅';
248+
break;
249+
case 'error':
250+
messageElement.classList.add('bg-red-50', 'text-red-800', 'border-l-red-400');
251+
icon = '❌';
252+
break;
253+
case 'warning':
254+
messageElement.classList.add('bg-yellow-50', 'text-yellow-800', 'border-l-yellow-400');
255+
icon = '⚠️';
256+
break;
257+
default: // info
258+
messageElement.classList.add('bg-blue-50', 'text-blue-800', 'border-l-blue-400');
259+
icon = 'ℹ️';
260+
}
261+
262+
messageElement.innerHTML = `<span class="text-gray-500">[${timestamp}]</span> ${icon} ${message}`;
263+
statusMessages.insertBefore(messageElement, statusMessages.firstChild);
264+
statusMessages.scrollTop = 0;
265+
266+
while (statusMessages.children.length > 15) {
267+
statusMessages.removeChild(statusMessages.lastChild);
268+
}
269+
}
270+
271+
function clearStatus() {
272+
const statusMessages = document.getElementById('statusMessages');
273+
statusMessages.innerHTML = '';
274+
formStatus.classList.add('hidden');
275+
showStatus('Status history cleared', 'info');
50276
}
51277
</script>
52278
</body>

0 commit comments

Comments
 (0)