HTMX — การพัฒนาเว็บแบบ Hypermedia สมัยใหม่

คู่มือฉบับสมบูรณ์สำหรับนักพัฒนาเว็บที่ต้องการสร้างแอปพลิเคชันแบบ Interactive โดยไม่พึ่งพา JavaScript Framework ขนาดใหญ่


ส่วนที่ 1: บทนำและแนวคิดพื้นฐาน

1.1 HTMX คืออะไร? — ที่มาและปรัชญาการออกแบบ

HTMX คือ JavaScript Library ขนาดเล็ก (~14KB เมื่อ gzip) ที่ช่วยให้ HTML Element ธรรมดาสามารถส่ง HTTP Request และอัปเดต DOM ได้โดยตรง โดยไม่ต้องเขียน JavaScript เพิ่มเติม หลักการทำงานของ HTMX คือการ ขยายความสามารถของ HTML ให้เกินขอบเขตที่มาตรฐานดั้งเดิมกำหนดไว้

1.1.1 ประวัติและที่มา

HTMX พัฒนาต่อยอดจากโปรเจกต์ intercooler.js ซึ่งสร้างโดย Carson Gross ในปี 2013 ก่อนที่จะเขียนใหม่ทั้งหมดและเปลี่ยนชื่อเป็น HTMX ในปี 2020 แนวคิดหลักมาจากการตั้งคำถามว่า "ทำไม HTML จึงจำกัดให้เฉพาะ <a> และ <form> เท่านั้นที่ส่ง HTTP Request ได้?"

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'secondaryColor': '#504945',
    'tertiaryColor': '#282828',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'nodeBorder': '#a89984',
    'clusterBkg': '#32302f',
    'titleColor': '#ebdbb2',
    'edgeLabelBackground': '#3c3836',
    'fontSize': '14px'
  }
}}%%
flowchart TD
    subgraph era1["ยุค 2013 (Era: 2013)"]
        direction LR
        A["intercooler.js - Carson Gross สร้าง - First Version"]
    end
    subgraph era2["ยุค 2015-2019 (Era: 2015-2019)"]
        direction LR
        B["intercooler.js v1.x - เพิ่ม Attributes - ปรับปรุง API"]
        C["ชุมชนเติบโต - Community Grows"]
    end
    subgraph era3["ยุค 2020 (Era: 2020)"]
        direction LR
        D["เขียนใหม่ทั้งหมด - Rewrite from Scratch"]
        E["เปลี่ยนชื่อเป็น HTMX - Rename to HTMX"]
    end
    subgraph era4["ยุค 2021-ปัจจุบัน (Era: 2021-Present)"]
        direction LR
        F["HTMX v1.x - Stable Release"]
        G["HTMX v2.0 - Breaking Changes - Modern API"]
        H["GitHub Stars > 40K - Popularity Surge"]
    end
    era1 --> era2 --> era3 --> era4
    B --> C
    D --> E
    F --> G --> H
    style A fill:#458588,color:#ebdbb2,stroke:#83a598
    style B fill:#458588,color:#ebdbb2,stroke:#83a598
    style C fill:#689d6a,color:#ebdbb2,stroke:#8ec07c
    style D fill:#d79921,color:#282828,stroke:#fabd2f
    style E fill:#d79921,color:#282828,stroke:#fabd2f
    style F fill:#b16286,color:#ebdbb2,stroke:#d3869b
    style G fill:#cc241d,color:#ebdbb2,stroke:#fb4934
    style H fill:#98971a,color:#282828,stroke:#b8bb26

รูปที่ 1.1: ไทม์ไลน์การพัฒนา HTMX จาก intercooler.js สู่ปัจจุบัน

1.1.2 ปรัชญาการออกแบบ (Design Philosophy)

HTMX ยึดถือหลักการ "HTML-First" ซึ่งมีแนวคิดสำคัญดังนี้:

<!-- ตัวอย่าง: ปรัชญา Locality of Behavior -->
<!-- ❌ วิธีเดิม: พฤติกรรมแยกออกจาก Element -->
<button id="load-btn">โหลดข้อมูล</button>
<script>
  document.getElementById('load-btn').addEventListener('click', function() {
    fetch('/api/data').then(r => r.json()).then(data => {
      // อัปเดต DOM...
    });
  });
</script>

<!-- ✅ วิธี HTMX: พฤติกรรมอยู่ที่ Element โดยตรง -->
<button hx-get="/api/data" hx-target="#result" hx-swap="innerHTML">
  โหลดข้อมูล
</button>
<div id="result"></div>

1.2 ปัญหาของ JavaScript-Heavy Frameworks — ทำไมเราถึงต้องการทางเลือกอื่น

1.2.1 ปัญหาหลักของ SPA Framework สมัยใหม่

การพัฒนาเว็บด้วย Single Page Application (SPA) framework อย่าง React, Vue, หรือ Angular มาพร้อมกับ trade-off ที่สำคัญ:

ปัญหา รายละเอียด ผลกระทบ
Bundle Size ขนาดใหญ่ React + ReactDOM ~130KB gzip โหลดช้าบน mobile/slow connection
JavaScript Required ถ้า JS ล้มเหลว ทั้งหน้าพัง Accessibility และ Resilience แย่ลง
State Management ซับซ้อน Redux, Zustand, Pinia, NgRx Learning curve สูง
Client-server Duplication Logic ซ้ำกันทั้ง Frontend และ Backend Maintenance cost เพิ่มขึ้น
SEO และ SSR ยาก ต้องการ Hydration ซับซ้อน Next.js/Nuxt เพิ่มความซับซ้อน
Build Process ซับซ้อน Webpack/Vite + TypeScript + Testing DevOps overhead สูง

1.2.2 ต้นทุนที่ซ่อนอยู่ของ JavaScript (Hidden Costs)

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'fontSize': '14px'
  }
}}%%
pie title ต้นทุนการพัฒนา SPA (SPA Development Cost Distribution)
    "Bundle Size / Network" : 20
    "State Management" : 25
    "Build Tooling" : 15
    "Testing Complexity" : 20
    "Team Learning Curve" : 20

รูปที่ 1.2: การกระจายต้นทุนในการพัฒนา SPA

1.2.3 เมื่อ HTMX เป็นคำตอบที่ดีกว่า

HTMX เหมาะสมอย่างยิ่งสำหรับ:


1.3 Hypermedia as the Engine of Application State (HATEOAS)

1.3.1 ความหมายของ HATEOAS

HATEOAS (Hypermedia as the Engine of Application State) คือ constraint หนึ่งของ REST Architecture ที่ระบุว่า Client ไม่ควรรู้จัก URL หรือ Action ล่วงหน้า แต่ควรค้นพบผ่าน Hypermedia ที่ Server ส่งกลับมา

กล่าวง่าย ๆ คือ Server ส่ง HTML กลับมาพร้อมกับ ลิงก์ และ ฟอร์ม ที่บอกว่า Client สามารถทำอะไรได้บ้าง ณ ขณะนั้น

<!-- ตัวอย่าง HATEOAS Response จาก Server -->
<!-- เมื่อ User ยังไม่ได้ Login -->
<div class="user-actions">
  <a hx-get="/login">เข้าสู่ระบบ</a>
  <a hx-get="/register">สมัครสมาชิก</a>
</div>

<!-- เมื่อ User Login แล้ว -->
<div class="user-actions">
  <a hx-get="/profile">โปรไฟล์</a>
  <a hx-get="/orders">คำสั่งซื้อ</a>
  <button hx-post="/logout">ออกจากระบบ</button>
</div>

1.3.2 HTMX กับ HATEOAS ในทางปฏิบัติ

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'clusterBkg': '#32302f',
    'fontSize': '14px'
  }
}}%%
sequenceDiagram
    participant Browser as 🌐 Browser (Client)
    participant Server as 🖥️ Server
    Browser->>Server: GET /products
    Server-->>Browser: HTML + hx-get="/products/1" links
    Note over Browser: แสดงรายการสินค้า
(Display product list) Browser->>Server: hx-get="/products/1" Server-->>Browser: HTML Fragment ของสินค้า + hx-post="/cart/add" Note over Browser: แสดงรายละเอียดสินค้า
(Display product detail) Browser->>Server: hx-post="/cart/add" (id=1) Server-->>Browser: HTML Fragment ตะกร้า + hx-delete="/cart/1" Note over Browser: อัปเดตตะกร้าสินค้า
(Update shopping cart)

รูปที่ 1.3: การทำงานของ HATEOAS ใน HTMX Application


1.4 REST และ Hypermedia — เว็บแบบดั้งเดิมที่ HTMX นำกลับมาใช้ใหม่

1.4.1 REST ที่แท้จริง vs REST ที่เราใช้กัน

Roy Fielding ผู้บัญญัติ REST ในวิทยานิพนธ์ปี 2000 ระบุว่า REST ต้องมี HATEOAS แต่ในทางปฏิบัติ "REST API" ส่วนใหญ่ที่เราเห็นในปัจจุบันเป็นเพียง HTTP API ที่ไม่ได้เป็น REST ที่แท้จริง

คุณสมบัติ REST จริง (Fielding) REST API ทั่วไป HTMX
Stateless
Uniform Interface
HATEOAS บังคับ ❌ มักขาด
Media Type Hypermedia (HTML) JSON HTML (Hypermedia)
Self-describing ⚠️ บางส่วน

1.4.2 ทำไม HTML ถึงเป็น Hypermedia ที่ดีที่สุด

HTML มีคุณสมบัติ Hypermedia โดยธรรมชาติ:

HTMX ขยายความสามารถนี้ให้กับ ทุก Element ใน HTML


1.5 HTMX เทียบกับ SPA Frameworks — React, Vue, Angular ต่างกันอย่างไร

1.5.1 ตารางเปรียบเทียบแบบละเอียด

หัวข้อ HTMX React Vue 3 Angular
Bundle Size ~14KB ~130KB ~90KB ~180KB
Learning Curve ต่ำมาก ปานกลาง ต่ำ-ปานกลาง สูง
State Management Server-side Client-side Client-side Client-side
Rendering Server (SSR) Client (CSR/SSR) Client (CSR/SSR) Client (CSR/SSR)
SEO ✅ ดีเยี่ยม ⚠️ ต้องการ SSR ⚠️ ต้องการ SSR ⚠️ ต้องการ SSR
TypeScript Support ❌ ไม่มี ✅ บังคับ
Component Model ไม่มี
Testing ง่าย ปานกลาง ปานกลาง ซับซ้อน
Backend Agnostic
Offline Support

1.5.2 สถาปัตยกรรมที่แตกต่างกัน

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'clusterBkg': '#32302f',
    'fontSize': '14px'
  }
}}%%
flowchart TB
    subgraph spa["SPA Architecture (React/Vue)"]
        direction TB
        S1["Server - (API Server)"] -->|JSON Data| C1["Client - (JavaScript Bundle)"]
        C1 -->|Virtual DOM| R1["Browser - (DOM Update)"]
        C1 -->|Manages| ST1["Client State - (Redux/Pinia)"]
    end
    subgraph htmx["HTMX Architecture"]
        direction TB
        S2["Server - (Template Engine)"] -->|HTML Fragment| C2["HTMX - (14KB Library)"]
        C2 -->|Direct DOM Update| R2["Browser - (Real DOM)"]
        S2 -->|Manages| ST2["Server State - (Session/DB)"]
    end
    style S1 fill:#458588,color:#ebdbb2,stroke:#83a598
    style C1 fill:#cc241d,color:#ebdbb2,stroke:#fb4934
    style R1 fill:#689d6a,color:#ebdbb2,stroke:#8ec07c
    style ST1 fill:#d79921,color:#282828,stroke:#fabd2f
    style S2 fill:#458588,color:#ebdbb2,stroke:#83a598
    style C2 fill:#b16286,color:#ebdbb2,stroke:#d3869b
    style R2 fill:#689d6a,color:#ebdbb2,stroke:#8ec07c
    style ST2 fill:#98971a,color:#282828,stroke:#b8bb26

รูปที่ 1.4: เปรียบเทียบสถาปัตยกรรม SPA vs HTMX


ส่วนที่ 2: การติดตั้งและเริ่มต้นใช้งาน

2.1 การติดตั้ง HTMX — CDN, npm, และการรวมกับ Build Tools

2.1.1 วิธีที่ 1: CDN (วิธีที่ง่ายที่สุด)

<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>HTMX Demo</title>
  <!-- ติดตั้งผ่าน CDN - ใช้ได้ทันที -->
  <script src="https://unpkg.com/htmx.org@2.0.4" 
          integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" 
          crossorigin="anonymous"></script>
</head>
<body>
  <h1>สวัสดี HTMX!</h1>
  <button hx-get="/hello" hx-target="#result">
    กดเพื่อโหลด
  </button>
  <div id="result"></div>
</body>
</html>

2.1.2 วิธีที่ 2: npm/yarn

# ติดตั้งผ่าน npm
npm install htmx.org

# ติดตั้งผ่าน yarn
yarn add htmx.org

# ติดตั้งผ่าน bun
bun add htmx.org

# ติดตั้งผ่าน pnpm
pnpm add htmx.org
// นำเข้าใน JavaScript/TypeScript โปรเจกต์
import htmx from 'htmx.org';

// หรือแบบ CommonJS
const htmx = require('htmx.org');

// ใน Vite/Webpack (import เพื่อให้ global htmx object พร้อมใช้)
import 'htmx.org';

2.1.3 วิธีที่ 3: ดาวน์โหลดและ Self-host

# ดาวน์โหลดไฟล์
curl -o htmx.min.js https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js

# วางในโปรเจกต์
cp htmx.min.js ./static/js/
<!-- ใช้ Local File -->
<script src="/static/js/htmx.min.js"></script>

2.2 โครงสร้างโปรเจกต์พื้นฐาน — การจัดไฟล์และ Server ที่ใช้คู่กัน

2.2.1 โครงสร้างไฟล์แนะนำ

my-htmx-app/
├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── htmx.min.js
├── templates/
│   ├── base.html          ← Layout หลัก
│   ├── partials/          ← HTML Fragments สำหรับ HTMX
│   │   ├── user-list.html
│   │   ├── product-card.html
│   │   └── notification.html
│   └── pages/             ← หน้าเต็ม
│       ├── index.html
│       └── about.html
├── app.py                 ← FastAPI/Flask Server
└── requirements.txt

2.2.2 Base Template พื้นฐาน

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="th">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{% block title %}My HTMX App{% endblock %}</title>
  
  <!-- CSS -->
  <link rel="stylesheet" href="/static/css/style.css">
  
  <!-- HTMX - โหลดก่อน body content -->
  <script src="/static/js/htmx.min.js"></script>
  
  <!-- Meta tag สำหรับ CSRF Protection -->
  <meta name="csrf-token" content="{{ csrf_token }}">
  
  <!-- กำหนด Default Configuration -->
  <meta name="htmx-config" content='{"defaultSwapStyle":"innerHTML", "timeout":5000}'>
</head>
<body>
  <!-- Navigation -->
  <nav>
    <a href="/" hx-boost="true">หน้าหลัก</a>
    <a href="/products" hx-boost="true">สินค้า</a>
    <a href="/about" hx-boost="true">เกี่ยวกับ</a>
  </nav>
  
  <!-- Main Content -->
  <main id="main-content">
    {% block content %}{% endblock %}
  </main>
  
  <!-- Notification Area -->
  <div id="notifications" class="notification-container"></div>
  
  {% block scripts %}{% endblock %}
</body>
</html>

2.3 Hello HTMX — ตัวอย่างแรกแบบ Step-by-Step

2.3.1 Server-side: FastAPI (Python)

# app.py - FastAPI Server สำหรับ Demo HTMX
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import random

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

# ข้อมูลตัวอย่าง (Sample Data)
sample_users = [
    {"id": 1, "name": "สมชาย ใจดี", "email": "somchai@example.com", "active": True},
    {"id": 2, "name": "สมหญิง รักเรียน", "email": "somying@example.com", "active": True},
    {"id": 3, "name": "วิชัย แข็งแกร่ง", "email": "wichai@example.com", "active": False},
]

# หน้าหลัก (Main Page)
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    return templates.TemplateResponse("pages/index.html", {"request": request})

# Endpoint แรก: ทักทายแบบง่าย (Simple Greeting)
@app.get("/hello", response_class=HTMLResponse)
async def hello():
    names = ["สมชาย", "สมหญิง", "วิชัย", "มานี", "ปิติ"]
    name = random.choice(names)
    return f"<p>สวัสดี, <strong>{name}</strong>! ยินดีต้อนรับสู่ HTMX 🎉</p>"

# Endpoint: รายการผู้ใช้ (User List)
@app.get("/users", response_class=HTMLResponse)
async def get_users(request: Request):
    return templates.TemplateResponse(
        "partials/user-list.html", 
        {"request": request, "users": sample_users}
    )

# Endpoint: เพิ่มผู้ใช้ (Add User)
@app.post("/users", response_class=HTMLResponse)
async def add_user(request: Request):
    form_data = await request.form()
    new_user = {
        "id": len(sample_users) + 1,
        "name": form_data.get("name"),
        "email": form_data.get("email"),
        "active": True
    }
    sample_users.append(new_user)
    # ส่งกลับเฉพาะ row ใหม่ (Return only the new row)
    return f"""
    <tr>
      <td>{new_user['id']}</td>
      <td>{new_user['name']}</td>
      <td>{new_user['email']}</td>
      <td><span class="badge active">Active</span></td>
    </tr>
    """

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)

2.3.2 Client-side: HTML ตัวอย่าง

<!-- templates/pages/index.html -->
{% extends "base.html" %}

{% block title %}Hello HTMX - Demo{% endblock %}

{% block content %}
<div class="container">
  <h1>ยินดีต้อนรับสู่ HTMX Demo</h1>
  
  <!-- ตัวอย่างที่ 1: Simple Click -->
  <section class="demo-section">
    <h2>ตัวอย่างที่ 1: การคลิกปุ่มง่าย ๆ</h2>
    
    <!-- 
      hx-get="/hello" → ส่ง GET Request ไปที่ /hello
      hx-target="#greeting" → ใส่ Response ใน #greeting
      hx-swap="innerHTML" → แทนที่เนื้อหาใน element
    -->
    <button 
      hx-get="/hello" 
      hx-target="#greeting"
      hx-swap="innerHTML"
      class="btn-primary">
      👋 ทักทาย!
    </button>
    
    <div id="greeting" class="result-box">
      (กดปุ่มเพื่อทักทาย)
    </div>
  </section>
  
  <!-- ตัวอย่างที่ 2: โหลดรายการ -->
  <section class="demo-section">
    <h2>ตัวอย่างที่ 2: โหลดรายการผู้ใช้</h2>
    
    <button 
      hx-get="/users"
      hx-target="#user-table"
      hx-swap="innerHTML"
      hx-indicator="#loading-spinner">
      📋 โหลดรายการผู้ใช้
    </button>
    
    <!-- Loading Indicator -->
    <span id="loading-spinner" class="htmx-indicator">
      ⏳ กำลังโหลด...
    </span>
    
    <div id="user-table">
      (รายการจะปรากฏที่นี่)
    </div>
  </section>
</div>
{% endblock %}

ส่วนที่ 3: Core Attributes หลัก

3.1 hx-get, hx-post, hx-put, hx-patch, hx-delete — การส่ง HTTP Request

3.1.1 HTTP Methods ทั้ง 5 แบบ

HTMX รองรับ HTTP Methods ครบทุกตัว ทำให้สามารถทำ CRUD Operations ได้อย่างสมบูรณ์:

<!-- GET: ดึงข้อมูล (Read) -->
<button hx-get="/products">ดูสินค้าทั้งหมด</button>

<!-- POST: สร้างข้อมูลใหม่ (Create) -->
<form hx-post="/products">
  <input name="name" placeholder="ชื่อสินค้า">
  <button type="submit">เพิ่มสินค้า</button>
</form>

<!-- PUT: อัปเดตข้อมูลทั้งหมด (Update/Replace) -->
<form hx-put="/products/1">
  <input name="name" value="สินค้าอัปเดต">
  <button type="submit">บันทึก</button>
</form>

<!-- PATCH: อัปเดตข้อมูลบางส่วน (Partial Update) -->
<button hx-patch="/products/1/toggle-active">
  เปิด/ปิด สถานะ
</button>

<!-- DELETE: ลบข้อมูล (Delete) -->
<button hx-delete="/products/1"
        hx-confirm="คุณต้องการลบสินค้านี้ใช่ไหม?">
  🗑️ ลบสินค้า
</button>

3.1.2 ตัวอย่างจริง: CRUD สำหรับ Task Manager

# tasks_api.py - API สำหรับ Task Manager
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

tasks = [
    {"id": 1, "title": "เรียน HTMX", "done": False},
    {"id": 2, "title": "สร้างโปรเจกต์แรก", "done": False},
    {"id": 3, "title": "Deploy ขึ้น Production", "done": False},
]
next_id = 4

def render_task(task: dict) -> str:
    """Render HTML ของ task เดียว"""
    done_class = "task-done" if task["done"] else ""
    check_icon = "✅" if task["done"] else "⬜"
    return f"""
    <li id="task-{task['id']}" class="task-item {done_class}">
      <button hx-patch="/tasks/{task['id']}/toggle"
              hx-target="#task-{task['id']}"
              hx-swap="outerHTML">
        {check_icon}
      </button>
      <span class="task-title">{task['title']}</span>
      <button hx-delete="/tasks/{task['id']}"
              hx-target="#task-{task['id']}"
              hx-swap="outerHTML"
              hx-confirm="ลบงานนี้?">
        🗑️
      </button>
    </li>
    """

# GET: ดึงรายการทั้งหมด
@app.get("/tasks", response_class=HTMLResponse)
async def get_tasks():
    items = "".join(render_task(t) for t in tasks)
    return f'<ul id="task-list" class="tasks">{items}</ul>'

# POST: เพิ่ม task ใหม่
@app.post("/tasks", response_class=HTMLResponse)
async def create_task(request: Request):
    global next_id
    form = await request.form()
    task = {"id": next_id, "title": form.get("title"), "done": False}
    tasks.append(task)
    next_id += 1
    return render_task(task)

# PATCH: สลับสถานะ done/undone
@app.patch("/tasks/{task_id}/toggle", response_class=HTMLResponse)
async def toggle_task(task_id: int):
    for task in tasks:
        if task["id"] == task_id:
            task["done"] = not task["done"]
            return render_task(task)
    return "", 404

# DELETE: ลบ task
@app.delete("/tasks/{task_id}", response_class=HTMLResponse)
async def delete_task(task_id: int):
    global tasks
    tasks = [t for t in tasks if t["id"] != task_id]
    return ""  # ส่ง empty string เพื่อลบ element ออกจาก DOM

3.2 hx-target — การกำหนดเป้าหมายการอัปเดต DOM

3.2.1 Selector ทุกรูปแบบ

hx-target ใช้ CSS Selector ในการระบุ Element ที่จะรับ Response:

<!-- Target by ID (ที่นิยมใช้มากที่สุด) -->
<button hx-get="/data" hx-target="#result">โหลด</button>
<div id="result"></div>

<!-- Target: this (Element ตัวเอง) -->
<div hx-get="/updated-content" hx-target="this" hx-trigger="click">
  คลิกเพื่ออัปเดต
</div>

<!-- Target: closest (หา ancestor ที่ใกล้ที่สุด) -->
<tr>
  <td>ข้อมูล</td>
  <td>
    <button hx-delete="/items/1" hx-target="closest tr" hx-swap="outerHTML">
      ลบ
    </button>
  </td>
</tr>

<!-- Target: find (หา descendant) -->
<div id="container">
  <button hx-get="/content" hx-target="find .content-area">โหลด</button>
  <div class="content-area">เนื้อหาจะปรากฏที่นี่</div>
</div>

<!-- Target: next (sibling ถัดไป) -->
<button hx-get="/help-text" hx-target="next .help">❓ ช่วยเหลือ</button>
<div class="help"></div>

<!-- Target: previous (sibling ก่อนหน้า) -->
<div class="preview"></div>
<textarea hx-post="/preview" hx-target="previous .preview" 
          hx-trigger="keyup delay:500ms">
</textarea>

<!-- Target: body (ทั้งหน้า) -->
<button hx-get="/full-page" hx-target="body">โหลดหน้าใหม่</button>

3.3 hx-swap — วิธีการแทนที่เนื้อหา

3.3.1 Swap Strategies ทั้งหมด

hx-swap Value คำอธิบาย ใช้เมื่อ
innerHTML แทนที่เนื้อหาภายใน (default) ต้องการอัปเดตเนื้อหาในกล่อง
outerHTML แทนที่ทั้ง Element ต้องการเปลี่ยน Element ทั้งใบ
beforebegin แทรกก่อน Element ใส่เนื้อหาก่อนกล่อง
afterbegin แทรกต้น children ใส่ที่ต้นรายการ
beforeend ต่อท้าย children ต่อท้ายรายการ (Append)
afterend แทรกหลัง Element ใส่เนื้อหาหลังกล่อง
delete ลบ Target Element ลบ Element ออก
none ไม่เปลี่ยน DOM ต้องการ side effects เท่านั้น
<!-- Demo: Swap Strategies ต่าง ๆ -->

<!-- 1. innerHTML: แทนที่เนื้อหาใน div -->
<div id="box">เนื้อหาเดิม</div>
<button hx-get="/new-content" 
        hx-target="#box" 
        hx-swap="innerHTML">
  แทนที่เนื้อหา
</button>

<!-- 2. outerHTML: แทนที่ทั้ง element -->
<div id="card" class="card">
  <button hx-get="/updated-card" 
          hx-target="#card" 
          hx-swap="outerHTML">
    รีเฟรชการ์ด
  </button>
</div>

<!-- 3. beforeend: ต่อท้าย List (นิยมใช้กับ Infinite Scroll) -->
<ul id="messages">
  <li>ข้อความที่ 1</li>
</ul>
<button hx-get="/more-messages" 
        hx-target="#messages" 
        hx-swap="beforeend">
  โหลดเพิ่ม ↓
</button>

<!-- 4. afterbegin: ใส่ตอนต้น (สำหรับ Notification Feed) -->
<ul id="notifications">
  <!-- การแจ้งเตือนใหม่จะถูกใส่ที่นี่ -->
</ul>
<button hx-get="/latest-notification" 
        hx-target="#notifications" 
        hx-swap="afterbegin">
  ตรวจสอบการแจ้งเตือน
</button>

<!-- 5. Swap with Transition (เพิ่ม Animation) -->
<div id="content" class="fade-in">
  เนื้อหาปัจจุบัน
</div>
<button hx-get="/new" 
        hx-target="#content" 
        hx-swap="innerHTML transition:true">
  สลับพร้อม Animation
</button>

<!-- 6. Swap with Delay (ชะลอก่อน Swap) -->
<button hx-delete="/item/1"
        hx-target="#item-1"
        hx-swap="outerHTML swap:500ms">
  ลบ (พร้อม Animation)
</button>

3.4 hx-trigger — การกำหนด Event ที่จะเรียก Request

3.4.1 Event Triggers ทั้งหมด

<!-- Default Triggers (ตาม Element Type) -->
<!-- button, input[type=submit]: click -->
<!-- input, textarea, select: change -->
<!-- form: submit -->

<!-- Custom Event Triggers -->

<!-- 1. Click (กดปุ่ม) -->
<button hx-get="/data" hx-trigger="click">โหลด</button>

<!-- 2. Change (เมื่อค่าเปลี่ยน) -->
<select hx-get="/filter" hx-trigger="change" hx-target="#results">
  <option value="all">ทั้งหมด</option>
  <option value="active">Active</option>
  <option value="inactive">Inactive</option>
</select>

<!-- 3. Keyup with Delay (พิมพ์แล้วหน่วง 500ms) -->
<input type="text" 
       name="search"
       hx-get="/search"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#search-results"
       placeholder="ค้นหา...">

<!-- 4. Load (เมื่อ Element โหลด) -->
<div hx-get="/lazy-content" hx-trigger="load">
  ⏳ กำลังโหลด...
</div>

<!-- 5. Revealed (เมื่อปรากฏใน Viewport - สำหรับ Lazy Load) -->
<div hx-get="/more-data" hx-trigger="revealed">
  โหลดเพิ่มเมื่อเลื่อนลงมา
</div>

<!-- 6. Every N Seconds (Polling) -->
<div hx-get="/live-stats" 
     hx-trigger="every 5s"
     hx-swap="innerHTML">
  กำลังโหลดสถิติ...
</div>

<!-- 7. Custom Event -->
<div hx-get="/refresh" hx-trigger="myCustomEvent">
  รอ Custom Event
</div>
<!-- เรียก Custom Event จาก JavaScript -->
<script>
  htmx.trigger('#my-div', 'myCustomEvent');
</script>

<!-- 8. Intersection Observer (เมื่อมองเห็น) -->
<div hx-get="/analytics" 
     hx-trigger="intersect once"
     hx-swap="none">
  (ติดตาม Analytics เมื่อมองเห็น)
</div>

<!-- 9. Multiple Triggers -->
<input hx-get="/validate"
       hx-trigger="focus, change, keyup delay:300ms changed"
       hx-target="next .error-msg">
<span class="error-msg"></span>

3.5 hx-boost — การเพิ่มประสิทธิภาพลิงก์และฟอร์มแบบอัตโนมัติ

3.5.1 วิธีการทำงานของ hx-boost

hx-boost แปลง <a> และ <form> ทั่วไปให้กลายเป็น AJAX Request โดยอัตโนมัติ โดยที่ไม่ต้องเขียน hx-get หรือ hx-post เพิ่ม ผลลัพธ์คือ User ได้รับประสบการณ์ที่ เร็วขึ้น เนื่องจากหน้าเว็บไม่ต้อง Full Reload

<!-- hx-boost บน body หรือ nav (ครอบคลุมทุก link/form ใน element) -->
<nav hx-boost="true">
  <a href="/">หน้าหลัก</a>           <!-- จะเป็น AJAX GET -->
  <a href="/products">สินค้า</a>      <!-- จะเป็น AJAX GET -->
  <a href="/about">เกี่ยวกับ</a>      <!-- จะเป็น AJAX GET -->
</nav>

<!-- ปิด boost สำหรับ link บางอัน -->
<a href="/download/file.pdf" hx-boost="false">ดาวน์โหลด PDF</a>

<!-- hx-boost บน form -->
<form action="/search" method="get" hx-boost="true">
  <input name="q" placeholder="ค้นหา...">
  <button type="submit">ค้นหา</button>
</form>

3.6 hx-push-url — การจัดการ Browser History และ URL

3.6.1 การทำงานกับ History API

<!-- อัปเดต URL เมื่อโหลดข้อมูล (เหมือน SPA Navigation) -->
<a href="/products/1" 
   hx-get="/products/1/content"
   hx-target="#main"
   hx-push-url="true">
  สินค้าชิ้นที่ 1
</a>

<!-- กำหนด URL ที่ต้องการ (ต่างจาก hx-get path) -->
<button hx-get="/products?category=electronics"
        hx-target="#products-list"
        hx-push-url="/products?category=electronics">
  อิเล็กทรอนิกส์
</button>

<!-- ไม่ push URL (default สำหรับ hx-get ปกติ) -->
<button hx-get="/modal-content"
        hx-target="#modal"
        hx-push-url="false">
  เปิด Modal
</button>

<!-- hx-replace-url: แทนที่ URL ใน History แทนที่จะ Push -->
<button hx-get="/search?q=htmx"
        hx-replace-url="/search?q=htmx">
  ค้นหา
</button>

ส่วนที่ 4: การส่งและรับข้อมูล

4.1 hx-include และ hx-params — การควบคุมข้อมูลที่ส่งไป Server

4.1.1 hx-include: รวม Input จากที่อื่น

<!-- รวม input จาก element อื่นที่ไม่ได้อยู่ใน form เดียวกัน -->
<div>
  <input id="user-id" type="hidden" name="user_id" value="42">
  <input id="filter-date" type="date" name="date" value="2024-01-15">
</div>

<!-- ปุ่มนี้จะรวมค่าจาก #user-id และ #filter-date ด้วย -->
<button hx-get="/report"
        hx-include="#user-id, #filter-date"
        hx-target="#report-area">
  📊 ดูรายงาน
</button>

<!-- รวมทุก input ใน Form (ใช้ CSS Selector) -->
<button hx-post="/submit-all"
        hx-include="closest form">
  ส่งฟอร์มทั้งหมด
</button>

<!-- hx-params: กรองพารามิเตอร์ที่ส่งไป -->
<form hx-post="/update-profile">
  <input name="username" value="john">
  <input name="email" value="john@example.com">
  <input name="password" value="secret">
  
  <!-- ส่งเฉพาะ username และ email (ไม่ส่ง password) -->
  <button hx-post="/update-profile"
          hx-params="username, email">
    อัปเดตโปรไฟล์
  </button>
  
  <!-- ส่งทุกอย่างยกเว้น password -->
  <button hx-post="/update-profile"
          hx-params="not password">
    อัปเดต (ไม่เปลี่ยนรหัสผ่าน)
  </button>
</form>

4.2 hx-vals — การเพิ่มค่าพิเศษลงใน Request

4.2.1 การใช้ hx-vals

<!-- hx-vals: เพิ่มค่าพิเศษ (JSON format) -->
<button hx-post="/purchase"
        hx-vals='{"product_id": 42, "quantity": 1, "currency": "THB"}'>
  🛒 เพิ่มลงตะกร้า
</button>

<!-- hx-vals แบบ Dynamic (ใช้ JavaScript) -->
<button hx-post="/add-to-cart"
        hx-vals='js:{product_id: getSelectedProductId(), timestamp: Date.now()}'>
  เพิ่มลงตะกร้า
</button>

<!-- ตัวอย่างจริง: ส่ง Context พิเศษ -->
<div class="product-card" data-category="electronics">
  <h3>โทรศัพท์มือถือ</h3>
  <button hx-post="/analytics/view"
          hx-vals='js:{
            category: this.closest(".product-card").dataset.category,
            page: window.location.pathname,
            userId: document.querySelector("meta[name=user-id]")?.content
          }'
          hx-swap="none">
    ดูรายละเอียด
  </button>
</div>

4.3 hx-headers — การกำหนด Custom HTTP Headers

4.3.1 Custom Headers และ CSRF

<!-- กำหนด Custom Header สำหรับทุก Request ใน section นี้ -->
<div hx-headers='{"X-Custom-Header": "my-value", "X-App-Version": "2.0"}'>
  <button hx-get="/api/data">โหลดข้อมูล</button>
  <button hx-post="/api/submit">ส่งข้อมูล</button>
</div>

<!-- CSRF Token ผ่าน Header -->
<body hx-headers='js:{"X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content}'>
  <!-- ทุก HTMX Request ใน body จะส่ง CSRF Token -->
  <form hx-post="/update">
    <input name="data" value="ข้อมูล">
    <button type="submit">บันทึก</button>
  </form>
</body>

<!-- Authorization Header สำหรับ API -->
<div hx-headers='js:{"Authorization": "Bearer " + localStorage.getItem("token")}'>
  <button hx-get="/api/protected">เข้าถึง API ที่ต้องการ Auth</button>
</div>

4.4 การจัดการ Response จาก Server — HTML Partial vs JSON

4.4.1 HTML Partial (แนะนำสำหรับ HTMX)

# server_responses.py - ตัวอย่าง Response ประเภทต่าง ๆ
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse

app = FastAPI()

# ✅ HTML Partial Response (แนะนำ)
@app.get("/products/{product_id}", response_class=HTMLResponse)
async def get_product_html(product_id: int):
    # ส่ง HTML Fragment กลับไปโดยตรง
    return f"""
    <div class="product-card" id="product-{product_id}">
      <h3>สินค้า #{product_id}</h3>
      <p class="price">฿1,299</p>
      <button hx-post="/cart/add"
              hx-vals='{{"product_id": {product_id}}}'
              hx-target="#cart-count"
              hx-swap="innerHTML">
        เพิ่มลงตะกร้า
      </button>
    </div>
    """

# ⚠️ JSON Response (ต้องใช้ JS เพิ่มเติม - ไม่แนะนำสำหรับ HTMX)
@app.get("/products/{product_id}/json")
async def get_product_json(product_id: int):
    return {"id": product_id, "name": "สินค้า", "price": 1299}

# HTTP Status Codes ที่ HTMX จัดการ
@app.post("/form-submit", response_class=HTMLResponse)
async def handle_form(request: Request):
    form = await request.form()
    
    if not form.get("name"):
        # 422: ข้อมูลไม่ถูกต้อง (HTMX จะยัง Swap ปกติ)
        response = HTMLResponse(
            content='<p class="error">❌ กรุณากรอกชื่อ</p>',
            status_code=422
        )
        return response
    
    # 200: สำเร็จ
    return f'<p class="success">✅ บันทึก "{form.get("name")}" สำเร็จ!</p>'

4.4.2 ตาราง HTTP Status Code กับ HTMX

Status Code ความหมาย HTMX พฤติกรรม
200 OK สำเร็จ Swap เนื้อหาตามปกติ
201 Created สร้างสำเร็จ Swap เนื้อหา
204 No Content สำเร็จ ไม่มีเนื้อหา ไม่ Swap
3xx Redirect Redirect ทำ Full Page Redirect
4xx Client Error ข้อผิดพลาดฝั่ง Client ไม่ Swap (default), ใช้ htmx:responseError จัดการ
422 Unprocessable Validation Error Swap ตามปกติ (พิเศษ)
5xx Server Error ข้อผิดพลาดฝั่ง Server ไม่ Swap

4.5 HX-Trigger Response Header — การส่ง Event กลับจาก Server

4.5.1 การใช้ HX-Trigger Header

# hx_trigger_examples.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
import json

app = FastAPI()

# ส่ง Event เดียว
@app.post("/save", response_class=HTMLResponse)
async def save_data(request: Request):
    form = await request.form()
    # บันทึกข้อมูล...
    
    response = HTMLResponse(content='<p>✅ บันทึกสำเร็จ!</p>')
    # บอกให้ HTMX fire event "dataSaved" บน Client
    response.headers["HX-Trigger"] = "dataSaved"
    return response

# ส่งหลาย Events พร้อมข้อมูล
@app.delete("/products/{product_id}", response_class=HTMLResponse)
async def delete_product(product_id: int):
    # ลบสินค้า...
    
    response = HTMLResponse(content="")  # ลบ element ออก
    response.headers["HX-Trigger"] = json.dumps({
        "productDeleted": {"id": product_id},
        "showNotification": {
            "message": f"ลบสินค้า #{product_id} สำเร็จ",
            "type": "success"
        }
    })
    return response

# HX-Trigger-After-Settle (fire หลัง DOM settle)
@app.post("/create-order")
async def create_order(request: Request):
    response = HTMLResponse(content='<p>สร้างคำสั่งซื้อแล้ว</p>')
    response.headers["HX-Trigger-After-Settle"] = "orderCreated"
    # HX-Trigger-After-Swap: fire หลัง Swap เสร็จ
    response.headers["HX-Trigger-After-Swap"] = "pageUpdated"
    return response
<!-- Client: รับฟัง Events จาก Server -->
<div id="notification-area"></div>

<!-- Component ที่รับฟัง dataSaved event -->
<div hx-on:dataSaved="this.classList.add('saved')">
  เนื้อหา
</div>

<!-- ใช้ JavaScript สำหรับ Event ซับซ้อน -->
<script>
  // รับฟัง showNotification event
  document.body.addEventListener("showNotification", function(evt) {
    const { message, type } = evt.detail;
    const area = document.getElementById("notification-area");
    area.innerHTML = `<div class="alert alert-${type}">${message}</div>`;
    
    // ซ่อนหลัง 3 วินาที
    setTimeout(() => area.innerHTML = "", 3000);
  });
  
  // รับฟัง productDeleted event
  document.body.addEventListener("productDeleted", function(evt) {
    console.log("ลบสินค้า ID:", evt.detail.id);
    // อัปเดต Cart count หรืออื่น ๆ
  });
</script>

ส่วนที่ 5: การจัดการ UI และ UX

5.1 Loading States และ htmx-indicator — การแสดงสถานะโหลด

5.1.1 การใช้ htmx-indicator

<!-- style.css ที่จำเป็น -->
<style>
  /* htmx-indicator ซ่อนอยู่โดย default */
  .htmx-indicator {
    opacity: 0;
    transition: opacity 500ms ease-in;
  }
  /* เมื่อ Request กำลังทำงาน: แสดง indicator */
  .htmx-request .htmx-indicator,
  .htmx-request.htmx-indicator {
    opacity: 1;
  }
  /* Spinner Animation */
  .spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 3px solid rgba(0,0,0,.1);
    border-top-color: #3498db;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
</style>

<!-- ตัวอย่าง 1: Spinner บน Button -->
<button hx-get="/slow-data"
        hx-target="#output"
        hx-indicator="#btn-spinner">
  โหลดข้อมูล
  <span id="btn-spinner" class="htmx-indicator spinner"></span>
</button>

<!-- ตัวอย่าง 2: Loading Overlay บน Section -->
<div id="data-section" style="position: relative;">
  <!-- Overlay ปรากฏระหว่างโหลด -->
  <div class="htmx-indicator" style="
    position: absolute; inset: 0;
    background: rgba(255,255,255,0.8);
    display: flex; align-items: center; justify-content: center;
  ">
    <span class="spinner"></span> กำลังโหลด...
  </div>
  
  <div id="data-content">เนื้อหา</div>
</div>

<button hx-get="/refresh"
        hx-target="#data-content"
        hx-indicator="#data-section">
  รีเฟรช
</button>

<!-- ตัวอย่าง 3: Global Loading Bar (Progress Bar ด้านบน) -->
<div id="global-loading" class="htmx-indicator" style="
  position: fixed; top: 0; left: 0; right: 0;
  height: 3px; background: #3498db;
  animation: loading-bar 1s ease-in-out infinite;
">
</div>

5.2 hx-confirm — การยืนยันก่อนดำเนินการ

5.2.1 hx-confirm พื้นฐาน

<!-- Confirm Dialog ปกติ (Browser native) -->
<button hx-delete="/products/1"
        hx-target="#product-1"
        hx-swap="outerHTML"
        hx-confirm="คุณต้องการลบสินค้านี้ใช่ไหม? การดำเนินการนี้ไม่สามารถย้อนกลับได้">
  🗑️ ลบสินค้า
</button>

<!-- Custom Confirm ด้วย JavaScript -->
<script>
  // Override confirm dialog ด้วย Custom Modal
  document.body.addEventListener('htmx:confirm', function(evt) {
    evt.preventDefault(); // หยุด default confirm
    
    const question = evt.detail.question;
    
    // แสดง Custom Modal
    showCustomConfirm(question, function(confirmed) {
      if (confirmed) {
        evt.detail.issueRequest(true); // ดำเนินการต่อ
      }
    });
  });
  
  function showCustomConfirm(message, callback) {
    // ใช้ SweetAlert2 หรือ Modal Library อื่น ๆ
    if (window.Swal) {
      Swal.fire({
        title: 'ยืนยันการดำเนินการ',
        text: message,
        icon: 'warning',
        showCancelButton: true,
        confirmButtonText: 'ยืนยัน',
        cancelButtonText: 'ยกเลิก',
        confirmButtonColor: '#d33',
      }).then(result => callback(result.isConfirmed));
    }
  }
</script>

5.3 hx-disable — การป้องกัน Double Submit

5.3.1 ป้องกัน Double Submit

<!-- hx-disable: ปิด HTMX บน element -->
<button hx-post="/submit"
        hx-disable>
  ปุ่มนี้ถูกปิด HTMX
</button>

<!-- ป้องกัน Double Submit ด้วย hx-disabled-elt -->
<form hx-post="/checkout"
      hx-disabled-elt="find button[type=submit]">
  <input name="address" placeholder="ที่อยู่จัดส่ง">
  <button type="submit">
    ชำระเงิน
    <span class="htmx-indicator"></span>
  </button>
</form>

<!-- ปิด Form ทั้งหมดระหว่างส่ง -->
<form hx-post="/important-action"
      hx-disabled-elt="this">
  <input name="data">
  <button>ส่ง</button>
</form>

5.4 CSS Transitions และ Animation — การทำ Smooth UI ด้วย HTMX

5.4.1 CSS-based Animations

/* htmx-animation.css */

/* Fade In เมื่อ Element ใหม่ปรากฏ */
.htmx-added {
  opacity: 0;
}
.htmx-added:not(.htmx-settling) {
  opacity: 1;
  transition: opacity 0.5s ease-in;
}

/* Fade Out เมื่อ Element เก่าหายไป */
.htmx-swapping {
  opacity: 1;
  transition: opacity 0.3s ease-out;
}
.htmx-swapping {
  opacity: 0;
}

/* Slide In from Top */
@keyframes slideInFromTop {
  from {
    transform: translateY(-20px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}

.slide-in {
  animation: slideInFromTop 0.3s ease-out;
}

/* View Transitions API (Modern Browsers) */
@view-transition {
  navigation: auto;
}
<!-- ใช้ Class-based Animation กับ HTMX -->
<ul id="item-list">
  <li class="slide-in">รายการที่ 1</li>
</ul>

<button hx-post="/items"
        hx-target="#item-list"
        hx-swap="beforeend">
  เพิ่มรายการ
</button>

<!-- Server ส่งกลับ HTML พร้อม class animation -->
<!-- <li class="slide-in">รายการใหม่</li> -->

5.5 hx-select — การเลือกเฉพาะส่วนจาก Response

5.5.1 hx-select เพื่อเลือก Fragment

<!-- Server ส่ง Full Page กลับ แต่เราต้องการเฉพาะ #content -->
<button hx-get="/full-page"
        hx-select="#content"
        hx-target="#main">
  โหลดเฉพาะเนื้อหาหลัก
</button>

<!-- hx-select-oob: ดึงหลาย Element พร้อมกัน -->
<button hx-get="/page-with-sidebar"
        hx-select="#main-content"
        hx-target="#content-area"
        hx-select-oob="#sidebar:innerHTML,#breadcrumb:innerHTML">
  โหลดหน้าพร้อม Sidebar
</button>

ส่วนที่ 6: เทคนิคขั้นสูง

6.1 Out-of-Band Swaps (hx-swap-oob) — การอัปเดตหลาย Element พร้อมกัน

6.1.1 การใช้ hx-swap-oob

hx-swap-oob (Out-of-Band) ช่วยให้ Server สามารถอัปเดต หลาย Element ในหน้าเว็บพร้อมกันใน Response เดียว โดยที่ Response หลักอัปเดต Target ปกติ ส่วน OOB Elements จะถูกอัปเดตตาม ID ของตัวเอง

# oob_example.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

cart_count = 0

@app.post("/cart/add", response_class=HTMLResponse)
async def add_to_cart(request: Request):
    global cart_count
    form = await request.form()
    product_id = form.get("product_id")
    cart_count += 1
    
    # Main Response: ยืนยันการเพิ่มสินค้า
    # OOB Response: อัปเดต Cart Badge และ Notification พร้อมกัน
    return f"""
    <div class="add-confirmation">
      ✅ เพิ่มสินค้า #{product_id} ลงตะกร้าแล้ว
    </div>
    
    <!-- OOB: อัปเดต Cart Count ที่ Navbar -->
    <span id="cart-count" hx-swap-oob="innerHTML">
      {cart_count}
    </span>
    
    <!-- OOB: แสดง Notification -->
    <div id="notification" hx-swap-oob="innerHTML">
      <div class="toast success">
        🛒 เพิ่มลงตะกร้าสำเร็จ! (รวม {cart_count} ชิ้น)
      </div>
    </div>
    """
<!-- index.html -->
<nav>
  <a href="/cart">
    🛒 ตะกร้า
    <span id="cart-count" class="badge">0</span>
  </a>
</nav>

<div id="notification"></div>

<div class="products">
  <div class="product-card">
    <h3>สินค้า A</h3>
    <form hx-post="/cart/add" hx-target="#add-feedback">
      <input type="hidden" name="product_id" value="101">
      <button type="submit">🛒 เพิ่มลงตะกร้า</button>
    </form>
  </div>
  <div id="add-feedback"></div>
</div>

6.2 Polling ด้วย hx-trigger="every Ns" — การดึงข้อมูลอัตโนมัติ

6.2.1 Auto-Polling

<!-- Polling ทุก 5 วินาที -->
<div hx-get="/live-stats"
     hx-trigger="every 5s"
     hx-swap="innerHTML"
     class="stats-panel">
  กำลังโหลดสถิติ...
</div>

<!-- Conditional Polling (หยุดเมื่อ Server บอก) -->
<div id="job-status"
     hx-get="/jobs/123/status"
     hx-trigger="every 2s"
     hx-swap="outerHTML">
  ⏳ กำลังประมวลผล...
</div>
<!-- Server ส่งกลับ: ถ้าเสร็จแล้ว ไม่ใส่ hx-trigger ใน Response -->
<!-- <div id="job-status">✅ ประมวลผลเสร็จแล้ว!</div> -->

<!-- Dashboard ที่ Refresh ข้อมูลหลายส่วนพร้อมกัน -->
<div class="dashboard">
  <div hx-get="/stats/users" hx-trigger="load, every 30s" hx-swap="innerHTML">
    <div class="spinner"></div>
  </div>
  <div hx-get="/stats/sales" hx-trigger="load, every 30s" hx-swap="innerHTML">
    <div class="spinner"></div>
  </div>
  <div hx-get="/stats/inventory" hx-trigger="load, every 60s" hx-swap="innerHTML">
    <div class="spinner"></div>
  </div>
</div>

6.3 Server-Sent Events (SSE) — Real-time Updates แบบ One-way

6.3.1 SSE กับ HTMX

# sse_server.py
import asyncio
import random
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

async def generate_stock_updates():
    """Generator สำหรับ SSE Stream"""
    stocks = {"KBANK": 152.0, "PTT": 35.5, "SCB": 112.0}
    
    while True:
        # สร้างข้อมูลหุ้นสุ่ม
        for ticker, price in stocks.items():
            change = random.uniform(-2, 2)
            stocks[ticker] = round(price + change, 2)
            direction = "up" if change > 0 else "down"
            
            html_content = f"""
            <div id="stock-{ticker}" hx-swap-oob="innerHTML">
              <span class="price {direction}">{stocks[ticker]:.2f}</span>
              <span class="change">{'▲' if change > 0 else '▼'} {abs(change):.2f}</span>
            </div>
            """
            # SSE Format: data: <content> -  - 
            yield f"data: {html_content} -  - "
        
        await asyncio.sleep(2)  # อัปเดตทุก 2 วินาที

@app.get("/stocks/stream")
async def stock_stream():
    return StreamingResponse(
        generate_stock_updates(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"
        }
    )
<!-- client: ใช้ HTMX SSE Extension -->
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>

<!-- ติดต่อ SSE Stream -->
<div hx-ext="sse" sse-connect="/stocks/stream">
  <h2>ราคาหุ้น Real-time</h2>
  
  <!-- Placeholder สำหรับแต่ละหุ้น (OOB จะอัปเดต) -->
  <div id="stock-KBANK" class="stock-row">
    <span class="ticker">KBANK</span>
    <span class="price">--</span>
  </div>
  <div id="stock-PTT" class="stock-row">
    <span class="ticker">PTT</span>
    <span class="price">--</span>
  </div>
  <div id="stock-SCB" class="stock-row">
    <span class="ticker">SCB</span>
    <span class="price">--</span>
  </div>
</div>

6.4 WebSockets กับ HTMX — Real-time แบบ Two-way

6.4.1 WebSocket Chat

# websocket_chat.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List
import json

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.connections: List[WebSocket] = []
    
    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.connections.append(ws)
    
    def disconnect(self, ws: WebSocket):
        self.connections.remove(ws)
    
    async def broadcast(self, html: str):
        """ส่ง HTML ไปยังทุก Connection"""
        for ws in self.connections:
            try:
                await ws.send_text(html)
            except:
                pass

manager = ConnectionManager()
message_count = 0

@app.websocket("/chat/ws")
async def chat_ws(websocket: WebSocket):
    global message_count
    await manager.connect(websocket)
    
    try:
        while True:
            data = await websocket.receive_text()
            msg_data = json.loads(data)
            message_count += 1
            
            # Broadcast HTML Fragment ไปทุก Client
            html = f"""
            <li id="msg-{message_count}" class="message animate-in">
              <strong>{msg_data['username']}:</strong> {msg_data['message']}
              <small>{msg_data.get('time', 'ตอนนี้')}</small>
            </li>
            """
            await manager.broadcast(html)
    except WebSocketDisconnect:
        manager.disconnect(websocket)
<!-- chat.html -->
<script src="https://unpkg.com/htmx-ext-ws@2.0.1/ws.js"></script>

<div id="chat-container" hx-ext="ws" ws-connect="/chat/ws">
  <!-- ข้อความจะถูกต่อท้ายที่นี่ -->
  <ul id="messages" hx-swap-oob="beforeend:#messages">
  </ul>
  
  <!-- ฟอร์มส่งข้อความ -->
  <form ws-send id="chat-form">
    <input type="hidden" name="username" value="ผู้ใช้">
    <input type="text" name="message" 
           placeholder="พิมพ์ข้อความ..." 
           autocomplete="off">
    <button type="submit">ส่ง</button>
  </form>
</div>

<script>
  // เพิ่ม timestamp และล้าง input หลังส่ง
  document.getElementById('chat-form').addEventListener('htmx:wsAfterSend', function() {
    this.querySelector('input[name=message]').value = '';
  });
</script>

6.5 Infinite Scroll และ Lazy Loading — Pattern ยอดนิยม

6.5.1 Infinite Scroll

# infinite_scroll.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

# ข้อมูลสินค้าตัวอย่าง 100 ชิ้น
all_products = [{"id": i, "name": f"สินค้า #{i}", "price": i * 100} for i in range(1, 101)]
PAGE_SIZE = 10

@app.get("/products", response_class=HTMLResponse)
async def get_products(page: int = 1):
    start = (page - 1) * PAGE_SIZE
    end = start + PAGE_SIZE
    products = all_products[start:end]
    has_more = end < len(all_products)
    
    items_html = "".join(f"""
    <div class="product-card">
      <h3>{p['name']}</h3>
      <p>฿{p['price']}</p>
    </div>
    """ for p in products)
    
    # ปุ่ม "โหลดเพิ่ม" จะปรากฏเฉพาะเมื่อยังมีข้อมูล
    # ใช้ hx-trigger="revealed" เพื่อ auto-load เมื่อเลื่อนลงมาถึง
    load_more = f"""
    <div id="load-trigger"
         hx-get="/products?page={page + 1}"
         hx-trigger="revealed"
         hx-target="#load-trigger"
         hx-swap="outerHTML"
         class="load-more-trigger">
      ⏳ กำลังโหลด...
    </div>
    """ if has_more else '<p class="end-msg">✅ แสดงสินค้าทั้งหมดแล้ว</p>'
    
    return items_html + load_more
<!-- infinite-scroll.html -->
<div id="products-container">
  <!-- โหลดหน้าแรกทันที -->
  <div hx-get="/products?page=1"
       hx-trigger="load"
       hx-target="this"
       hx-swap="outerHTML">
    ⏳ กำลังโหลดสินค้า...
  </div>
</div>

6.6 hx-on และ JavaScript Event Handling — การผสาน JS เมื่อจำเป็น

6.6.1 hx-on Attribute

<!-- hx-on: จัดการ HTMX Events โดยตรงใน HTML -->

<!-- Log ก่อน Request ส่ง -->
<button hx-get="/data"
        hx-on:htmx:before-request="console.log('กำลังส่ง Request...')">
  โหลด
</button>

<!-- Reset Form หลัง Submit สำเร็จ -->
<form hx-post="/messages"
      hx-target="#messages"
      hx-swap="beforeend"
      hx-on:htmx:after-request="this.reset()">
  <input name="text" placeholder="ข้อความ">
  <button type="submit">ส่ง</button>
</form>

<!-- ดักจับ Error -->
<div hx-get="/might-fail"
     hx-on:htmx:response-error="
       alert('เกิดข้อผิดพลาด: ' + event.detail.xhr.status)
     ">
  ข้อมูล
</div>

<!-- Lifecycle Events ที่สำคัญ -->
<div hx-get="/data"
     hx-on:htmx:before-send="this.classList.add('loading')"
     hx-on:htmx:after-settle="this.classList.remove('loading')"
     hx-on:htmx:swap="console.log('DOM อัปเดตแล้ว')">
  เนื้อหา
</div>

ส่วนที่ 7: การใช้งานร่วมกับ Backend

7.1 HTMX กับ Python (FastAPI / Django / Flask)

7.1.1 FastAPI + Jinja2 (Complete Example)

# fastapi_htmx_complete.py
from fastapi import FastAPI, Request, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from typing import Optional
import sqlite3
import contextlib

app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")

# Database Setup (SQLite สำหรับ Demo)
def get_db():
    conn = sqlite3.connect("demo.db")
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()

def init_db():
    with sqlite3.connect("demo.db") as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS todos (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                completed BOOLEAN DEFAULT FALSE,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        """)
        # ใส่ข้อมูลตัวอย่าง
        conn.execute("INSERT OR IGNORE INTO todos (id, title) VALUES (1, 'เรียน HTMX')")
        conn.execute("INSERT OR IGNORE INTO todos (id, title) VALUES (2, 'สร้าง Project')")
        conn.commit()

init_db()

# Middleware: ตรวจสอบว่าเป็น HTMX Request หรือไม่
def is_htmx(request: Request) -> bool:
    return request.headers.get("HX-Request") == "true"

# Main Page
@app.get("/", response_class=HTMLResponse)
async def index(request: Request, db=Depends(get_db)):
    todos = db.execute("SELECT * FROM todos ORDER BY created_at DESC").fetchall()
    return templates.TemplateResponse("todos.html", {
        "request": request,
        "todos": todos
    })

# Get all todos (HTML Fragment)
@app.get("/todos", response_class=HTMLResponse)
async def get_todos(request: Request, db=Depends(get_db)):
    todos = db.execute("SELECT * FROM todos ORDER BY created_at DESC").fetchall()
    return templates.TemplateResponse("partials/todo-list.html", {
        "request": request,
        "todos": todos
    })

# Create todo
@app.post("/todos", response_class=HTMLResponse)
async def create_todo(request: Request, db=Depends(get_db)):
    form = await request.form()
    title = form.get("title", "").strip()
    
    if not title:
        return HTMLResponse('<p class="error">กรุณากรอกชื่องาน</p>', status_code=422)
    
    cursor = db.execute("INSERT INTO todos (title) VALUES (?)", (title,))
    db.commit()
    todo_id = cursor.lastrowid
    todo = db.execute("SELECT * FROM todos WHERE id=?", (todo_id,)).fetchone()
    
    return templates.TemplateResponse("partials/todo-item.html", {
        "request": request, "todo": todo
    })

# Toggle complete
@app.patch("/todos/{todo_id}/toggle", response_class=HTMLResponse)
async def toggle_todo(todo_id: int, request: Request, db=Depends(get_db)):
    db.execute("UPDATE todos SET completed = NOT completed WHERE id=?", (todo_id,))
    db.commit()
    todo = db.execute("SELECT * FROM todos WHERE id=?", (todo_id,)).fetchone()
    return templates.TemplateResponse("partials/todo-item.html", {
        "request": request, "todo": todo
    })

# Delete todo
@app.delete("/todos/{todo_id}", response_class=HTMLResponse)
async def delete_todo(todo_id: int, db=Depends(get_db)):
    db.execute("DELETE FROM todos WHERE id=?", (todo_id,))
    db.commit()
    return HTMLResponse("")  # ลบ Element ออก

7.1.2 Django Template (การใช้กับ Django)

# django_views.py
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse
from django.views.decorators.http import require_http_methods
from .models import Product

def is_htmx(request):
    return request.headers.get("HX-Request") == "true"

def product_list(request):
    products = Product.objects.all()
    
    # ถ้าเป็น HTMX Request → ส่งเฉพาะ Fragment
    if is_htmx(request):
        return render(request, "partials/product-list.html", {"products": products})
    
    # ถ้าเป็น Normal Request → ส่ง Full Page
    return render(request, "products.html", {"products": products})

@require_http_methods(["DELETE"])
def delete_product(request, pk):
    product = get_object_or_404(Product, pk=pk)
    product.delete()
    return HttpResponse("")  # ลบ Element

7.2 HTMX กับ Go (Gin / Echo / Templ)

7.2.1 Go + Gin + html/template

// main.go - Go + Gin + HTMX
package main

import (
    "html/template"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
)

type Task struct {
    ID    int
    Title string
    Done  bool
}

var tasks = []Task{
    {1, "เรียน Go", false},
    {2, "สร้าง HTMX App", false},
    {3, "Deploy ขึ้น Server", false},
}
var nextID = 4

// Render Task Item HTML
func renderTaskItem(t Task) string {
    checkIcon := "⬜"
    doneClass := ""
    if t.Done {
        checkIcon = "✅"
        doneClass = "task-done"
    }
    return `<li id="task-` + strconv.Itoa(t.ID) + `" class="task-item ` + doneClass + `">
        <button hx-patch="/tasks/` + strconv.Itoa(t.ID) + `/toggle"
                hx-target="#task-` + strconv.Itoa(t.ID) + `"
                hx-swap="outerHTML">` + checkIcon + `</button>
        <span>` + t.Title + `</span>
        <button hx-delete="/tasks/` + strconv.Itoa(t.ID) + `"
                hx-target="#task-` + strconv.Itoa(t.ID) + `"
                hx-swap="outerHTML"
                hx-confirm="ลบงานนี้?">🗑️</button>
    </li>`
}

func main() {
    r := gin.Default()
    r.LoadHTMLGlob("templates/*")

    // GET /tasks
    r.GET("/tasks", func(c *gin.Context) {
        html := `<ul id="task-list">`
        for _, t := range tasks {
            html += renderTaskItem(t)
        }
        html += `</ul>`
        c.Data(http.StatusOK, "text/html", []byte(html))
    })

    // POST /tasks
    r.POST("/tasks", func(c *gin.Context) {
        title := c.PostForm("title")
        if title == "" {
            c.Data(http.StatusUnprocessableEntity, "text/html",
                []byte(`<p class="error">กรุณากรอกชื่องาน</p>`))
            return
        }
        task := Task{ID: nextID, Title: title, Done: false}
        tasks = append(tasks, task)
        nextID++
        c.Data(http.StatusOK, "text/html", []byte(renderTaskItem(task)))
    })

    // PATCH /tasks/:id/toggle
    r.PATCH("/tasks/:id/toggle", func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        for i := range tasks {
            if tasks[i].ID == id {
                tasks[i].Done = !tasks[i].Done
                c.Data(http.StatusOK, "text/html", []byte(renderTaskItem(tasks[i])))
                return
            }
        }
        c.Status(http.StatusNotFound)
    })

    // DELETE /tasks/:id
    r.DELETE("/tasks/:id", func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        for i, t := range tasks {
            if t.ID == id {
                tasks = append(tasks[:i], tasks[i+1:]...)
                break
            }
        }
        c.Data(http.StatusOK, "text/html", []byte(""))
    })

    r.Run(":8080")
}

7.3 HTMX กับ Node.js (Express / Hono)

7.3.1 Hono + HTMX (แนะนำสำหรับ Bun)

// server.ts - Hono + HTMX (รันด้วย Bun)
import { Hono } from "hono";
import { serveStatic } from "hono/bun";

const app = new Hono();

interface Todo {
  id: number;
  title: string;
  done: boolean;
}

let todos: Todo[] = [
  { id: 1, title: "เรียน Hono", done: false },
  { id: 2, title: "ทดลอง HTMX + Bun", done: false },
];
let nextId = 3;

// Helper: render todo item
const renderTodo = (t: Todo) => `
<li id="todo-${t.id}" class="${t.done ? "done" : ""}">
  <button hx-patch="/todos/${t.id}/toggle"
          hx-target="#todo-${t.id}"
          hx-swap="outerHTML">
    ${t.done ? "✅" : "⬜"}
  </button>
  ${t.title}
  <button hx-delete="/todos/${t.id}"
          hx-target="#todo-${t.id}"
          hx-swap="outerHTML"
          hx-confirm="ลบ?">🗑️</button>
</li>`;

// Static files
app.use("/static/*", serveStatic({ root: "./" }));

// GET /
app.get("/", (c) => {
  const todosHtml = todos.map(renderTodo).join("");
  return c.html(`
    <!DOCTYPE html>
    <html>
    <head>
      <script src="https://unpkg.com/htmx.org@2.0.4"></script>
    </head>
    <body>
      <h1>Todo App (Hono + HTMX)</h1>
      <form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend"
            hx-on:htmx:after-request="this.reset()">
        <input name="title" placeholder="งานใหม่...">
        <button>เพิ่ม</button>
      </form>
      <ul id="todo-list">${todosHtml}</ul>
    </body>
    </html>
  `);
});

// POST /todos
app.post("/todos", async (c) => {
  const form = await c.req.formData();
  const title = (form.get("title") as string)?.trim();
  if (!title) return c.html('<p class="error">กรุณากรอกชื่องาน</p>', 422);
  const todo: Todo = { id: nextId++, title, done: false };
  todos.push(todo);
  return c.html(renderTodo(todo));
});

// PATCH /todos/:id/toggle
app.patch("/todos/:id/toggle", (c) => {
  const id = parseInt(c.req.param("id"));
  const todo = todos.find((t) => t.id === id);
  if (!todo) return c.text("Not found", 404);
  todo.done = !todo.done;
  return c.html(renderTodo(todo));
});

// DELETE /todos/:id
app.delete("/todos/:id", (c) => {
  const id = parseInt(c.req.param("id"));
  todos = todos.filter((t) => t.id !== id);
  return c.html("");
});

export default { port: 3000, fetch: app.fetch };
// รัน: bun run server.ts

7.4 การออกแบบ Partial Template — แนวทาง Server-side Rendering ที่ดี

7.4.1 หลักการออกแบบ Partial Templates

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'clusterBkg': '#32302f',
    'fontSize': '14px'
  }
}}%%
flowchart TB
    subgraph full["Full Page Request"]
        direction LR
        FP1["base.html"] --> FP2["page.html"]
        FP2 --> FP3["partials/component.html"]
    end
    subgraph htmxreq["HTMX Partial Request"]
        direction LR
        HR1["partials/component.html - เท่านั้น"]
    end
    Client1["🌐 Browser - (Full Request)"] --> full
    Client2["🌐 Browser - (HTMX Request - hx-get)"] --> htmxreq
    full --> Resp1["🖥️ Full HTML Page"]
    htmxreq --> Resp2["📄 HTML Fragment"]
    style FP1 fill:#458588,color:#ebdbb2,stroke:#83a598
    style FP2 fill:#458588,color:#ebdbb2,stroke:#83a598
    style FP3 fill:#689d6a,color:#ebdbb2,stroke:#8ec07c
    style HR1 fill:#d79921,color:#282828,stroke:#fabd2f

รูปที่ 7.1: การแยก Full Page vs Partial Template

# template_pattern.py - Pattern ที่ดีสำหรับ HTMX + Jinja2
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

def render(request: Request, full_template: str, 
           partial_template: str, context: dict):
    """
    ส่ง Full Page สำหรับ Normal Request
    ส่ง Partial สำหรับ HTMX Request
    """
    is_htmx = request.headers.get("HX-Request") == "true"
    template = partial_template if is_htmx else full_template
    return templates.TemplateResponse(template, {"request": request, **context})

@app.get("/products", response_class=HTMLResponse)
async def products(request: Request):
    products_data = [{"id": 1, "name": "สินค้า A"}, {"id": 2, "name": "สินค้า B"}]
    return render(
        request,
        full_template="pages/products.html",
        partial_template="partials/product-list.html",
        context={"products": products_data}
    )

ส่วนที่ 8: รูปแบบและ Pattern ที่นิยม

8.1.1 Live Search Implementation

# search_api.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

# ข้อมูลสินค้า (ตัวอย่าง)
PRODUCTS = [
    {"id": 1, "name": "iPhone 15 Pro", "category": "โทรศัพท์", "price": 45900},
    {"id": 2, "name": "Samsung Galaxy S24", "category": "โทรศัพท์", "price": 39900},
    {"id": 3, "name": "MacBook Air M3", "category": "แล็บท็อป", "price": 49900},
    {"id": 4, "name": "iPad Pro 2024", "category": "แท็บเล็ต", "price": 32900},
    {"id": 5, "name": "AirPods Pro", "category": "หูฟัง", "price": 8990},
    {"id": 6, "name": "Apple Watch Ultra 2", "category": "สมาร์ทวอทช์", "price": 29900},
    {"id": 7, "name": "Dell XPS 15", "category": "แล็บท็อป", "price": 55000},
    {"id": 8, "name": "Sony WH-1000XM5", "category": "หูฟัง", "price": 12990},
]

@app.get("/search", response_class=HTMLResponse)
async def search(q: str = "", category: str = ""):
    results = PRODUCTS
    
    # กรองตาม keyword
    if q:
        results = [p for p in results if q.lower() in p["name"].lower()]
    
    # กรองตาม category
    if category:
        results = [p for p in results if p["category"] == category]
    
    if not results:
        return '<p class="no-results">🔍 ไม่พบสินค้าที่ค้นหา</p>'
    
    items = "".join(f"""
    <div class="search-result-item">
      <div class="product-info">
        <strong>{p['name']}</strong>
        <span class="category-badge">{p['category']}</span>
      </div>
      <span class="price">฿{p['price']:,}</span>
      <button hx-post="/cart/add"
              hx-vals='{{"product_id": {p["id"]}}}'
              hx-swap="none">
        🛒 เพิ่ม
      </button>
    </div>
    """ for p in results)
    
    count_html = f'<p class="result-count">พบ {len(results)} รายการ</p>'
    return count_html + items
<!-- live-search.html -->
<div class="search-container">
  <div class="search-controls">
    <!-- Search Input พร้อม Live Update -->
    <input type="text"
           name="q"
           id="search-input"
           placeholder="ค้นหาสินค้า..."
           hx-get="/search"
           hx-trigger="keyup changed delay:300ms, search"
           hx-target="#search-results"
           hx-include="[name='q'], [name='category']"
           hx-indicator="#search-spinner"
           autocomplete="off">
    
    <span id="search-spinner" class="htmx-indicator">🔍</span>
    
    <!-- Filter by Category -->
    <select name="category"
            hx-get="/search"
            hx-trigger="change"
            hx-target="#search-results"
            hx-include="#search-input">
      <option value="">ทุกหมวดหมู่</option>
      <option value="โทรศัพท์">📱 โทรศัพท์</option>
      <option value="แล็บท็อป">💻 แล็บท็อป</option>
      <option value="หูฟัง">🎧 หูฟัง</option>
      <option value="แท็บเล็ต">📟 แท็บเล็ต</option>
    </select>
  </div>
  
  <!-- Results Area -->
  <div id="search-results" class="search-results">
    <p class="hint">พิมพ์เพื่อค้นหาสินค้า...</p>
  </div>
</div>

8.2 Inline Editing

8.2.1 Click-to-Edit Pattern

# inline_edit.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

users_db = {
    1: {"id": 1, "name": "สมชาย ใจดี", "email": "somchai@example.com", "role": "Admin"},
    2: {"id": 2, "name": "สมหญิง รักเรียน", "email": "somying@example.com", "role": "User"},
}

def render_user_row(user: dict) -> str:
    """แสดงแถวข้อมูลแบบปกติ"""
    return f"""
    <tr id="user-row-{user['id']}">
      <td>{user['id']}</td>
      <td class="editable">{user['name']}</td>
      <td class="editable">{user['email']}</td>
      <td>{user['role']}</td>
      <td>
        <button hx-get="/users/{user['id']}/edit"
                hx-target="#user-row-{user['id']}"
                hx-swap="outerHTML">
          ✏️ แก้ไข
        </button>
      </td>
    </tr>
    """

def render_edit_row(user: dict) -> str:
    """แสดงแถวข้อมูลแบบ Editable"""
    return f"""
    <tr id="user-row-{user['id']}">
      <td>{user['id']}</td>
      <td><input name="name" value="{user['name']}" class="inline-input"></td>
      <td><input name="email" value="{user['email']}" class="inline-input"></td>
      <td>{user['role']}</td>
      <td>
        <button hx-put="/users/{user['id']}"
                hx-include="closest tr"
                hx-target="#user-row-{user['id']}"
                hx-swap="outerHTML">
          💾 บันทึก
        </button>
        <button hx-get="/users/{user['id']}"
                hx-target="#user-row-{user['id']}"
                hx-swap="outerHTML">
          ❌ ยกเลิก
        </button>
      </td>
    </tr>
    """

@app.get("/users/{user_id}/edit", response_class=HTMLResponse)
async def get_edit_form(user_id: int):
    user = users_db.get(user_id)
    if not user:
        return "ไม่พบผู้ใช้", 404
    return render_edit_row(user)

@app.get("/users/{user_id}", response_class=HTMLResponse)
async def get_user_row(user_id: int):
    user = users_db.get(user_id)
    if not user:
        return "ไม่พบผู้ใช้", 404
    return render_user_row(user)

@app.put("/users/{user_id}", response_class=HTMLResponse)
async def update_user(user_id: int, request: Request):
    form = await request.form()
    user = users_db.get(user_id)
    if not user:
        return "ไม่พบผู้ใช้", 404
    user["name"] = form.get("name", user["name"])
    user["email"] = form.get("email", user["email"])
    return render_user_row(user)

8.3 Dynamic Form Validation

8.3.1 Real-time Validation

# validation.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
import re

app = FastAPI()

existing_emails = {"admin@example.com", "user@example.com"}

@app.get("/validate/email", response_class=HTMLResponse)
async def validate_email(email: str = ""):
    if not email:
        return '<span class="hint">กรุณากรอกอีเมล</span>'
    
    email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    
    if not re.match(email_regex, email):
        return '<span class="error">❌ รูปแบบอีเมลไม่ถูกต้อง</span>'
    
    if email in existing_emails:
        return '<span class="error">❌ อีเมลนี้ถูกใช้งานแล้ว</span>'
    
    return '<span class="success">✅ อีเมลนี้ใช้งานได้</span>'

@app.get("/validate/username", response_class=HTMLResponse)
async def validate_username(username: str = ""):
    if len(username) < 3:
        return '<span class="error">❌ ต้องมีอย่างน้อย 3 ตัวอักษร</span>'
    if not re.match(r'^[a-zA-Z0-9_]+$', username):
        return '<span class="error">❌ ใช้ได้เฉพาะตัวอักษร ตัวเลข และ _</span>'
    return '<span class="success">✅ Username นี้ใช้ได้</span>'
<!-- register-form.html -->
<form hx-post="/register" hx-target="#form-result">
  <div class="field-group">
    <label>Username</label>
    <input type="text"
           name="username"
           hx-get="/validate/username"
           hx-trigger="keyup changed delay:400ms"
           hx-target="next .validation-msg"
           placeholder="username">
    <span class="validation-msg"></span>
  </div>
  
  <div class="field-group">
    <label>อีเมล</label>
    <input type="email"
           name="email"
           hx-get="/validate/email"
           hx-trigger="keyup changed delay:500ms, blur"
           hx-target="next .validation-msg"
           placeholder="email@example.com">
    <span class="validation-msg"></span>
  </div>
  
  <button type="submit">สมัครสมาชิก</button>
  <div id="form-result"></div>
</form>

8.4 Pagination และ Load More

# pagination.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
import math

app = FastAPI()

# สร้างข้อมูลตัวอย่าง 50 รายการ
ITEMS = [{"id": i, "title": f"บทความที่ {i}", "author": f"ผู้เขียน {i%5+1}"} 
         for i in range(1, 51)]
PER_PAGE = 5

@app.get("/articles", response_class=HTMLResponse)
async def get_articles(page: int = 1):
    total = len(ITEMS)
    total_pages = math.ceil(total / PER_PAGE)
    start = (page - 1) * PER_PAGE
    items = ITEMS[start:start + PER_PAGE]
    
    items_html = "".join(f"""
    <article class="article-card">
      <h3>{item['title']}</h3>
      <p>โดย: {item['author']}</p>
    </article>
    """ for item in items)
    
    # Pagination Controls
    prev_btn = f"""
    <button hx-get="/articles?page={page-1}"
            hx-target="#articles-container"
            hx-swap="innerHTML"
            hx-push-url="/articles?page={page-1}">
      ← ก่อนหน้า
    </button>
    """ if page > 1 else ""
    
    next_btn = f"""
    <button hx-get="/articles?page={page+1}"
            hx-target="#articles-container"
            hx-swap="innerHTML"
            hx-push-url="/articles?page={page+1}">
      ถัดไป →
    </button>
    """ if page < total_pages else ""
    
    pagination = f"""
    <div class="pagination">
      {prev_btn}
      <span>หน้า {page} จาก {total_pages}</span>
      {next_btn}
    </div>
    """
    
    return items_html + pagination

8.5 Modal / Dialog แบบ Dynamic

# modal_api.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get("/modal/product/{product_id}", response_class=HTMLResponse)
async def get_product_modal(product_id: int):
    # ดึงข้อมูลสินค้า (ตัวอย่าง)
    product = {
        "id": product_id,
        "name": f"สินค้า #{product_id}",
        "price": product_id * 500,
        "description": "รายละเอียดสินค้าที่น่าสนใจ...",
        "stock": 10
    }
    
    return f"""
    <div id="modal-overlay" 
         class="modal-overlay"
         hx-on:click="htmx.remove(this)">
      <div class="modal-content" onclick="event.stopPropagation()">
        <button class="modal-close" 
                hx-on:click="htmx.remove(document.getElementById('modal-overlay'))">
        </button>
        <h2>{product['name']}</h2>
        <p class="price">฿{product['price']:,}</p>
        <p>{product['description']}</p>
        <p>สินค้าคงเหลือ: {product['stock']} ชิ้น</p>
        <div class="modal-actions">
          <button hx-post="/cart/add"
                  hx-vals='{{"product_id": {product["id"]}}}'
                  hx-target="#cart-count"
                  hx-on:htmx:after-request="htmx.remove(document.getElementById('modal-overlay'))">
            🛒 เพิ่มลงตะกร้า
          </button>
        </div>
      </div>
    </div>
    """
<!-- product-list.html -->
<div class="product-grid">
  <div class="product-card">
    <h3>สินค้า #1</h3>
    <button hx-get="/modal/product/1"
            hx-target="body"
            hx-swap="beforeend">
      🔍 ดูรายละเอียด
    </button>
  </div>
</div>

<style>
.modal-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
  z-index: 1000;
}
.modal-content {
  background: white; padding: 2rem;
  border-radius: 8px; max-width: 500px; width: 90%;
  position: relative;
}
.modal-close {
  position: absolute; top: 1rem; right: 1rem;
  background: none; border: none; font-size: 1.5rem; cursor: pointer;
}
</style>

ส่วนที่ 9: การรักษาความปลอดภัย

9.1 CSRF Protection กับ HTMX

9.1.1 CSRF Token Implementation

CSRF (Cross-Site Request Forgery) เป็นการโจมตีที่ให้เว็บไซต์ที่เป็นอันตรายส่ง Request แทนผู้ใช้โดยไม่ได้รับอนุญาต HTMX ต้องการการป้องกัน CSRF เช่นเดียวกับ Form ธรรมดา

# csrf_protection.py
from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.responses import HTMLResponse
import secrets
import hashlib
import hmac

app = FastAPI()
SECRET_KEY = "your-secret-key-change-this-in-production"

def generate_csrf_token(session_id: str) -> str:
    """สร้าง CSRF Token จาก Session ID"""
    return hmac.new(
        SECRET_KEY.encode(),
        session_id.encode(),
        hashlib.sha256
    ).hexdigest()

def verify_csrf_token(session_id: str, token: str) -> bool:
    """ตรวจสอบ CSRF Token"""
    expected = generate_csrf_token(session_id)
    return hmac.compare_digest(expected, token)

# Middleware สำหรับ CSRF
@app.middleware("http")
async def csrf_middleware(request: Request, call_next):
    # ตรวจสอบเฉพาะ Mutating Methods
    if request.method in ("POST", "PUT", "PATCH", "DELETE"):
        # ดึง CSRF Token จาก Header (HTMX ส่งผ่าน Custom Header)
        csrf_token = request.headers.get("X-CSRF-Token")
        session_id = request.cookies.get("session_id", "")
        
        if not csrf_token or not verify_csrf_token(session_id, csrf_token):
            return HTMLResponse(
                content='<p class="error">❌ CSRF Token ไม่ถูกต้อง</p>',
                status_code=403
            )
    
    return await call_next(request)
<!-- ใส่ CSRF Token ใน Meta Tag -->
<meta name="csrf-token" content="{{ csrf_token }}">

<!-- ส่ง CSRF Token ผ่าน Header ทุก HTMX Request -->
<body hx-headers='js:{"X-CSRF-Token": document.querySelector("meta[name=csrf-token]").content}'>
  <!-- ทุก HTMX Request จะส่ง CSRF Token โดยอัตโนมัติ -->
  <form hx-post="/update-profile">
    <input name="email" type="email">
    <button type="submit">บันทึก</button>
  </form>
</body>

9.2 การป้องกัน XSS ใน HTML Response

9.2.1 XSS Prevention

XSS (Cross-Site Scripting) เป็นความเสี่ยงสำคัญสำหรับ HTMX เนื่องจาก Server ส่ง HTML กลับมาโดยตรง ต้องระวัง HTML Injection จากข้อมูลที่ผู้ใช้ป้อนเข้ามา

# xss_prevention.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
import html

app = FastAPI()

def safe_html(value: str) -> str:
    """Escape HTML Special Characters เพื่อป้องกัน XSS"""
    return html.escape(str(value), quote=True)

# ❌ ไม่ปลอดภัย: Direct String Interpolation
@app.get("/unsafe/search")
async def unsafe_search(q: str = ""):
    # ถ้า q = "<script>alert('XSS')</script>" จะทำงานได้!
    return HTMLResponse(f"<p>ค้นหา: {q}</p>")

# ✅ ปลอดภัย: Escape HTML
@app.get("/safe/search")
async def safe_search(q: str = ""):
    safe_q = safe_html(q)
    return HTMLResponse(f"<p>ค้นหา: {safe_q}</p>")

# ✅ ปลอดภัย: ใช้ Template Engine (Jinja2 auto-escape)
# Jinja2 จะ escape HTML โดยอัตโนมัติเมื่อใช้ {{ variable }}
# ต้องใช้ {{ variable | safe }} เท่านั้นเมื่อต้องการ HTML จริง ๆ

# Content Security Policy Header
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self' https://unpkg.com; "
        "style-src 'self' 'unsafe-inline';"
    )
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    return response

9.3 Rate Limiting กับ Partial Requests

9.3.1 Rate Limiting Middleware

# rate_limiting.py
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from collections import defaultdict
from datetime import datetime, timedelta
import asyncio

app = FastAPI()

# Simple In-memory Rate Limiter
class RateLimiter:
    def __init__(self, max_requests: int = 30, window_seconds: int = 60):
        self.max_requests = max_requests
        self.window = timedelta(seconds=window_seconds)
        self.requests: dict = defaultdict(list)
    
    def is_allowed(self, client_ip: str) -> bool:
        now = datetime.now()
        # ลบ Requests เก่าออก
        self.requests[client_ip] = [
            t for t in self.requests[client_ip] 
            if now - t < self.window
        ]
        
        if len(self.requests[client_ip]) >= self.max_requests:
            return False
        
        self.requests[client_ip].append(now)
        return True

limiter = RateLimiter(max_requests=30, window_seconds=60)

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    client_ip = request.client.host
    is_htmx = request.headers.get("HX-Request") == "true"
    
    if not limiter.is_allowed(client_ip):
        if is_htmx:
            # ส่ง Error เป็น HTML Fragment
            return HTMLResponse(
                content="""
                <div class="rate-limit-error">
                  ⚠️ คุณส่งคำขอมากเกินไป กรุณารอสักครู่แล้วลองใหม่
                </div>
                """,
                status_code=429,
                headers={"Retry-After": "60"}
            )
        else:
            return HTMLResponse("Too Many Requests", status_code=429)
    
    return await call_next(request)

ส่วนที่ 10: ประสิทธิภาพและการทดสอบ

10.1 การวัด Performance ของ HTMX App

10.1.1 Metrics ที่ต้องติดตาม

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'clusterBkg': '#32302f',
    'fontSize': '14px'
  }
}}%%
flowchart LR
    subgraph client["Client Metrics"]
        C1["Time to First Byte - (TTFB)"]
        C2["First Contentful Paint - (FCP)"]
        C3["Largest Contentful Paint - (LCP)"]
        C4["HTMX Request Time - (Partial Load)"]
    end
    subgraph server["Server Metrics"]
        S1["Response Time - (Server Processing)"]
        S2["Template Render Time"]
        S3["DB Query Time"]
        S4["Memory Usage"]
    end
    Client["🌐 User"] --> C1 --> C2 --> C3
    C3 --> C4
    C4 --> S1 --> S2 --> S3

รูปที่ 10.1: Performance Metrics ใน HTMX Application

10.1.2 Performance สมการเปรียบเทียบ

สมการคำนวณ Time Saved เมื่อใช้ HTMX แทน Full Page Reload:

Tsaved = Tfull - Tpartial

โดยที่:

ตัวอย่างการคำนวณ:

สมมติว่า:

Tsaved = 800 - 50 = 750 ms

ประสิทธิภาพที่เพิ่มขึ้น (Performance Gain):

Gain = Tfull-Tpartial Tfull × 100 = 800-50 800 × 100 = 93.75 %

10.2 Caching Partial Responses

10.2.1 Server-side Caching

# caching.py
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse
import hashlib
import time
from functools import lru_cache

app = FastAPI()

# Simple In-memory Cache
cache: dict = {}
CACHE_TTL = 300  # 5 นาที

def get_cache_key(path: str, params: dict) -> str:
    """สร้าง Cache Key จาก Path และ Parameters"""
    data = f"{path}:{sorted(params.items())}"
    return hashlib.md5(data.encode()).hexdigest()

def get_cached(key: str):
    if key in cache:
        data, timestamp = cache[key]
        if time.time() - timestamp < CACHE_TTL:
            return data
        del cache[key]
    return None

def set_cache(key: str, value: str):
    cache[key] = (value, time.time())

@app.get("/products/featured", response_class=HTMLResponse)
async def get_featured_products(request: Request, response: Response):
    cache_key = get_cache_key("/products/featured", {})
    
    # ตรวจสอบ Cache
    cached = get_cached(cache_key)
    if cached:
        response.headers["X-Cache"] = "HIT"
        return cached
    
    # สร้าง Response ใหม่
    html = """
    <div class="featured-products">
      <div class="product-card">สินค้าแนะนำ 1</div>
      <div class="product-card">สินค้าแนะนำ 2</div>
    </div>
    """
    
    set_cache(cache_key, html)
    response.headers["X-Cache"] = "MISS"
    response.headers["Cache-Control"] = f"public, max-age={CACHE_TTL}"
    return html

10.3 การทดสอบ (Testing) — Unit และ Integration Test

10.3.1 Integration Testing กับ FastAPI

# test_htmx_app.py
import pytest
from fastapi.testclient import TestClient
from app import app  # import จาก main app

client = TestClient(app)

# Helper: Header สำหรับ HTMX Request
HTMX_HEADERS = {
    "HX-Request": "true",
    "HX-Current-URL": "http://localhost/",
}

class TestProductAPI:
    """Test Cases สำหรับ Product Endpoints"""
    
    def test_get_products_full_page(self):
        """ทดสอบ Full Page Request"""
        response = client.get("/products")
        assert response.status_code == 200
        assert "<!DOCTYPE html>" in response.text
    
    def test_get_products_htmx_request(self):
        """ทดสอบ HTMX Partial Request"""
        response = client.get("/products", headers=HTMX_HEADERS)
        assert response.status_code == 200
        # HTMX Request ไม่ควรได้ Full HTML
        assert "<!DOCTYPE html>" not in response.text
        assert "product-card" in response.text
    
    def test_create_product_success(self):
        """ทดสอบสร้างสินค้าสำเร็จ"""
        response = client.post(
            "/products",
            data={"name": "สินค้าทดสอบ", "price": "999"},
            headers=HTMX_HEADERS
        )
        assert response.status_code == 200
        assert "สินค้าทดสอบ" in response.text
    
    def test_create_product_validation_error(self):
        """ทดสอบ Validation Error"""
        response = client.post(
            "/products",
            data={"name": ""},  # ชื่อว่าง
            headers=HTMX_HEADERS
        )
        assert response.status_code == 422
        assert "error" in response.text.lower()
    
    def test_delete_product_returns_empty(self):
        """ทดสอบลบสินค้า (ควรได้ empty response)"""
        response = client.delete("/products/1", headers=HTMX_HEADERS)
        assert response.status_code == 200
        assert response.text.strip() == ""
    
    def test_oob_swap_headers(self):
        """ทดสอบว่า Response มี OOB Elements"""
        response = client.post(
            "/cart/add",
            data={"product_id": "1"},
            headers=HTMX_HEADERS
        )
        assert response.status_code == 200
        assert 'hx-swap-oob' in response.text

ส่วนที่ 11: Ecosystem และเครื่องมือเสริม

11.1 HTMX Extensions

11.1.1 Extensions ยอดนิยม

Extension วัตถุประสงค์ การใช้งาน
preload Preload ลิงก์ล่วงหน้า hx-ext="preload"
class-tools จัดการ CSS Classes hx-ext="class-tools"
alpine-morph Morph DOM กับ Alpine.js hx-ext="alpine-morph"
ws WebSocket Support hx-ext="ws"
sse Server-Sent Events hx-ext="sse"
json-enc ส่งข้อมูลเป็น JSON hx-ext="json-enc"
response-targets Target ตาม Status Code hx-ext="response-targets"
<!-- Extension: response-targets (จัดการ Error Response) -->
<script src="https://unpkg.com/htmx-ext-response-targets@2.0.0/response-targets.js"></script>

<form hx-post="/submit"
      hx-ext="response-targets"
      hx-target="#success-msg"
      hx-target-422="#validation-errors"
      hx-target-500="#server-error">
  <input name="data">
  <button type="submit">ส่ง</button>
</form>

<div id="success-msg"></div>
<div id="validation-errors" class="errors"></div>
<div id="server-error" class="server-error"></div>

<!-- Extension: preload -->
<script src="https://unpkg.com/htmx-ext-preload@2.0.1/preload.js"></script>
<div hx-ext="preload">
  <!-- Preload เมื่อ hover -->
  <a href="/about" preload="mouseover">เกี่ยวกับเรา</a>
  <!-- Preload ทันที -->
  <a href="/popular" preload>บทความยอดนิยม</a>
</div>

11.2 Alpine.js กับ HTMX — การผสานสำหรับ Client-side Logic

11.2.1 HTMX + Alpine.js Combination

Alpine.js เป็น JavaScript Framework ขนาดเล็ก (~15KB) ที่เหมาะกับการทำ Client-side Interactivity ที่ HTMX ไม่ถนัด เช่น Dropdown, Toggle, Local State

<!-- ใช้ Alpine.js กับ HTMX ร่วมกัน -->
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>

<!-- ตัวอย่าง: Accordion กับ Server-loaded Content -->
<div x-data="{ open: false }">
  <button @click="open = !open" 
          :aria-expanded="open">
    <span x-text="open ? '▼' : '▶'"></span>
    รายละเอียดเพิ่มเติม
  </button>
  
  <!-- Alpine ควบคุมการแสดง, HTMX โหลดเนื้อหา -->
  <div x-show="open"
       x-transition
       hx-get="/details/1"
       hx-trigger="intersect once"
       hx-target="this">
    <div class="htmx-indicator">⏳ กำลังโหลด...</div>
  </div>
</div>

<!-- ตัวอย่าง: Shopping Cart พร้อม Local State -->
<div x-data="{ cart: [], total: 0 }">
  <!-- เพิ่มสินค้า (HTMX อัปเดต Server, Alpine อัปเดต UI ทันที) -->
  <button hx-post="/cart/add"
          hx-vals='{"product_id": 1, "price": 999}'
          hx-on:htmx:after-request="cart.push({id: 1, price: 999}); total += 999">
    🛒 เพิ่ม (฿999)
  </button>
  
  <!-- แสดง Local State ด้วย Alpine -->
  <div>
    รวม: ฿<span x-text="total.toLocaleString()">0</span>
    (<span x-text="cart.length">0</span> ชิ้น)
  </div>
</div>

11.3 เครื่องมือ Debug — htmx.logAll() และ Browser DevTools

11.3.1 Debugging HTMX

<!-- เปิด Debug Mode -->
<script>
  // Log ทุก HTMX Event ใน Console
  htmx.logAll();
  
  // ดู HTMX Version
  console.log("HTMX Version:", htmx.version);
  
  // Debug Specific Element
  htmx.on("#my-button", "htmx:beforeRequest", function(evt) {
    console.log("Request Details:", evt.detail);
  });
  
  // ดู Configuration ปัจจุบัน
  console.log("HTMX Config:", htmx.config);
  
  // หยุด Debug Mode
  // htmx.logNone();
</script>

<!-- HTMX Meta Configuration -->
<meta name="htmx-config" content='{
  "defaultSwapStyle": "innerHTML",
  "defaultSwapDelay": 0,
  "defaultSettleDelay": 20,
  "historyCacheSize": 10,
  "refreshOnHistoryMiss": false,
  "timeout": 0,
  "wsReconnectDelay": "full-jitter",
  "allowScriptTags": false,
  "inlineScriptNonce": "",
  "includeIndicatorStyles": true,
  "selfRequestsOnly": true
}'>

ส่วนที่ 12: กรณีศึกษาและสรุป

12.1 กรณีศึกษา: แปลง SPA เป็น HTMX App

12.1.1 ขั้นตอนการ Migrate

# migration_example.py
# กรณีศึกษา: แปลง React Product Catalog เป็น HTMX

# BEFORE: React API Endpoint (JSON)
# @app.get("/api/products")
# def get_products_json():
#     return {"products": [...], "total": 100}  # JSON

# AFTER: HTMX HTML Endpoint
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse

app = FastAPI()

PRODUCTS = [
    {"id": i, "name": f"สินค้า #{i}", "price": i*100, "category": "อิเล็กทรอนิกส์"}
    for i in range(1, 21)
]

@app.get("/products", response_class=HTMLResponse)
async def get_products(request: Request, category: str = "", q: str = ""):
    products = PRODUCTS
    if category:
        products = [p for p in products if p["category"] == category]
    if q:
        products = [p for p in products if q.lower() in p["name"].lower()]
    
    is_htmx = request.headers.get("HX-Request") == "true"
    
    grid_html = f"""
    <div id="products-grid" class="grid">
      {"".join(f'''
      <div class="product-card" id="product-{p["id"]}">
        <h3>{p["name"]}</h3>
        <p class="price">฿{p["price"]}</p>
        <button hx-post="/cart/add"
                hx-vals=\'{{"product_id": {p["id"]}}}\'>
          🛒 เพิ่ม
        </button>
      </div>
      ''' for p in products)}
    </div>
    <p>{len(products)} สินค้า</p>
    """
    
    if is_htmx:
        return grid_html
    
    # Full Page
    return f"""
    <!DOCTYPE html><html><head>
      <script src="https://unpkg.com/htmx.org@2.0.4"></script>
    </head><body>
      <h1>สินค้าของเรา</h1>
      <input hx-get="/products" hx-trigger="keyup delay:300ms"
             hx-target="#products-grid" name="q" placeholder="ค้นหา...">
      {grid_html}
    </body></html>
    """

12.2 เมื่อไหร่ควรใช้ HTMX และเมื่อไหร่ไม่ควรใช้

12.2.1 Decision Matrix

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'clusterBkg': '#32302f',
    'fontSize': '14px'
  }
}}%%
flowchart TD
    Start([🤔 เริ่มต้น: ต้องการ Interactivity?]) --> Q1{ต้องการ Offline Support?}
    Q1 -->|ใช่| SPA[✅ ใช้ SPA Framework - React / Vue / Angular]
    Q1 -->|ไม่| Q2{ต้องการ Complex - Client-side State?}
    Q2 -->|ใช่ มาก| SPA
    Q2 -->|ไม่ หรือน้อย| Q3{ต้องการ SEO ดี?}
    Q3 -->|ใช่| HTMX[✅ ใช้ HTMX - เหมาะมาก!]
    Q3 -->|ไม่สำคัญ| Q4{ทีมถนัด JS มาก?}
    Q4 -->|ใช่| SPA
    Q4 -->|ไม่| HTMX
    style HTMX fill:#98971a,color:#282828,stroke:#b8bb26
    style SPA fill:#458588,color:#ebdbb2,stroke:#83a598
    style Start fill:#d79921,color:#282828,stroke:#fabd2f

รูปที่ 12.1: Decision Tree สำหรับการเลือกใช้ HTMX

Scenario HTMX SPA
เว็บสินค้า / E-commerce ✅ ดีเยี่ยม ⚠️ Overkill
Admin Dashboard ✅ ดีเยี่ยม ✅ ดี
Web App ซับซ้อน ⚠️ จำกัด ✅ ดีเยี่ยม
Mobile App PWA ❌ ไม่เหมาะ ✅ ดีเยี่ยม
Blog / CMS ✅ ดีเยี่ยม ⚠️ Overkill
Real-time Chat ✅ ได้ (SSE/WS) ✅ ดี
Data Visualization ⚠️ จำกัด ✅ ดีเยี่ยม
Form-heavy App ✅ ดีเยี่ยม ✅ ดี

12.3 อนาคตของ HTMX และ Hypermedia-driven Applications

12.3.1 แนวโน้มและอนาคต

HTMX กำลังเติบโตอย่างต่อเนื่อง ด้วยเหตุผลหลายประการ:

  1. Web Components + HTMX — การผสาน Native Browser Features กับ HTMX
  2. View Transitions API — Animation ที่ลื่นไหลด้วย Browser API ใหม่
  3. Templ (Go) / Marko (JS) — Template Engines ที่ออกแบบมาสำหรับ HTMX
  4. Datastar — รุ่นต่อจาก HTMX ที่รวม Signal-based Reactivity
  5. htmx v2.x — Core ที่เล็กลงและ Extension System ที่ยืดหยุ่นกว่า
<!-- ตัวอย่าง: View Transitions API + HTMX (อนาคต) -->
<head>
  <meta name="htmx-config" content='{"useViewTransitionsIfAvailable": true}'>
</head>

<!-- Navigation จะมี Smooth Transition -->
<nav hx-boost="true">
  <a href="/home">หน้าหลัก</a>
  <a href="/about">เกี่ยวกับ</a>
</nav>

<!-- CSS View Transitions -->
<style>
@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: fade-out 0.3s ease-in;
}
::view-transition-new(root) {
  animation: fade-in 0.3s ease-out;
}
</style>

12.4 แหล่งเรียนรู้เพิ่มเติมและชุมชน HTMX

12.4.1 ทรัพยากรที่แนะนำ

ประเภท แหล่งข้อมูล URL
Official Docs htmx.org https://htmx.org
Reference HTMX Attributes https://htmx.org/reference
Examples HTMX Examples https://htmx.org/examples
Discord HTMX Community https://htmx.org/discord
GitHub Source Code https://github.com/bigskysoftware/htmx
Book Hypermedia Systems https://hypermedia.systems (ฟรี)
YouTube Fireship HTMX Video
Extension htmx-ext https://extensions.htmx.org

12.4.2 สรุปภาพรวม

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#3c3836',
    'primaryTextColor': '#ebdbb2',
    'primaryBorderColor': '#a89984',
    'lineColor': '#d79921',
    'background': '#282828',
    'mainBkg': '#3c3836',
    'clusterBkg': '#32302f',
    'fontSize': '14px'
  }
}}%%
mindmap
  root((HTMX))
    Core Attributes
      hx-get/post/put/patch/delete
      hx-target
      hx-swap
      hx-trigger
    Advanced Features
      SSE / WebSocket
      Out-of-Band Swaps
      Polling
      Infinite Scroll
    Ecosystem
      Alpine.js
      Hyperscript
      Extensions
    Backend Support
      Python FastAPI
      Go Gin/Echo
      Node.js Hono
      PHP Laravel
    Security
      CSRF Protection
      XSS Prevention
      Rate Limiting

รูปที่ 12.2: Mind Map สรุป HTMX Ecosystem


สรุปท้ายบท: HTMX ไม่ใช่คำตอบสำหรับทุกปัญหา แต่สำหรับเว็บแอปพลิเคชันที่ต้องการ ความเรียบง่าย, ประสิทธิภาพสูง, SEO ดี และ ทีมพัฒนาขนาดเล็ก — HTMX คือตัวเลือกที่คุ้มค่าอย่างยิ่ง ด้วยปรัชญา Hypermedia-first ที่นำพาเราย้อนกลับสู่รากเหง้าของ Web ในแบบที่ทันสมัยและมีประสิทธิภาพกว่าเดิม