Skip to content

Code refactor

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.

The vast majority of our code is dedicated to validating the form data. Letโ€™s extract this logic into a separate module.

  1. Create a new file called validateForm.js in your project directory.

    Create validateForm.js

  2. Inside this new file, create a function called validateForm that takes the form data as an argument, and export this from the file:

    validateForm.js
    export function validateForm({ userEmail, userLevel, userHours }) {
    // Validation logic here
    }
  3. Next, go back to your index.js file.

    Start by cutting the maxHoursPerLevel object from the top of the handleSubmit function:

    index.js
    // Capture user's input on form submission
    let 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 input
    let errors = {};
    // Helper function to add error messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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 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 messages
    let 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 content
    errorDiv.appendChild(ul);
    }
    }
    if (Object.keys(errors).length > 0) {
    return;
    }
    console.log({ errors });
    });
    // Calculate the total cost
    // Display the total cost to the user

    Paste 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
    }
  4. Back inside index.js, cut the validation logic from the handleSubmit function.

    index.js
    // Capture user's input on form submission
    let form = document.querySelector("form");
    form.addEventListener("submit", function (event) {
    8 collapsed lines
    event.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 input
    let errors = {};
    // Helper function to add error messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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 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 messages
    let 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 content
    errorDiv.appendChild(ul);
    }
    }
    if (Object.keys(errors).length > 0) {
    return;
    }
    console.log({ errors });
    });
    // Calculate the total cost
    // Display the total cost to the user
  5. 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 messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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 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 messages
    let 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 content
    errorDiv.appendChild(ul);
    }
    }
    if (Object.keys(errors).length > 0) {
    return;
    }
    }
  6. Next, letโ€™s separate the logic further.

    Currently, we have a function called validateForm, but itโ€™s actually doing two things:

    1. Validating the form data
    2. 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 called displayErrors that takes the errors object as an argument:

    validateForm.js
    export function displayErrors(errors) {
    }
    export function validateForm({ userEmail, userLevel, userHours }) {
    73 collapsed lines
    const maxHoursPerLevel = {
    basic: 5,
    advanced: 10,
    };
    let errors = {};
    // Helper function to add error messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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 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 messages
    let 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 content
    errorDiv.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 new displayErrors 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 messages
    let 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 content
    errorDiv.appendChild(ul);
    }
    }
    }
    export function validateForm({ userEmail, userLevel, userHours }) {
    43 collapsed lines
    const maxHoursPerLevel = {
    basic: 5,
    advanced: 10,
    };
    let errors = {};
    // Helper function to add error messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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 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 messages
    let 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 content
    errorDiv.appendChild(ul);
    }
    }
    if (Object.keys(errors).length > 0) {
    return;
    }
    }
  7. Now, letโ€™s call the new displayErrors function if any errors exist, and return false from the validateForm function to indicate that the form data is invalid:

    validateForm.js
    export function displayErrors(errors) {
    24 collapsed lines
    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 messages
    let 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 content
    errorDiv.appendChild(ul);
    }
    }
    }
    export function validateForm({ userEmail, userLevel, userHours }) {
    43 collapsed lines
    const maxHoursPerLevel = {
    basic: 5,
    advanced: 10,
    };
    let errors = {};
    // Helper function to add error messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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;
    }
    }
  8. 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 following return statement:

    validateForm.js
    export function validateForm({ userEmail, userLevel, userHours }) {
    43 collapsed lines
    const maxHoursPerLevel = {
    basic: 5,
    advanced: 10,
    };
    let errors = {};
    // Helper function to add error messages
    function addError(field, message) {
    if (!errors[field]) {
    errors[field] = { messages: [] };
    }
    errors[field].messages.push(message);
    }
    // Check if the user has provided an email address
    if (userEmail === "") {
    addError("email", "Please enter your email address.");
    }
    // Check if the user has selected a level
    if (userLevel === "") {
    addError("level", "Please select a level of study");
    }
    // Check if the user has specified at least one hour of study
    if (isNaN(userHours) || userHours < 1) {
    addError("hoursPerWeek", "Please enter at least one hour of tuition.");
    }
    // Check if the userLevel exists in the maxHoursPerLevel object
    if (!maxHoursPerLevel.hasOwnProperty(userLevel)) {
    addError("level", "Invalid level of study selected.");
    }
    // Check if the number of hours requested is within the allowed range
    const 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),
    };
    }

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.

  1. 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 submission
    let 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 input
    const result = validateForm({ userEmail, userLevel, userHours });
    console.log({ errors });
    console.log({ result });
    });
    // Calculate the total cost
    // Display the total cost to the user
  2. 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?

    Result console log results

    If all is working as it should, you should see that result is false 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.

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 called validateForm.js.
  • Separated the validation logic into two functions: validateForm and displayErrors.
  • Ensured that the validateForm function returns the valid form data as an object, or false if the form data is invalid.
  • Imported the validateForm function back into our index.js file, and called it from there.
  • Tested the form submission in the browser console to ensure that the result object is returned correctly.

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.