Now.js Framework Documentation

Now.js Framework Documentation

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

TH 30 Nov 2025 11:56

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

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

📋 สารบัญ

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

ภาพรวม

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-source-key="data.avatar.0.url" data-source-attr="src" class="avatar">
  <span data-source-key="data.name">Loading...</span>
  <span data-source-key="data.email">Loading...</span>
</div>

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

  • data-source-key - path ไปยังข้อมูล (dot notation)
  • data-source-attr - attribute ที่จะอัพเดท (textContent, src, href, innerHTML, etc.)
  • data-source-template - template แบบง่าย เช่น "Hello, {data.name}!"

เหมาะกับ:

  • แสดงข้อมูลผู้ใช้ที่ 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>
      <li data-for="user of data">
        <img data-bind="user.avatar" data-attr="src">
        <span data-bind="user.name"></span>
        <span data-if="user.status === 'active'">Active</span>
      </li>
    </ul>
  </template>
</div>

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

  • data-for - loop ข้อมูล
  • data-if - เงื่อนไขการแสดงผล
  • data-bind - ผูกข้อมูล
  • และ 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 ใหม่)

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

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

<div data-component="api"
     data-endpoint="/api/users"
     data-method="GET">
  <template>
    <div data-for="item in data">
      <h3>{item.name}</h3>
      <p>{item.email}</p>
    </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 ระยะเวลาแคช (มิลลิวินาที)

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

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

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

สามารถใช้ 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>

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 สถานะ: กำลังโหลด, ข้อผิดพลาด, ไม่มีข้อมูล