Dự án tổng hợp: ứng dụng ghi chú chạy trên trình duyệt với thêm/sửa/xóa, tìm kiếm có debounce, lưu localStorage, xuất/nhập tệp JSON, chia mô-đun và có kiểm thử.
- Tổ chức dự án theo mô-đun ES
- Áp dụng DOM, sự kiện, closure, Web Storage
- Tách logic thuần để kiểm thử
- Triển khai lên GitHub Pages
- Single source of truth
- Một nơi duy nhất lưu trạng thái, giao diện vẽ lại từ đó.
- Render
- Hàm vẽ giao diện từ dữ liệu.
- Event delegation
- Gắn một bộ nghe sự kiện ở phần tử cha cho mọi phần tử con.
Khái niệm và lý thuyết
Kiến trúc: notes.js chứa hàm thuần (thêm, sửa, xóa, lọc) nhận mảng cũ trả mảng mới; storage.js đọc/ghi localStorage; main.js giữ trạng thái và hàm render(). Mọi thao tác đều theo vòng: sự kiện → cập nhật trạng thái → lưu → render. Đây chính là tư tưởng của React, Vue mà bạn sẽ học tiếp.
Cấu trúc
ghi-chu/
├── index.html
├── style.css
└── src/
├── notes.js # logic thuần
├── storage.js
├── main.js
└── notes.test.jsnotes.js
export const them = (ds, noiDung) =>
[{ id: crypto.randomUUID(), noiDung, ghim: false, capNhat: Date.now() }, ...ds];
export const sua = (ds, id, noiDung) =>
ds.map(n => n.id === id ? { ...n, noiDung, capNhat: Date.now() } : n);
export const xoa = (ds, id) => ds.filter(n => n.id !== id);
export const ghim = (ds, id) => ds.map(n => n.id === id ? { ...n, ghim: !n.ghim } : n);
export const loc = (ds, tu) =>
ds.filter(n => n.noiDung.toLowerCase().includes(tu.toLowerCase()))
.toSorted((a, b) => b.ghim - a.ghim || b.capNhat - a.capNhat);main.js
import * as N from "./notes.js";
import { doc, ghi } from "./storage.js";
let ds = doc(), tuKhoa = "";
const $ = s => document.querySelector(s);
function capNhat(moi) { ds = moi; ghi(ds); render(); }
function render() {
$("#ds").innerHTML = N.loc(ds, tuKhoa).map(n => `
<li data-id="${n.id}" class="${n.ghim ? "ghim" : ""}">
<p></p>
<button data-act="ghim">📌</button><button data-act="xoa">🗑</button>
</li>`).join("");
$("#ds").querySelectorAll("li").forEach((li, i) =>
li.querySelector("p").textContent = N.loc(ds, tuKhoa)[i].noiDung); // tránh XSS
}
$("#form").addEventListener("submit", e => {
e.preventDefault();
const v = $("#o-nhap").value.trim();
if (v) { capNhat(N.them(ds, v)); e.target.reset(); }
});
$("#ds").addEventListener("click", e => { // event delegation
const act = e.target.dataset.act, id = e.target.closest("li")?.dataset.id;
if (act === "xoa" && confirm("Xóa ghi chú?")) capNhat(N.xoa(ds, id));
if (act === "ghim") capNhat(N.ghim(ds, id));
});
let hen;
$("#tim").addEventListener("input", e => {
clearTimeout(hen); hen = setTimeout(() => { tuKhoa = e.target.value; render(); }, 250);
});
render();Nội dung người dùng gán bằng textContent, không chèn thẳng vào innerHTML để tránh lỗ hổng XSS.
Đẩy thư mục lên GitHub, vào Settings → Pages chọn nhánh main: ứng dụng có địa chỉ công khai sau khoảng một phút.
Mô-đun ES không chạy khi mở tệp trực tiếp (file://). Dùng tiện ích Live Server của VS Code hoặc npx serve.
- Hoàn thiện ứng dụng theo khung trên, thêm chức năng sửa khi nhấp đúp.
- Thêm nút xuất toàn bộ ghi chú ra tệp JSON và nhập lại từ tệp.
- Viết test cho notes.js bằng Vitest (Bài 15).
- Viết lại bằng TypeScript theo loạt bài TypeScript.
- Tách logic thuần, lưu trữ, giao diện.
- Vòng sự kiện → trạng thái → lưu → render.
- Event delegation và debounce giúp hiệu quả.
- Dùng textContent để an toàn trước XSS.