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.
Testing your Task Manager
Before calling the project done, walk through a short manual test checklist. Add a task and confirm it appears with the correct priority. Reload the page — tasks should persist from
localStorage. Toggle complete, switch filters, delete a task, and clear completed. Try edge cases: empty title (should be rejected), very long title, special characters like <script> (should display as text, not execute). Open DevTools → Application → Local Storage and verify the JSON shape matches your data model.If something breaks, use breakpoints in
render() and your event handlers rather than sprinkling console.log everywhere. The state → render → event loop is the same mental model React uses with useState and useEffect — mastering it here makes framework learning much faster.Growing beyond vanilla JavaScript
The Task Manager is intentionally framework-free so you see the underlying mechanics. Next steps might include splitting
app.js into ES modules (state.js, render.js, storage.js), adding CSS for responsive layout, or rebuilding the same app in React to compare approaches. You could also add a backend: store tasks via fetch to an API and practice async error handling from lesson six.Publishing is simple: static HTML, CSS, and JS can live on GitHub Pages, Netlify, or any static host. No build step is required for this version — a reminder that not every project needs a complex toolchain.
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.