Skip to Content
Course content

183: Building a Client-Side Form Validator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Alright, let's get into it. We've talked about DOM manipulation and event listeners, but now we're going to put them together for something you'll do in almost every professional project: form validation. I don't want to just give you a finished function to copy-paste. Instead, let's build a newsletter signup form from scratch and see where we trip up.

The "Wait, where did my data go?" moment

I'll start with a basic setup. I've got an email input and a "Terms of Service" checkbox. My goal is simple: don't let the form submit if the email is empty or the box isn't checked. Here is my first attempt at the JavaScript:

const form = document.querySelector('#signup-form');

form.addEventListener('submit', (e) => {
    const email = document.querySelector('#email').value;
    const terms = document.querySelector('#terms').checked;

    if (!email || !terms) {
        console.log('Something is missing!');
    } else {
        console.log('Form is valid! Sending data...');
    }
});

I hit "Submit" with an empty email field. I see "Something is missing!" in the console for a split second, and then... the page refreshes and the console clears. Everything is gone. Why? Because that's the default behavior of an HTML form—it tries to send a GET or POST request to a server and reloads the page. To stop this and handle the validation ourselves, we need to tell the browser to stay put.

form.addEventListener('submit', (e) => {
    e.preventDefault(); // This is the magic line.
    
    const email = document.querySelector('#email').value;
    // ... the rest of the logic
});

Hardcoding our way into a headache

Now the page stays put, and my console logs are working. But let's look at the logic. Right now, I'm just checking if the values exist. That's not real validation. I need to make sure the email actually looks like an email. I'll throw in a Regular Expression (regex) for that.

if (!email) {
    alert('Email is required');
} else if (!/\S+@\S+\.\S+/.test(email)) {
    alert('Email is invalid');
} else if (!terms) {
    alert('You must agree to the terms');
}

This works, but honestly? It's gross. If this form grows from two fields to twenty, I'm going to have a mountain of else if statements. It's hard to read and even harder to maintain. I've seen juniors do this in production code, and it's a nightmare to debug. We need a way to separate the rules from the execution.

Cleaning up the logic with a rules object

I want a system where I can just define what "valid" means for each field. Let's try creating a mapping of input names to validation functions. This way, the logic for "how to validate an email" is separate from the "submit" event.

const validationRules = {
    email: (value) => {
        return /\S+@\S+\.\S+/.test(value) ? null : 'Please enter a valid email address';
    },
    terms: (value, element) => {
        return element.checked ? null : 'You must accept the terms';
    }
};

form.addEventListener('submit', (e) => {
    e.preventDefault();
    let isFormValid = true;

    // I'll loop through the rules we defined
    for (const field in validationRules) {
        const input = document.querySelector(`#${field}`);
        const errorMessage = validationRules[field](input.value, input);

        if (errorMessage) {
            isFormValid = false;
            console.log(`${field}: ${errorMessage}`);
        }
    }

    if (isFormValid) {
        alert('Success!');
    }
});

This is much better. If I add a "Username" field tomorrow, I just add one line to the validationRules object. The loop handles the rest. I'm treating the validation as data rather than a sequence of commands.

Making it look like a real error

The only problem left is the user experience. console.log and alert are for developers, not users. I want the error message to appear directly under the input field in red text. To do this, I'll need to dynamically create or update an error element in the DOM.

I'll modify the loop to find a sibling element for the error message. If it doesn't exist, I'll create it. If the field is now valid, I'll remove the message.

const showError = (input, message) => {
    const wrapper = input.parentElement;
    let errorDisplay = wrapper.querySelector('.error-msg');

    if (!errorDisplay) {
        errorDisplay = document.createElement('span');
        errorDisplay.className = 'error-msg';
        errorDisplay.style.color = 'red';
        errorDisplay.style.fontSize = '12px';
        wrapper.appendChild(errorDisplay);
    }
    errorDisplay.textContent = message;
    input.classList.add('input-error');
};

const clearError = (input) => {
    const wrapper = input.parentElement;
    const errorDisplay = wrapper.querySelector('.error-msg');
    if (errorDisplay) errorDisplay.remove();
    input.classList.remove('input-error');
};

Now, inside my for...in loop, instead of console.log, I just call showError(input, errorMessage) or clearError(input). By separating the UI logic (showing the error) from the validation logic (checking the regex), we've built a system that's flexible, scalable, and actually usable.




📋 Practical Task

Exercise: The "Secure Password" Validator

Build a form with two fields: "Password" and "Confirm Password". Implement a client-side validator using the "rules object" pattern explored in the lesson. Your validator must enforce the following:

  • The Password must be at least 8 characters long.
  • The Password must contain at least one number.
  • The Confirm Password field must match the Password field exactly.

Ensure that the form does not submit if any of these conditions are unmet, and display specific error messages under each input field rather than using alerts.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.