Building a Complete Project
Putting it all together
You've learned variables, functions, DOM manipulation, arrays, objects, async code, error handling, events, and storage. Now let's build a real project that uses all of it — a Task Manager app.
This isn't a tutorial to copy-paste. It's a walkthrough of how an experienced developer thinks through building a feature-complete application.
Step 1: Plan the structure
Before writing code, define what the app does:
- Add tasks with a title and priority
- Mark tasks as complete
- Delete tasks
- Filter by status (all, active, completed)
- Persist tasks in localStorage
- Show a count of remaining tasks
Step 2: The HTML
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Task Manager</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<h1>Task Manager</h1>
<form id="task-form">
<input type="text" id="task-input" placeholder="What needs to be done?" required />
<select id="priority-select">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
<button type="submit">Add</button>
</form>
<div class="filters">
<button data-filter="all" class="active">All</button>
<button data-filter="active">Active</button>
<button data-filter="completed">Completed</button>
</div>
<ul id="task-list"></ul>
<footer id="task-footer">
<span id="task-count"></span>
<button id="clear-completed">Clear completed</button>
</footer>
</div>
<script src="app.js" defer></script>
</body>
</html>Step 3: Data model
Define how tasks are stored. Each task is an object:
JavaScript
const task = {
id: Date.now(),
title: 'Learn JavaScript',
priority: 'high',
completed: false,
createdAt: new Date().toISOString(),
};Step 4: State management
Keep all state in one place and render from it:
JavaScript
let tasks = loadTasks();
let currentFilter = 'all';
function loadTasks() {
try {
return JSON.parse(localStorage.getItem('tasks')) || [];
} catch {
return [];
}
}
function saveTasks() {
localStorage.setItem('tasks', JSON.stringify(tasks));
}This pattern — a single source of truth that you save and render from — scales to any size application. React, Vue, and every modern framework is built on this idea.
Step 5: Rendering
Write a single
render() function that updates the entire UI based on current state:JavaScript
function render() {
const list = document.getElementById('task-list');
const count = document.getElementById('task-count');
const filtered = tasks.filter(task => {
if (currentFilter === 'active') return !task.completed;
if (currentFilter === 'completed') return task.completed;
return true;
});
list.innerHTML = filtered.map(task => `
<li class="task ${task.completed ? 'completed' : ''}" data-id="${task.id}">
<input type="checkbox" ${task.completed ? 'checked' : ''} class="toggle" />
<span class="task-title">${escapeHtml(task.title)}</span>
<span class="priority priority-${task.priority}">${task.priority}</span>
<button class="delete" aria-label="Delete task">×</button>
</li>
`).join('');
const remaining = tasks.filter(t => !t.completed).length;
count.textContent = `${remaining} task${remaining !== 1 ? 's' : ''} remaining`;
}Escaping user input
Never insert raw user text into HTML. This prevents XSS attacks:
JavaScript
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}Step 6: Event handlers
JavaScript
const form = document.getElementById('task-form');
const input = document.getElementById('task-input');
const prioritySelect = document.getElementById('priority-select');
const list = document.getElementById('task-list');
form.addEventListener('submit', (e) => {
e.preventDefault();
const title = input.value.trim();
if (!title) return;
tasks.push({
id: Date.now(),
title,
priority: prioritySelect.value,
completed: false,
createdAt: new Date().toISOString(),
});
input.value = '';
saveTasks();
render();
});
list.addEventListener('click', (e) => {
const id = Number(e.target.closest('.task')?.dataset.id);
if (!id) return;
if (e.target.classList.contains('toggle')) {
const task = tasks.find(t => t.id === id);
if (task) task.completed = !task.completed;
}
if (e.target.classList.contains('delete')) {
tasks = tasks.filter(t => t.id !== id);
}
saveTasks();
render();
});
document.querySelector('.filters').addEventListener('click', (e) => {
if (!e.target.dataset.filter) return;
currentFilter = e.target.dataset.filter;
document.querySelectorAll('.filters button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === currentFilter);
});
render();
});
document.getElementById('clear-completed').addEventListener('click', () => {
tasks = tasks.filter(t => !t.completed);
saveTasks();
render();
});
render();Step 7: What makes this "professional"
Notice the patterns used:
- Single source of truth — all data in the
tasksarray. - Declarative rendering —
render()rebuilds the UI from state. You never manually update individual DOM elements. - Event delegation — one listener on the list handles all task interactions.
- Input sanitization —
escapeHtmlprevents XSS. - Defensive coding —
try/catcharoundJSON.parse, null checks with optional chaining. - Separation of concerns — data logic (saveTasks, loadTasks) is separate from UI logic (render).
Enhancements to try
Once the basic app works, challenge yourself:
- Drag and drop reordering — use the HTML5 drag and drop API.
- Due dates — add a date picker and show overdue tasks in red.
- Categories/tags — group tasks and filter by tag.
- Keyboard shortcuts — press Enter to add, Delete to remove.
- Undo — keep a history stack and allow Ctrl+Z.
- Export/Import — download tasks as JSON and load from a file.
Key takeaway
Building a complete project teaches you more than any individual lesson. The core pattern — state, render, events, persistence — is the foundation of every modern web application. Once you understand this flow, learning React, Vue, or any framework becomes much easier because they all follow the same principles.