-
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)
155: Practice Exercise: Building a Form Validation Library
By now, you've probably written a few scripts that check if an email field is empty or if a password is long enough. That works for one form, but once you're building a real application with ten different forms, you'll realize you're copy-pasting the same logic everywhere. That's why we're building a library—a centralized piece of logic that can handle validation for any form you throw at it.
How do I make the library reusable instead of just writing a bunch of if-statements?
The secret is to stop thinking about "the email field" and start thinking about "rules." Instead of hardcoding logic, I recommend using a configuration object (a schema) that tells the library which rules apply to which fields. I usually go with a strategy pattern here, where each rule is just a function that returns true or false.
const validationRules = {
required: (value) => value.trim() !== '',
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
minLength: (value, length) => value.length >= length,
};
// Then you define your form's specific needs
const registrationSchema = {
username: { rules: ['required', 'minLength:5'] },
email: { rules: ['required', 'email'] },
};
Notice how minLength:5 is a string? You'll need to split that string in your library logic to extract the "5" and pass it as an argument to the rule function. It's a bit more setup upfront, but it means you never have to write another if (username.length < 5) block again.
Should the library handle showing the error messages in the HTML?
In my experience, this is where most developers trip up. They make the library search for .error-message spans and inject text. Don't do that. It ties your logic to a specific HTML structure, which makes the library useless the moment you change your CSS framework or move to a different project.
Instead, have your library return a "result" object. Let the code that calls the library decide how to display the errors. It should look something like this:
const results = validator.validate(formData, registrationSchema);
if (!results.isValid) {
// The library just tells you WHAT is wrong;
// you decide HOW to show it.
Object.keys(results.errors).forEach(field => {
const input = document.querySelector(`[name="${field}"]`);
input.classList.add('input-error');
showTooltip(input, results.errors[field]);
});
}
What about complex rules, like making sure two password fields match?
Standard rules like "required" are easy, but "Password Confirmation" is a different beast because it depends on the value of another field. To handle this, your validation function needs access to the entire form data object, not just the single value of the field being checked.
I usually allow the schema to accept a custom function as a rule. If the rule is a function, the library just executes it and passes in the full data set. It looks like this in practice:
const registrationSchema = {
password: { rules: ['required'] },
confirmPassword: {
rules: [
'required',
(value, allValues) => value === allValues.password
],
message: 'Passwords do not match!'
},
};
By passing allValues, you give the validator the context it needs to perform cross-field validation without hardcoding specific field names into the library's core engine.
📋 Practical Task
Exercise: Building a Multi-Field User Registration Validator
Your task is to build a small validation library and implement it on a registration form. Your library must be decoupled from the DOM.
- The Library: Create a
FormValidatorclass or object that takes a schema and a data object. It should return an object containingisValid(boolean) anderrors(an object mapping field names to error messages). - The Rules: Implement at least three built-in rules:
required,email, andminLength. - Custom Logic: Implement a "password match" check using a custom function in the schema to ensure the
passwordandconfirmPasswordfields are identical. - The Implementation: Write a script that intercepts a form submit event, gathers the input values into an object, passes them to your library, and then manually updates the DOM to show errors only for the fields that failed.
There are no comments for now.