📖 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
});
                        

References