Fetch Data from the Node.js API
The browser's built-in fetch() API lets us call our Node.js server without any libraries. Let's load our todos and render them dynamically:
// static/dashboard.js
async function loadStats() {
const res = await fetch('/api/todos');
const { todos } = await res.json();
const total = todos.length;
const done = todos.filter(t => t.done).length;
const pending = total - done;
// Update stat cards
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-done').textContent = done;
document.getElementById('stat-pending').textContent = pending;
// Draw the bar chart
drawBarChart(document.getElementById('chart'), [
{ label: 'Total', value: total, color: '#ff6b35' },
{ label: 'Done', value: done, color: '#10b981' },
{ label: 'Pending', value: pending, color: '#f7931e' }
]);
}
// Run on page load
loadStats();
Drawing a Bar Chart on HTML5 Canvas
The <canvas> element gives us a 2D drawing API. No Chart.js needed:
function drawBarChart(canvas, data) {
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
const padding = 40;
const barWidth = (W - padding * 2) / data.length - 10;
const maxValue = Math.max(...data.map(d => d.value), 1);
// Clear canvas
ctx.clearRect(0, 0, W, H);
// Background
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, W, H);
// Draw each bar
data.forEach((item, i) => {
const barH = ((item.value / maxValue) * (H - padding * 2));
const x = padding + i * (barWidth + 10);
const y = H - padding - barH;
// Bar
ctx.fillStyle = item.color;
ctx.fillRect(x, y, barWidth, barH);
// Value label on top
ctx.fillStyle = '#e0e0e0';
ctx.font = '14px Segoe UI';
ctx.textAlign = 'center';
ctx.fillText(item.value, x + barWidth / 2, y - 8);
// Category label at bottom
ctx.fillStyle = '#b0b0b0';
ctx.font = '12px Segoe UI';
ctx.fillText(item.label, x + barWidth / 2, H - 10);
});
// Y-axis line
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, H - padding);
ctx.stroke();
}
Rendering a Dynamic Todo List
function renderTodos(todos) {
const list = document.getElementById('todo-list');
list.innerHTML = '';
if (todos.length === 0) {
list.innerHTML = '<li class="empty">No todos yet!</li>';
return;
}
todos.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item${todo.done ? ' done' : ''}`;
const span = document.createElement('span');
span.textContent = todo.title; // textContent is safe — no XSS risk
const toggleBtn = document.createElement('button');
toggleBtn.textContent = todo.done ? 'Undo' : '✓';
toggleBtn.addEventListener('click', () => toggleTodo(todo.id));
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '✕';
deleteBtn.className = 'danger';
deleteBtn.addEventListener('click', () => deleteTodo(todo.id));
li.append(span, toggleBtn, deleteBtn);
list.appendChild(li);
});
}
async function toggleTodo(id) {
await fetch(`/todos/${id}/toggle`, { method: 'POST' });
loadStats(); // refresh
}
async function deleteTodo(id) {
await fetch(`/todos/${id}/delete`, { method: 'POST' });
loadStats(); // refresh
}
textContent vs innerHTML: Always use
textContent to set user data in the DOM — it treats the value as plain text, not HTML. Using innerHTML with user data creates XSS vulnerabilities.
Adding a Todo Without Page Reload
document.getElementById('add-form')
.addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('todo-input');
const title = input.value.trim();
if (!title) return;
const res = await fetch('/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title })
});
if (res.ok) {
input.value = '';
loadStats(); // refresh the list + chart
} else {
const err = await res.json();
alert(err.error);
}
});
CSS Animations Without Libraries
/* static/style.css */
/* Fade in new items */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.todo-item {
animation: fadeIn 0.2s ease-out;
}
/* Progress bar */
.progress-bar {
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #ff6b35, #10b981);
transition: width 0.5s ease;
}
// Update progress bar
function updateProgress(done, total) {
const pct = total ? (done / total) * 100 : 0;
document.querySelector('.progress-fill').style.width = pct + '%';
}
Key takeaways:
- Built-in
fetch()— no axios, no xhr wrappers needed - HTML5 Canvas 2D API — full chart drawing, no Chart.js required
- Use
textContentnotinnerHTMLfor user data - DOM manipulation is powerful without React or Vue
- CSS
@keyframes+transition— animations without JavaScript libraries
What's Next
Module 2 is complete! In Post #8, we start Module 3 — the real-time Chat app. We'll implement WebSockets from scratch using Node's net module and raw TCP connections.