Form Elements
JavaScript can access form elements through document.querySelector or directly via the form's named properties.
// Select the form
const form = document.querySelector('#signup-form');
// Access fields by name via the form's elements collection
const emailInput = form.elements['email']; // or form.elements.email
const passInput = form.elements['password'];
// Or query directly
const nameInput = document.querySelector('input[name="username"]');
// HTMLFormControlsCollection - live collection of all form controls
console.log(form.elements.length); // number of controls
// Form properties
console.log(form.action); // form's action URL
console.log(form.method); // 'get' or 'post'
console.log(form.name); // form's name attribute
// Programmatic submit and reset
form.submit(); // submits without firing 'submit' event - prefer requestSubmit()
form.requestSubmit(); // fires 'submit' event and runs validation
form.reset(); // resets all fields to default values
Input Events
const searchInput = document.querySelector('#search');
// input - fires on every change (keystroke, paste, cut, voice input)
searchInput.addEventListener('input', function(e) {
console.log('Current value:', e.target.value);
performSearch(e.target.value);
});
// change - fires when value is committed (blur for text, instant for select/checkbox)
searchInput.addEventListener('change', function(e) {
console.log('Committed value:', e.target.value);
});
// focus / blur - element gains or loses keyboard focus
searchInput.addEventListener('focus', () => searchInput.classList.add('focused'));
searchInput.addEventListener('blur', () => searchInput.classList.remove('focused'));
// Debounce: delay processing until user stops typing
let debounceTimer;
searchInput.addEventListener('input', function(e) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetchSearchResults(e.target.value); // only fires 300ms after last keystroke
}, 300);
});
// keydown for keyboard shortcuts / preventing input
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
this.value = '';
this.blur();
}
if (e.key === 'Enter') {
e.preventDefault(); // don't submit form on Enter in a text field
submitSearch(this.value);
}
});
Reading Form Values
Different input types need different properties to read their current value.
// Text, email, password, textarea - use .value
const email = document.querySelector('input[name="email"]').value.trim();
// Number input - .value returns a string, always parse
const age = parseInt(document.querySelector('input[type="number"]').value, 10);
const price = parseFloat(document.querySelector('input[name="price"]').value);
// Checkbox - use .checked (boolean)
const agreed = document.querySelector('input[name="agree"]').checked;
// Radio buttons - find the checked one
const genderInput = document.querySelector('input[name="gender"]:checked');
const gender = genderInput ? genderInput.value : null;
// Select (single) - use .value
const country = document.querySelector('select[name="country"]').value;
// Select (multiple) - collect all selected options
const multiSelect = document.querySelector('select[name="tags"]');
const selectedTags = Array.from(multiSelect.selectedOptions).map(opt => opt.value);
// File input - access the FileList
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0]; // first selected file
if (file) {
console.log(file.name, file.size, file.type);
}
// Range input
const volume = document.querySelector('input[type="range"]').value; // '0' to '100'
Submit Handling
const form = document.querySelector('#contact-form');
form.addEventListener('submit', async function(e) {
e.preventDefault(); // stop default page reload
// Collect values
const data = {
name: form.elements['name'].value.trim(),
email: form.elements['email'].value.trim(),
message: form.elements['message'].value.trim(),
};
// Basic client-side check (HTML validation should handle most)
if (!data.name || !data.email || !data.message) {
showError('All fields are required.');
return;
}
// Show loading state
const submitBtn = form.querySelector('[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Server error: ' + response.status);
showSuccess('Message sent!');
form.reset();
} catch (err) {
showError('Failed to send. Please try again.');
console.error(err);
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Send';
}
});
FormData API
const form = document.querySelector('#upload-form');
form.addEventListener('submit', async function(e) {
e.preventDefault();
// FormData automatically collects all named fields including files
const formData = new FormData(form);
// Read values
console.log(formData.get('email')); // first value for 'email'
console.log(formData.getAll('interests')); // array (for multi-select/checkboxes)
console.log(formData.has('avatar')); // boolean
formData.set('timestamp', Date.now()); // add extra fields
formData.delete('csrf_token'); // remove a field
// Send as multipart/form-data (best for file uploads)
await fetch('/api/upload', {
method: 'POST',
body: formData // DO NOT set Content-Type header - browser sets boundary automatically
});
});
// Convert to plain object for JSON (no file inputs)
function formToObject(form) {
const fd = new FormData(form);
const obj = {};
for (const [key, value] of fd.entries()) {
if (key in obj) {
// Handle multiple values (checkboxes with same name)
obj[key] = [].concat(obj[key], value);
} else {
obj[key] = value;
}
}
return obj;
}
// Or use Object.fromEntries (loses duplicate keys)
const simple = Object.fromEntries(new FormData(form));
Validation API
The Constraint Validation API exposes HTML validation rules (required, minlength, pattern) through JavaScript without reimplementing them manually.
const emailInput = document.querySelector('input[type="email"]');
// validity object - flags for each constraint
console.log(emailInput.validity.valueMissing); // true if required and empty
console.log(emailInput.validity.typeMismatch); // true if not valid email format
console.log(emailInput.validity.tooShort); // true if below minlength
console.log(emailInput.validity.tooLong); // true if above maxlength
console.log(emailInput.validity.patternMismatch);// true if pattern attribute fails
console.log(emailInput.validity.rangeUnderflow); // true if below min (number)
console.log(emailInput.validity.rangeOverflow); // true if above max (number)
console.log(emailInput.validity.valid); // true only if ALL constraints pass
// validationMessage - browser's localized error string
console.log(emailInput.validationMessage); // 'Please enter an email address.'
// checkValidity() - returns boolean and fires 'invalid' event if invalid
if (!emailInput.checkValidity()) {
console.log('Invalid:', emailInput.validationMessage);
}
// reportValidity() - same as checkValidity() + shows browser error tooltip
emailInput.reportValidity();
// setCustomValidity() - set or clear a custom error
emailInput.setCustomValidity('This email is already registered.');
// Clear with empty string to mark as valid again
emailInput.setCustomValidity('');
// Validate entire form
const form = document.querySelector('form');
if (!form.checkValidity()) {
console.log('Form has errors');
// form.reportValidity(); would show browser tooltips for invalid fields
}
Custom Validation UI
Use novalidate on the form to disable browser bubbles, then build your own error display with full control.
// HTML: <form id="reg-form" novalidate>
const form = document.querySelector('#reg-form');
function showFieldError(input, message) {
clearFieldError(input);
input.classList.add('is-invalid');
const error = document.createElement('div');
error.className = 'invalid-feedback';
error.textContent = message;
input.after(error);
}
function clearFieldError(input) {
input.classList.remove('is-invalid');
input.nextElementSibling?.classList.contains('invalid-feedback')
&& input.nextElementSibling.remove();
}
function validateField(input) {
clearFieldError(input);
if (input.validity.valueMissing) {
showFieldError(input, `${input.labels[0]?.textContent ?? 'This field'} is required.`);
return false;
}
if (input.validity.typeMismatch) {
showFieldError(input, 'Please enter a valid ' + input.type + '.');
return false;
}
if (input.validity.tooShort) {
showFieldError(input, `Minimum ${input.minLength} characters required.`);
return false;
}
// Custom async check (e.g., email already taken)
return true;
}
// Validate on blur for immediate feedback
form.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('blur', () => validateField(input));
});
form.addEventListener('submit', function(e) {
e.preventDefault();
const inputs = Array.from(form.querySelectorAll('input, select, textarea'));
const allValid = inputs.every(validateField);
if (allValid) submitForm();
});
Dynamic Form Patterns
// Pattern 1: Show/hide fields based on selection
document.querySelector('select[name="role"]').addEventListener('change', function() {
const adminPanel = document.querySelector('#admin-options');
adminPanel.hidden = this.value !== 'admin';
});
// Pattern 2: Character counter
const bio = document.querySelector('textarea[name="bio"]');
const counter = document.querySelector('#bio-counter');
bio.addEventListener('input', function() {
const remaining = 280 - this.value.length;
counter.textContent = remaining + ' characters remaining';
counter.classList.toggle('text-danger', remaining < 20);
});
// Pattern 3: Add/remove dynamic fields (e.g., add more email inputs)
let fieldCount = 1;
document.querySelector('#add-email').addEventListener('click', function() {
fieldCount++;
const wrapper = document.createElement('div');
wrapper.className = 'field-row';
wrapper.innerHTML = `
<input type="email" name="emails[]" placeholder="Email ${fieldCount}">
<button type="button" class="btn-remove">Remove</button>
`;
document.querySelector('#email-fields').append(wrapper);
});
// Event delegation for remove buttons
document.querySelector('#email-fields').addEventListener('click', function(e) {
if (e.target.matches('.btn-remove')) {
e.target.closest('.field-row').remove();
}
});
// Pattern 4: Auto-format input as user types (phone number)
document.querySelector('input[name="phone"]').addEventListener('input', function() {
const digits = this.value.replace(/\D/g, '').slice(0, 10);
this.value = digits.replace(/(\d{3})(\d{3})(\d{4})/, '$1-$2-$3');
});
Client-side validation improves UX but is never a security boundary. Users can bypass JavaScript entirely. Always validate and sanitize all input on the server before saving to a database or executing any logic.