DOM Project — Complete Guide
In this tutorial, you will learn about DOM Project. We cover key concepts, practical examples, and best practices to help you master this topic.
Build a complete task management application using vanilla DOM APIs — selecting, creating, events, delegation, templating, storage, and performance optimization.
What You'll Learn
- How to combine all DOM concepts into a real application
- How to structure vanilla JavaScript for maintainability
- How to handle state management without a framework
- How to persist data with localStorage
Project Overview
You will build a Kanban-style task board with three columns: To Do, In Progress, and Done. Users can add tasks, move them between columns, edit descriptions, and delete tasks. All data persists in localStorage.
Features
- Add new tasks with title and description
- Move tasks between columns via drag-and-drop or buttons
- Edit task titles inline
- Delete tasks
- Persistent storage with localStorage
- Keyboard shortcuts for power users
flowchart LR A[Task Board] --> B[To Do] A --> C[In Progress] A --> D[Done] B --> E[Add Task Form] E --> B B --> F[Move Right] F --> C C --> G[Move Left] G --> B C --> H[Move Right] H --> D D --> I[Move Left] I --> C B --> J[Delete] C --> J D --> J
Step 1: HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Board</title>
<link rel="stylesheet" href="board.css">
</head>
<body>
<header class="board-header">
<h1>Task Board</h1>
<div class="board-actions">
<button id="add-task-btn" class="btn btn-primary">+ Add Task</button>
<button id="clear-board-btn" class="btn btn-danger">Clear Board</button>
</div>
</header>
<main class="board" id="task-board">
<div class="column" data-status="todo">
<div class="column-header">
<h2>To Do</h2>
<span class="task-count" id="count-todo">0</span>
</div>
<div class="column-body" id="column-todo"></div>
</div>
<div class="column" data-status="in-progress">
<div class="column-header">
<h2>In Progress</h2>
<span class="task-count" id="count-in-progress">0</span>
</div>
<div class="column-body" id="column-in-progress"></div>
</div>
<div class="column" data-status="done">
<div class="column-header">
<h2>Done</h2>
<span class="task-count" id="count-done">0</span>
</div>
<div class="column-body" id="column-done"></div>
</div>
</main>
<!-- Task creation modal -->
<div class="modal-overlay" id="task-modal">
<div class="modal">
<h2>New Task</h2>
<form id="task-form">
<div class="form-group">
<label for="task-title">Title</label>
<input type="text" id="task-title" required maxlength="100">
</div>
<div class="form-group">
<label for="task-desc">Description</label>
<textarea id="task-desc" rows="3" maxlength="500"></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Task</button>
<button type="button" class="btn btn-secondary" id="modal-cancel">Cancel</button>
</div>
</form>
</div>
</div>
<template id="task-card-template">
<div class="task-card" draggable="true">
<div class="task-header">
<span class="task-title-display" contenteditable="false"></span>
<button class="task-delete" title="Delete task">×</button>
</div>
<p class="task-description"></p>
<div class="task-actions">
<button class="task-move-left" title="Move left">←</button>
<button class="task-move-right" title="Move right">→</button>
</div>
</div>
</template>
<script src="board.js"></script>
</body>
</html>
Step 2: State Management
// State management
const State = {
tasks: [],
nextId: 1,
load() {
try {
const saved = localStorage.getItem('taskBoard');
if (saved) {
const data = JSON.parse(saved);
this.tasks = data.tasks || [];
this.nextId = data.nextId || 1;
}
} catch (e) {
console.warn('Failed to load state:', e);
this.tasks = [];
}
console.log(`Loaded ${this.tasks.length} tasks`);
},
save() {
try {
localStorage.setItem('taskBoard', JSON.stringify({
tasks: this.tasks,
nextId: this.nextId
}));
} catch (e) {
console.warn('Failed to save state:', e);
}
},
addTask(title, description) {
const task = {
id: this.nextId++,
title: title.trim(),
description: description.trim(),
status: 'todo',
createdAt: Date.now()
};
this.tasks.push(task);
this.save();
return task;
},
moveTask(id, direction) {
const task = this.tasks.find(t => t.id === id);
if (!task) return false;
const statuses = ['todo', 'in-progress', 'done'];
const currentIndex = statuses.indexOf(task.status);
const newIndex = currentIndex + direction;
if (newIndex < 0 || newIndex >= statuses.length) return false;
task.status = statuses[newIndex];
this.save();
return true;
},
deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id);
this.save();
},
updateTitle(id, newTitle) {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.title = newTitle.trim();
this.save();
}
},
clearBoard() {
this.tasks = [];
this.save();
},
getTasksByStatus(status) {
return this.tasks.filter(t => t.status === status);
}
};
Step 3: DOM Rendering
// DOM Controller
const Board = {
init() {
this.cacheDOM();
this.bindEvents();
State.load();
this.render();
console.log('Task board initialized');
},
cacheDOM() {
this.columns = {
todo: document.getElementById('column-todo'),
'in-progress': document.getElementById('column-in-progress'),
done: document.getElementById('column-done')
};
this.counts = {
todo: document.getElementById('count-todo'),
'in-progress': document.getElementById('count-in-progress'),
done: document.getElementById('count-done')
};
this.template = document.getElementById('task-card-template');
this.modal = document.getElementById('task-modal');
this.form = document.getElementById('task-form');
this.titleInput = document.getElementById('task-title');
this.descInput = document.getElementById('task-desc');
this.addBtn = document.getElementById('add-task-btn');
this.clearBtn = document.getElementById('clear-board-btn');
this.cancelBtn = document.getElementById('modal-cancel');
},
bindEvents() {
// Add task
this.addBtn.addEventListener('click', () => this.showModal());
this.cancelBtn.addEventListener('click', () => this.hideModal());
this.form.addEventListener('submit', (e) => this.handleFormSubmit(e));
// Clear board
this.clearBtn.addEventListener('click', () => {
if (confirm('Clear all tasks?')) {
State.clearBoard();
this.render();
}
});
// Click outside modal
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.hideModal();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hideModal();
if (e.key === 'n' && e.ctrlKey) {
e.preventDefault();
this.showModal();
}
});
// Event delegation for task actions
const board = document.getElementById('task-board');
board.addEventListener('click', (e) => {
const taskCard = e.target.closest('.task-card');
if (!taskCard) return;
const taskId = parseInt(taskCard.dataset.id);
if (e.target.matches('.task-delete')) {
State.deleteTask(taskId);
this.render();
} else if (e.target.matches('.task-move-left')) {
State.moveTask(taskId, -1);
this.render();
} else if (e.target.matches('.task-move-right')) {
State.moveTask(taskId, 1);
this.render();
}
});
// Inline editing
board.addEventListener('blur', (e) => {
if (e.target.matches('.task-title-display')) {
const taskCard = e.target.closest('.task-card');
const taskId = parseInt(taskCard.dataset.id);
State.updateTitle(taskId, e.target.textContent);
}
}, true);
// Drag and drop
board.addEventListener('dragstart', (e) => {
const card = e.target.closest('.task-card');
if (card) {
e.dataTransfer.setData('text/plain', card.dataset.id);
card.classList.add('dragging');
}
});
board.addEventListener('dragend', (e) => {
const card = e.target.closest('.task-card');
if (card) card.classList.remove('dragging');
});
board.addEventListener('dragover', (e) => {
const column = e.target.closest('.column-body');
if (column) {
e.preventDefault();
column.classList.add('drag-over');
}
});
board.addEventListener('dragleave', (e) => {
const column = e.target.closest('.column-body');
if (column) column.classList.remove('drag-over');
});
board.addEventListener('drop', (e) => {
const column = e.target.closest('.column-body');
if (!column) return;
e.preventDefault();
column.classList.remove('drag-over');
const taskId = parseInt(e.dataTransfer.getData('text/plain'));
const targetStatus = column.parentElement.dataset.status;
const task = State.tasks.find(t => t.id === taskId);
if (task && task.status !== targetStatus) {
task.status = targetStatus;
State.save();
this.render();
}
});
},
showModal() {
this.titleInput.value = '';
this.descInput.value = '';
this.modal.classList.add('visible');
this.titleInput.focus();
},
hideModal() {
this.modal.classList.remove('visible');
},
handleFormSubmit(e) {
e.preventDefault();
const title = this.titleInput.value.trim();
const desc = this.descInput.value.trim();
if (!title) return;
State.addTask(title, desc);
this.hideModal();
this.render();
},
render() {
for (const status of ['todo', 'in-progress', 'done']) {
const tasks = State.getTasksByStatus(status);
const column = this.columns[status];
const fragment = document.createDocumentFragment();
tasks.forEach(task => {
const clone = this.template.content.cloneNode(true);
const card = clone.querySelector('.task-card');
card.dataset.id = task.id;
clone.querySelector('.task-title-display').textContent = task.title;
clone.querySelector('.task-description').textContent = task.description || 'No description';
const moveLeft = clone.querySelector('.task-move-left');
const moveRight = clone.querySelector('.task-move-right');
moveLeft.disabled = status === 'todo';
moveRight.disabled = status === 'done';
fragment.appendChild(clone);
});
column.innerHTML = '';
column.appendChild(fragment);
this.counts[status].textContent = tasks.length;
}
console.log(`Rendered ${State.tasks.length} tasks`);
}
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => Board.init());
Step 5: CSS Styling
/* board.css — minimal styling for the task board */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #f5f5f5; color: #333; }
.board-header { background: #2c3e50; color: white; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
.board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; padding: 1rem; max-width: 1200px; margin: 0 auto; }
.column { background: #e8e8e8; border-radius: 8px; min-height: 400px; }
.column-header { padding: 1rem; border-bottom: 2px solid #ddd; display: flex; justify-content: space-between; }
.column-body { padding: 0.5rem; min-height: 300px; }
.task-card { background: white; border-radius: 6px; padding: 0.75rem; margin-bottom: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: grab; }
.task-card.dragging { opacity: 0.5; }
.column-body.drag-over { background: #d4edda; border-radius: 4px; }
.task-header { display: flex; justify-content: space-between; align-items: start; }
.task-title-display { font-weight: 600; cursor: text; }
.task-description { font-size: 0.9em; color: #666; margin: 0.5rem 0; }
.task-actions { display: flex; gap: 0.25rem; justify-content: flex-end; }
.task-actions button { border: 1px solid #ddd; background: white; padding: 2px 8px; border-radius: 3px; cursor: pointer; }
.task-actions button:disabled { opacity: 0.3; cursor: not-allowed; }
.task-delete { background: none; border: none; color: #e74c3c; font-size: 1.2em; cursor: pointer; }
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); justify-content: center; align-items: center; }
.modal-overlay.visible { display: flex; }
.modal { background: white; padding: 2rem; border-radius: 8px; width: 400px; max-width: 90vw; }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.25rem; font-weight: 500; }
.form-group input, .form-group textarea { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; }
.form-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
.btn { padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.9em; }
.btn-primary { background: #3498db; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn-secondary { background: #95a5a6; color: white; }
Expected Output
The task board loads with any saved tasks from localStorage. Users can add tasks via the modal (or Ctrl+N shortcut). Tasks appear in the "To Do" column initially. Use arrow buttons or drag-and-drop to move tasks between columns. Click a title to edit it inline. Click X to delete. The "Clear Board" button removes all tasks after confirmation. Task counts update in each column header.
Common Mistakes
- Not sanitizing task input — Users could enter HTML that breaks the display. Use textContent for rendering (which is already done in the template).
- Forgetting to save state after each operation — Every add, move, edit, or delete must call State.save() to persist changes.
- Not cleaning up drag state — The drag-over class and dragging class must be removed in dragend and dragleave to avoid visual artifacts.
- Allowing edit on empty title — When editing inline, ensure the title is not empty after blur. Reset to previous value if empty.
- Not handling multiple rapid adds — The modal should not allow creating duplicate empty tasks. Disable the submit button if title is empty.
Challenge Extensions
- Add due dates with date picker input
- Implement task categories with color labels
- Add search/filter functionality
- Add a dark mode toggle
- Implement undo/redo for state changes
- Add task reordering within a column (sortable)
- Add a sidebar showing task statistics
What's Next
Now that you have mastered DOM manipulation, explore Web Components to learn how to create reusable custom elements with encapsulated DOM and styles. Or dive into Shadow DOM for deeper style isolation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro