Now.js Framework Documentation

Now.js Framework Documentation

ApiComponent - ส่วนห่อ UI สำหรับการเรียก API

TH 04 Sep 2026 01:40

ApiComponent - ส่วนห่อ UI สำหรับการเรียก API

เอกสารฉบับนี้อธิบาย ApiComponent ซึ่งทำหน้าที่เป็นส่วนห่อส่วนติดต่อผู้ใช้ เพื่อเรียกใช้งาน API และนำผลลัพธ์ไปแสดงบน HTML ได้อย่างสะดวก โดยไม่จำเป็นต้องเขียนโค้ด JavaScript เพิ่มเติม

📋 สารบัญ

  1. ภาพรวม
  2. การติดตั้งและนำเข้า
  3. การใช้งานพื้นฐาน
  4. คุณสมบัติ HTML
  5. ตัวเลือกการกำหนดค่า
  6. ระบบเทมเพลต
  7. การแบ่งหน้า
  8. การดึงข้อมูลเป็นช่วง
  9. อีเวนต์
  10. การจัดการคำตอบ 403 / 401
  11. CSS State Classes
  12. ตัวอย่างการใช้งาน

ภาพรวม

ApiComponent เป็นคอมโพเนนต์สำหรับส่วนติดต่อผู้ใช้ที่ช่วยให้เรียก API แล้วนำข้อมูลมาเรนเดอร์ใน HTML ได้อย่างง่ายดาย โดยลดภาระการเขียน JavaScript ด้วยตนเอง

ฟีเจอร์หลัก

  • ✅ การกำหนดผ่าน HTML: ตั้งค่าทั้งหมดผ่าน HTML attributes ได้ทันที
  • ✅ รองรับเทมเพลต: ใช้ระบบเทมเพลตของ Now.js เพื่อจัดรูปแบบการแสดงผล
  • ✅ โหลดอัตโนมัติ: ดึงข้อมูลทันทีเมื่อคอมโพเนนต์ถูกสร้าง
  • ✅ ระบบแคช: เก็บข้อมูลไว้ชั่วคราวเพื่อลดการเรียก API ซ้ำ
  • ✅ การแบ่งหน้า: รองรับทั้งการแบ่งหน้าปกติและการโหลดแบบเลื่อนต่อเนื่อง
  • ✅ การดึงข้อมูลเป็นช่วง: อัปเดตข้อมูลตามช่วงเวลาที่กำหนดอัตโนมัติ
  • ✅ ระบบอีเวนต์: ส่งอีเวนต์เมื่อมีการเปลี่ยนแปลงสถานะหรือข้อมูล
  • ✅ ตรรกะการลองใหม่: พยายามเรียกซ้ำอัตโนมัติเมื่อเกิดข้อผิดพลาดที่กำหนดได้
  • ✅ สถานะการโหลด: จัดการสถานะกำลังโหลด ข้อผิดพลาด และไม่มีข้อมูลให้อัตโนมัติ

เมื่อไหร่ควรใช้ ApiComponent

✅ ใช้ ApiComponent เมื่อ:

  • ต้องการแสดงข้อมูลจาก API บน HTML โดยแทบไม่ต้องเขียนโค้ดเพิ่ม
  • ต้องการโครงสร้างที่ใช้งานง่าย (ลดการเขียน JavaScript ด้วยตนเอง)
  • ต้องการระบบแบ่งหน้าหรือการเลื่อนโหลดต่อเนื่อง
  • ต้องการให้ข้อมูลรีเฟรชอัตโนมัติแบบเป็นช่วง
  • ต้องการให้สถานะการโหลด ข้อผิดพลาด และกรณีไม่มีข้อมูลจัดการให้อัตโนมัติ

❌ ไม่ควรใช้ ApiComponent เมื่อ:

  • ต้องการควบคุมการแสดงผลอย่างละเอียดทุกขั้น (แนะนำให้ใช้ ApiService คู่กับโค้ดที่เขียนเอง)
  • ต้องทำงานกับข้อมูลเรียลไทม์ที่ซับซ้อนมาก (พิจารณาใช้ WebSocket หรือโซลูชันเฉพาะ)
  • ต้องการตรรกะที่สลับซับซ้อนและควบคุมทุกรายละเอียดของการเรียก API

การติดตั้งและนำเข้า

ApiComponent โหลดมาพร้อมกับ Now.js Framework และพร้อมใช้งานทันทีผ่าน window object:

// ไม่ต้อง import - พร้อมใช้งานทันที
console.log(window.ApiComponent); // อ็อบเจ็กต์ ApiComponent ที่ผูกไว้กับ window

สิ่งที่ต้องพึ่งพา

การใช้งาน ApiComponent ต้องอาศัยโมดูลต่อไปนี้:

  • HttpClient หรือ simpleFetch – สำหรับจัดการคำขอ HTTP
  • TemplateManager – สำหรับเรนเดอร์เทมเพลต (ทางเลือก หากต้องการใช้ระบบเทมเพลตขั้นสูง)

สองโหมดการทำงาน

ApiComponent รองรับ 2 โหมดการทำงาน ที่แตกต่างกัน โดยจะตรวจสอบอัตโนมัติว่าควรใช้โหมดไหน:

1. Simple Binding Mode (โหมดการผูกข้อมูลแบบง่าย)

เมื่อไหร่: เมื่อ ไม่มี <template> element ภายใน component

จุดประสงค์: แสดงข้อมูลแบบง่าย เช่น ชื่อผู้ใช้, รูปภาพ, ตัวเลข โดยอัพเดทค่าใน elements ที่มีอยู่แล้ว

ตัวอย่าง:

<!-- Simple Binding Mode - ไม่มี template -->
<div data-component="api"
     data-endpoint="/api/v1/auth/me"
     data-cache="true">
  <img data-attr="src:data.avatar.0.url" class="avatar">
  <span data-text="data.name">Loading...</span>
  <span data-text="data.email">Loading...</span>
</div>

คุณสมบัติที่รองรับ:

  • data-text - path ไปยังข้อมูลสำหรับ textContent (dot notation) เช่น data-text="data.name"
  • data-attr - กำหนดค่า attribute จาก data โดยใช้รูปแบบ attributeName:expression เช่น data-attr="src:data.avatar.0.url"

เหมาะกับ:

  • แสดงข้อมูลผู้ใช้ที่ login (topbar, profile card)
  • Badge แสดงจำนวน (notifications, cart count)
  • ข้อมูลเดี่ยวๆ ที่ไม่ซับซ้อน

2. Template Rendering Mode (โหมดการเรนเดอร์เทมเพลต)

เมื่อไหร่: เมื่อ มี <template> element ภายใน component

จุดประสงค์: สร้าง HTML ใหม่จาก template สำหรับข้อมูลที่ซับซ้อน เช่น lists, cards, dynamic content

ตัวอย่าง:

<!-- Template Rendering Mode - มี template -->
<div data-component="api"
     data-endpoint="/api/users">
  <template>
    <ul data-for="user of data">
      <template>
        <li>
          <img data-attr="src:user.avatar, alt:user.name">
          <span data-text="user.name"></span>
          <span data-if="user.status === 'active'">Active</span>
        </li>
      </template>
    </ul>
  </template>
</div>

คุณสมบัติที่รองรับ:

  • data-text - แสดงค่าข้อความ
  • data-attr - แสดงค่า attribute เช่น src, href หรือ value
  • data-for - loop ข้อมูล
  • data-if - เงื่อนไขการแสดงผล
  • data-bind - bind ข้อมูลเชิงโครงสร้างเข้า element ประเภท data-table / data-line-items เมื่อจำเป็น
  • และ directives อื่นๆ ของ TemplateManager

เหมาะกับ:

  • แสดง lists (users, products, posts)
  • Dynamic components ที่มีหลาย items
  • UI ที่ต้องการ loops และ conditions

เปรียบเทียบสองโหมด

Feature Simple Binding Template Rendering
Template Element ❌ ไม่มี ✅ มี
Use Case ข้อมูลเดี่ยว Lists/Collections
Complexity ง่าย ซับซ้อน
HTML Output อัพเดทค่าเดิม สร้าง HTML ใหม่
Loops ❌ ✅
Conditions ❌ ✅
Performance เร็วกว่า ช้ากว่า (สร้าง HTML ใหม่)

คอมโพเนนต์ซ้อนกัน

คอมโพเนนต์วางซ้อนอยู่ในอีกคอมโพเนนต์ได้ เหตุผลที่พบบ่อยคือปลายทางหนึ่งตอบสิ่งที่ทั้งหน้าต้องใช้
(ผู้ใช้ที่ล็อกอินอยู่, แผนที่สิทธิ์) ส่วนอีกปลายทางตอบเรื่องเฉพาะของหน้านั้น:

<main data-component="api" data-endpoint="/api/v2/session">
  <div data-component="api" data-endpoint="/api/v2/users/{id}">
    <h1 data-text="username"></h1>

    <!-- `role` มาจากปลายทางชั้นใน ส่วน `permissions` มาจากชั้นนอก -->
    <section data-if="role === 'webadmin' && permissions['customer.manage']">
      ...
    </section>
  </div>
</main>

คอมโพเนนต์ชั้นในเห็นฟิลด์ของตัวเอง บวกกับฟิลด์ของคอมโพเนนต์ทุกชั้นที่ครอบอยู่ ขอบเขตถูกรวมจาก
ชั้นนอกสุดเข้ามา ชื่อฟิลด์ที่ซ้ำกันจึงยึดของชั้นในสุดเสมอ — ลำดับเดียวกับการไล่ขอบเขตซ้อนที่อื่นทั้งหมด
ในเฟรมเวิร์ก

ลำดับการตอบกลับไม่มีผล คำขอทั้งสองยิงพร้อมกันและอันไหนเสร็จก่อนก็ได้ หลังคอมโพเนนต์หนึ่งเรนเดอร์
เสร็จ มันจะสั่งให้คอมโพเนนต์ที่ซ้อนอยู่ข้างในเรนเดอร์ซ้ำด้วยขอบเขตที่รวมแล้วของตัวมันเอง ไม่ว่าคำตอบไหน
จะมาถึงทีหลัง ส่วนที่อยู่ข้างในก็จบด้วยคำตอบของคอมโพเนนต์ชั้นในเสมอ

ผลที่ตามมาสองข้อที่ควรรู้:

  • ฟิลด์ชื่อเดียวกันที่มีทั้งสองชั้นจะได้ค่าของชั้นใน ถ้าทั้งสองชั้นหมายถึงคนละเรื่องจริง ๆ
    ให้ตั้งชื่อฟิลด์ของปลายทางชั้นนอกให้ต่างออกไป (เช่น user.role แทน role เปล่า ๆ)
  • คอมโพเนนต์ชั้นในเรนเดอร์เพิ่มหนึ่งครั้งต่อการโหลดของชั้นนอกหนึ่งครั้ง ตัวจัดการ data-on-load
    จึงต้องเรียกซ้ำได้โดยผลไม่เปลี่ยน เพราะมันจะถูกเรียกอีกในการเรนเดอร์รอบนั้น

ข้อมูลที่เป็นอาร์เรย์ไม่มีชื่อฟิลด์ให้รวม คอมโพเนนต์ที่ปลายทางตอบเป็นอาร์เรย์เปล่า ๆ จึงใช้ข้อมูลของ
ตัวเองตามเดิมและไม่รับค่าจากชั้นนอกเลย

การใช้งานพื้นฐาน

1. การใช้งานผ่าน HTML (แนะนำ) — HTML-Based

<div data-component="api"
     data-endpoint="/api/users"
     data-method="GET">
  <template>
    <div data-for="item in data">
      <template>
        <article>
          <h3 data-text="item.name"></h3>
          <p data-text="item.email"></p>
        </article>
      </template>
    </div>
  </template>
</div>

2. การใช้งานผ่าน JavaScript

const element = document.querySelector('#my-api-component');

const instance = ApiComponent.create(element, {
  endpoint: '/api/users',
  method: 'GET',
  autoload: true
});

3. การเริ่มทำงานอัตโนมัติ

ApiComponent จะสร้าง instance อัตโนมัติสำหรับ elements ที่มี data-component="api":

<!-- จะถูกสร้างอัตโนมัติเมื่อหน้าโหลด -->
<div data-component="api" data-endpoint="/api/products"></div>

คุณสมบัติ HTML

ApiComponent รองรับ attributes เหล่านี้สำหรับการกำหนดค่า:

คุณสมบัติพื้นฐาน

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-endpoint string - URL ของ API (จำเป็น)
data-method string 'GET' วิธีการ HTTP (GET, POST, PUT, DELETE)
data-base-url string '' Base URL สำหรับ API
data-autoload boolean true โหลดข้อมูลอัตโนมัติเมื่อสร้าง component
data-timeout number 30000 เวลารอคำขอ (มิลลิวินาที)

คุณสมบัติด้านแคช

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-cache boolean false เปิดใช้งานการแคช
data-cache-time number 60000 ระยะเวลาแคช (มิลลิวินาที)

data-cache ใช้กับคำขอ GET เท่านั้น โดยการ refresh แบบ explicit และ polling จะข้าม cache เพื่อดึงข้อมูลใหม่เสมอ

คุณสมบัติเทมเพลต

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-template string - ID ของเทมเพลตภายนอก

คุณสมบัติการแบ่งหน้า

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-pagination boolean true เปิดใช้งานการแบ่งหน้า
data-page-param string 'page' ชื่อพารามิเตอร์หน้าที่ส่งไปยังเซิร์ฟเวอร์
data-limit-param string 'pageSize' ชื่อพารามิเตอร์จำนวนรายการต่อหน้า
data-page-size number 10 จำนวนรายการต่อหน้า

คุณสมบัติการดึงข้อมูลเป็นช่วง

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-polling boolean false เปิดใช้งานการดึงข้อมูลเป็นช่วงๆ
data-polling-interval number 30000 ช่วงเวลาการดึงข้อมูล (มิลลิวินาที)

คุณสมบัติอีเวนต์

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-refresh-event string - ชื่ออีเวนต์ที่ใช้ทริกเกอร์การรีเฟรช (คั่นด้วย comma)

คุณสมบัติข้อมูล

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-params JSON {} พารามิเตอร์สำหรับ query string
data-data JSON {} ข้อมูลสำหรับส่งใน body ของคำขอ
data-headers JSON {} เฮดเดอร์ที่กำหนดเอง
data-data-path string 'data' เส้นทางไปยังข้อมูลใน response
data-meta-path string 'meta' เส้นทางไปยัง meta ใน response

คุณสมบัติ URL Parameters

ApiComponent สามารถอ่าน URL query parameters และรวมเข้าไปใน API request โดยอัตโนมัติ:

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-url-params boolean false เปิดใช้งานการอ่าน URL query parameters
data-url-params-fields string - ชื่อ fields ที่ต้องการอ่านจาก URL (คั่นด้วย comma)
data-url-params-required boolean false แสดงข้อผิดพลาดถ้า params ที่กำหนดไม่มี

ตัวอย่าง - หน้ายืนยันอีเมล:

<!-- URL: /activate?id=abc123 -->
<div data-component="api"
     data-endpoint="api/auth/activate"
     data-url-params="true"
     data-url-params-fields="id"
     data-url-params-required="true">
  <div class="api-loading-content">กำลังตรวจสอบ...</div>
  <div class="api-success-content">ยืนยันอีเมลสำเร็จ!</div>
  <div class="api-error-content">ลิงก์ไม่ถูกต้อง</div>
</div>

Component จะเรียก api/auth/activate?id=abc123 โดยอัตโนมัติเมื่อโหลด

การใช้คุณสมบัติแบบรวบยอด

สามารถใช้ data-props เพื่อกำหนดค่าทั้งหมดในรูปแบบ JSON:

<div data-component="api"
     data-props='{
       "endpoint": "/api/users",
       "method": "GET",
       "cache": true,
       "cacheTime": 300000,
       "params": {"status": "active"}
     }'>
</div>

ตัวเลือกการกำหนดค่า

เมื่อสร้าง instance ด้วย JavaScript สามารถกำหนดค่าได้ทั้งหมด:

const instance = ApiComponent.create(element, {
  // การตั้งค่าทั่วไป
  method: 'GET',
  baseURL: '',
  endpoint: '/api/users',
  autoload: true,
  timeout: 30000,

  // การแคชข้อมูล
  cache: false,
  cacheTime: 60000,

  // การลองใหม่
  retry: 3,

  // ข้อความสถานะ
  loadingText: 'กำลังโหลดข้อมูล...',
  errorText: 'เกิดข้อผิดพลาด',
  emptyText: 'ไม่พบข้อมูล',

  // ฟังก์ชันจัดการอีเวนต์
  onSuccess: (data, response) => {
    console.log('โหลดข้อมูลสำเร็จ:', data);
  },
  onError: (error) => {
    console.error('เกิดข้อผิดพลาด:', error);
  },
  onEmpty: () => {
    console.log('ไม่มีข้อมูลให้แสดง');
  },

  // การแบ่งหน้า
  pagination: true,
  pageParam: 'page',
  limitParam: 'pageSize',
  pageSize: 10,

  // การดึงข้อมูลแบบเป็นช่วง
  polling: false,
  pollingInterval: 30000,

  // อีเวนต์ที่ตั้งค่าให้ทริกเกอร์การรีเฟรช
  refreshEvent: 'user:updated',

  // ข้อมูลส่งออก
  params: {},
  data: {},
  headers: {},

  // เส้นทางภายในข้อมูลตอบกลับ
  dataPath: 'data',
  metaPath: 'meta',

  // เทมเพลตที่ต้องการใช้
  template: 'my-template-id',

  // โหมดดีบัก
  debug: false
});

ระบบเทมเพลต

ApiComponent ใช้ TemplateManager ของ Now.js สำหรับแสดงผล

เทมเพลตแบบฝัง

<div data-component="api" data-endpoint="/api/users">
  <template>
    <div data-for="user in data">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
  <span data-if="user.status === 'active'">ใช้งาน</span>
    </div>
  </template>
</div>

เทมเพลตภายนอก

<!-- กำหนดเทมเพลต -->
<template id="user-template">
  <div class="user-card">
    <h3>{name}</h3>
    <p>{email}</p>
  </div>
</template>

<!-- เรียกใช้เทมเพลต -->
<div data-component="api"
     data-endpoint="/api/users"
     data-template="user-template">
</div>

ไดเรกทีฟของเทมเพลต

ApiComponent รองรับ directives ของ TemplateManager:

data-for

<template>
  <ul>
    <li data-for="item in data">
      {item.name}
    </li>
  </ul>
</template>

data-if

<template>
  <div data-for="user in data">
    <h3>{user.name}</h3>
  <span data-if="user.status === 'active'" class="badge">ใช้งาน</span>
  <span data-if="user.status === 'inactive'" class="badge">ไม่ใช้งาน</span>
  </div>
</template>

ลูปซ้อน

<template>
  <div data-for="category in data">
    <h2>{category.name}</h2>
    <ul>
      <li data-for="product in category.products">
        {product.name} - {product.price}
      </li>
    </ul>
  </div>
</template>

การแบ่งหน้า

ApiComponent รองรับการแบ่งหน้าแบบ server-side

การแบ่งหน้าแบบพื้นฐาน

<div data-component="api"
     data-endpoint="/api/products"
     data-pagination="true"
     data-page-size="20">
  <template>
    <div data-for="product in data">
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  </template>

  <!-- ปุ่มควบคุมการแบ่งหน้า -->
  <button onclick="this.closest('.api-component').apiInstance.loadMore()">
    โหลดเพิ่ม
  </button>
</div>

กำหนดพารามิเตอร์แบ่งหน้า

<div data-component="api"
     data-endpoint="/api/posts"
     data-page-param="p"
     data-limit-param="limit"
     data-page-size="15">
</div>

Request จะส่งเป็น:

GET /api/posts?p=1&limit=15

รูปแบบ Response ที่คาดหวัง

{
  "data": [
    {"id": 1, "name": "Item 1"},
    {"id": 2, "name": "Item 2"}
  ],
  "meta": {
    "total": 100,
    "last_page": 10,
    "current_page": 1,
    "per_page": 10
  }
}

ปิดการแบ่งหน้า

<div data-component="api"
     data-endpoint="/api/settings"
     data-pagination="false">
</div>

การดึงข้อมูลเป็นช่วง

ApiComponent สามารถ poll endpoint อัตโนมัติเพื่ออัปเดตข้อมูล

เปิดใช้งาน Polling

<div data-component="api"
     data-endpoint="/api/notifications"
     data-polling="true"
     data-polling-interval="10000">
  <!-- จะโหลดข้อมูลใหม่ทุก 10 วินาที -->
</div>

ควบคุม Polling ด้วย JavaScript

const instance = ApiComponent.getInstance('#my-component');

// เริ่มการ Polling
ApiComponent.startPolling(instance);

// หยุดการ Polling
ApiComponent.stopPolling(instance);

อีเวนต์ของ Polling

element.addEventListener('api:polling:start', (e) => {
  console.log('เริ่ม Polling');
});

element.addEventListener('api:polling:stop', (e) => {
  console.log('หยุด Polling');
});

อีเวนต์

ApiComponent ส่งอีเวนต์ เมื่อมีการเปลี่ยนแปลง

ประเภทของอีเวนต์

อีเวนต์ เมื่อไหร่ รายละเอียด
api:init คอมโพเนนต์ถูกเริ่มใช้งาน {instance}
api:loaded โหลดข้อมูลสำเร็จ {data, response, fromCache}
api:error เกิดข้อผิดพลาด {error, message}
api:empty ไม่มีข้อมูลจาก API -
api:content-rendered แสดงผลคอนเทนต์เสร็จ {data}
api:polling:start เริ่ม Polling -
api:polling:stop หยุด Polling -
api:destroy ลบคอมโพเนนต์ออก -

การฟังอีเวนต์

const element = document.querySelector('#my-api-component');

element.addEventListener('api:loaded', (e) => {
  console.log('โหลดข้อมูลสำเร็จ:', e.detail.data);
  console.log('จาก cache หรือไม่:', e.detail.fromCache);
});

element.addEventListener('api:error', (e) => {
  console.error('ข้อผิดพลาด:', e.detail.error);
});

element.addEventListener('api:empty', (e) => {
  console.log('ไม่มีข้อมูลให้แสดง');
});

รีเฟรชเมื่อมีอีเวนต์ที่กำหนดเอง

<div data-component="api"
     data-endpoint="/api/cart"
     data-refresh-event="cart:updated">
  <!-- จะ refresh เมื่อมีการส่ง event 'cart:updated' -->
</div>

<script>
// ที่ไหนก็ได้ในโค้ด
EventManager.emit('cart:updated');
</script>

การจัดการคำตอบ 403 / 401

ApiComponent จัดการสองสถานะ HTTP นี้ให้เองโดยแอปไม่ต้องเขียนโค้ดเพิ่ม:

สถานะ พฤติกรรมเริ่มต้น
403 Forbidden RouterManager.navigate() ไปยังเส้นทาง "ไม่มีสิทธิ์" ที่แอปตั้งค่าไว้
401 Unauthorized RouterManager.navigate('/login')

ตั้งค่าเส้นทางเมื่อไม่มีสิทธิ์

ปลายทางของ 403 ไม่ได้ตรึงไว้ที่ /403 — อ่านจาก
RouterManager.config.auth.redirects.forbidden ค่าเดียวกับที่ AuthGuard ใช้
ตอนตรวจสิทธิ์ระดับเส้นทาง เพื่อให้การ redirect จาก API กับจากเส้นทางไปจบที่หน้า
เดียวกันเสมอ ถ้าแอปไม่เคยตั้งค่านี้ไว้ จะ fallback ไปที่ /forbidden:

Now.init({
  router: {
    auth: {
      redirects: {
        forbidden: '/forbidden',  // ต้องเป็นเส้นทางที่แอปลงทะเบียนไว้จริง
        unauthorized: '/login'
      }
    }
  }
});

แอปที่ลงทะเบียนเส้นทางไว้ชื่ออื่น (หรือไม่ได้ลงทะเบียน /forbidden เลย) ต้อง
ตั้งค่านี้เอง — การ redirect ไปยังเส้นทางที่ไม่มีอยู่จริงจะตกไปที่ตัวจัดการ
"ไม่พบหน้า" ของ router ซึ่งแทนที่เนื้อหาทั้งหน้า ไม่ใช่แค่วิดเจ็ตที่ล้มเหลว

instance ที่ถูกซ่อนหรือถูกลบไปแล้วจะไม่ redirect

data-component="api" ที่อยู่ข้างในกล่องที่คุมด้วย data-if ยังคงยิงคำขอของ
ตัวเองทันทีที่ถูกสร้างขึ้น — การที่ data-if ซ่อนกล่องครอบไม่ได้หยุดคำขอของ
คอมโพเนนต์ข้างในเลย (ดูเหตุผลใน หัวข้อ "ข้อควรระวัง" ของเอกสาร data-if)
เรื่องนี้สำคัญที่สุดกับวิดเจ็ตที่จำกัดเฉพาะแอดมินซึ่งฝังอยู่ในหน้าที่คนทั่วไปก็เข้าได้:

<div data-if="permissions['settings.manage']">
  <div data-component="api" data-endpoint="/api/v2/admin-only-widget">
    ...
  </div>
</div>

สำหรับผู้ใช้ที่ไม่มีสิทธิ์ settings.manage กล่องครอบด้านนอกถูกลบทิ้งทันที
แต่คำขอของคอมโพเนนต์ข้างในได้ยิงออกไปแล้วและจะได้คำตอบ 403 กลับมา ก่อนจะทำ
อะไรกับคำตอบนั้น ApiComponent จะเช็คก่อนว่า instance นี้ยังเป็นส่วนหนึ่งของหน้า
จริงหรือไม่ — ยังถูกติดตามอยู่ และ element ของมันยังติดอยู่กับ document.body
หรือไม่ ถ้าไม่ใช่แล้ว การจัดการ 403/401 จะถูกข้ามไปทั้งหมด ไม่มีการ redirect
ไม่มีอะไรมากไปกว่า warning ตอนแรก วิดเจ็ตที่ผู้ใช้ไม่เคยเห็นเลยไม่มีสิทธิ์ไป
ยึดการนำทางของทั้งหน้าเพราะคำขอของมันเองที่ล้มเหลวและไม่มีความหมายแล้ว มีแค่
instance ที่ยังมีชีวิตอยู่จริงบนหน้าจอเท่านั้นที่ 403/401 ของมันจะสั่ง redirect

CSS State Classes

ApiComponent จะเพิ่ม CSS classes อัตโนมัติเพื่อบอกสถานะปัจจุบันของคอมโพเนนต์ โดยใช้ prefix api- เพื่อหลีกเลี่ยงการชนกับชื่อ class ทั่วไป

State Classes

Class เมื่อไหร่ คำอธิบาย
api-component ตลอดเวลา ถูกเพิ่มเมื่อคอมโพเนนต์ถูกเริ่มต้น
api-loading ระหว่างดึงข้อมูล กำลังโหลดข้อมูลจาก API
api-content หลังโหลดสำเร็จ โหลดและแสดงข้อมูลสำเร็จแล้ว
api-error เมื่อเกิดข้อผิดพลาด เกิดข้อผิดพลาดระหว่างโหลดข้อมูล
api-empty เมื่อไม่มีข้อมูล API ส่งข้อมูลว่างหรือ array ว่าง

ตัวอย่างการใช้งาน

/* จัดสไตล์สถานะกำลังโหลด */
.api-component.api-loading {
  opacity: 0.6;
  pointer-events: none;
}

.api-component.api-loading::after {
  content: 'กำลังโหลด...';
  display: block;
  text-align: center;
  padding: 20px;
}

/* จัดสไตล์สถานะข้อผิดพลาด */
.api-component.api-error {
  background-color: #fee;
  border: 1px solid #fcc;
  padding: 20px;
}

.api-component.api-error::before {
  content: '⚠️ เกิดข้อผิดพลาดในการโหลดข้อมูล';
  display: block;
  color: #c00;
  font-weight: bold;
  margin-bottom: 10px;
}

/* จัดสไตล์สถานะไม่มีข้อมูล */
.api-component.api-empty {
  text-align: center;
  padding: 40px;
  color: #999;
}

.api-component.api-empty::after {
  content: 'ไม่มีข้อมูล';
  display: block;
}

/* จัดสไตล์สถานะมีข้อมูล */
.api-component.api-content {
  /* สไตล์สำหรับเนื้อหาของคุณ */
}

ตัวอย่างแบบสมบูรณ์

<div data-component="api"
     data-endpoint="/api/products"
     class="product-list">
  <template>
    <div data-for="product in data" class="product-card">
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  </template>
</div>

<style>
/* สไตล์พื้นฐาน */
.product-list {
  min-height: 200px;
  transition: all 0.3s ease;
}

/* สถานะกำลังโหลด */
.product-list.api-loading {
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s ease-in-out infinite;
}

@keyframes loading {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* สถานะข้อผิดพลาด */
.product-list.api-error {
  background-color: #fee2e2;
  border: 2px solid #ef4444;
  padding: 20px;
  border-radius: 8px;
}

/* สถานะไม่มีข้อมูล */
.product-list.api-empty {
  background-color: #f9fafb;
  border: 2px dashed #d1d5db;
  padding: 40px;
  text-align: center;
  color: #6b7280;
}

/* โหลดข้อมูลสำเร็จ */
.product-list.api-content {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}
</style>

ประโยชน์ของ prefix api-

การใช้ prefix api- ช่วยป้องกันการชนกับชื่อ class ที่ใช้บ่อย:

  • ❌ .loading - อาจชนกับ loading class ของคุณเอง

  • ❌ .error - เป็นชื่อที่ใช้กันมากในหลายโปรเจค

  • ❌ .content - ใช้กันมากมายสำหรับ layout

  • ❌ .empty - ใช้กันบ่อยสำหรับสถานะว่าง

  • ✅ .api-loading - เฉพาะเจาะจงสำหรับ ApiComponent

  • ✅ .api-error - ชัดเจนว่ามาจาก ApiComponent

  • ✅ .api-content - ไม่ชนกับ layout classes

  • ✅ .api-empty - เฉพาะเจาะจงสำหรับสถานะข้อมูล API

API สำหรับ JavaScript

ApiComponent มี methods สำหรับควบคุมผ่าน JavaScript

สร้าง Instance

const instance = ApiComponent.create(element, options);

อ่าน Instance

// จาก element โดยตรง
const instance = ApiComponent.getInstance(element);

// จาก selectors หรือ ID
const instance = ApiComponent.getInstance('#my-component');

// จาก property บน element
const instance = element.apiInstance;

โหลดข้อมูล

// โหลดข้อมูลใหม่และรีเซ็ตหน้า (Load data & reset pagination)
await ApiComponent.loadData(instance);

// โหลดเพิ่มและต่อท้ายข้อมูลเดิม (Load more & append)
await ApiComponent.loadMore(instance);

รีเฟรชข้อมูล

// รีเฟรชและกลับไปหน้าแรก (Refresh & reset to page 1)
ApiComponent.refresh(instance);

// หรือเรียกผ่านเมธอดของ instance
instance.refresh();

ส่งข้อมูล

// ส่งข้อมูลด้วย POST
await ApiComponent.submit(instance, {
  name: 'John Doe',
  email: 'john@example.com'
});

ควบคุม Polling

// เริ่มการ Polling
ApiComponent.startPolling(instance);

// หยุดการ Polling
ApiComponent.stopPolling(instance);

การจัดการ Cache

// ล้าง cache ทั้งหมด
ApiComponent.clearCache();

ลบ Instance

// ทำลาย instance และเคลียร์ทรัพยากร
ApiComponent.destroy(instance);

ตัวอย่างการใช้งาน

1. แสดงรายการสินค้า

<div data-component="api"
     data-endpoint="/api/products"
     data-cache="true"
     data-cache-time="300000">
  <template>
    <div class="product-grid">
      <div data-for="product in data" class="product-card">
        <img data-src="product.image" data-alt="product.name">
        <h3>{product.name}</h3>
        <p class="price">{product.price} บาท</p>
    <button>เพิ่มในตะกร้า</button>
      </div>
    </div>
  </template>
</div>

2. โปรไฟล์ผู้ใช้ด้วย POST

<form id="profile-form">
  <input type="text" name="name" placeholder="ชื่อ">
  <input type="email" name="email" placeholder="อีเมล">
  <button type="submit">บันทึก</button>
</form>

<div id="profile-result"
     data-component="api"
     data-endpoint="/api/profile"
     data-method="POST"
     data-autoload="false">
  <template>
    <div class="alert success">
      โปรไฟล์ถูกอัปเดต: {data.name}
    </div>
  </template>
</div>

<script>
document.getElementById('profile-form').addEventListener('submit', async (e) => {
  e.preventDefault();

  const formData = new FormData(e.target);
  const data = Object.fromEntries(formData);

  const instance = ApiComponent.getInstance('#profile-result');
  await ApiComponent.submit(instance, data);
});
</script>

3. เลื่อนหน้าแบบไม่สิ้นสุด

<div data-component="api"
     data-endpoint="/api/posts"
     data-page-size="20"
     id="posts">
  <template>
    <div data-for="post in data" class="post">
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </div>
  </template>
</div>

<script>
// เลื่อนแบบไม่สิ้นสุด
window.addEventListener('scroll', () => {
  if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) {
    const instance = ApiComponent.getInstance('#posts');
    if (!instance.loading && instance.page < instance.totalPages) {
      ApiComponent.loadMore(instance);
    }
  }
});
</script>

4. แจ้งเตือนแบบเรียลไทม์

<div data-component="api"
     data-endpoint="/api/notifications"
     data-polling="true"
     data-polling-interval="10000"
     data-cache="false">
  <template>
    <div class="notification-list">
      <div data-for="notif in data" class="notification">
        <span class="icon" data-if="notif.type === 'info'">ℹ️</span>
        <span class="icon" data-if="notif.type === 'warning'">⚠️</span>
        <span class="icon" data-if="notif.type === 'error'">❌</span>
        <div class="content">
          <h4>{notif.title}</h4>
          <p>{notif.message}</p>
          <time>{notif.created_at}</time>
        </div>
      </div>
    </div>
  </template>
</div>

5. ค้นหาด้วย Debounce

<input type="text" id="search-input" placeholder="ค้นหาสินค้า...">

<div data-component="api"
     data-endpoint="/api/products/search"
     data-autoload="false"
     id="search-results">
  <template>
    <div data-for="product in data">
      <h4>{product.name}</h4>
      <p>{product.description}</p>
    </div>
  </template>
</div>

<script>
let searchTimeout;
const searchInput = document.getElementById('search-input');
const instance = ApiComponent.getInstance('#search-results');

searchInput.addEventListener('input', (e) => {
  clearTimeout(searchTimeout);

  searchTimeout = setTimeout(() => {
    instance.options.params = { q: e.target.value };
    ApiComponent.refresh(instance);
  }, 300);
});
</script>

6. กำหนดพารามิเตอร์แบบไดนามิก

<select id="category-select">
  <option value="electronics">Electronics</option>
  <option value="clothing">Clothing</option>
  <option value="books">Books</option>
</select>

<div data-component="api"
     data-endpoint="/api/products"
     data-params='{"category": "electronics"}'
     id="products">
  <template>
    <div data-for="product in data">
      <h3>{product.name}</h3>
    </div>
  </template>
</div>

<script>
document.getElementById('category-select').addEventListener('change', (e) => {
  const instance = ApiComponent.getInstance('#products');
  instance.options.params.category = e.target.value;
  ApiComponent.refresh(instance);
});
</script>

7. รองรับข้อมูลซ้อนกัน

<div data-component="api"
     data-endpoint="/api/categories">
  <template>
    <div data-for="category in data" class="category">
      <h2>{category.name}</h2>
      <div class="products">
        <div data-for="product in category.products" class="product">
          <h4>{product.name}</h4>
          <p>{product.price} บาท</p>
        </div>
      </div>
    </div>
  </template>
</div>

8. ✅ ใช้ data-path เมื่อโครงสร้าง response ซับซ้อน

<!-- โครงสร้าง Response: { "success": true, "result": { "items": [...] } } -->
<div data-component="api"
     data-endpoint="/api/items"
     data-data-path="result.items">
  <template>
    <div data-for="item in data">
      {item.name}
    </div>
  </template>
</div>

9. ✅ ใช้ events เพื่อทำ custom actions

<!-- ตะกร้าสินค้า -->
<div data-component="api"
     data-endpoint="/api/cart"
     data-refresh-event="cart:updated"
     id="cart">
  <template>
    <div>
      จำนวนสินค้า: {data.count} รายการ
      ราคารวม: {data.total} บาท
    </div>
  </template>
</div>

<!-- ปุ่มเพิ่มในตะกร้า -->
<button onclick="addToCart(123)">เพิ่มในตะกร้า</button>

<script>
async function addToCart(productId) {
  await fetch('/api/cart/add', {
    method: 'POST',
    body: JSON.stringify({ productId }),
    headers: {'Content-Type': 'application/json'}
  });

  // ทริกเกอร์ให้รีเฟรช
  EventManager.emit('cart:updated');
}
</script>

10. ✅ การจัดการข้อผิดพลาดด้วย UI ที่กำหนดเอง

<div data-component="api"
     data-endpoint="/api/products"
     id="products">
  <template>
    <div data-for="product in data">
      <h3>{product.name}</h3>
    </div>
  </template>
</div>

<script>
const element = document.getElementById('products');

element.addEventListener('api:error', (e) => {
  const errorDiv = document.createElement('div');
  errorDiv.className = 'alert alert-error';
  errorDiv.textContent = 'ไม่สามารถโหลดรายการสินค้าได้ ';

  const retryBtn = document.createElement('button');
  retryBtn.textContent = 'ลองอีกครั้ง';
  retryBtn.onclick = () => {
    ApiComponent.refresh(e.detail.instance);
    errorDiv.remove();
  };

  errorDiv.appendChild(retryBtn);
  element.insertBefore(errorDiv, element.firstChild);
});
</script>

อ้างอิง API

เมธอดของ ApiComponent

// สร้างคอมโพเนนต์
create(element: HTMLElement|string, options?: object): Instance

// อ่าน instance ที่มีอยู่
getInstance(element: HTMLElement|string): Instance|null

// จัดการข้อมูล
loadData(instance: Instance, append?: boolean): Promise<void>
refresh(instance: Instance): void
loadMore(instance: Instance): void
submit(instance: Instance, data: object): Promise<void>

// ควบคุม Polling
startPolling(instance: Instance): void
stopPolling(instance: Instance): void

// จัดการ cache
clearCache(): void

// วงจรชีวิตของคอมโพเนนต์
destroy(instance: Instance): boolean
init(options?: object): Promise<ApiComponent>

คุณสมบัติของ Instance

{
  id: string,
  element: HTMLElement,
  options: object,
  data: any,
  loading: boolean,
  error: string|null,
  timestamp: number|null,
  page: number,
  totalPages: number,
  pageSize: number,
  totalItems: number,
  polling: boolean,
  timer: number|null,
  abortController: AbortController|null,
  templateContent: string|null
}

แนวทางปฏิบัติที่ดี

1. ✅ ใช้ HTML attributes แทนการเขียน JavaScript

<!-- ❌ ไม่ดี: ใช้ JavaScript เมื่อไม่จำเป็น -->
<div id="products"></div>
<script>
ApiComponent.create('#products', {endpoint: '/api/products'});
</script>

<!-- ✅ ดี: ใช้ HTML attributes -->
<div data-component="api" data-endpoint="/api/products"></div>

2. ✅ เพิ่มประสิทธิภาพด้วยการแคช

<!-- ✅ ดี: Cache data ที่ไม่เปลี่ยนแปลงบ่อย -->
<div data-component="api"
     data-endpoint="/api/categories"
     data-cache="true"
     data-cache-time="300000">
</div>

3. ✅ ใช้เทมเพลตภายนอกเมื่อใช้ซ้ำ

<!-- ✅ ดี: Template แยกออกมา ใช้ซ้ำได้ -->
<template id="product-card">
  <div class="card">
    <h3>{name}</h3>
    <p>{price}</p>
  </div>
</template>

<div data-component="api"
     data-endpoint="/api/products"
     data-template="product-card">
</div>

<div data-component="api"
     data-endpoint="/api/featured-products"
     data-template="product-card">
</div>

4. ✅ ล้าง Instance เมื่อไม่ใช้แล้ว

// ✅ ดี: Destroy instance เมื่อไม่ใช้แล้ว
const instance = ApiComponent.getInstance('#my-component');
ApiComponent.destroy(instance);

// หรือเมื่อถอด element ออกจาก DOM
 element.remove(); // ล้างอัตโนมัติ ถ้าใช้ ComponentManager

5. ✅ ใช้ Pagination สำหรับข้อมูลจำนวนมาก

<!-- ✅ ดี: ใช้ pagination สำหรับข้อมูลจำนวนมาก -->
<div data-component="api"
     data-endpoint="/api/posts"
     data-page-size="20">
</div>

6. ✅ ตรวจสอบข้อมูลว่างในเทมเพลต

<div data-component="api"
     data-endpoint="/api/notifications">
  <template>
    <div data-if="data.length > 0">
      <div data-for="notif in data">
        {notif.message}
      </div>
    </div>
    <div data-if="data.length === 0">
  ไม่มีการแจ้งเตือน
    </div>
  </template>
</div>

7. ✅ ใช้ Polling อย่างชาญฉลาด

<!-- ❌ ไม่ดี: Polling ทุก 1 วินาที -->
<div data-polling="true" data-polling-interval="1000"></div>

<!-- ✅ ดี: Polling ทุก 30 วินาที หรือใช้ WebSocket -->
<div data-polling="true" data-polling-interval="30000"></div>

8. ✅ ปิด autoload เมื่อควบคุมการโหลดเอง

<!-- ✅ ดี: ปิด autoload เมื่อต้องการควบคุมเอง -->
<div data-component="api"
     data-endpoint="/api/search"
     data-autoload="false"
     id="search">
</div>

<script>
// โหลดเมื่อผู้ใช้พิมพ์ค้นหา
searchInput.addEventListener('input', (e) => {
  const instance = ApiComponent.getInstance('#search');
  instance.options.params = {q: e.target.value};
  ApiComponent.refresh(instance);
});
</script>

9. ✅ ใช้ data-path เมื่อโครงสร้าง response ซับซ้อน

<!-- ✅ ดี: ระบุ path เมื่อ API response ไม่ใช่ {data: [...]} -->
<div data-component="api"
     data-endpoint="/api/legacy"
     data-data-path="result.items"
     data-meta-path="result.pagination">
</div>

10. ✅ ใช้ events เพื่อทำ custom actions

// ✅ ดี: ใช้ events เพื่อทำ custom actions
element.addEventListener('api:loaded', (e) => {
  // อัปเดต UI elements อื่น ๆ
  updateStats(e.detail.data);

  // ติดตามการโหลดข้อมูล
  analytics.track('data_loaded', {
    endpoint: e.detail.instance.options.endpoint
  });
});

สรุป

เมื่อไหร่ควรใช้ ApiComponent

กรณีการใช้งาน ใช้ ApiComponent?
แสดงรายการข้อมูลจาก API ✅ ใช่ - HTML-based
ต้องการ pagination ✅ ใช่ - built-in support
ต้องการ infinite scroll ✅ ใช่ - loadMore()
Auto-refresh ข้อมูล ✅ ใช่ - polling support
ต้องการควบคุมละเอียด ❌ ไม่ - ใช้ ApiService
Real-time ซับซ้อน ❌ ไม่ - ใช้ WebSocket
Form submission ✅ ใช้ได้ - submit()
Simple static content ❌ ไม่จำเป็น

ฟีเจอร์หลัก

คุณสมบัติ พร้อมใช้งาน หมายเหตุ
HTML Attributes ✅ ไม่ต้องใช้ JavaScript
Template System ✅ รวมกับ TemplateManager
Auto-Loading ✅ กำหนดค่าได้
Caching ✅ เก็บในหน่วยความจำ
Pagination ✅ ฝั่งเซิร์ฟเวอร์
Polling ✅ รีเฟรชอัตโนมัติ
Events ✅ อีเวนต์ที่กำหนดเอง
Retry ✅ ลองใหม่อัตโนมัติเมื่อเกิดข้อผิดพลาด
Loading States ✅ สถานะ: กำลังโหลด, ข้อผิดพลาด, ไม่มีข้อมูล