📖 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 export keyword to expose functionality and the import keyword 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 use import and export statements.

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 add and subtract functions are named exports. You must use the exact name when importing them.
  • The multiply function 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 add and subtract using curly braces.
  • We imported the default multiply function 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);
    });
}

References