📖 Introduction to ES6 Modules
Modules are a fundamental part of modern JavaScript development, allowing you to organize your code into reusable, manageable components. ES6 introduced native module support, which is now widely used in web development. Understanding how to structure code using modules is essential for building scalable and maintainable applications.
Task
Your task is to learn how to use ES6 modules to break your code into smaller, self-contained units. We will explore how to use the import and export keywords to create and utilize modules. Mastering this will prepare you to structure complex applications efficiently.
Core Principles
- Separation of Concerns
- Modules allow you to separate functionality by responsibility, making your code easier to understand, test, and maintain.
- Reusability
- Once you create a module, you can easily reuse it across different parts of your application, avoiding code duplication.
- Loose Coupling
- Modules should rely on each other as little as possible, allowing you to update or replace one module without impacting others.
- Single Source of Truth
- This principle ensures that your data is consistent by relying on a single, authoritative source, avoiding conflicting information from different parts of your application.
Commmon Methods
- Import and Export
- JavaScript modules use the
exportkeyword to expose functionality and theimportkeyword to bring that functionality into other files. - Default vs. Named Exports
- Modules can have a single default export or multiple named exports. Understanding the difference between these will help you better structure your code.
- Using
type="module"in Script Tags - When using ES6 modules in the browser, you must specify
type="module"in the<script>tag. This tells the browser to treat the file as a module and allows you to useimportandexportstatements.
Coding Examples
Let us start with a basic example of creating and using a module.
Exporting and Importing Functions
In ES6, you can export functions or variables from a module and then import them into other modules. This ensures that your data is always consistent by relying on a single source, avoiding conflicting information.
mathOperations.js
// This module provides basic mathematical operations.
// It demonstrates the use of named exports (for `add` and `subtract`) and a default export (for `multiply`).
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export default function multiply(a, b) {
return a * b;
}
- The
addandsubtractfunctions are named exports. You must use the exact name when importing them. - The
multiplyfunction is a default export. It can be imported with any name.
main.js
Now, we will see how to import and use these functions in another file. Notice that the import statement uses relative referencing to identify the location of the import file.
// This file imports functions from the mathOperations module and uses them in a simple calculator.
// Notice the difference between importing named exports (add, subtract) and the default export (multiply).
import multiply, { add, subtract } from './mathOperations.js';
console.log(add(2, 3)); // 5
console.log(subtract(5, 3)); // 2
console.log(multiply(4, 2)); // 8
- We imported the named functions
addandsubtractusing curly braces. - We imported the default
multiplyfunction without curly braces and can rename it if needed. - The output shows how each function works, demonstrating the simplicity of using modules.
Putting It Into Action
Let us apply what you have learned in a practical example. Below is a simple note manager that uses modular code. This example illustrates how to use ES6 modules to separate different concerns of an application, keeping each module focused on a single responsibility. Try to create the app using the guidelines below.
Project Setup
First, create the file system to support organizing your modular code.
To keep the project organized and scalable, it's important to structure your files in a logical directory hierarchy. Here's how you should arrange your files.
Organizing Your Files
/notes-manager
/data
dataManager.js
/ui
uiManager.js
index.html
main.js
- /notes-manager
- This folder will store all files in your application. As the top-level directory, it is also referred to as the `root` directory.
- /data
- This folder will store all modules related to managing your application's data, such as adding and retrieving notes. It is a sub-directory, nested in the root directory.
- /ui
- This folder will contain modules related to rendering and interacting with the user interface. It is a sub-directory, nested in the root directory.
- index.html
- Your main HTML file that loads the project upon recieving a client request from the browser. This file is located in the root directory of your app.
- main.js
- This file coordinates all interactions between the different modules. This file is located in the root directory of your app.
HTML Home Page
Create the index.html file to serve for client request. This is the main HTML file for the Notes Manager app. The structure is simple, with an input field, a button to add notes, and an area to display the notes. Place it in the root directory of your app.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notes Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Notes Manager</h1>
<div class="mb-3">
<input type="text" id="note-input" class="form-control" placeholder="Enter a new note">
<button id="add-note-btn" class="btn btn-primary mt-3">Add Note</button>
</div>
<ul id="notes-list" class="list-group"></ul>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
JavaScript Files
Create each JS file and place it in the directory structure as described above. Pay close attention to the import statements to ensure the path to the import file is using the correct relative reference.
main.js
The main module coordinates interactions between the data and UI modules, ensuring that each part of the application communicates effectively. This approach keeps the core logic separate, making it easier to add new features or update existing ones. Place this file in the root directory of your app.
// This file coordinates the interaction between the dataManager and uiManager modules.
// It handles the main application logic, including adding notes and triggering UI updates.
import { addNote } from './data/dataManager.js';
import { renderNotes } from './ui/uiManager.js';
document.getElementById('add-note-btn').addEventListener('click', () => {
const noteInput = document.getElementById('note-input');
const note = noteInput.value.trim();
if (note) {
addNote(note);
renderNotes();
noteInput.value = ''; // Clear the input field
}
});
renderNotes(); // Initial render
dataManager.js
This module manages the application's data, including adding, deleting, and retrieving notes. By keeping the data logic separate, it becomes easier to test and maintain. Place this file in the /data directory of your app.
// This module is responsible for managing the notes data.
// It provides functions to add, delete, and retrieve notes from the central data store (an array).
export const notes = [];
export function addNote(note) {
notes.push(note);
}
export function deleteNote(index) {
notes.splice(index, 1);
}
export function getNotes() {
return notes;
}
uiManager.js
The UI module handles rendering the notes list in the DOM, keeping the presentation logic separate from the data logic. This separation makes it easier to update the UI without affecting how data is managed. Place this file in the /ui directory of your app.
// This module handles the user interface and DOM interactions.
// It retrieves notes from the dataManager and renders them in the UI, while also managing note deletion.
import { getNotes, deleteNote } from '../data/dataManager.js';
export function renderNotes() {
const notesList = document.getElementById('notes-list');
notesList.innerHTML = ''; // Clear the list before rendering
getNotes().forEach((note, index) => {
const listItem = document.createElement('li');
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
listItem.textContent = note;
// Create a delete button
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-danger btn-sm';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => {
deleteNote(index); // Call the deleteNote function with the note's index
renderNotes(); // Re-render the notes list
});
// Add the delete button to the list item
listItem.appendChild(deleteButton);
notesList.appendChild(listItem);
});
}
Challenge
Now it is time to practice. Your challenge is to expand the notes application by adding a module that handles categorization and filtering. Ensure your modules remain loosely coupled.
In order to check your learning, you should attempt to create a solution before revealing the provided solution below.
HTML Update
First, update the HTML to include a category input field.
<input type="text" id="category-input" class="form-control mb-2" placeholder="Enter a category">
categoryManager.js
Next, add a new sub-directory to the app root directory named features. Crate a file named categoryManager.js with the new code below and place this file in the /features directory.
// This module manages the categorization of notes by allowing the addition of categories
// and retrieving a list of all available categories. It helps organize notes for easier filtering
// and enhances the modularity of the application by handling category-specific logic separately.
export const categories = [];
export function addCategory(category) {
if (!categories.includes(category)) {
categories.push(category);
}
}
export function getCategories() {
return categories;
}
main.js (Updated)
// Includes an import statement to bring in the addCategory function from the categoryManager module.
// This allows the app to store the category value when a new note is added.
import { addNote } from './data/dataManager.js';
import { renderNotes } from './ui/uiManager.js';
import { addCategory } from './features/categoryManager.js'; // New import to handle category management
document.getElementById('add-note-btn').addEventListener('click', () => {
const noteInput = document.getElementById('note-input');
const categoryInput = document.getElementById('category-input'); // Capture the category input
const note = noteInput.value.trim();
const category = categoryInput.value.trim(); // Store the category value
if (note) {
addCategory(category); // Store the category using the categoryManager
addNote(note, category); // Store the note and its category
renderNotes();
noteInput.value = ''; // Clear the input field
categoryInput.value = ''; // Clear the category input field
}
});
renderNotes(); // Initial render
dataManager.js (Updated)
// Stores both the note content and its category as part of a note object.
// This update allows us to capture and manage both pieces of information together when adding, retrieving, or deleting notes.
export const notes = [];
export function addNote(note, category) {
// Store the note as an object with 'note' and 'category' properties
notes.push({ note, category });
}
export function deleteNote(index) {
notes.splice(index, 1);
}
export function getNotes() {
return notes;
}
uiManager.js (Updated)
// Displays the note along with its category
import { getNotes, deleteNote } from '../data/dataManager.js';
export function renderNotes() {
const notesList = document.getElementById('notes-list');
notesList.innerHTML = '';
getNotes().forEach((note, index) => {
const listItem = document.createElement('li');
listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
listItem.textContent = `${note.note} (Category: ${note.category})`; // Display note with category
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-danger btn-sm';
deleteButton.textContent = 'Delete';
deleteButton.addEventListener('click', () => {
deleteNote(index);
renderNotes();
});
listItem.appendChild(deleteButton);
notesList.appendChild(listItem);
});
}