โหมดมืด
บทที่ 12 — DOM และ Events: ควบคุมหน้าเว็บ
บทนี้คือเหตุผลที่ JavaScript เกิดมา — ทำให้หน้าเว็บ "มีชีวิต" ตอบสนองผู้ใช้ได้
อ่านจบบทนี้คุณจะ:
- เข้าใจ DOM — โครงสร้างของหน้าเว็บที่ JavaScript ควบคุมได้
- ค้นหาและแก้ไของค์ประกอบในหน้าเว็บ
- จัดการ event — คลิก, พิมพ์, submit ฟอร์ม
- โหลดข้อมูลจาก server ด้วย
fetch - เก็บข้อมูลในเบราว์เซอร์
ใช้เวลา 3 ชั่วโมง — บทนี้ต้องลองในเบราว์เซอร์
⚠️ ต่างจากบทที่ผ่านมา: บทก่อนหน้าใช้ Node.js รันในเทอร์มินัล — บทนี้ต้องรันในเบราว์เซอร์ เพราะ DOM มีแค่ในเบราว์เซอร์เท่านั้น
🖥️ วิธีลองโค้ดในบทนี้ (เลือกวิธีใดวิธีหนึ่ง):
วิธีที่ 1 — พิมพ์ใน DevTools Console โดยตรง (เร็วสุด สำหรับทดสอบบรรทัดเดียว):
- เปิด Chrome/Firefox
- กด F12 (Windows/Linux) หรือ Cmd+Option+I (Mac) เพื่อเปิด DevTools
- คลิก tab "Console"
- พิมพ์โค้ดแล้วกด Enter
วิธีที่ 2 — สร้างไฟล์ HTML + JS (แนะนำสำหรับโค้ดที่ยาวขึ้น):
- สร้างไฟล์
index.html:html(attribute<!DOCTYPE html> <html lang="th"> <head><meta charset="UTF-8"><title>ทดสอบ DOM</title></head> <body> <h1>หัวข้อ</h1> <p>ย่อหน้า</p> <script src="app.js" defer></script> </body> </html>defer= รอให้ HTML โหลดครบก่อนค่อยรัน JS — สำคัญมาก ดูหมายเหตุใน Part 5.1)- สร้างไฟล์
app.jsในโฟลเดอร์เดียวกัน- เปิด
index.htmlในเบราว์เซอร์ (double-click หรือลาก file เข้า Chrome)- เปิด DevTools (F12) → tab Console เพื่อดูผล
console.log
Part 1: DOM คืออะไร
1.1 หน้าเว็บ = ต้นไม้
เมื่อเบราว์เซอร์โหลดหน้า HTML — มันแปลง HTML เป็นโครงสร้าง "ต้นไม้" ที่อยู่ใน memory เรียกว่า DOM (Document Object Model)
ลองดู HTML นี้:
html
<body>
<h1>หัวข้อ</h1>
<p>ย่อหน้า <strong>ตัวหนา</strong></p>
</body>เบราว์เซอร์แปลงเป็นต้นไม้:
แต่ละกล่องในต้นไม้เรียก "element" (หรือ "node" — โหนด คือคำรวมที่ใช้เรียก element, ข้อความ หรือส่วนต่าง ๆ ใน DOM)
1.2 DOM = หน้าเว็บที่ JavaScript จับต้องได้
DOM คือ "ตัวแทน" ของหน้าเว็บในรูปแบบที่ JavaScript เข้าถึงได้ — JavaScript สามารถ:
- อ่าน — ดูว่าในหน้ามีอะไร
- แก้ไข — เปลี่ยนข้อความ, สี, เพิ่ม/ลบ element
- ตอบสนอง — รับ event (คลิก, พิมพ์) แล้วทำงาน
💡 เปรียบเทียบ: HTML = "พิมพ์เขียวบ้าน" (เอกสารนิ่ง ๆ) — DOM = "บ้านจริงที่สร้างเสร็จ" ที่เราเดินเข้าไปจัดเฟอร์นิเจอร์ได้ — JavaScript = "เจ้าของบ้าน" ที่เข้าไปจัดการ
1.3 จุดเข้าถึง — document
document คือ object ที่เป็น "ราก" ของ DOM — ทุกอย่างเริ่มจากตรงนี้:
javascript
document.body // element <body>
document.title // ชื่อหน้า (จาก <title>)Part 2: ค้นหา Element
ก่อนจะแก้ไของค์ประกอบ — ต้อง "หา" มันเจอก่อน
2.1 querySelector — หาด้วย CSS selector
querySelector คือวิธีหา element ที่ใช้บ่อยที่สุด — มันใช้ "CSS selector" ซึ่งเป็นรูปแบบสำหรับ "เลือก" element ในหน้าเว็บ ไม่ต้องรู้ CSS ก่อน แค่จำ 3 รูปแบบหลัก:
tag— เลือกตาม tag เช่น"h1","button","p".class— เลือกตาม class เช่น".card",".primary"#id— เลือกตาม id เช่น"#header","#loginForm"
javascript
document.querySelector("h1") // <h1> ตัวแรก
document.querySelector(".card") // element ที่มี class "card" ตัวแรก
document.querySelector("#header") // element ที่มี id "header"
document.querySelector("button.primary") // <button> ที่มี class "primary"
document.querySelector("ul > li") // <li> ที่เป็นลูกตรงของ <ul>CSS selector ที่ใช้บ่อย:
tag— เช่นdiv,p,button.class— class#id— idparent child— ลูกหลาน
querySelector คืน element ตัวแรกที่เจอ — ถ้าไม่เจอคืน null
2.2 querySelectorAll — หาทั้งหมด
javascript
const allCards = document.querySelectorAll(".card");
// คืน NodeList (คล้าย array) ของ element ทุกตัวที่ตรง
allCards.forEach(card => {
console.log(card);
});querySelectorAll คืน NodeList (= "list ของ node" — คล้าย array แต่ไม่ใช่ array แท้)
NodeList ทำอะไรได้และทำอะไรไม่ได้:
javascript
allCards.forEach(card => { ... }); // ✅ forEach ใช้ได้
allCards.length; // ✅ .length ใช้ได้
// ❌ map/filter ใช้ไม่ได้กับ NodeList โดยตรง
// allCards.map(...) → TypeError
// ✅ แปลงเป็น array ก่อน — แล้วใช้ map/filter ได้ทุกอย่าง
const cardsArray = [...allCards]; // spread เป็น array
cardsArray.map(card => card.textContent); // ✅ ได้แล้ว💡 NodeList จาก
querySelectorAllเป็น "static" — คือ snapshot ณ เวลาที่ query ถ้าเพิ่ม/ลบ element ทีหลัง NodeList ไม่อัปเดตตาม ต่างจากgetElementsByClassName/getElementsByTagNameที่คืน "live" collection ที่อัปเดตตัวเองอัตโนมัติ⚠️ อีกจุดต่าง:
getElementsByClassName/getElementsByTagNameคืนHTMLCollectionไม่ใช่NodeList— และHTMLCollectionไม่มี.forEach()เลย (ต่างจากNodeListที่มี) ถ้าจะใช้forEach/map/filterกับผลลัพธ์จากสองตัวนี้ ต้องแปลงเป็น array ก่อนเสมอ ([...collection])
2.3 วิธีเก่า (รู้จักไว้)
javascript
document.getElementById("header") // หาด้วย id
document.getElementsByClassName("card") // หาด้วย class
document.getElementsByTagName("p") // หาด้วย tag→ ใช้ querySelector/querySelectorAll เป็นหลัก — มันยืดหยุ่นกว่า (ใช้ CSS selector ได้ทุกแบบ)
2.4 เดินทางในต้นไม้
จาก element หนึ่ง — ไปหา element ที่เกี่ยวข้องได้:
javascript
const el = document.querySelector(".item");
el.parentElement // element แม่
el.children // element ลูกทั้งหมด
el.firstElementChild // ลูกตัวแรก
el.nextElementSibling // element พี่น้องตัวถัดไป
el.closest(".container") // element แม่/ปู่ที่ใกล้สุดที่ตรง selectorPart 3: อ่านและแก้ไข Element
3.1 ข้อความ — textContent (ปลอดภัย เป็น default)
⚠️ XSS (Cross-Site Scripting) = การโจมตีโดยฝัง script ผ่าน input ของผู้ใช้
ผู้ไม่หวังดีส่งข้อความที่มี
<script>หรือonerror=...เข้ามา ถ้าเว็บนำข้อความนั้นไปแสดงแบบ "แปล HTML" (ไม่ใช่ข้อความล้วน) — script นั้นจะรันในเบราว์เซอร์ของเหยื่อทันที และขโมย cookie/token ได้
วิธีที่ปลอดภัยและง่ายที่สุดในการอ่าน/เปลี่ยนข้อความใน element คือ textContent — มันมองทุกอย่างเป็นข้อความล้วน (ไม่แปล HTML tag) จึงปลอดภัยจาก XSS โดยอัตโนมัติ:
javascript
const heading = document.querySelector("h1");
heading.textContent // อ่านข้อความ
heading.textContent = "หัวข้อใหม่"; // เปลี่ยนข้อความ→ กฎข้อ 1: ถ้าจะแสดง "ข้อความ" ใช้ textContent ตลอด ไม่ต้องคิด
3.2 HTML — innerHTML (ระวัง XSS!)
javascript
const div = document.querySelector(".content");
div.innerHTML = "<strong>ตัวหนา</strong> และข้อความ"; // string ตายตัวที่เราเขียนเอง — ปลอดภัยinnerHTML ใส่ HTML เข้าไป (เบราว์เซอร์จะแปล tag)
⚠️ 🔴 อันตรายร้ายแรง — XSS: ห้าม ใส่ข้อมูลจากผู้ใช้ (หรือจาก API/URL/database) ลง innerHTML ตรง ๆ:
javascript
// ❌ 🔴 อันตราย — userInput มาจากผู้ใช้
div.innerHTML = userInput;
// ตัวอย่าง userInput ที่อันตราย:
// '<img src=x onerror="fetch(`/api/steal?c=${document.cookie}`)">'
// → script ใน onerror รันทันทีตอนแปล HTML — cookie ผู้ใช้ถูกขโมยวิธีจัดการ ขึ้นกับว่าต้องการแสดง "ข้อความ" หรือ "HTML จริง ๆ":
(1) ถ้าแค่ "ข้อความ" → ใช้ textContent (default — ปลอดภัย 100%):
javascript
// ✅ ปลอดภัย — แปลทุกอย่างเป็นข้อความล้วน
div.textContent = userInput;(2) ถ้าต้อง "อนุญาต HTML บางอย่าง" (เช่น Markdown ที่ render แล้ว) → ต้อง sanitize ก่อน (ทำให้สะอาด คือกรอง tag/attribute อันตรายออก):
⚠️ DOMPurify เป็น library ภายนอก ไม่ได้มากับเบราว์เซอร์ — ต้องติดตั้งก่อนด้วย
npm install dompurify(แนวคิด package manager จะเรียนละเอียดบทที่ 13) และตัวอย่างimport DOMPurify from "dompurify"ด้านล่างนี้ใช้ได้เมื่อมี bundler (เช่น Vite/webpack) จัดการให้เท่านั้น — เขียนimportแบบนี้ใน<script>ธรรมดาบนเบราว์เซอร์ตรง ๆ (ตามวิธีลองโค้ดของบทนี้) จะไม่ทำงาน
javascript
// ✅ วิธี ก: Sanitizer API ของเบราว์เซอร์ — ตรวจสอบสถานะ/support ล่าสุดที่ caniuse.com ก่อนใช้เสมอ
// (ยังไม่รองรับในทุกเบราว์เซอร์ ณ ตอนที่เขียนนี้ — ต้อง feature-detect และมี fallback)
if (typeof div.setHTML === "function") {
div.setHTML(userInput); // กรอง <script>, onerror, ฯลฯ ออกอัตโนมัติ
} else {
// fallback สำหรับเบราว์เซอร์ที่ยังไม่รองรับ
div.innerHTML = DOMPurify.sanitize(userInput);
}
// ✅ วิธี ข: ใช้ library DOMPurify (แนะนำ — ใช้ได้ทุกเบราว์เซอร์ เป็นมาตรฐานวงการ)
// ต้องมี bundler (Vite/webpack) + npm install dompurify ก่อนถึงจะ import แบบนี้ได้
import DOMPurify from "dompurify";
div.innerHTML = DOMPurify.sanitize(userInput);💡 คำแนะนำ:
- งานทั่วไป → ใช้
textContentเป็น default (ปลอดภัย พอใช้ 95% ของกรณี)- ต้องแสดง rich text จากผู้ใช้ (comment, post, Markdown) → ใช้ DOMPurify เพราะรองรับทุกเบราว์เซอร์และเป็นมาตรฐานวงการที่ผ่านการตรวจสอบความปลอดภัยมาเยอะ — ถือเป็นตัวเลือกหลักที่แนะนำ
- Sanitizer API ในตัว (
setHTML()) — support ยังไม่ครบทุกเบราว์เซอร์ ตรวจสอบ caniuse.com ก่อนใช้ในงานจริง อย่าถือว่าเทียบเท่า DOMPurify ในความน่าเชื่อถือ
⚠️ ห้ามแก้ปัญหาด้วยการ "filter เอง" (เช่น replace("<script>", "")) — ผู้โจมตีหลบเลี่ยงได้ง่ายมาก (เช่น <scr<script>ipt>) ต้องใช้ library/API ที่ออกแบบมาเพื่องานนี้เท่านั้น
3.3 Attribute
เราอ่าน/ตั้ง/ลบ attribute ของ element (เช่น href, src, value) ด้วย getAttribute/setAttribute/removeAttribute — หรือเข้าถึง attribute ยอดนิยมตรง ๆ ผ่าน property (link.href, input.value):
javascript
const link = document.querySelector("a");
link.getAttribute("href") // อ่าน attribute
link.setAttribute("href", "/new") // ตั้ง attribute
link.removeAttribute("target") // ลบ attribute
// attribute ที่ใช้บ่อย เข้าถึงตรง ๆ ได้
link.href
img.src
input.value // ค่าใน input
input.disabled = true // ปิดการใช้งาน3.4 Class — classList
javascript
const box = document.querySelector(".box");
box.classList.add("active") // เพิ่ม class
box.classList.remove("hidden") // ลบ class
box.classList.toggle("open") // มี → ลบ, ไม่มี → เพิ่ม
box.classList.contains("active") // เช็คว่ามี class ไหม→ เปลี่ยนหน้าตา element ด้วยการเพิ่ม/ลบ class (ไม่ใช่ไปแก้ style ตรง ๆ) — เพราะ class ควบคุมที่ CSS โค้ดเป็นระเบียบกว่า
3.5 Style — แก้ตรง ๆ (ใช้น้อย)
javascript
const el = document.querySelector(".box");
el.style.color = "red";
el.style.fontSize = "20px"; // ⚠️ camelCase! (ไม่ใช่ font-size)CSS property ที่มีขีดกลาง (font-size) → ใน JavaScript เขียนแบบ camelCase (fontSize)
→ ส่วนใหญ่แนะนำใช้ classList มากกว่า แก้ style ตรง ๆ
Part 4: สร้างและลบ Element
4.1 สร้าง element ใหม่
การเพิ่มเนื้อหาลงหน้าเว็บด้วยโค้ดทำเป็น 3 ขั้น: สร้าง element ด้วย createElement, ใส่เนื้อหา/class, แล้วเอาเข้า DOM ด้วย appendChild — เป็นพื้นฐานของการสร้าง UI แบบ dynamic:
javascript
// 1. สร้าง element
const newItem = document.createElement("li");
// 2. ใส่เนื้อหา
newItem.textContent = "รายการใหม่";
newItem.classList.add("item");
// 3. ใส่เข้าหน้าเว็บ
const list = document.querySelector("ul");
list.appendChild(newItem); // เพิ่มเป็นลูกตัวสุดท้าย4.2 วิธีใส่ element
มีหลายเมธอดสำหรับวาง element ในตำแหน่งต่าง ๆ — ต่อท้าย (appendChild), แทรกหน้าสุด (prepend), แทรกก่อน/หลัง element อื่น (before/after) และลบออก (remove):
javascript
parent.appendChild(child) // เพิ่มเป็นลูกตัวสุดท้าย
parent.prepend(child) // เพิ่มเป็นลูกตัวแรก
el.before(sibling) // ใส่ก่อนหน้า el
el.after(sibling) // ใส่หลัง el
el.remove() // ลบ el ออก4.3 ⚠️ ระวัง — แก้ DOM บ่อย ๆ ทำให้ช้า
ทุกครั้งที่แก้ DOM — เบราว์เซอร์ต้อง "คำนวณ layout ใหม่" ถ้าทำในloop ที่วนเยอะ → ช้า
javascript
// ❌ ช้า — แก้ DOM 1000 ครั้ง
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = i;
list.appendChild(li); // แตะ DOM ทุกรอบ
}
// ✅ เร็ว — สร้างทั้งหมดก่อน แล้วใส่ทีเดียว
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = i;
fragment.appendChild(li); // ใส่ใน fragment (ยังไม่แตะหน้าจริง)
}
list.appendChild(fragment); // ใส่หน้าจริงครั้งเดียวDocumentFragment คือ "ที่พักชั่วคราว" — สร้างทุกอย่างในนั้นก่อน แล้วใส่หน้าจริงรวดเดียว
Part 5: Events — ตอบสนองผู้ใช้
นี่คือหัวใจของการทำเว็บแบบ interactive — "เมื่อผู้ใช้ทำ X ให้โปรแกรมทำ Y"
5.1 addEventListener — ดักจับ event
⚠️ ระวัง —
<script>ต้องอยู่หลังจากที่ element โหลดแล้ว! ถ้าใส่<script>ใน<head>ของ HTML —querySelectorอาจได้nullเพราะ HTML ยังโหลดไม่ครบตอนโค้ดรัน แล้วจะ error ตอนเรียก.addEventListenerบนnullวิธีแก้ (เลือกอย่างใดอย่างหนึ่ง):
- ✅ ใส่
<script>ก่อนปิด</body>(วิธีที่ง่ายสุด)- ✅ ใส่ attribute
deferใน script tag:<script src="app.js" defer></script>(ดูตัวอย่างในส่วน setup ด้านบน)
javascript
const button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("ปุ่มถูกคลิก!");
});อธิบาย addEventListener("click", fn):
"click"— ชนิดของ event ที่จะดักจับfn— function ที่จะทำเมื่อ event เกิด (เรียก "event handler" = ฟังก์ชันรับเหตุการณ์)
→ "เมื่อ button ถูก click ให้ทำ function นี้"
5.2 ชนิดของ event ที่ใช้บ่อย
มี event ให้ดักจับมากมายตามชนิดการกระทำ — เมาส์ (click, mouseover), คีย์บอร์ด (keydown), ฟอร์ม (submit, input, change), หน้าต่าง (load, scroll) รวมตัวที่ใช้บ่อยไว้ด้านล่าง:
text
เมาส์: click, dblclick, mousedown, mouseup, mouseover, mousemove
คีย์บอร์ด: keydown, keyup
ฟอร์ม: submit, change, input, focus, blur
หน้าต่าง: load, resize, scroll5.3 Event Object — ข้อมูลของ event
ทุก event handler จะได้รับ "event object" เป็น argument ซึ่งบรรจุข้อมูลของ event นั้น — เช่น event.target (element ที่ถูกกระทำ), event.key (ปุ่มที่กด) ใช้ข้อมูลนี้ตอบสนองได้ตรงจุด:
javascript
button.addEventListener("click", (event) => {
console.log(event.target); // element ที่ถูกคลิก
console.log(event.type); // "click"
});
input.addEventListener("keydown", (event) => {
console.log(event.key); // ปุ่มที่กด เช่น "Enter", "a"
if (event.key === "Enter") {
console.log("กด Enter");
}
});5.4 preventDefault — ยกเลิกพฤติกรรม default
บาง element มี "พฤติกรรม default" — เช่น คลิก link → เปลี่ยนหน้า, submit form → reload หน้า
event.preventDefault() ยกเลิกพฤติกรรมนั้น:
javascript
form.addEventListener("submit", (event) => {
event.preventDefault(); // ไม่ให้ reload หน้า
console.log("จัดการ form ด้วย JavaScript เอง");
});5.5 ลบ event listener
วิธีเดิม — removeEventListener:
javascript
function handleClick() {
console.log("คลิก");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick); // ลบ⚠️ ตอนลบ ต้องส่ง function "ตัวเดียวกัน" — ดังนั้นต้องเก็บ function ไว้ในตัวแปร (ลบ arrow function ที่เขียนตรง ๆ เช่น addEventListener("click", () => {...}) ไม่ได้ เพราะเราไม่มี reference ของฟังก์ชันนั้นแล้ว)
วิธีสมัยใหม่ — AbortController (จำจากบทที่ 09)
ปัญหาของ removeEventListener คือถ้ามี listener หลายตัว ต้องเก็บ reference ทุกตัวและเรียก remove ทีละตัว → ยุ่งและพลาดง่าย
วิธีที่นิยมในปี 2026 — ใช้ AbortController ตัวเดียวคุมทุก listener:
javascript
const controller = new AbortController();
const { signal } = controller;
button.addEventListener("click", handleClick, { signal });
input.addEventListener("input", handleInput, { signal });
window.addEventListener("resize", handleResize, { signal });
// ทีหลัง — ลบทั้งหมดทีเดียว
controller.abort();ทุก listener ที่ผูก signal ตัวเดียวกัน จะถูกลบพร้อมกันเมื่อเรียก abort() — ใช้ดีมากใน:
- การ cleanup ตอน component ถูก unmount (React/Vue/Angular)
- การยกเลิกตอน user navigate ไปหน้าอื่น
- การจัดการ listener หลายตัวในที่เดียว
💡 AbortController ตัวเดียวกัน ใช้คุมได้ทั้ง
fetch,setTimeout(ผ่าน wrapper), และ event listener — pattern เดียว จัดการ async ทั้งหมด
Part 6: Event Bubbling และ Delegation
6.1 Event Bubbling (เหตุการณ์ลอยขึ้น) — event "ลอยขึ้น"
เมื่อ event เกิดที่ element ใด — event นั้นจะ "ส่งต่อ" ขึ้นไปหา element แม่ ปู่ ทวด ตามลำดับ จนถึง document เหมือนฟองอากาศลอยขึ้นจากก้นแก้ว
พูดง่าย ๆ: คลิก element ลูก → event ก็เกิดที่ element แม่ด้วย
html
<div class="card">
<button>คลิกฉัน</button>
</div>javascript
document.querySelector(".card").addEventListener("click", () => {
console.log("card ถูกคลิก");
});
// คลิกที่ button → "card ถูกคลิก" ก็ทำงานด้วย! (event ลอยขึ้นจาก button ไป card)นี่เรียก "bubbling" — event ลอยจากในออกนอก
6.2 Event Delegation (การมอบหมาย event) — เทคนิคที่ทรงพลัง
ปัญหา — ถ้ามีปุ่ม 100 ปุ่ม จะใส่ listener 100 ตัวไหม?
ใช้ delegation — ใส่ listener ที่ "แม่" ตัวเดียว แล้วใช้ bubbling:
javascript
// แทนที่จะใส่ listener ทุก <li>
document.querySelector("ul").addEventListener("click", (event) => {
// ใช้ closest() แทนการเช็ค tagName ตรง ๆ
// เพราะถ้า <li> มี element ลูกข้างใน เช่น <li><span>ข้อความ</span></li>
// การคลิกที่ <span> จะทำให้ event.target เป็น span (ไม่ใช่ li)
// closest() จะหา li ที่ใกล้ที่สุดจาก element ที่คลิกขึ้นไป
const li = event.target.closest("li");
if (li) {
console.log("คลิก li:", li.textContent);
}
});ข้อดี:
- ใส่ listener ตัวเดียว (ไม่ใช่ 100 ตัว) — ประหยัด
- ใช้ได้กับ element ที่เพิ่มทีหลังด้วย — เพราะ listener อยู่ที่แม่ ลูกใหม่ที่เพิ่มมาก็โดน bubbling
→ delegation เป็นเทคนิคที่ใช้บ่อยในงานจริง
Part 7: จัดการ Form
💡 วิธีลองตัวอย่างในส่วนนี้: HTML form ต้องอยู่ใน
index.htmlและ JavaScript อยู่ใน<script>ด้านล่างหรือในไฟล์.jsแยก (ตามที่ตั้งค่าไว้ในส่วน setup ด้านบน) — ไม่ใช่รันใน DevTools Console ตรง ๆ
html
<form id="loginForm">
<input name="email" type="email" required>
<input name="password" type="password" required>
<button type="submit">เข้าสู่ระบบ</button>
</form>javascript
const form = document.querySelector("#loginForm");
form.addEventListener("submit", (event) => {
event.preventDefault(); // ไม่ให้ reload หน้า
// ดึงค่าจาก form
const formData = new FormData(form);
const data = Object.fromEntries(formData);
// data = { email: "...", password: "..." }
console.log(data);
// ส่งข้อมูลไป server...
});อธิบาย:
new FormData(form)—FormDataคือ object พิเศษที่เบราว์เซอร์ให้มาเพื่อ "อ่านค่าทุก input ในฟอร์ม" ในที่เดียว — สร้างด้วยnew FormData(formElement)แล้วได้กลุ่มของ(name, value)ของทุก input ที่มี attributename=ในฟอร์มนั้นObject.fromEntries(formData)— แปลง FormData (ที่เป็นคล้าย ๆ Map) ให้เป็น object ปกติ ใช้งานง่าย (จำObject.fromEntriesจากบทที่ 06)
7.1 event ของ input
javascript
const input = document.querySelector("input");
input.addEventListener("input", (event) => {
console.log(event.target.value); // ทำงานทุกครั้งที่พิมพ์
});
input.addEventListener("change", (event) => {
console.log("ค่าเปลี่ยน (หลังออกจาก input)");
});input— ทุกครั้งที่พิมพ์change— เมื่อพิมพ์เสร็จและออกจาก input
Part 8: fetch — โหลดข้อมูลจาก server
จากบทที่ 09 — fetch คือคำสั่งโหลดข้อมูลจาก server (และมันคืน Promise)
8.1 โหลดข้อมูล (GET)
javascript
async function loadUsers() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json(); // แปลง response เป็น object
return users;
} catch (error) {
console.log("โหลดล้มเหลว:", error.message);
return [];
}
}อธิบาย:
fetch(url)— เริ่มโหลด คืน Promiseresponse.ok—trueถ้า status 200-299response.json()— แปลง response เป็น object (คืน Promise → ต้องawait)
8.2 ส่งข้อมูล (POST)
javascript
async function createUser(userData) {
const response = await fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userData), // แปลง object เป็น JSON string
});
return response.json();
}
createUser({ name: "Alice", email: "alice@example.com" });method, headers, body คือการตั้งค่าของ request:
method= วิธีการ HTTP (GET = ขอข้อมูล, POST = ส่งข้อมูลใหม่, PUT = แก้ของเดิม, DELETE = ลบ)headers= หัวข้อข้อมูล บอก server ว่าข้อมูลเป็นแบบไหน (เช่นContent-Type: application/json= ข้อมูลเป็น JSON)body= เนื้อหา/ข้อมูลที่ส่งไปกับ request
Part 9: เก็บข้อมูลในเบราว์เซอร์
เบราว์เซอร์มีที่เก็บข้อมูลให้ JavaScript ใช้
9.1 localStorage — เก็บถาวร
javascript
// บันทึก
localStorage.setItem("username", "Alice");
// อ่าน
const name = localStorage.getItem("username"); // "Alice"
// ลบ
localStorage.removeItem("username");
localStorage.clear(); // ลบทั้งหมดlocalStorage เก็บข้อมูลถาวร — ปิดเบราว์เซอร์แล้วเปิดใหม่ก็ยังอยู่
⚠️ localStorage เก็บได้แค่ string — ถ้าจะเก็บ object ต้องแปลงเป็น JSON ก่อน:
javascript
const user = { name: "Alice", age: 25 };
localStorage.setItem("user", JSON.stringify(user)); // บันทึก
const saved = JSON.parse(localStorage.getItem("user")); // อ่านกลับ⚠️ อย่าเก็บข้อมูลลับ (เช่น token, password) ใน localStorage — script ร้าย (XSS) เข้าถึงได้ผ่าน localStorage.getItem() ทำให้ข้อมูลลับถูกขโมยได้ (token ควรเก็บใน HttpOnly cookie ซึ่งเป็นวิธีเก็บข้อมูลฝั่ง server ที่ JavaScript ในเว็บอ่านไม่ได้ — เรียนในบท backend ซึ่งยังไม่ถึงตอนนี้)
⚠️ ข้อจำกัดอื่นของ localStorage ที่ควรรู้:
- ขนาด ~5-10 MB ต่อ origin (เว็บไซต์หนึ่ง) — ใหญ่กว่านี้ใช้ IndexedDB
- API เป็น synchronous (รอเสร็จก่อน) — ถ้าเขียน/อ่านข้อมูลขนาดใหญ่จะ block main thread ทำให้หน้าค้าง สำหรับข้อมูลก้อนใหญ่ใช้ IndexedDB (async, ไม่บล็อก)
- เก็บได้แค่ string เท่านั้น
localStorageอาจ throw SecurityError ในบางบริบท เช่น iframe ข้าม origin, storage เต็ม, หรือ Safari Private Mode — ถ้าจะใช้ใน production ควรห่อด้วย try/catch:javascriptfunction safeGet(key, fallback = "") { try { return localStorage.getItem(key) ?? fallback; } catch { return fallback; } } function safeSet(key, value) { try { localStorage.setItem(key, value); } catch { /* เก็บไม่ได้ — ไม่ทำอะไร */ } }
9.2 sessionStorage — เก็บชั่วคราว
เหมือน localStorage แต่ข้อมูลหายเมื่อปิด tab:
javascript
sessionStorage.setItem("temp", "value");Part 10: Lab — ลงมือทำ
Lab 1: Todo List
ฝึกรวมทุกอย่างในบทเป็น Todo List จริง — อ่านค่าจาก input, สร้าง element, ผูก event (click ลบ, Enter เพิ่ม), และแก้ DOM เห็นภาพการสร้าง UI โต้ตอบได้ครบวงจร:
index.html:
html
<!DOCTYPE html>
<html lang="th">
<head><meta charset="UTF-8"><title>Todo List</title></head>
<body>
<input id="taskInput" placeholder="เพิ่มงาน">
<button id="addBtn">เพิ่ม</button>
<ul id="taskList"></ul>
<script src="app.js" defer></script>
</body>
</html>app.js:
javascript
const input = document.querySelector("#taskInput");
const addBtn = document.querySelector("#addBtn");
const taskList = document.querySelector("#taskList");
addBtn.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
const li = document.createElement("li");
li.textContent = text;
// คลิก li เพื่อลบ (delegation ก็ได้ แต่นี่ทำตรง ๆ)
li.addEventListener("click", () => li.remove());
taskList.appendChild(li);
input.value = ""; // ล้างช่อง input
});
// กด Enter ก็เพิ่มได้
input.addEventListener("keydown", (event) => {
if (event.key === "Enter") addBtn.click();
});Lab 2: โหลดและแสดงข้อมูล
ฝึกผสาน async (บท 09) กับ DOM — โหลดข้อมูลจาก API แล้วสร้าง element แสดงผล พร้อมจัดการสถานะ loading และ error:
ส่วน HTML ใน index.html:
html
<ul id="userList"></ul>
<script src="app.js" defer></script>app.js:
javascript
async function showUsers() {
const list = document.querySelector("#userList");
list.textContent = "กำลังโหลด...";
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
list.innerHTML = ""; // ล้าง (ปลอดภัย — เราไม่ได้ใส่ข้อมูลจากผู้ใช้ แค่เซ็ตเป็นค่าว่าง)
users.forEach(user => {
const li = document.createElement("li");
li.textContent = `${user.name} (${user.email})`;
list.appendChild(li);
});
} catch (error) {
list.textContent = "โหลดล้มเหลว";
}
}
showUsers();Lab 3: Form validation
ฝึกจัดการฟอร์ม — ดัก event submit, ใช้ preventDefault ยกเลิกการ reload หน้า, อ่านค่าด้วย FormData แล้ว validate ก่อนส่ง (alert() = แสดง popup กล่องข้อความของเบราว์เซอร์ — ใช้สะดวกตอนเรียน แต่งานจริงนิยมแสดงข้อความใต้ input แทน):
ส่วน HTML ใน index.html (ก่อน <script>):
html
<form id="signupForm">
<input name="email" type="email" placeholder="อีเมล" required>
<input name="password" type="password" placeholder="รหัสผ่าน" required>
<button type="submit">สมัคร</button>
</form>app.js:
javascript
const form = document.querySelector("#signupForm");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(form));
if (!data.email.includes("@")) {
alert("email ไม่ถูกต้อง");
return;
}
if (data.password.length < 8) {
alert("รหัสผ่านต้องยาวอย่างน้อย 8 ตัว");
return;
}
console.log("ส่งข้อมูล:", data);
});Lab 4: เก็บค่าใน localStorage
ส่วน HTML ใน index.html (ก่อน <script>):
html
<input id="nameInput" placeholder="ชื่อของคุณ">app.js:
javascript
const input = document.querySelector("#nameInput");
// โหลดค่าเก่าตอนเปิดหน้า
input.value = localStorage.getItem("savedName") ?? "";
// บันทึกทุกครั้งที่พิมพ์
input.addEventListener("input", () => {
localStorage.setItem("savedName", input.value);
});Part 11: Checkpoint
- DOM คืออะไร? ต่างจาก HTML ยังไง?
querySelectorต่างquerySelectorAllยังไง?textContentต่างinnerHTMLยังไง? อันไหนปลอดภัยกว่า ทำไม?- ทำไมควรเปลี่ยนหน้าตา element ด้วย
classListแทนstyle? DocumentFragmentใช้แก้ปัญหาอะไร?addEventListenerรับ argument อะไรบ้าง?event.preventDefault()ทำอะไร?- Event bubbling คืออะไร?
- Event delegation คืออะไร? มีประโยชน์ยังไง?
localStorageเก็บอะไรได้? เก็บ object ยังไง?
Part 12: สรุปบทนี้
- DOM = หน้าเว็บในรูปแบบต้นไม้ที่ JavaScript จับต้องได้
querySelector/querySelectorAll= ค้นหา element ด้วย CSS selectortextContent(ปลอดภัย) /innerHTML(ระวัง XSS)- เปลี่ยนหน้าตาด้วย
classList— ดีกว่าแก้styleตรง ๆ - สร้าง element ด้วย
createElement— ใส่ด้วยappendChild— ใช้DocumentFragmentตอนเพิ่มเยอะ addEventListener= ดักจับ event — handler รับ event objectpreventDefault= ยกเลิกพฤติกรรม default (เช่น form ไม่ reload)- Event delegation = ใส่ listener ที่แม่ — ใช้ bubbling — ประหยัด + รองรับ element ใหม่
fetch= โหลด/ส่งข้อมูลกับ serverlocalStorage= เก็บข้อมูลถาวร (เก็บได้แค่ string — object ต้อง JSON)
บทต่อไป (สุดท้ายของหมวด JavaScript) — เครื่องมือสมัยใหม่