Form handling, serialization and validation
Read every input type correctly, serialise a form including files, intercept submission, and get a clear picture of what the Validate plugin adds.
Reading every kind of control
// Text, textarea, select, number, date — all one accessor
$('#email').val();
// Checkboxes and radios: read the group, not the individual input
$('input[name="notify"]:checked').map((_, el) => el.value).get(); // ['email', 'sms']
$('#terms').prop('checked'); // boolean
// A single checkbox with no value attribute reports 'on' — always set value in markup
console.log($('#newsletter').val()); // 'on' unless value="yes" is present
// Multi-select returns an array
$('#tags').val(); // ['red', 'blue']
// File inputs: .val() returns a fake path; use the DOM property for real Files
const files = $('#attachment')[0].files; // FileList
const first = files[0] ?? null;
console.log(first?.name, first?.size, first?.type);
// Files can only be appended to FormData; they cannot be serialised
const formData = new FormData($('#upload')[0]);
formData.append('attachment', first);| Control | Read with | Returns |
|---|---|---|
input[type=text] | .val() | String |
input[type=number] | .val() then parse | String — never a number |
input[type=checkbox] (group) | :checked + map | Array of values |
input[type=radio] | :checked | Zero or one value |
select | .val() | String, or an array when multiple |
input[type=file] | [0].files | FileList |
input[type=range] | .val() | String — parse it |
⚠️
.val() always returns a string or an array of strings. Summing form values without a parse is the classic bug: '1' + '2' is '12', and a validation check like value > 100 compares a string against a number. Parse at the boundary and validate the parsed value.Serialization
// serialize() -> a query string. Only successful controls are included.
$('#signup').serialize();
// "email=a%40b.test&plan=team¬ify=email¬ify=sms"
// serializeArray() -> an array of name/value pairs. Better when names repeat.
$('#signup').serializeArray();
// [{ name: 'email', value: 'a@b.test' }, { name: 'notify', value: 'email' }, ...]
// Turning that array into an object loses repeated names — handle them deliberately.
function toObject(pairs) {
return pairs.reduce((acc, { name, value }) => {
if (name in acc) {
acc[name] = [].concat(acc[name], value); // collect repeats into an array
} else {
acc[name] = value;
}
return acc;
}, {});
}
console.log(toObject($('#signup').serializeArray()));
// What is excluded: disabled controls, unchecked boxes, inputs with no name,
// and file inputs. Submit buttons are excluded unless they were the trigger.
// A <fieldset disabled> excludes everything inside it.
// Files must go through FormData instead:
const fd = new FormData(document.getElementById('signup'));
fetch('/api/signup', { method: 'POST', body: fd })
.then((res) => res.json())
.then(console.log);- Unchecked checkboxes are simply absent, not
false. The server must interpret "missing" as false; do not require a default in the form. - A control with a duplicate name serialises twice, which is correct for a checkbox group and usually wrong for a text input.
- Serialisation uses
encodeURIComponentsemantics, so values with newlines and ampersands survive the round trip. FormDatafrom a form element is the modern equivalent and handles files, which the jQuery helpers never did.
Submission, validation and the plugin
// Intercepting submission: always preventDefault, then decide.
$('#signup').on('submit', function (event) {
event.preventDefault();
const $form = $(this);
// The browser's own checks first — free, and it focuses the first problem.
if (!$form[0].checkValidity()) {
$form[0].reportValidity();
return;
}
const payload = toObject($form.serializeArray());
delete payload.passwordConfirm; // never send the confirmation
$form.find(':submit').prop('disabled', true); // stop a double submit
fetch($form.attr('action'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(async (res) => {
if (res.status === 422) {
const { errors } = await res.json(); // { email: 'Already registered' }
$form.find('.field-error').text(''); // clear previous
Object.entries(errors).forEach(([field, message]) => {
$form.find(`[name="${field}"]`).addClass('is-invalid')
.attr('aria-invalid', 'true')
.siblings('.field-error').text(message);
});
$form.find('[aria-invalid="true"]').first().trigger('focus');
return;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
window.location.assign('/welcome');
})
.catch((error) => $form.find('.form-error-summary').text(error.message).show())
.finally(() => $form.find(':submit').prop('disabled', false));
});| jQuery Validate feature | What it gives you | Cost |
|---|---|---|
rules option | Declarative per-field constraints | A second source of truth beside server rules |
messages option | Custom error text | I18n needs a message catalogue |
remote rule | Async check such as a taken username | One request per keystroke unless debounced |
| Custom methods | $.validator.addMethod | You are maintaining a validation DSL |
valid() / element() | Programmatic checks | Useful when the submit is triggered by a wizard |
| Unobtrusive integration | Reads HTML5 attributes | Actively discouraged in jQuery Validate's current documentation |
| Nothing | The Constraint Validation API | Free, no dependency, already in every browser |
The honest position on jQuery Validate: it earns its place only when the form is complex enough to need cross-field rules in a DSL and the project is already committed to jQuery. For a straightforward signup form, the Constraint Validation API plus server-side validation is less code and less maintenance.
FAQ
Why are my unchecked checkboxes missing from the data?
That is how HTML form submission has always worked: an unchecked checkbox is not a successful control, so it is absent. Either treat absence as false on the server, or add a hidden input with the same name and a default value before the checkbox.
How do I validate before enabling the submit button?
Listen to
input and change on the form, run form.checkValidity(), and toggle the button's disabled property. Debounce if the form is large, and make sure the button's disabled state is not the only place the reason is communicated.Related
Attributes, properties and the data cache Deferreds, promises and async patterns
Last refreshed 2026-09-18.