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.jsin your project directory.
-
Inside this new file, create a function called
validateFormthat takes the form data as an argument, andexportthis from the file:validateForm.js export function validateForm({ userEmail, userLevel, userHours }) {// Validation logic here} -
Next, go back to your
index.jsfile.Start by cutting the
maxHoursPerLevelobject from the top of thehandleSubmitfunction: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.jsmodule, 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 thehandleSubmitfunction.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.jsand 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
validateForminto two separate functions.At the very top of
validateForm.js, create a new function calleddisplayErrorsthat takes theerrorsobject 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
validateFormfunction and paste it into the newdisplayErrorsfunction: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”falseif form data is invalidNow, let’s call the new
displayErrorsfunction if any errors exist, and returnfalsefrom thevalidateFormfunction 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
validateFormfunction, add the followingreturnstatement: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
resultschanges in the browser console?
If all is working as it should, you should see that
resultisfalsewhen 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.jsfile into a separate module calledvalidateForm.js. - Separated the validation logic into two functions:
validateFormanddisplayErrors. - Ensured that the
validateFormfunction returns the valid form data as an object, orfalseif the form data is invalid. - Imported the
validateFormfunction back into ourindex.jsfile, and called it from there. - Tested the form submission in the browser console to ensure that the
resultobject 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.