WDD 231: Web Frontend Development I

W03 Learning Activity: Modular JavaScript with ES Modules

Overview

You may have seen or written JavaScript code that is long and difficult to read. This is often the result of developing code in one single file. This makes the code hard to maintain and understand, especially as a project grows in size and complexity. To address this issue, JavaScript ES Modules were introduced to provide a way to organize code into smaller, reusable pieces called modules.

Prepare

ES Modules provide ways to help you write cleaner, more organized, and more manageable code by breaking it down into smaller, reusable parts. The following sections address the key concepts and explain how and why to use modules.

Native Browser Support

In order to use ES Module features, the type="module" attribute is required in the <script> tag. This attribute tells the browser to treat the script as a module, enabling the use of import and export statements within the script.

For example, you would reference a module script named app like this:

<script type="module" src="scripts/app.mjs"></script>

Note that the script file uses the .mjs extension to indicate that it is a module. Either .js or .mjs works as long as the script tag has type="module". This course uses .mjs to make module files easy to spot.

Module scripts are deferred by default, so they don't block the HTML parser during loading. As a result, there is no need to add the defer attribute to module script references.

Modules must be served over HTTP, for example, with Live Server. Opening the HTML file directly from your file system fails with a CORS error in the console.

Better Code Organization

ES Modules allow you to split your code into separate files. This allows you to have better code organization as you keep related functionality together and separate unrelated code. For example, you can have one module for handling DOM manipulation and another module for managing data storage.

Modules can export and import functions, objects, or variables between different files.

// in storage.mjs - handles data management
export function getData() { /* implementation */ }
export const total = 0;
export default class StorageManager { /* implementation */ }
// in output.mjs - handles UI rendering
export default function renderProductCard() { }
// in app.mjs - imports and uses both modules
import StorageManager, { getData, total } from './storage.mjs';
import renderProductCard from './output.mjs';

The import statement brings functionality from other modules into your current file. Each module can have multiple named exports (using curly braces when importing) and one default export (imported without curly braces). Default exports are useful for a module's primary function or class.

Modules can be imported in any order, and the module system ensures that dependencies are resolved correctly.

Modules are scoped to the module itself, meaning that variables and functions defined in a module are not accessible in the global scope unless explicitly exported. This helps to prevent naming conflicts and keeps the global scope clean.

Reusability

Modules can be reused across different parts of your application or even in different projects. This promotes code reuse and reduces duplication, making it easier to maintain and update your codebase.

For example, if you have a module that handles user authentication, you can use it in multiple applications without having to rewrite the code. This is especially useful for libraries or frameworks that provide common functionality that can be shared across different projects.

By using modules, you can create a library of reusable components that can be easily imported and used in other parts of your application. This not only saves time but also helps to keep your codebase clean and organized.

Activity Instructions

In this activity you will convert the existing application structure to one that uses modules.

Inspect

  1. Open modules.html page and view/inspect the HTML and JavaScript.
  2. Note that there is currently only one deferred JavaScript file, named modules.js.
  3. Review the code. This code will be put into modules.

    Note that a <form> element is not used for the input. This is because submitting a form reloads the page by default. This is not desired behavior in this case. Instead, the buttons trigger the enroll and drop behavior through event listeners. A form element could be used, but the default behavior of the form would need to be prevented using the event.preventDefault() method.

Setup and Structure

  1. Inside your week3 folder create a new styles folder and a new scripts folder.
  2. Inside the week3 folder, create your own modules.html file. Inside the styles folder, create a new modules.css file. Inside the scripts folder, create a modules.mjs file.
  3. Then copy the HTML, CSS, and JavaScript from the example web page provided in the Inspect step into these files.
  4. Create a new JavaScript file named course.mjs.
  5. Create a new JavaScript file named sections.mjs.
  6. Create a new JavaScript file named output.mjs.

course.mjs file changes

This file will contain the course object section data and its methods that are used to enroll and drop students from the course.

  1. Move the byuiCourse object into the course file from the modules.mjs file.
  2. In this course.mjs file, export the byuiCourse object as the default. This line can be the last line of the file.
    export default byuiCourse;
  3. Finally, remove the renderSections(this.sections); line of code in the changeEnrollment method of the byuiCourse object. Otherwise, a runtime error will occur when an update is attempted, because this function is no longer available to call within this new module file.

sections.mjs file changes

This file will contain the function that populates the section selection element on the page.

  1. Move the setSectionSelection function from the modules file into the sections file.
  2. export the setSectionSelection function as a named export, and add a sections parameter to the function.
    export function setSectionSelection(sections) { ... }
  3. Find the byuiCourse.sections.forEach((section) => { line of code and remove the byuiCourse. from the beginning of that line.

output.mjs file changes

This file will contain the functions that are used to render the course title and sections to the page.

  1. Move the setTitle and renderSections functions into the output file.
  2. export the setTitle and renderSections functions as named exports.
    export function setTitle(course) { ... }
    export function renderSections(sections) { ... }

modules.mjs file changes

The content of this script file is now reduced to event listeners and the function calls.

  1. At the top of the file, import the byuiCourse object from the course module.
    import byuiCourse from './course.mjs';
  2. Next, import the setSectionSelection function from the sections module.
    import { setSectionSelection } from './sections.mjs';

    The curly braces are required because this is a named export. Without them, JavaScript looks for a default export. This function could be converted to a default export in the module, but it is not necessary.

  3. Next, import the named function exports from the output file.
    Check Your Understanding
    import { setTitle, renderSections } from './output.mjs';

    Note that the two functions are enclosed in curly braces and separated with a comma.

  4. Add renderSections(byuiCourse.sections); to both event listeners in order to update the output after the enroll or drop button is clicked.
    document.querySelector("#enrollStudent").addEventListener("click", function () {
      const sectionNum = Number(document.querySelector("#sectionNumber").value);
      byuiCourse.changeEnrollment(sectionNum);
      renderSections(byuiCourse.sections);
    });
            
    document.querySelector("#dropStudent").addEventListener("click", function () {
      const sectionNum = Number(document.querySelector("#sectionNumber").value);
      byuiCourse.changeEnrollment(sectionNum, false);
      renderSections(byuiCourse.sections);
    });

HTML Script Reference

  1. Change the script reference in the modules.html file to use the new module structure by removing the defer attribute and adding the type="module" attribute.
    <script type="module" src="scripts/modules.mjs"></script>

Test and Share

  1. Test your code to ensure that it works as expected. You should be able to select a section and increase or decrease the displayed total enrollment for that section.
  2. Share your code and issues, if any, with your peers on Microsoft Teams for review.

Optional Resources