Hướng dẫn từng bước, rõ ràng để xây một ứng dụng to-do nhỏ: lập kế hoạch tính năng, tạo màn hình, thêm logic, lưu dữ liệu, kiểm thử và đưa lên mạng.

Khi tôi nói “ứng dụng” trong hướng dẫn này, tôi ám chỉ một web app nhỏ: một trang đơn bạn mở trong trình duyệt và nó phản hồi với những gì bạn nhấp và gõ. Không cần cài đặt, không cần tài khoản, không cần cấu hình phức tạp—chỉ là một dự án đơn giản bạn có thể chạy cục bộ.
Cuối cùng, bạn sẽ có một ứng dụng to‑do có thể:
localStorage (vậy đóng tab không làm mất dữ liệu)Nó sẽ không hoàn hảo hay “chuẩn doanh nghiệp”, và đó là mục đích. Đây là dự án cho người mới, giúp học những kiến thức cơ bản mà không ném vào nhiều công cụ.
Bạn sẽ xây ứng dụng từng bước và nắm được các phần cốt lõi của cách hoạt động của ứng dụng phía giao diện:
Giữ cho đơn giản. Bạn chỉ cần:
Nếu bạn biết cách tạo một thư mục và chỉnh sửa vài file, bạn đã sẵn sàng.
Trước khi viết bất kỳ mã nào, hãy quyết định “thành công” trông như thế nào. Hướng dẫn này xây một ứng dụng nhỏ với một mục tiêu rõ: giúp bạn theo dõi các nhiệm vụ cần làm.
Viết một câu mục tiêu để luôn giữ trong đầu khi xây:
“Ứng dụng này cho phép tôi thêm nhiệm vụ vào danh sách để không quên.”
Chỉ vậy thôi. Nếu bạn muốn thêm lịch, nhắc nhở, thẻ hay tài khoản, để dành ý đó cho sau.
Tạo hai danh sách nhanh:
Bắt buộc (cho dự án này):
localStorage)Tùy chọn (không cần hôm nay): ngày tới hạn, ưu tiên, danh mục, tìm kiếm, kéo thả, đồng bộ mây.
Giữ phần “bắt buộc” nhỏ giúp bạn hoàn thành thực sự.
Ứng dụng này có thể là một trang duy nhất với:
Hãy cụ thể để không bị vướng:
Khi đã quyết xong, bạn sẵn sàng tạo file dự án.
Trước khi viết mã, hãy tạo một “ngôi nhà” nhỏ cho ứng dụng. Giữ file có tổ chức từ đầu sẽ làm các bước sau dễ hơn.
Tạo một thư mục mới trên máy và đặt tên ví dụ todo-app. Thư mục này sẽ chứa mọi thứ cho dự án.
Bên trong thư mục đó, tạo ba file:
index.html (cấu trúc trang)styles.css (giao diện và bố cục)app.js (hành vi và tương tác)Nếu máy của bạn ẩn phần mở rộng file (ví dụ “.html”), hãy chắc bạn đang tạo file thật. Lỗi phổ biến là tạo index.html.txt vô tình.
Mở thư mục todo-app trong editor (VS Code, Sublime Text, v.v.). Sau đó mở index.html trong trình duyệt.
Lúc này trang có thể trống—điều đó không sao. Chúng ta sẽ thêm nội dung ở bước tiếp theo.
Khi chỉnh sửa file, trình duyệt không tự cập nhật (trừ khi bạn dùng công cụ làm việc đó).
Quy trình cơ bản là:
Nếu có gì “không hoạt động”, làm mới là bước đầu tiên nên thử.
Bạn có thể chạy bằng cách nhấp đúp index.html, nhưng một máy chủ cục bộ tránh một số vấn đề lạ sau này (đặc biệt khi lưu dữ liệu hoặc tải file).
Các lựa chọn thân thiện với người mới:
python -m http.server
Rồi mở địa chỉ nó in ra (thường là http://localhost:8000) trong trình duyệt.
Bây giờ chúng ta tạo khung sạch cho ứng dụng. HTML này chưa làm gì tương tác (phần đó ở sau), nhưng nó cho JavaScript các điểm rõ ràng để đọc và ghi.
Chúng ta sẽ bao gồm:
Giữ tên đơn giản và dễ đọc. ID/classes rõ ràng giúp JavaScript lấy phần tử mà không nhầm lẫn.
index.html<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>To‑Do App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="app" id="app">
<h1 class="app__title" id="appTitle">My To‑Do List</h1>
<form class="task-form" id="taskForm">
<label class="task-form__label" for="taskInput">New task</label>
<div class="task-form__row">
<input
id="taskInput"
class="task-form__input"
type="text"
placeholder="e.g., Buy milk"
autocomplete="off"
/>
<button id="addButton" class="task-form__button" type="submit">
Add
</button>
</div>
</form>
<ul class="task-list" id="taskList" aria-label="Task list"></ul>
</main>
<script src="app.js"></script>
</body>
</html>
Đó là khung. Chú ý chúng ta dùng id="taskInput" và id="taskList"—đó là hai phần tử bạn sẽ thao tác nhiều nhất trong JavaScript.
Trang đã có, nhưng có lẽ trông như tài liệu thô. Một chút CSS giúp dễ dùng: khoảng cách, chữ dễ đọc và nút nhìn như có thể nhấn.
Một hộp căn giữa giúp tập trung nội dung và tránh kéo dãn qua màn hình rộng.
/* Basic page setup */
body {
font-family: Arial, sans-serif;
background: #f6f7fb;
margin: 0;
padding: 24px;
}
/* Centered app container */
.container {
max-width: 520px;
margin: 0 auto;
background: #ffffff;
padding: 16px;
border-radius: 10px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
}
Mỗi nhiệm vụ nên trông như một “hàng” riêng, với khoảng cách thoải mái.
ul { list-style: none; padding: 0; margin: 16px 0 0; }
li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid #e7e7ee;
border-radius: 8px;
margin-bottom: 10px;
}
.task-text { flex: 1; }
Khi một nhiệm vụ hoàn thành, nó nên thay đổi về mặt hiển thị để dễ nhìn.
.done .task-text {
text-decoration: line-through;
color: #777;
opacity: 0.85;
}
Giữ các nút cùng cỡ và kiểu để chúng giống một phần của cùng một ứng dụng.
button {
border: none;
border-radius: 8px;
padding: 8px 10px;
cursor: pointer;
}
button:hover { filter: brightness(0.95); }
Đó là đủ để có giao diện sạch và thân thiện—không cần mánh khóe. Bây giờ chúng ta sẽ nối hành vi bằng JavaScript.
Khi đã có input, nút và danh sách, ta sẽ làm cho chúng hoạt động. Mục tiêu đơn giản: khi ai đó gõ nhiệm vụ và nhấn Add (hoặc Enter), một mục mới xuất hiện trong danh sách.
Trong file JavaScript, trước hết lấy các phần tử cần thiết, sau đó lắng nghe hai hành động: click nút và phím Enter trong ô nhập.
const taskInput = document.querySelector('#taskInput');
const addBtn = document.querySelector('#addBtn');
const taskList = document.querySelector('#taskList');
function addTask() {
const text = taskInput.value.trim();
// Block empty tasks (including ones that are just spaces)
if (text === '') return;
const li = document.createElement('li');
li.textContent = text;
taskList.appendChild(li);
// Clear the input and put the cursor back
taskInput.value = '';
taskInput.focus();
}
addBtn.addEventListener('click', addTask);
taskInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
addTask();
}
});
trim() để loại khoảng trắng đầu/cuối.li mới, đặt nội dung văn bản và thêm vào danh sách.Nếu chẳng có gì xảy ra, kiểm tra lại ID phần tử trong HTML có khớp với selector trong JS hay không (đó là lỗi phổ biến của người mới).
Giờ bạn đã thêm được nhiệm vụ, hãy làm cho chúng có thể hành động: đánh dấu hoàn thành và xoá.
Thay vì lưu nhiệm vụ như chuỗi, lưu chúng dưới dạng đối tượng. Điều này cho mỗi nhiệm vụ một định danh ổn định và nơi lưu trạng thái “done”:
text: nội dung nhiệm vụdone: true hoặc falseid: số duy nhất để ta tìm/xoá đúng nhiệm vụVí dụ:
let tasks = [
{ id: 1, text: "Buy milk", done: false },
{ id: 2, text: "Email Sam", done: true }
];
Khi hiển thị mỗi nhiệm vụ lên trang, bao gồm checkbox hoặc nút “Done”, và thêm nút “Delete”.
Event listener là cách phản ứng với click. Gắn nó vào nút (hoặc chính vùng danh sách), khi người dùng nhấp, mã của bạn sẽ chạy.
Một mẫu thân thiện với người mới là event delegation: đặt một listener click trên container danh sách, rồi kiểm tra phần tử nào bị nhấn.
function toggleDone(id) {
tasks = tasks.map(t => t.id === id ? { ...t, done: !t.done } : t);
renderTasks();
}
function deleteTask(id) {
tasks = tasks.filter(t => t.id !== id);
renderTasks();
}
document.querySelector("#taskList").addEventListener("click", (e) => {
const id = Number(e.target.dataset.id);
if (e.target.matches(".toggle")) toggleDone(id);
if (e.target.matches(".delete")) deleteTask(id);
});
Trong renderTasks() của bạn:
data-id="${task.id}" vào mỗi nút..done).Hiện tại danh sách to‑do của bạn có vấn đề khó chịu: nếu làm mới trang hoặc đóng tab, mọi thứ biến mất.
Đó là vì nhiệm vụ chỉ tồn tại trong bộ nhớ JavaScript. Khi tải lại, bộ nhớ đó bị reset.
localStorage có sẵn trong trình duyệt. Bạn có thể tưởng tượng nó như một chiếc hộp nhỏ nơi lưu chuỗi văn bản dưới một tên (key). Rất hợp cho dự án người mới vì không cần server hay tài khoản.
Chúng ta sẽ lưu toàn bộ danh sách nhiệm vụ dưới dạng JSON, rồi tải lại khi mở trang.
Bất cứ khi nào bạn thêm, đánh dấu hay xóa, gọi saveTasks().
const STORAGE_KEY = "todo.tasks";
function saveTasks(tasks) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}
Ở chỗ cập nhật tasks, thực hiện ngay:
saveTasks(tasks);
renderTasks(tasks);
Khi trang mở, đọc giá trị đã lưu. Nếu chưa có gì, dùng danh sách rỗng.
function loadTasks() {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : [];
}
let tasks = loadTasks();
renderTasks(tasks);
Thế là: ứng dụng nhớ nhiệm vụ sau khi làm mới.
Mẹo: localStorage chỉ lưu chuỗi, nên JSON.stringify() biến mảng thành chuỗi, và JSON.parse() biến chuỗi đó trở lại mảng khi tải.
Kiểm thử nghe chán nhưng đó là cách nhanh nhất để biến ứng dụng từ “chạy trên máy tôi” thành “chạy ổn mọi lúc”. Làm một lượt nhanh sau mỗi thay đổi nhỏ.
Thực hiện theo luồng chính:
Nếu bước nào thất bại, sửa trước khi thêm tính năng mới. Dự án nhỏ nhanh bẩn khi bạn chồng nhiều lỗi.
Đó là những input bạn không thiết kế nhưng người dùng sẽ thử:
Sửa phổ biến là chặn nhiệm vụ rỗng:
const text = input.value.trim();
addButton.disabled = text === "";
(Bạn có thể chạy đoạn này trên mỗi sự kiện input, và kiểm tra lại trước khi thêm.)
Khi click mà không thấy gì, thường là:
app.js không tìm thấy).Khi có lỗi lặt vặt, log thông tin:
console.log("Adding:", text);
console.log("Current tasks:", tasks);
Mở Console của trình duyệt để xem lỗi (chữ đỏ). Sửa xong thì gỡ những log này cho gọn.
Một ứng dụng to‑do chỉ thật sự “hoàn chỉnh” khi dễ dùng cho người thật—trên điện thoại, với bàn phím và với công cụ hỗ trợ như screen reader.
Trên màn hình nhỏ, nút nhỏ rất khó nhấn. Cho các phần có thể nhấn đủ không gian:
Nếu dùng CSS, tăng padding, font-size và gap thường giúp nhiều.
Screen reader cần tên rõ ràng cho điều khiển.
label thật (tốt nhất). Nếu bạn không muốn hiển thị, có thể ẩn về mặt hiển thị bằng CSS nhưng vẫn giữ trong HTML.aria-label="Delete task" để screen reader không chỉ đọc “button” mà không biết làm gì.Điều này giúp mọi người hiểu từng điều khiển mà không phải đoán mò.
Đảm bảo có thể dùng toàn bộ app mà không cần chuột:
form để Enter hoạt động tự nhiên).Dùng cỡ chữ dễ đọc (16px là nền tảng tốt) và tương phản màu mạnh (chữ tối trên nền sáng hoặc ngược lại). Tránh chỉ dùng màu để hiển thị trạng thái “đã xong”—hãy thêm kiểu rõ ràng như gạch ngang kèm trạng thái.
Khi mọi thứ hoạt động, dành 10–15 phút để dọn dẹp. Việc này giúp sửa lỗi sau này dễ hơn và bạn hiểu lại dự án khi quay lại.
Giữ nhỏ và dễ đoán:
/index.html — cấu trúc trang (input, nút, danh sách)/styles.css — giao diện (khoảng cách, font, kiểu “đã xong”)/app.js — hành vi (thêm, chuyển trạng thái, xóa, lưu/tải)/README.md — ghi chú cho “tôi của tương lai”Nếu bạn thích, có thể dùng thư mục con:
/css/styles.css/js/app.jsChỉ cần chắc link và script trỏ đúng đường dẫn.
Một vài mẹo nhanh:
taskInput, taskList, saveTasks()Ví dụ dễ đọc:
renderTasks(tasks)addTask(text)toggleTask(id)deleteTask(id)README.md có thể đơn giản:
index.html trong trình duyệt)Ít nhất, nén (zip) thư mục sau khi hoàn thành một mốc (ví dụ “localStorage hoạt động”). Nếu muốn quản lý phiên bản, Git rất hữu ích—nhưng tùy chọn. Một bản sao lưu có thể cứu bạn khỏi xóa nhầm.
Đưa lên mạng nghĩa là đặt các file (HTML, CSS, JavaScript) ở nơi công khai để người khác mở link và dùng. Vì ứng dụng to‑do này là “site tĩnh” (chạy trong trình duyệt, không cần server), bạn có thể host miễn phí trên vài dịch vụ.
Các bước tổng quan:
Nếu dùng file rời, kiểm tra tên file khớp chính xác với đường dẫn (styles.css vs style.css).
Nếu bạn muốn cách đơn giản nhất:
localStorage hoạt động).Khi ổn, gửi link cho một người bạn và nhờ họ thử—đôi mắt mới thường bắt lỗi nhanh.
Bạn đã xây một ứng dụng to‑do hoạt động. Nếu muốn học thêm mà không nhảy vào dự án lớn, những nâng cấp này mang lại giá trị và dạy các mô hình hữu ích.
Thêm nút “Edit” cạnh mỗi nhiệm vụ. Khi nhấn, đổi nhãn thành ô nhập nhỏ (được điền sẵn), kèm “Save” và “Cancel”.
Gợi ý: giữ dữ liệu nhiệm vụ dưới dạng mảng đối tượng (có id và text). Việc chỉnh sửa là: tìm theo id, cập nhật text, render lại và lưu.
Thêm ba nút ở trên: All, Active, Done.
Lưu bộ lọc hiện tại vào biến như currentFilter = 'all'. Khi render, hiển thị:
Giữ nhẹ nhàng:
YYYY-MM-DD) và hiển thị cạnh nhiệm vụThêm một trường cũng dạy bạn cách cập nhật mô hình dữ liệu và giao diện cùng lúc.
Khi sẵn sàng, ý tưởng lớn là: thay vì lưu vào localStorage, bạn gửi nhiệm vụ tới một API (server) bằng fetch(). Server lưu vào cơ sở dữ liệu, nên nhiệm vụ có thể đồng bộ giữa thiết bị.
Nếu muốn thử bước này mà không viết lại hết, nền tảng tạo prototype như Koder.ai có thể giúp bạn nhanh: mô tả các endpoint, bảng cơ sở dữ liệu, thay đổi UI trong chat, và vẫn xuất mã nguồn khi bạn muốn học hoặc tuỳ chỉnh dự án React/Go/PostgreSQL được sinh ra.
Thử xây một ứng dụng ghi chú (có tìm kiếm) hoặc tracker thói quen (check-in hàng ngày). Chúng dùng lại cùng kỹ năng: render danh sách, chỉnh sửa, lưu và thiết kế UI cơ bản.