📖 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) => {