Code refactor
Introduction
Section titled โIntroductionโOpen index.js
and take a look at how many lines of code you have written so far.
If you have been following the previous guides, you will have around 100 lines of code, and weโve not even finished the project yet!
This is a common problem when coding - as you add more features to your project, your codebase grows and becomes harder to manage.
One way to solve this problem is to refactor your code.
Refactoring is the process of restructuring your code without changing its external behaviour.
In this guide, we will refactor our code by splitting our large functions into smaller, more manageable modules.
Extracting validation logic
Section titled โExtracting validation logicโThe vast majority of our code is dedicated to validating the form data. Letโs extract this logic into a separate module.
Create a new module
Section titled โCreate a new moduleโ-
Create a new file called
validateForm.js
in your project directory. -
Inside this new file, create a function called
validateForm
that takes the form data as an argument, andexport
this from the file:validateForm.js export function validateForm({ userEmail, userLevel, userHours }) {// Validation logic here} -
Next, go back to your
index.js
file.Start by cutting the
maxHoursPerLevel
object from the top of thehandleSubmit
function:index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();const maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = document.querySelector("#hoursPerWeek").value;74 collapsed lines// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}if (Object.keys(errors).length > 0) {return;}console.log({ errors });});// Calculate the total cost// Display the total cost to the userPaste this into your new
validateForm.js
module, and ensure it is placed between the function curly braces:validateForm.js export function validateForm({ userEmail, userLevel, userHours }) {const maxHoursPerLevel = {basic: 5,advanced: 10,};// Validation logic here} -
Back inside
index.js
, cut the validation logic from thehandleSubmit
function.index.js // Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {8 collapsed linesevent.preventDefault();// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = document.querySelector("#hoursPerWeek").value;// Validate the user's inputlet errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}if (Object.keys(errors).length > 0) {return;}console.log({ errors });});// Calculate the total cost// Display the total cost to the user -
With the code still in your clipboard memory, navigate across to
validateForm.js
and paste it directly into your new function.Take care to ensure the code is placed between the curly braces of the function, and that you have not missed any lines:
validateForm.js export function validateForm({ userEmail, userLevel, userHours }) {const maxHoursPerLevel = {basic: 5,advanced: 10,};let errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}if (Object.keys(errors).length > 0) {return;}} -
Next, letโs separate the logic further.
Currently, we have a function called
validateForm
, but itโs actually doing two things:- Validating the form data
- Displaying the error messages on the form
This means it is violating a key programming principle: the Single Responsibility Principle (SRP).
Letโs separate the two concerns of
validateForm
into two separate functions.At the very top of
validateForm.js
, create a new function calleddisplayErrors
that takes theerrors
object as an argument:validateForm.js export function displayErrors(errors) {}export function validateForm({ userEmail, userLevel, userHours }) {73 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};let errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}if (Object.keys(errors).length > 0) {return;}}With this in place, cut the error handling logic from the
validateForm
function and paste it into the newdisplayErrors
function:validateForm.js export function displayErrors(errors) {for (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}}export function validateForm({ userEmail, userLevel, userHours }) {43 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};let errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}// Add error class to input elements with errorsfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}if (Object.keys(errors).length > 0) {return;}} -
Return
Section titled โReturn false if form data is invalidโfalse
if form data is invalidNow, letโs call the new
displayErrors
function if any errors exist, and returnfalse
from thevalidateForm
function to indicate that the form data is invalid:validateForm.js export function displayErrors(errors) {24 collapsed linesfor (let field in errors) {let inputElement = document.querySelector(`#${field}`);let labelElement = document.querySelector(`label[for=${field}]`);if (inputElement) {inputElement.classList.add("error-input");}if (labelElement) {labelElement.classList.add("error-label");}// Populate the error message div with an unordered list of error messageslet errorDiv = document.querySelector(`#${field}-error`);if (errorDiv) {errorDiv.classList.add("error-message");let ul = document.createElement("ul");errors[field].messages.forEach((message) => {let li = document.createElement("li");li.textContent = message;ul.appendChild(li);});errorDiv.innerHTML = ""; // Clear any existing contenterrorDiv.appendChild(ul);}}}export function validateForm({ userEmail, userLevel, userHours }) {43 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};let errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}if (Object.keys(errors).length > 0) {displayErrors(errors);return false;}} -
Returning valid data
Section titled โReturning valid dataโOur final step is to return the valid data from our function in an object that becomes the โsingle source of truthโ for the rest of our application code.
At the very end of the
validateForm
function, add the followingreturn
statement:validateForm.js export function validateForm({ userEmail, userLevel, userHours }) {43 collapsed linesconst maxHoursPerLevel = {basic: 5,advanced: 10,};let errors = {};// Helper function to add error messagesfunction addError(field, message) {if (!errors[field]) {errors[field] = { messages: [] };}errors[field].messages.push(message);}// Check if the user has provided an email addressif (userEmail === "") {addError("email", "Please enter your email address.");}// Check if the user has selected a levelif (userLevel === "") {addError("level", "Please select a level of study");}// Check if the user has specified at least one hour of studyif (isNaN(userHours) || userHours < 1) {addError("hoursPerWeek", "Please enter at least one hour of tuition.");}// Check if the userLevel exists in the maxHoursPerLevel objectif (!maxHoursPerLevel.hasOwnProperty(userLevel)) {addError("level", "Invalid level of study selected.");}// Check if the number of hours requested is within the allowed rangeconst maxAllowedHours = maxHoursPerLevel[userLevel];if (userHours > maxAllowedHours) {addError("hoursPerWeek",`You can only study a maximum of ${maxAllowedHours} hours per week.`);}if (Object.keys(errors).length > 0) {displayErrors(errors);return false;}return {userEmail,userLevel,userHours: parseInt(userHours),};}
Importing the validation module
Section titled โImporting the validation moduleโNow that we have extracted our validation logic into a separate module, we can import
it back into our index.js
file, and call the validateForm
function from there.
-
Open
index.js
.Add the code highlighted in green, and delete the code highlighted in red:
index.js import { validateForm } from "./validateForm.js";// Capture user's input on form submissionlet form = document.querySelector("form");form.addEventListener("submit", function (event) {event.preventDefault();const maxHoursPerLevel = {basic: 5,advanced: 10,};// Store the user's email address as userEmail (string/text)let userEmail = document.querySelector("#email").value;// Store the user's level as userLevel (string/text)let userLevel = document.querySelector("#level").value;// Store the user's hours of study as userHours (number)let userHours = document.querySelector("#hoursPerWeek").value;// Validate the user's inputconst result = validateForm({ userEmail, userLevel, userHours });console.log({ errors });console.log({ result });});// Calculate the total cost// Display the total cost to the user -
Save your changes.
In your browser, try submitting the form once with invalid values, and then again with valid ones.
What do you notice about how the value of
results
changes in the browser console?If all is working as it should, you should see that
result
isfalse
when the form data is invalid, but becomes an object containing the valid form data when it is valid.Remember this behaviour in the next guide, as will use this to decide how to proceed with the rest of our application logic.
Summary
Section titled โSummaryโIn this guide, we refactored our code by splitting our large functions into smaller, more manageable modules.
Weโve shortened a roughly 100 line file into fewer than 25!
To do this, we:
- Extracted the validation logic from our
index.js
file into a separate module calledvalidateForm.js
. - Separated the validation logic into two functions:
validateForm
anddisplayErrors
. - Ensured that the
validateForm
function returns the valid form data as an object, orfalse
if the form data is invalid. - Imported the
validateForm
function back into ourindex.js
file, and called it from there. - Tested the form submission in the browser console to ensure that the
result
object is returned correctly.
Next steps
Section titled โNext stepsโIn the next guide, we will fix the problem with error messages not disappearing after the form is submitted a second time, and then move on to calculating the total cost of the userโs tuition.