📖 Building Modular Apps with ES6

As applications grow in complexity, it becomes increasingly important to organize code in a way that is maintainable, scalable, and easy to understand. ES6 modules allow developers to break down code into small, manageable units, each with its own responsibility. This approach, known as modular architecture, helps improve code reusability, testing, and separation of concerns.

In this article, we will explore how to bring together multiple modules in a coordinated way to create a functional Task Organizer application. As applications grow in complexity, properly managing module communication becomes crucial for maintainability and scalability.

Task

Your task is to create a small Task Organizer application that allows users to add tasks and display them in a well-organized UI. We'll focus on handling cross-module communication and managing state between modules, ensuring a clean and modular codebase.

Core Principles

Separation of Concerns
Each module should handle a specific responsibility, such as data management, UI rendering, or user interaction. By keeping these concerns separate, we create more maintainable code.
Cross-Module Communication
Modules should communicate by exposing functions and data to each other via imports and exports. This allows modules to remain independent while still collaborating.
Managing Shared State
Shared data, like the list of tasks, should be managed centrally (e.g., within a data module) and accessed by other modules when needed.

Project Setup

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


/task-organizer
    /data
        dataManager.js
    /features
        priorityManager.js
    /ui
        uiManager.js
    index.html
    main.js
/data
This folder will store all modules related to managing your application's data, such as adding and retrieving tasks.
/features
This folder will house additional functionality modules, such as sorting tasks by priority.
index.html
/ui
This folder will contain modules related to rendering and interacting with the user interface.
Your main HTML file that loads the project.
main.js
This file coordinates all interactions between the different modules.

Coding Examples

We'll now break down the Task Organizer into modules that handle data management, UI updates, and task sorting.

dataManager.js

This module handles all task-related data management. It keeps the task data centralized and provides methods for adding, deleting, and retrieving tasks. By isolating the data logic, we ensure that the other modules don't have to worry about how data is stored or managed.


// Handles task data operations, including adding, deleting, and retrieving tasks.
export const tasks = [];

export function addTask(task) {
    tasks.push(task);
}

export function deleteTask(index) {
    tasks.splice(index, 1);
}

export function getTasks() {
    return tasks;
}

uiManager.js

The UI Manager module is responsible for rendering the tasks in the UI. It listens for changes and updates the DOM accordingly. By keeping the rendering logic separate, this module focuses solely on how data is presented, allowing us to modify the UI without affecting the data layer.


// Manages UI rendering and user interactions, including displaying tasks and handling deletions.
import { getTasks, deleteTask } from '../data/dataManager.js';

export function renderTasks() {
    const tasksList = document.getElementById('tasks-list');
    tasksList.innerHTML = ''; // Clear the task-list before rendering

    getTasks().forEach((task, index) => {
        const listItem = document.createElement('li');
        listItem.className = 'list-group-item d-flex justify-content-between align-items-center';
        listItem.textContent = `${task.task} (${task.priority})`;

        const deleteButton = document.createElement('button');
        deleteButton.className = 'btn btn-danger btn-sm';
        deleteButton.textContent = 'Delete';
        deleteButton.addEventListener('click', () => {
            deleteTask(index);
            renderTasks(); // Re-render the tasks list
        });

        listItem.appendChild(deleteButton);
        tasksList.appendChild(listItem);
    });
}

main.js

The main.js file coordinates all interactions between the modules. It listens for user input (e.g., adding a task), updates the data module, and then triggers a re-render of the UI. This file acts as the glue that connects the different parts of the application while keeping each module focused on its specific task.


// Coordinates the app's core functionality, connecting UI interactions with data operations.
import { addTask } from './data/dataManager.js';
import { renderTasks } from './ui/uiManager.js';

document.getElementById('add-task-btn').addEventListener('click', () => {
    const taskInput = document.getElementById('task-input');
    const prioritySelect = document.getElementById('priority-select');

    const task = {
        task: taskInput.value.trim(),
        priority: prioritySelect.value // Capture the selected priority
    };

    if (task.task) {
        addTask(task);
        renderTasks();
        taskInput.value = ''; // Clear input
    }
});

renderTasks(); // Initial render

Putting It Into Action

Below is a complete example demonstrating the Task Organizer application using modular code. This example shows how the modules work together in a coordinated manner.

index.html


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Organizer</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>Task Organizer</h1>
        <div>
            <input type="text" id="task-input" class="form-control mb-2" placeholder="Enter a new task">
            <select id="priority-select" class="form-control mb-2">
                <option value="High">High</option>
                <option value="Medium">Medium</option>
                <option value="Low">Low</option>
            </select>
            <button id="add-task-btn" class="btn btn-primary">Add Task</button>
            <ul id="tasks-list" class="list-group mt-3"></ul>
        </div>
    </div>

    <script type="module" src="./main.js"></script>
</body>
</html>

Challenge

Now it is time to practice. Your challenge is to expand the Task Organizer by adding a priority feature. Users should be able to assign tasks as High, Medium, or Low priority and see the tasks ordered accordingly.

In order to check your learning, you should attempt to create a solution before revealing the provided solution below.

priorityManager.js (New Module)

Create a new file in the /features directory to handle task prioritization.


// This module handles sorting tasks by priority. It defines the priority levels
// and provides functions to order tasks based on their priority, ensuring that
// tasks are displayed in the correct order (High, Medium, Low).                                
export const priorities = ['High', 'Medium', 'Low'];

export function getPriorityOrder(priority) {
    return priorities.indexOf(priority);
}

export function sortTasksByPriority(tasks) {
    return tasks.sort((a, b) => getPriorityOrder(a.priority) - getPriorityOrder(b.priority));
}
uiManager.js (Update)

Update your renderTasks function to use the sorting feature.


// Add an import statement to import the priorityManager
import { sortTasksByPriority } from '../features/priorityManager.js';

// Add a call to the sortTasksByPriority() method of the priorityManager
// This needs to be nested in the renderTasks() function after clearing the task-list
const sortedTasks = sortTasksByPriority(getTasks());

// Now we need to replace the getTasks() method in the base app 
// to call the sortedTasksByPriority() using the sortedTasks object
// Change this 
getTasks().forEach((task, index) => {
// To this
sortedTasks.forEach((task, index) => {

References