JavaScript Forms and DOM

Read and modify form values, handle user input in real time, submit data with fetch, and validate forms with the Constraint Validation API.

Beginner 11 min read 10 examples

Form Elements

JavaScript can access form elements through document.querySelector or directly via the form's named properties.

JavaScript
// 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

JavaScript
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.

JavaScript
// 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

JavaScript
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

JavaScript
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.

JavaScript
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.

JavaScript
// 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

JavaScript
// 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');
});
Always validate server-side too

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.

Frequently Asked Questions

The input event fires on every keystroke or value change immediately as the user types. The change event fires when the element loses focus (blur) after its value has changed for text inputs, or immediately when a checkbox/radio/select changes. Use input for real-time feedback (live search, character count) and change for final value commitment.

Listen for the submit event on the form element and call event.preventDefault() to stop the default page reload. Then collect form data with new FormData(formElement) or manually build a JSON object, and send it with fetch(). Always validate before sending and handle errors from the server response.

HTML attributes like required, minlength, pattern, type="email" define constraints. The browser checks these automatically on form submit and also exposes them via JS: input.validity.valid (boolean), input.validationMessage (browser error string), input.checkValidity() (fires invalid event and returns boolean), and input.setCustomValidity("msg") to set custom errors. Use novalidate on the form to disable browser UI and build your own.

FormData is a built-in class that collects all named form fields into a key-value structure, including file inputs. Use it when submitting multipart/form-data (especially with file uploads) since it handles encoding automatically. For JSON APIs, you can also convert it: Object.fromEntries(new FormData(form)) gives a plain object (but loses multiple values for the same name - use formData.getAll("tags") for multi-select).