
Web Development Essentials 035.3: Khái niệm cơ bản về SQL
Tạo bảng SQLite, hiểu khóa chính và CHECK, dùng CRUD có WHERE và bind tham số từ Node.js; kiểm tra transaction rollback, mở lại dữ liệu và API driver.

Chọn node DOM, phân biệt collection sống và tĩnh, textContent và innerHTML; cập nhật class, thuộc tính và sự kiện nút bằng chuột, bàn phím trong lab DEMO.
Một nút “Đánh dấu đã đọc” đổi màu nhưng người dùng bàn phím không kích hoạt được. Một thông báo khác lấy nội dung từ trường nhập rồi chèn thẳng vào innerHTML. Cả hai đều có vẻ hoạt động trong một lần bấm chuột, nhưng cơ chế tương tác và cách xử lý dữ liệu chưa đúng. Bài này nối HTML, CSS và JavaScript bằng một trạng thái nhỏ có thể kiểm tra rõ ràng.
Bài 034.4, Thao tác JavaScript đối với Nội dung và Kiểu trang Web, tham chiếu giáo trình 030-100 trang in 271–289, được viết độc lập và cập nhật ngày 23/10/2026. Bạn cần biết module, hàm, điều kiện và selector CSS. Kết quả cần đạt là chọn đúng node, thay văn bản mà không phân tích nó thành HTML, cập nhật class/thuộc tính và xử lý sự kiện bằng điều khiển HTML có ngữ nghĩa.
DOM là giao diện lập trình biểu diễn tài liệu thành các node. Document không phải một phần tử HTML; nó là node tài liệu chứa các thành phần như doctype và phần tử html. Trong cây còn có text node và comment, không chỉ các element. Parser xây dựng cây dần khi đọc HTML, nên không phải đợi toàn bộ tài liệu tải xong thì DOM mới bắt đầu tồn tại.
Thay đổi DOM có thể thay đổi nội dung và kết quả render, nhưng không có nghĩa mọi phép gán đều được vẽ lên màn hình ngay tại chính dòng lệnh đó. Browser còn lập lịch layout và paint. Khi tìm lỗi, đọc trạng thái DOM và computed style cùng kết quả hiển thị, tránh dựa vào giả định “đã đổi biến thì người dùng chắc chắn đã nhìn thấy”.
DOM hiện tại cũng không luôn giống nguyên văn View Source. Parser có thể sửa cấu trúc HTML không hợp lệ, còn script thêm hoặc xóa node sau khi tải. DevTools Elements cho biết cây đang được sử dụng; tệp HTML gốc cho biết đầu vào. Hai nguồn trả lời hai câu hỏi khác nhau.
getElementById("status") trả một element hoặc null; tham số là id, không thêm dấu #.querySelector("#status") trả element khớp đầu tiên hoặc null, dùng cú pháp selector.querySelectorAll(".item") trả NodeList tĩnh của các element khớp tại thời điểm gọi.getElementsByClassName("item") trả HTMLCollection sống, cập nhật khi DOM thay đổi phù hợp.getElementsByTagName("p") trả HTMLCollection sống theo tên thẻ.Một tập sống có thể đổi length và chỉ mục ngay khi bạn xóa class trong vòng lặp đang duyệt nó. Nếu cần ảnh chụp danh sách để thay đổi từng mục, querySelectorAll hoặc chuyển collection thành array có thể giúp ý định rõ. Node trong danh sách tĩnh vẫn là tham chiếu tới element thật, không phải bản sao nội dung của element.
Selector sai cú pháp có thể ném SyntaxError; selector hợp lệ nhưng không khớp trả null hoặc danh sách rỗng tùy API. Hai trường hợp không giống nhau. Nếu id được tạo từ dữ liệu ngoài và có ký tự đặc biệt, cần xử lý cú pháp selector phù hợp, chẳng hạn CSS.escape, hoặc dùng getElementById khi mục đích chỉ là tìm id. Không ghép chuỗi selector tùy tiện từ đầu vào.
p:nth-child(2) là p đồng thời ở vị trí con thứ hai trong các element cùng cha, không phải luôn là p thứ hai. Khi có h2 đứng trước, kết quả sẽ khác. Với mục tiêu “p thứ hai theo loại”, nth-of-type có ngữ nghĩa phù hợp hơn. Đặt id hoặc class có vai trò rõ thường dễ bảo trì hơn dựa vào vị trí ngẫu nhiên trong tài liệu.
TextContent đặt nội dung văn bản. Chuỗi <b>DEMO</b> được hiển thị như văn bản có dấu ngoặc, không tạo phần tử b. InnerHTML phân tích chuỗi thành markup khi gán và thay cây con tương ứng. Nó có thể hữu ích cho nội dung HTML đã kiểm soát, nhưng là điểm nhận dữ liệu nhạy cảm: chuỗi không tin cậy có thể tạo XSS qua nhiều cơ chế, không chỉ thẻ script.
Đừng xem việc script chèn qua innerHTML không chạy trong một trường hợp là bằng chứng chuỗi HTML an toàn. Nếu chỉ cần hiển thị nội dung người dùng hoặc API dưới dạng chữ, dùng textContent. Nếu thật sự phải nhận rich HTML, cần chính sách xử lý và công cụ sanitization phù hợp, cùng các biện pháp phòng thủ liên quan; đó là phạm vi riêng, không được thay bằng một phép replace đơn giản.
Gán textContent cũng thay toàn bộ cây con của node đích. Chọn một vùng kết quả nhỏ, tránh thay cả body rồi mất điều khiển hoặc listener đã gắn. Khi tạo cấu trúc mới, createElement và append giúp xây từng element có chủ đích. Nội dung được chuyển qua API DOM vẫn cần kiểm tra theo ngữ cảnh thuộc tính: đặt một URL không tin cậy vào href không tự an toàn chỉ vì không dùng innerHTML.

SetAttribute đặt giá trị thuộc tính dạng chuỗi; removeAttribute loại thuộc tính. Property của object DOM có thể phản ánh thuộc tính nhưng không phải mọi cặp luôn đồng nhất. Với input, thuộc tính value mô tả giá trị mặc định còn property value phản ánh giá trị hiện tại sau tương tác theo các quy tắc của điều khiển. Kiểm tra đúng lớp khi đọc dữ liệu form.
Thuộc tính boolean như disabled được bật bởi sự hiện diện. disabled="false" không phải cách bật lại nút. Dùng property disabled bằng false hoặc removeAttribute theo nhu cầu. Aria-expanded và aria-pressed có giá trị chuỗi “true”/“false” với ngữ nghĩa riêng, không áp dụng máy móc quy tắc boolean HTML đó cho chúng.
ClassList có add, remove và toggle để cập nhật token class mà không phải ghép lại toàn bộ chuỗi className. Toggle với đối số force boolean giúp class phản ánh trạng thái nguồn một cách xác định. Class thay trình bày theo CSS; nó không tự tạo vai trò, quyền truy cập hoặc nội dung thông báo cho công nghệ hỗ trợ.
Trong tài liệu cũ có thể gặp display hidden. Giá trị đó không hợp lệ; CSS dùng display none để bỏ hộp hiển thị. Hidden là thuộc tính HTML riêng. Khi cần ẩn một vùng, phải cân nhắc ngữ nghĩa và focus; không để người dùng đang tương tác trong vùng rồi làm nó biến mất mà không có phương án focus phù hợp. Lab dưới chỉ đánh dấu, không ẩn nội dung.
Dùng button cho thao tác bấm, a có href cho điều hướng. Một div nhận click chưa tự có focus, kích hoạt bàn phím hay ngữ nghĩa nút. Button type button tránh submit ngoài ý định nếu sau này đặt vào form. Trình duyệt hỗ trợ Enter/Space theo hành vi nút, nên không cần tự giả lập mọi phím cho ví dụ này.
AddEventListener gắn hàm xử lý mà không viết mã JavaScript trực tiếp trong thuộc tính onclick. Bạn vẫn cần nhận biết onclick, mouseover và mouseout trong mã cũ, nhưng chức năng chính không nên chỉ có khi hover. Sự kiện click của nút có thể được kích hoạt bằng bàn phím; đừng gọi nó là sự kiện chỉ dành cho chuột.
Listener nhận event; currentTarget chỉ đối tượng đang chạy listener đó, còn target có thể là node con nơi sự kiện bắt đầu. Khi thêm sự kiện trên container, cần hiểu bubbling và giới hạn phần tử đích. Lab gắn trực tiếp vào một nút để giữ phạm vi nhỏ. Không dùng preventDefault theo thói quen nếu bạn vẫn muốn hành vi mặc định của liên kết hoặc form.
Tạo ba tệp trong thư mục DEMO và dùng HTTP server loopback của bài trước. Trang vẫn có hướng dẫn đọc được khi JavaScript không chạy; nút ban đầu disabled để không tạo lời hứa tương tác sai. Khi module kiểm tra đủ phần tử, nó mới bật nút. Không có đăng nhập, mạng ứng dụng hoặc lưu trữ lâu dài.
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DEMO DOM và sự kiện</title>
<link rel="stylesheet" href="style.css">
<script type="module" src="./app.js"></script>
</head>
<body>
<main>
<h1>Hướng dẫn DEMO</h1>
<section id="guide" class="guide">
<h2>Trước khi gửi yêu cầu</h2>
<p>Ghi triệu chứng, không ghi mật khẩu.</p>
</section>
<button id="mark" type="button" aria-pressed="false" disabled>Đánh dấu đã đọc</button>
<p id="status" role="status">Chưa đánh dấu.</p>
<p id="literal"></p>
</main>
</body>
</html>
body { color: #213641; background: #fff; font-family: system-ui, sans-serif; line-height: 1.6; }
main { max-width: 40rem; margin: auto; padding: 1rem; }
.guide { border: 2px solid #607d8b; padding: 1rem; }
.guide.is-read { border-color: #166534; background: #eef4f2; }
button { font: inherit; margin-top: 1rem; padding: 0.5rem 1rem; }
button:focus-visible { outline: 3px solid #b45309; outline-offset: 3px; }
const button = document.getElementById("mark");
const guide = document.querySelector("#guide");
const status = document.querySelector("#status");
const literal = document.querySelector("#literal");
if (!button || !guide || !status || !literal) {
throw new Error("DEMO thiếu phần tử bắt buộc");
}
literal.textContent = "<b>DEMO chỉ là văn bản</b>";
let read = false;
button.addEventListener("click", function () {
read = !read;
guide.classList.toggle("is-read", read);
button.setAttribute("aria-pressed", String(read));
status.textContent = read ? "Đã đánh dấu đã đọc." : "Chưa đánh dấu.";
});
button.removeAttribute("disabled");
Sau khi tải, nút được bật, aria-pressed false và chưa có class is-read. Dòng có ngoặc b phải hiện nguyên văn và không tạo node b bên trong literal. Bấm nút: class xuất hiện, aria-pressed true, thông báo đổi; bấm lại trả về trạng thái đầu. Nhãn nút giữ ổn định để diễn đạt một toggle button, còn trạng thái pressed và thông báo phản ánh thay đổi.
Tải lại trang rồi nhấn Tab, Enter và Space để thử cùng đường xử lý. Focus nên vẫn thấy trên nút, không bị dời sang thông báo chỉ để đọc nó. Role status tạo vùng thông báo lịch sự cho công nghệ hỗ trợ, nhưng bạn vẫn cần thử với trình đọc màn hình mục tiêu trước khi cam kết cách đọc cụ thể. Kiểm tra DOM không thay thế thử trải nghiệm thực tế đó.
Trong bản sao, đổi id guide ở HTML thành missing rồi tải lại. Module phải báo lỗi rõ và nút giữ disabled. Khôi phục id và xác nhận nút hoạt động lại. Cách này giúp nhận ra HTML và script không còn cùng hợp đồng; không nuốt lỗi bằng optional chaining để trang trông im lặng nhưng chức năng vẫn hỏng.
Lab được chạy ngày 23/10/2026 bằng Chromium 151.0.7922.34 với HTTP loopback, context mới và chặn origin ngoài. Assertion xác nhận trạng thái nút, class, aria-pressed, thông báo, click, Enter/Space và focus; chuỗi có ngoặc không tạo element b. Tập sống/tĩnh, selector sai cú pháp, DOM thiếu phần tử và phục hồi cũng được kiểm tra. Chưa thử tiếng đọc của trình đọc màn hình hoặc lưu trạng thái phía server.
Trong Console của bản DEMO, lấy một NodeList bằng querySelectorAll và một HTMLCollection theo class guide, rồi thêm class đó vào một element mới. Quan sát danh sách tĩnh giữ số mục ban đầu còn tập sống cập nhật. Xóa node vừa tạo khi kết thúc; đây là thí nghiệm bộ nhớ DOM, không phải thay đổi tệp nguồn.
Vì sao textContent phù hợp với chuỗi có dấu ngoặc? Tại sao querySelector sai cú pháp khác không khớp? Class is-read có tự thành trạng thái accessible không? Vì sao listener dùng click trên button phục vụ bàn phím tốt hơn div chỉ có mouseover? Tải lại trang làm mất read vì lab không lưu dữ liệu; đừng nhận màu đã đổi là bằng chứng một yêu cầu đã được ghi vào server.


Tạo bảng SQLite, hiểu khóa chính và CHECK, dùng CRUD có WHERE và bind tham số từ Node.js; kiểm tra transaction rollback, mở lại dữ liệu và API driver.

Tạo route Express, phục vụ CSS và render EJS; đọc query, params, body, kiểm tra dữ liệu form, escape HTML và phân biệt XSS với CSRF qua lab loopback.

Chạy Node.js LTS, phân biệt ES module và CommonJS, package.json và lockfile; cài package DEMO cục bộ, dùng npm ci và kiểm tra server HTTP trên loopback.

Tổ chức hàm JavaScript với if, switch, vòng lặp và return; hiểu truthy, so sánh, break, continue và xử lý Promise bằng async/await qua dữ liệu DEMO.