Skip to content

บทที่ 12 — DOM และ Events: ควบคุมหน้าเว็บ

← บทที่ 11 | สารบัญ | บทที่ 13: Modern Tools →

บทนี้คือเหตุผลที่ JavaScript เกิดมา — ทำให้หน้าเว็บ "มีชีวิต" ตอบสนองผู้ใช้ได้

อ่านจบบทนี้คุณจะ:

  • เข้าใจ DOM — โครงสร้างของหน้าเว็บที่ JavaScript ควบคุมได้
  • ค้นหาและแก้ไของค์ประกอบในหน้าเว็บ
  • จัดการ event — คลิก, พิมพ์, submit ฟอร์ม
  • โหลดข้อมูลจาก server ด้วย fetch
  • เก็บข้อมูลในเบราว์เซอร์

ใช้เวลา 3 ชั่วโมง — บทนี้ต้องลองในเบราว์เซอร์

⚠️ ต่างจากบทที่ผ่านมา: บทก่อนหน้าใช้ Node.js รันในเทอร์มินัล — บทนี้ต้องรันในเบราว์เซอร์ เพราะ DOM มีแค่ในเบราว์เซอร์เท่านั้น

🖥️ วิธีลองโค้ดในบทนี้ (เลือกวิธีใดวิธีหนึ่ง):

วิธีที่ 1 — พิมพ์ใน DevTools Console โดยตรง (เร็วสุด สำหรับทดสอบบรรทัดเดียว):

  1. เปิด Chrome/Firefox
  2. กด F12 (Windows/Linux) หรือ Cmd+Option+I (Mac) เพื่อเปิด DevTools
  3. คลิก tab "Console"
  4. พิมพ์โค้ดแล้วกด Enter

วิธีที่ 2 — สร้างไฟล์ HTML + JS (แนะนำสำหรับโค้ดที่ยาวขึ้น):

  1. สร้างไฟล์ index.html:
    html
    <!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>
    (attribute defer = รอให้ HTML โหลดครบก่อนค่อยรัน JS — สำคัญมาก ดูหมายเหตุใน Part 5.1)
  2. สร้างไฟล์ app.js ในโฟลเดอร์เดียวกัน
  3. เปิด index.html ในเบราว์เซอร์ (double-click หรือลาก file เข้า Chrome)
  4. เปิด 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 — id
  • parent 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 แม่/ปู่ที่ใกล้สุดที่ตรง selector

Part 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> ของ HTMLquerySelector อาจได้ 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, scroll

5.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 ที่มี attribute name= ในฟอร์มนั้น
  • 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) — เริ่มโหลด คืน Promise
  • response.oktrue ถ้า status 200-299
  • response.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:
    javascript
    function 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

  1. DOM คืออะไร? ต่างจาก HTML ยังไง?
  2. querySelector ต่าง querySelectorAll ยังไง?
  3. textContent ต่าง innerHTML ยังไง? อันไหนปลอดภัยกว่า ทำไม?
  4. ทำไมควรเปลี่ยนหน้าตา element ด้วย classList แทน style?
  5. DocumentFragment ใช้แก้ปัญหาอะไร?
  6. addEventListener รับ argument อะไรบ้าง?
  7. event.preventDefault() ทำอะไร?
  8. Event bubbling คืออะไร?
  9. Event delegation คืออะไร? มีประโยชน์ยังไง?
  10. localStorage เก็บอะไรได้? เก็บ object ยังไง?

Part 12: สรุปบทนี้

  • DOM = หน้าเว็บในรูปแบบต้นไม้ที่ JavaScript จับต้องได้
  • querySelector/querySelectorAll = ค้นหา element ด้วย CSS selector
  • textContent (ปลอดภัย) / innerHTML (ระวัง XSS)
  • เปลี่ยนหน้าตาด้วย classList — ดีกว่าแก้ style ตรง ๆ
  • สร้าง element ด้วย createElement — ใส่ด้วย appendChild — ใช้ DocumentFragment ตอนเพิ่มเยอะ
  • addEventListener = ดักจับ event — handler รับ event object
  • preventDefault = ยกเลิกพฤติกรรม default (เช่น form ไม่ reload)
  • Event delegation = ใส่ listener ที่แม่ — ใช้ bubbling — ประหยัด + รองรับ element ใหม่
  • fetch = โหลด/ส่งข้อมูลกับ server
  • localStorage = เก็บข้อมูลถาวร (เก็บได้แค่ string — object ต้อง JSON)

บทต่อไป (สุดท้ายของหมวด JavaScript) — เครื่องมือสมัยใหม่


← บทที่ 11 | สารบัญ | บทที่ 13: Modern Tools →