Skip to content

Commit d2db0d3

Browse files
author
gauthier witkowski
committed
Improve form validation
1 parent 4fb5da9 commit d2db0d3

File tree

1 file changed

+81
-55
lines changed

1 file changed

+81
-55
lines changed

AjaxForm.js

Lines changed: 81 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,119 +7,145 @@
77
* Author: Raspgot
88
*/
99

10-
const RECAPTCHA_SITE_KEY = 'YOUR_RECAPTCHA_SITE_KEY'; // Replace with your public reCAPTCHA key
10+
const RECAPTCHA_SITE_KEY = 'YOUR_RECAPTCHA_SITE_KEY'; // Replace with your public reCAPTCHA site key
1111

1212
document.addEventListener('DOMContentLoaded', () => {
1313
'use strict';
1414

15-
// Get the form element that uses Bootstrap's validation classes
1615
const form = document.querySelector('.needs-validation');
17-
if (!form) return; // Stop if no form is found on the page
16+
if (!form) return;
1817

19-
// Select additional elements: loading spinner, submit button, and alert box
2018
const spinner = document.getElementById('loading-spinner');
2119
const submitButton = form.querySelector('button[type="submit"]');
2220
const alertContainer = document.getElementById('alert-status');
2321

24-
/**
25-
* Enable live validation as the user types or selects options
26-
*/
27-
form.querySelectorAll('input, select, textarea').forEach((field) => {
28-
const eventType = field.tagName === 'SELECT' ? 'change' : 'input';
22+
let inFlight = false;
2923

30-
field.addEventListener(eventType, () => {
24+
// Live validation
25+
form.querySelectorAll('input, select, textarea').forEach((field) => {
26+
field.addEventListener(field.tagName === 'SELECT' ? 'change' : 'input', () => {
3127
if (!field.value.trim()) {
32-
// Clear feedback if field is empty
3328
field.classList.remove('is-valid', 'is-invalid');
3429
} else if (field.checkValidity()) {
35-
// Field is valid
3630
field.classList.add('is-valid');
3731
field.classList.remove('is-invalid');
3832
} else {
39-
// Field is invalid
4033
field.classList.add('is-invalid');
4134
field.classList.remove('is-valid');
4235
}
4336
});
4437
});
4538

46-
/**
47-
* Handle form submission with AJAX
48-
*/
39+
// Handle form submission with AJAX
4940
form.addEventListener('submit', async (event) => {
50-
event.preventDefault(); // Stop the default form submission
51-
event.stopPropagation(); // Prevent the event from bubbling up
41+
event.preventDefault();
42+
event.stopPropagation();
5243

53-
// Clear any previous validation feedback
44+
if (inFlight) return;
45+
46+
form.classList.remove('was-validated');
5447
form.querySelectorAll('.is-valid, .is-invalid').forEach((el) => el.classList.remove('is-valid', 'is-invalid'));
5548

56-
// Check HTML5 field validity (required, pattern, etc.)
49+
// Native HTML5 validity check
5750
if (!form.checkValidity()) {
58-
form.classList.add('was-validated'); // Bootstrap validation feedback
59-
form.querySelector(':invalid')?.focus(); // Focus first invalid input
51+
form.classList.add('was-validated');
52+
form.querySelector(':invalid')?.focus();
6053
return;
6154
}
6255

63-
// Show loading spinner and disable the submit button
64-
spinner.classList.remove('d-none');
65-
submitButton.disabled = true;
56+
const formData = new FormData(form);
57+
const endpoint = 'AjaxForm.php';
58+
59+
// Show loading spinner and disable form
60+
if (spinner) spinner.classList.remove('d-none');
61+
if (submitButton) submitButton.disabled = true;
62+
form.querySelectorAll('input, select, textarea, button').forEach((el) => {
63+
if (el !== submitButton) el.disabled = true;
64+
});
65+
inFlight = true;
6666

6767
try {
68+
if (!RECAPTCHA_SITE_KEY || RECAPTCHA_SITE_KEY === 'YOUR_RECAPTCHA_SITE_KEY') {
69+
throw new Error('⚠️ Missing reCAPTCHA site key.');
70+
}
71+
if (typeof grecaptcha === 'undefined' || !grecaptcha?.ready) {
72+
throw new Error('⚠️ reCAPTCHA not loaded.');
73+
}
74+
6875
// Wait for reCAPTCHA to be ready and get the token
69-
const token = await new Promise((resolve) => {
70-
grecaptcha.ready(() => {
71-
grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'submit' }).then(resolve);
72-
});
76+
const token = await new Promise((resolve, reject) => {
77+
try {
78+
grecaptcha.ready(() => {
79+
grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'submit' }).then(resolve).catch(reject);
80+
});
81+
} catch (e) {
82+
reject(e);
83+
}
7384
});
7485

75-
// Prepare form data, including the reCAPTCHA token
76-
const formData = new FormData(form);
86+
// Append token to input form
7787
formData.append('recaptcha_token', token);
7888

7989
// Send data using Fetch API (AJAX)
80-
const response = await fetch('AjaxForm.php', {
90+
const response = await fetch(endpoint, {
8191
method: 'POST',
8292
body: formData,
93+
headers: { Accept: 'application/json' },
8394
});
8495

85-
// If server responded with an error (like 500), throw an exception
86-
if (!response.ok) throw new Error('Network error: ' + response.status);
96+
if (!response.ok) {
97+
throw new Error(`⚠️ Network error: ${response.status}`);
98+
}
99+
100+
let result;
101+
try {
102+
result = await response.json();
103+
} catch {
104+
throw new Error('⚠️ Invalid JSON response.');
105+
}
87106

88-
// Parse the JSON response from the server
89-
const result = await response.json();
90-
const { message, success, field } = result;
91-
const alertType = success ? 'success' : 'danger';
107+
const success = !!result?.success;
108+
const message = result?.message || (success ? 'Success.' : 'An error occurred.');
109+
const field = result?.field;
92110

93-
// Highlight the invalid field if the server specified one
111+
// Highlight the invalid field
94112
if (field) {
95-
const fieldEl = form.querySelector(`[name="${field}"]`);
96-
if (fieldEl) {
97-
fieldEl.classList.add('is-invalid');
98-
fieldEl.reportValidity(); // Show native tooltip message
99-
fieldEl.focus();
113+
const target = form.querySelector(`[name="${CSS.escape(field)}"]`);
114+
if (target) {
115+
target.classList.add('is-invalid');
116+
target.focus();
100117
form.classList.remove('was-validated');
101118
}
102119
}
103120

104-
// Display the success or error message
105-
alertContainer.className = `alert alert-${alertType} fade show`;
106-
alertContainer.textContent = message;
107-
alertContainer.classList.remove('d-none');
108-
alertContainer.scrollIntoView({ behavior: 'smooth' });
121+
if (alertContainer) {
122+
alertContainer.className = `alert alert-${success ? 'success' : 'danger'} fade show`;
123+
alertContainer.textContent = message;
124+
alertContainer.classList.remove('d-none');
125+
alertContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
126+
}
109127

110128
// If the form was submitted successfully, reset it
111129
if (success) {
112130
form.reset();
113131
form.classList.remove('was-validated');
114132
form.querySelectorAll('.is-valid, .is-invalid').forEach((el) => el.classList.remove('is-valid', 'is-invalid'));
115133
}
116-
} catch (error) {
117-
// Log unexpected errors (network, parse errors, etc.)
118-
console.error('An error occurred:', error);
134+
} catch (err) {
135+
console.error(err);
136+
if (alertContainer) {
137+
alertContainer.className = 'alert alert-danger fade show';
138+
alertContainer.textContent = err?.message || 'Unexpected error.';
139+
alertContainer.classList.remove('d-none');
140+
}
119141
} finally {
120-
// Always hide the spinner and re-enable the submit button
121-
spinner.classList.add('d-none');
122-
submitButton.disabled = false;
142+
// Hide loading spinner and enable form
143+
if (spinner) spinner.classList.add('d-none');
144+
if (submitButton) submitButton.disabled = false;
145+
form.querySelectorAll('input, select, textarea, button').forEach((el) => {
146+
if (el !== submitButton) el.disabled = false;
147+
});
148+
inFlight = false;
123149
}
124150
});
125151
});

0 commit comments

Comments
 (0)