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
- Open modules.html page and view/inspect the HTML and JavaScript.
- Note that there is currently only one deferred JavaScript file, named modules.js.
- 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 aformreloads 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 theevent.preventDefault()method.
Setup and Structure
- Inside your week3 folder create a new styles folder and a new scripts folder.
- 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.
- Then copy the HTML, CSS, and JavaScript from the example web page provided in the Inspect step into these files.
- Create a new JavaScript file named course.mjs.
- Create a new JavaScript file named sections.mjs.
- 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.
- Move the
byuiCourseobject into the course file from the modules.mjs file. - In this course.mjs file,
exportthebyuiCourseobject as thedefault. This line can be the last line of the file.export default byuiCourse; - Finally, remove the
renderSections(this.sections);line of code in thechangeEnrollmentmethod of thebyuiCourseobject. 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.
- Move the
setSectionSelectionfunction from the modules file into the sections file. exportthesetSectionSelectionfunction as a named export, and add asectionsparameter to the function.export function setSectionSelection(sections) { ... }- Find the
byuiCourse.sections.forEach((section) => {line of code and remove thebyuiCourse.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.
- Move the
setTitleandrenderSectionsfunctions into the output file. exportthesetTitleandrenderSectionsfunctions 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.
- At the top of the file,
importthebyuiCourseobject from the course module.import byuiCourse from './course.mjs'; - Next,
importthesetSectionSelectionfunction 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.
- Next,
importthe 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.
- 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
- Change the
scriptreference in the modules.html file to use the new module structure by removing thedeferattribute and adding thetype="module"attribute.<script type="module" src="scripts/modules.mjs"></script>
Test and Share
- 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.
- Share your code and issues, if any, with your peers on Microsoft Teams for review.
Optional Resources
- JavaScript Modules – MDN
- Basic Modules – MDN Repository