📖 Expanding Functionality with Modular Code
As applications grow, new features often need to be added while preserving modularity and clean code architecture. This article focuses on how to extend a modular application with additional functionality without tightly coupling new features to existing code. This approach makes it easier to maintain and scale applications over time.
Task
Your task is to learn how to add new features to a modular application while keeping the code organized and easy to manage. We'll demonstrate how to add a new module for searching tasks by keyword, showing how to introduce this feature without altering existing modules. This approach emphasizes clean, scalable architecture as you expand the app's functionality.
Core Principles
- Modularity
- Even as new features are added, the application's code should remain modular, with each module responsible for a single aspect of the functionality.
- Extensibility
- New features should be added in a way that doesn't disrupt or require changes to existing code, maintaining loose coupling.
- Scalability
- As applications grow, properly managed modules allow new features to integrate smoothly, supporting both current and future functionality.
Project Setup
As your project grows, organizing your files into appropriate directories becomes essential for maintainability and scalability. For this example, you'll arrange your project files into a clear directory structure.
Organizing Your Files
/task-organizer
/data
dataManager.js
/features
searchManager.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 filtering tasks by priority or implementing a search feature.
- /ui
- This folder will contain modules related to rendering and interacting with the user interface.
- index.html
- Your main HTML file that loads the project.
- main.js
- This file coordinates all interactions between the different modules.
Once your files are organized into this structure, you can proceed with the code implementation. Remember to update your import paths to match this directory setup, as needed. This approach not only keeps your project organized but also mirrors how larger applications are structured in the real world, helping you prepare for more advanced development.
Coding Examples
We'll break down how to add a new module for filtering tasks based on a search keyword. The example shows how to introduce this feature without altering existing modules.
dataManager.js
The dataManager.js
module is responsible for handling the core data logic of the app. It manages tasks, including adding, deleting, and retrieving them. This module is designed to focus solely on data management, which allows the application logic to be separated cleanly from other concerns like the UI.
// Manages the application's tasks: 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 uiManager.js
module handles everything related to the user interface. This includes rendering the list of tasks and managing the delete button functionality. Separating the UI logic like this keeps the presentation code distinct from both data management and feature-specific logic.
// Renders the tasks in the UI and handles the delete button functionality.
import { getTasks, deleteTask } from '../data/dataManager.js';
export function renderTasks(tasks = getTasks()) {
const tasksList = document.getElementById('tasks-list');
tasksList.innerHTML = ''; // Clear the list before rendering
tasks.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;
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 serves as the entry point of the application. It coordinates interactions between the UI, data, and feature modules. This file listens for user events, such as adding tasks and searching, and triggers the appropriate functions in other modules. By keeping main.js
focused on connecting different parts of the app, the code remains organized and easy to extend.
// Coordinates user interactions and renders the tasks.
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 task = {
task: taskInput.value.trim()
};
if (task.task) {
addTask(task);
renderTasks();
taskInput.value = ''; // Clear input
}
});
renderTasks(); // Initial render
Putting It Into Action
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">
<button id="add-task-btn" class="btn btn-primary mb-3">Add Task</button>
<ul id="tasks-list" class="list-group"></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 app by adding a search feature that filters tasks by keyword. This should be handled by a new module, maintaining the principles of modularity and loose coupling.
In order to check your learning, you should attempt to create a solution before revealing the provided solution below.
index.html (Add to form after the Add Task field)
<input type="text" id="search-input" class="form-control mb-2" placeholder="Search tasks by keyword">
searchManager.js (New Module)
This module will be placed in a new sub-directory named /features.
- This module handles filtering tasks based on a search keyword, keeping the filtering logic separate from other modules.
- By creating this as a standalone module, you maintain loose coupling and make the code easier to manage.
// searchManager.js
import { getTasks } from '../data/dataManager.js';
export function filterTasksByKeyword(keyword) {
return getTasks().filter(task => task.task.toLowerCase().includes(keyword.toLowerCase()));
}
main.js
// Add this code to the existing main.js
import { filterTasksByKeyword } from './features/searchManager.js';
document.getElementById('search-input').addEventListener('input', (e) => {
const keyword = e.target.value.trim();
const filteredTasks = filterTasksByKeyword(keyword);
renderTasks(filteredTasks); // Update the UI based on the filtered list
});