← Lập trình JavaScript cơ bản

Bài 12 · Nâng cao · 30 phút· Cập nhật 11/06/2026

Dự án: app web nhỏ

Biên soạn bởi Nguyễn Anh Tuấn

Dự án JavaScript cuối khoá: dựng app To-Do hoàn chỉnh - tách lõi khỏi UI, render DOM, event delegation, lưu bằng localStorage + JSON. Chạy được ngay.

Bài cuối: ráp mọi thứ thành một app To-Do chạy được - thử ngay bên dưới (thêm, đánh dấu, lọc, xoá):

  • Học closure & this
  • Hiểu event loop
  • Làm app To-Do
2 chưa xong · 1/3 hoàn thành

Mỗi việc là một object gồm id, text, done; cả app là một mảng các việc. Nguyên tắc xuyên suốt: tách LÕI (logic thuần) khỏi UI (DOM) - đúng cách mọi công cụ trong khoá được dựng.

mô hình dữ liệu

// Mot viec la mot object:
const task = { id: 1, text: "Hoc JS", done: false };

// Ca app la mot MANG cac viec:
let tasks = [];
  • App = dữ liệu (mảng task) + lõi + render + sự kiện + lưu trữ.
  • Tách lõi (logic thuần) khỏi UI (DOM) để test & bảo trì dễ.
  • Vòng lặp một chiều: sự kiện → cập nhật state → render lại.

Toàn bộ logic là hàm thuần: nhận dữ liệu, trả về dữ liệu MỚI (bất biến) - dựng từ map/filter/reduce của Bài 5. Không đụng DOM, nên test được độc lập.

store.js - không có DOM ở đây

function addTask(tasks, text) {
  const id = tasks.reduce((m, t) => Math.max(m, t.id), 0) + 1;
  return [...tasks, { id, text, done: false }];     // mang MOI
}

function toggleTask(tasks, id) {
  return tasks.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
}

function deleteTask(tasks, id) {
  return tasks.filter((t) => t.id !== id);
}

function stats(tasks) {
  const done = tasks.reduce((n, t) => n + (t.done ? 1 : 0), 0);
  return { total: tasks.length, done, active: tasks.length - done };
}
  • Lõi = hàm thuần: vào dữ liệu → ra dữ liệu MỚI; không phụ thuộc DOM.
  • Dùng map/filter/reduce + spread (Bài 5); luôn bất biến.
  • Vì thuần nên kiểm thử dễ - đúng tinh thần các công cụ tương tác của khoá.

Render là một hàm: nhận mảng task → vẽ ra màn hình (Bài 9). Dùng textContent cho nội dung người dùng nhập (an toàn, tránh XSS).

ui.js - vẽ từ dữ liệu

const ul = document.querySelector("#ds");

function render(tasks) {
  ul.innerHTML = "";                       // xoa danh sach cu
  for (const t of tasks) {
    const li = document.createElement("li");
    li.dataset.id = t.id;                  // nho id de xu ly su kien
    li.textContent = (t.done ? "✓ " : "") + t.text;   // an toan

    const nut = document.createElement("button");
    nut.className = "xoa";                 // main.js nhan ra qua .matches(".xoa")
    nut.dataset.id = t.id;
    nut.textContent = "×";
    li.append(" ", nut);

    ul.append(li);
  }
}
  • render(tasks): nhận dữ liệu → cập nhật DOM (Bài 9).
  • textContent an toàn cho dữ liệu người dùng (tránh XSS).
  • Cách đơn giản & ít lỗi: vẽ LẠI danh sách sau mỗi thay đổi.

Nối lõi với giao diện bằng sự kiện (Bài 9). Mỗi thao tác đi theo một vòng: sự kiện → cập nhật state bằng lõi → render lại.

main.js - điều phối

form.addEventListener("submit", (e) => {
  e.preventDefault();                 // khong reload trang
  tasks = addTask(tasks, input.value);
  input.value = "";
  render(tasks);
});

// Mot listener cho ca danh sach (event delegation):
ul.addEventListener("click", (e) => {
  const id = Number(e.target.dataset.id);
  if (e.target.matches(".xoa")) tasks = deleteTask(tasks, id);
  else tasks = toggleTask(tasks, id);
  render(tasks);
});
  • Vòng một chiều: sự kiện → cập nhật state (lõi) → render lại.
  • preventDefault() chặn form tải lại trang.
  • Event delegation: một listener ở cha xử lý mọi dòng (Bài 9).

Để app “nhớ” qua mỗi lần mở, lưu vào localStorage dưới dạng JSON (Bài 5). Muốn đồng bộ nhiều thiết bị thì cần server - gọi bằng fetch (Bài 10) + async/await (Bài 8).

lưu & nạp

function luu(tasks) {
  localStorage.setItem("tasks", JSON.stringify(tasks));   // object -> chuoi
}
function nap() {
  return JSON.parse(localStorage.getItem("tasks") || "[]"); // chuoi -> object
}
// goi luu(tasks) sau moi thay doi; tasks = nap() luc khoi dong.

// Dong bo voi SERVER (tuy chon) - fetch (Bai 10) + async/await (Bai 8):
// (jsonplaceholder: API gia-lap mien phi, chay that - doi sang server cua ban sau)
async function taiTuServer() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  return res.json();
}
  • localStorage giữ dữ liệu qua các lần tải lại; chỉ lưu CHUỖI → JSON.stringify/parse.
  • Lưu sau mỗi thay đổi; nạp lúc khởi động.
  • Đa thiết bị/đám mây → server qua fetch + async/await (Bài 8 & 10).

Giới hạn của localStorage

localStorage chỉ nằm trên MỘT trình duyệt/máy, dung lượng nhỏ (~5MB) và chỉ chứa chuỗi. Đừng lưu dữ liệu lớn hay nhạy cảm vào đó - khi cần, hãy dùng server.

Cuối cùng, tách file theo vai trò (Bài 11): store.js (lõi), ui.js (render), main.js (điều phối) - rồi npm run build để xuất bản.

ba file, đúng ranh giới lõi-UI

// store.js - loi (khong dung DOM)
export { addTask, toggleTask, deleteTask, stats };

// ui.js - render DOM
export function render(tasks) { /* ... ve danh sach ... */ }

// main.js - dieu phoi: gan su kien, goi store + ui
import { addTask, toggleTask } from "./store.js";
import { render } from "./ui.js";
  • Tách store (lõi) / ui (render) / main (điều phối) - đúng ranh giới lõi-UI.
  • import/export nối các file (Bài 11); Vite gộp & tối ưu.
  • npm run build → thư mục dist/ để deploy lên web.

Chúc mừng - bạn đã hoàn thành khoá! 🎉

Bạn đã đi trọn 12 bài: JS chạy ở đâu → biến & kiểu → rẽ nhánh & vòng lặp → hàm/closure/this → mảng & object → class & prototype → xử lý lỗi → bất đồng bộ → DOM & sự kiện → fetch & JSON → module & npm → và giờ là một app web chạy được. Không chỉ VIẾT được JavaScript, bạn còn HIỂU nó vận hành thế nào. Bước tiếp: làm một dự án của riêng bạn, rồi khám phá một framework (Svelte/React) để dựng app lớn hơn.

Câu hỏi thường gặp

Vì lõi (hàm thuần trên dữ liệu) test được độc lập, không cần DOM; UI chỉ lo render. Sửa giao diện không đụng logic và ngược lại. Đây đúng là nguyên tắc mọi công cụ tương tác trong khoá này dùng - và là lý do chúng kiểm thử được.

Dữ liệu bất biến dễ suy luận và so sánh “trước/sau”, tránh bug do chia sẻ tham chiếu, và hợp với cách các framework (React/Svelte) phát hiện thay đổi để vẽ lại. map/filter/spread (Bài 5) trả về dữ liệu MỚI một cách tự nhiên.

Là kho key-value của trình duyệt, giữ qua các lần tải lại trang. Nó chỉ lưu CHUỖI → dùng JSON.stringify khi lưu và JSON.parse khi đọc. Hợp cho dữ liệu nhỏ; dữ liệu lớn hoặc đồng bộ đa thiết bị thì cần server.

localStorage là đồng bộ nên app cơ bản không cần. Khi muốn đồng bộ với SERVER (lưu đám mây, dùng nhiều thiết bị), bạn gọi API bằng fetch (Bài 10) + async/await (Bài 8) để tải/đẩy dữ liệu mà không chặn giao diện.

Vì task được thêm/xoá liên tục. Thay vì gắn (và gỡ) listener cho từng dòng, ta gắn MỘT listener ở thẻ cha (ul) rồi dựa vào event.target để biết nút nào bị bấm (Bài 9). Gọn và không sợ sót khi danh sách thay đổi.

Tách theo vai trò: store.js (lõi, không đụng DOM), ui.js (render DOM), main.js (gắn sự kiện & điều phối). import/export nối chúng (Bài 11), rồi Vite gộp & build. Ranh giới này chính là “lõi tách khỏi UI”.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Vì sao app tách LÕI (logic thuần) khỏi UI (DOM)?

  1. 1

    Dùng thử

    Với app ở Bước 1: thêm vài task, đánh dấu xong, đổi bộ lọc. Quan sát số đếm thay đổi.

    Hoàn thành khi: Thêm/đánh dấu/lọc hoạt động; phần thống kê (chưa xong / hoàn thành) cập nhật.

  2. 2

    Hàm thuần

    Tự viết toggleTask(tasks, id) trả về MẢNG MỚI, không sửa mảng gốc.

    Hoàn thành khi: Dùng map; chỉ task khớp id đổi done; mảng gốc giữ nguyên.

  3. 3

    Render

    Viết render(tasks) tạo một <li> cho mỗi task (map + createElement/textContent).

    Hoàn thành khi: Danh sách trên trang khớp đúng dữ liệu; dùng textContent (an toàn).

  4. 4

    Sự kiện

    Gắn submit form để thêm; dùng delegation trên ul để đánh dấu/xoá.

    Hoàn thành khi: Mỗi thao tác: cập nhật state bằng lõi → gọi render lại.

  5. 5

    localStorage

    Lưu tasks mỗi khi đổi (JSON.stringify) và nạp lúc mở (JSON.parse).

    Hoàn thành khi: Tải lại trang vẫn còn task như trước.

  6. 6

    Mở rộng

    Thêm MỘT tính năng: sửa nội dung task, hoặc “xoá đã xong”, hoặc đếm còn lại.

    Hoàn thành khi: Tính năng mới chạy; bạn giải thích được luồng sự kiện → state → render.