JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
183: Building a Client-Side Form Validator
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.
There are no comments for now.