Now.js Framework Documentation

Now.js Framework Documentation

TableManager

TH 09 May 2026 02:50

TableManager

ภาพรวม

TableManager คือระบบจัดการ data tables ใน Now.js Framework รองรับ sorting, filtering, pagination และ API integration

ใช้เมื่อ:

  • ต้องการ data tables
  • ต้องการ sorting และ filtering
  • ต้องการ pagination
  • ต้องการ CRUD operations
  • ต้องการ row selection

ทำไมต้องใช้:

  • ✅ Auto-load from API
  • ✅ Sorting (client/server)
  • ✅ Filtering
  • ✅ Pagination
  • ✅ Row selection (checkbox)
  • ✅ Inline editing
  • ✅ URL state persistence
  • ✅ Sortable rows (drag & drop)
  • ✅ UX ลากแถว: ghost เป็นไอคอนติดเมาส์, placeholder เส้นประ, ล็อกการลากแนวตั้ง

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

HTML Declarative

<table data-component="table"
       data-source="/api/users"
       data-page-size="10">
  <thead>
    <tr>
      <th data-field="id">ID</th>
      <th data-field="name" data-sortable>Name</th>
      <th data-field="email" data-sortable>Email</th>
      <th data-field="created_at" data-sortable>Created</th>
    </tr>
  </thead>
  <tbody>
    <!-- Data will be rendered here -->
  </tbody>
</table>

With Filters

<div data-table-toolbar="users-table">
  <input data-filter="name" placeholder="Search name...">
  <select data-filter="status">
    <option value="">All</option>
    <option value="active">Active</option>
    <option value="inactive">Inactive</option>
  </select>
</div>

<table id="users-table" data-component="table" data-source="/api/users">
  ...
</table>

ฟอร์มกรองภายนอก (data-table-filter)

คุณสามารถสร้างฟอร์มกรองแยกจากตาราง แล้วเชื่อมด้วย data-table-filter="tableId" เพื่อออกแบบ UI ได้อิสระ (รองรับตัวกรองช่วง เช่น วันที่/ราคา) โดยตารางยังจัดการโหลด/ซิงก์สถานะ, URL params และ options จาก API ให้อัตโนมัติ

<!-- External Filter Form -->
<form data-table-filter="usage" class="table_nav">
  <div>
    <label>Show</label>
    <select name="pageSize">
      <option value="10">10 entries</option>
      <option value="25">25 entries</option>
      <option value="50">50 entries</option>
    </select>
  </div>

  <!-- ตัวกรองช่วงวันที่ -->
  <input type="date" name="from">
  <input type="date" name="to">

  <!-- ค้นหา -->
  <input type="search" name="search" placeholder="Search...">

  <button type="submit">Apply</button>
</form>

<!-- Table -->
<table data-table="usage"
       data-source="api/index/usage"
       data-default-sort="created_at desc"
       data-page-size="25"
       data-search-columns="name,topic,reason">
  <thead>
    <tr>
      <th data-field="id" data-sort="id">ID</th>
      <th data-field="created_at" data-sort="created_at" data-format="date">Created</th>
      <th data-field="topic" data-sort="topic">Topic</th>
      <th data-field="name" data-sort="name">Name</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

หลักการทำงาน

  • data-table-filter="tableId" ต้องตรงกับ data-table ของตาราง
  • กด submit ฟอร์มจะรีโหลดข้อมูลตาราง (ไม่มี page reload)
  • ตัวกรองช่วงใช้คู่ชื่อ เช่น from/to, price_min/price_max และส่งค่า non-empty ทั้งหมดไปยัง API
  • URL ซิงก์อัตโนมัติและกู้ค่าฟอร์มเมื่อโหลดหน้า
  • ถ้า API ส่ง options กลับมา (เช่น { status: [{value:'active', text:'Active'}] }) จะเติมลง select ที่ name ตรงกันอัตโนมัติ

ตัวอย่างคำขอ API

GET /api/index/usage?page=1&pageSize=25&from=2024-01-01&to=2024-12-31&search=test

หมายเหตุ

  • ฟิลเตอร์ใน <th> ยังใช้งานได้; เมื่อมี external form ระบบยังอ่าน metadata จาก <th> (ค่า default / options) แต่ใช้ UI จากฟอร์มภายนอกแทน
  • ตั้งชื่อฟิลด์ให้ตรงกับพารามิเตอร์ที่ API รองรับ (เช่น from, to, search)

Data Attributes

Attribute Description
data-component="table" Initialize table
data-table Table identifier (required for all tables)
data-source API endpoint
data-cache เปิด cache สำหรับคำขอ GET จาก data-source
data-cache-time อายุ cache ของ data-source หน่วยมิลลิวินาที
data-load-target CSS selector ของ container ที่จะถูก bind ใหม่จาก payload ของตารางหลังโหลดข้อมูลสำเร็จ
data-page-size จำนวนรายการต่อหน้า
data-show-footer เปิดการประมวลผล footer
data-footer-aggregates mapping แบบ legacy สำหรับสร้าง aggregate อัตโนมัติใน tfoot ที่ว่าง

เมื่อมีการแก้ไขข้อมูลผ่าน TableManager แล้วต้อง reload แหล่งข้อมูลระยะไกล การ reload รอบนั้นจะข้าม cache อัตโนมัติ เพื่อให้ข้อมูลหลัง action สดเสมอ
| data-page-sizes | ตัวเลือกจำนวนรายการต่อหน้า |
| data-default-sort | การเรียงลำดับเริ่มต้น (เช่น "name asc" หรือ "created_at desc,name asc") |
| data-show-checkbox | แสดง checkbox เลือกแถว |
| data-show-caption | แสดง/ซ่อน caption (ค่าเริ่มต้น: true, แต่ถูกปิดอัตโนมัติเมื่อใช้ data-editable-rows="true") |
| data-editable-rows | เปิดใช้การแก้ไขแถว (เพิ่ม/ลบ) |
| data-row-sortable | เปิดการลากเรียงลำดับแถว (ทำงานอิสระจาก data-editable-rows) |
| data-sortable-rows | เปิดใช้ลากวางเรียงลำดับ |
| data-url-params | บันทึกสถานะใน URL |

Declarative Load Target Binding

ใช้ data-load-target เมื่อต้องการให้ response ของตารางไป hydrate container อื่นต่อโดยไม่ต้องเขียน JavaScript เฉพาะหน้าเอง หลังจาก TableManager normalize payload แล้ว ระบบจะส่ง payload เดียวกันเข้า target ผ่าน TemplateManager.processTemplate() และ processDataOnLoad()

เหมาะกับกรณี header, summary card, ตัวนับผลลัพธ์, empty state หรือ side panel ที่ควรเปลี่ยนตาม response ชุดเดียวกับตาราง

state ที่ target ได้รับ

target จะได้รับ normalized payload บน context.state เช่น:

{
  data: [...],
  meta: {
    page: 1,
    pageSize: 25,
    total: 120,
    totalPages: 5
  },
  filters: {},
  options: {},
  columns: [],
  params: {type: 'customer'},
  tableId: 'customers',
  tableSource: 'api/customer/customers',
  isServerSide: true,
  hasData: true,
  empty: false,
  raw: {/* payload ต้นฉบับ */}
}

array หลักของแถวข้อมูลจะถูกเปิดให้ใช้บน context.data ด้วย

ตัวอย่าง: ให้ header เปลี่ยนตาม payload ของตาราง

<header id="customer-table-header">
  <div>
    <h1 data-text="params.type === 'partner' ? '{LNG_Partner}' : '{LNG_Customer}'">Customer</h1>
    <p data-text="params.type === 'partner' ? 'Manage partner information' : 'Manage customer information'">
      Manage customer information
    </p>
  </div>
  <a class="btn btn-primary icon-new"
     href="/customer"
     data-attr="href:'/customer?type=' + (params.type || 'customer')"
     data-text="params.type === 'partner' ? '{LNG_Add} {LNG_Partner}' : '{LNG_Add} {LNG_Customer}'">
    {LNG_Add} {LNG_Customer}
  </a>
</header>

<table data-table="customers"
       data-source="api/customer/customers"
       data-load-target="#customer-table-header"
       data-page-size="25">
  <thead>...</thead>
  <tbody></tbody>
</table>

หมายเหตุ

  • data-load-target ทำงานหลัง normalize ข้อมูลแล้ว ดังนั้น target ใช้ data, meta, filters, options และค่าปัจจุบันใน table params ได้ทันที
  • target ใช้ directive ปกติของ TemplateManager ได้ เช่น data-text, data-attr, data-class, data-if, data-for และ data-on-load
  • target จะถูก bind ใหม่ทุกครั้งที่ตาราง reload จาก API หรือ state ผ่าน TableManager
  • ถ้า API รู้ label หรือ summary ที่ถูกต้องอยู่แล้ว ควรส่งมาใช้ตรงๆ จาก server; ใช้ params.* สำหรับค่าที่มาจาก state ของตารางหรือ URL params เป็นหลัก

ซ่อน/แสดงคอลัมน์

คุณสามารถซ่อนคอลัมน์ได้ 2 วิธี:

1. ใช้ data-visible="false" หรือ data-hidden="true" ใน HTML

<table data-table="users" data-source="/api/users">
  <thead>
    <tr>
      <th data-field="id" data-visible="false">ID</th>
      <th data-field="internal_code" data-hidden="true">Internal Code</th>
      <th data-field="name">Name</th>
      <th data-field="email">Email</th>
    </tr>
  </thead>
</table>

data-hidden="true" เป็น alias ของ data-visible="false" ใช้แบบไหนใน HTML declarative ก็ได้ และทั้งคู่จะซ่อนทั้ง header กับ body cells ของคอลัมน์นั้น

2. ใช้ JavaScript แบบ dynamic

// ซ่อนคอลัมน์
TableManager.toggleColumnVisibility('users', 'email', false);

// แสดงคอลัมน์
TableManager.toggleColumnVisibility('users', 'email', true);

หมายเหตุ: คอลัมน์ที่ซ่อนจะไม่แสดงทั้ง <th> และ <td> แต่ยังคงข้อมูลไว้ใน memory

Built-in Cell Formatting

ใช้ data-format บน <th> ของคอลัมน์ เมื่อต้องการให้ TableManager จัดรูปแบบข้อความที่แสดงผลโดยไม่ต้องเขียน custom formatter เอง

Attribute ใช้กับ คำอธิบาย
data-format HTML headers และ dynamic columns รูปแบบ built-in เช่น text, number, currency, percent, lookup, date, time, datetime, bytes, duration, boolean, humanize, truncate
data-decimals number, currency, percent จำนวนตำแหน่งทศนิยมแบบ fix
data-locale number, currency, percent locale ที่ส่งต่อเข้า Intl.NumberFormat
data-currency currency currency code/label ที่ส่งเข้า shared currency formatter
data-pattern date, time, datetime รูปแบบการแสดงวันที่/เวลา ที่ส่งให้ Utils.date.format()
data-options lookup fallback options สำหรับกรณีที่ไม่มี options จาก API/filter

คอลัมน์แบบ currency ตอนนี้ใช้ formatter contract เดียวกับ template bindings แล้ว ดังนั้นพฤติกรรมเรื่องทศนิยมจะสอดคล้องกันระหว่าง data-text="amount | currency:THB:th-TH:4" และคอลัมน์ของตาราง

ตัวอย่างแบบ Static HTML

<table data-table="stockMovements" data-source="api/inventory/stockmovements">
  <thead>
    <tr>
      <th data-field="quantity" data-format="currency" data-decimals="4">Quantity</th>
      <th data-field="unit_cost" data-format="currency" data-currency="THB" data-locale="th-TH" data-decimals="2">Unit Cost</th>
      <th data-field="occurred_at" data-format="date" data-pattern="D MMM YYYY">Date</th>
    </tr>
  </thead>
</table>

ตัวอย่างแบบ Dynamic Columns

{
  "columns": [
    {
      "field": "quantity",
      "label": "Quantity",
      "format": "currency",
      "decimals": 4
    },
    {
      "field": "total_cost",
      "label": "Total",
      "format": "currency",
      "currency": "THB",
      "locale": "th-TH",
      "decimals": 2
    }
  ],
  "data": []
}

หมายเหตุ:

  • lookup ยัง resolve options จาก API response, filter options หรือ data-options ตามลำดับเดิม
  • built-in formats ที่ไม่ใช่ lookup จะอ่าน attributes ของคอลัมน์โดยตรง จึงประกาศ decimals, currency, locale, pattern แยกต่อคอลัมน์ได้
  • ใช้ formatter เมื่อเซลล์ต้องการ logic ที่อิงทั้งแถว, custom HTML หรือ pipeline การ render ที่กำหนดเอง

Element ในเซลล์ (ElementManager)

เรนเดอร์ form control ภายในเซลล์ด้วย data-cell-element (หรือใช้ data-type เป็น shorthand) โดย TableManager จะส่ง attributes เข้า ElementManager และเก็บ data-element-id ไว้ที่เซลล์

Attribute ที่รองรับ: placeholder, class, read-only, disabled, required, wrapper, options (select), multiple, allow-empty, rows/cols (textarea), min/max/step (number), datalist, pattern, min-length, max-length (ใช้เมื่อค่ามากกว่า 0 เท่านั้น)

ตัวอย่าง

<table data-table="element-table" data-source="/api/users" data-editable-rows="true">
  <thead>
    <tr>
      <th data-field="name" data-cell-element="text" data-placeholder="Full name"></th>
      <th data-field="email" data-type="email" data-placeholder="user@example.com"></th>
      <th data-field="status" data-cell-element="select" data-options='{"1":"Active","0":"Inactive"}' data-allow-empty="true"></th>
      <th data-field="note" data-cell-element="textarea" data-rows="2" data-max-length="200" data-placeholder="Internal note"></th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

Dynamic Columns

สร้าง table headers อัตโนมัติจาก API response โดยไม่ต้องกำหนด <thead> ใน HTML

Basic Usage

<table data-table="langEditor"
       data-attr="data:translate"
       data-dynamic-columns="true"
       data-editable-rows="true">
  <tbody></tbody>
</table>

API Response Format

{
  "translate": {
    "columns": [
      {
        "field": "key",
        "label": "Key",
        "cellElement": "text",
        "placeholder": "Enter key",
        "i18n": true
      },
      {
        "field": "th",
        "label": "Thai",
        "cellElement": "textarea",
        "rows": 3,
        "i18n": true
      }
    ],
    "data": [
      {"key": "Welcome", "th": "ยินดีต้อนรับ"},
      {"key": "Goodbye", "th": "ลาก่อน"}
    ]
  }
}

Column Metadata Attributes

สามารถกำหนด attributes ต่อไปนี้ใน column definition:

Attribute Type Description
field string Required - ชื่อ field
label string Required - หัวข้อคอลัมน์
cellElement string ประเภท element (text, textarea, select, color, etc)
type string ชนิดข้อมูล (text, number, date)
placeholder string Placeholder text
class string CSS class สำหรับ &lt;th&gt;
cellClass string CSS class สำหรับ &lt;td&gt;
i18n boolean เปิดใช้งานการแปลภาษา
sort boolean เปิดใช้งานการเรียงลำดับ
filter boolean เปิดใช้งานตัวกรอง
formatter string ชื่อ formatter function
format string รูปแบบการแสดงผล
locale string locale สำหรับ number, currency และ percent
currency string currency code/label สำหรับ format: 'currency'
decimals number จำนวนตำแหน่งทศนิยมแบบ fix สำหรับ number, currency, percent
options object ตัวเลือกสำหรับ select
rows/cols number สำหรับ textarea
min/max/step number สำหรับ number input
minLength/maxLength number ความยาวข้อความ
pattern string RegEx pattern
readOnly boolean Read-only mode
disabled boolean Disabled state
required boolean Required field
autoNumber boolean สร้างเลขลำดับแถวแบบอัตโนมัติให้คอลัมน์นี้
visible boolean แสดง/ซ่อนคอลัมน์ (false = ซ่อน)
hidden boolean alias ของ visible: false ใช้เพื่อความเข้ากันได้กับ data-hidden="true"

Data Binding (data-attr)

ใช้ data-attr เพื่อ bind ข้อมูลจาก nested object ใน API response หรือจาก state ของ form/page:

รูปแบบ: data-attr="data:path.to.data"

ตัวอย่าง:

<!-- Simple binding -->
<table data-table="users" data-attr="data:users"></table>
<!-- API: {"users": [...]} -->

<!-- Nested binding -->
<table data-table="langEditor" data-attr="data:translate"></table>
<!-- API: {"translate": {"columns": [...], "data": [...]}} -->

<!-- Deep nested -->
<table data-table="monthlySales" data-attr="data:report.sales.monthly"></table>
<!-- API: {"report": {"sales": {"monthly": [...]}}} -->

เมื่อ bind ผ่าน data-attr นั้น TableManager รองรับได้ทั้ง:

  • array ของ rows ตรงๆ โดยใช้ <thead> ที่ประกาศไว้ใน HTML
  • object มาตรฐานแบบ {data: [...], columns: [...], meta: {...}} สำหรับกรณี dynamic columns และ metadata

รูปแบบนี้เหมาะกับตารางย่อยแบบ read-only ภายในฟอร์ม เช่น payment history หรือ status log เมื่อ parent form โหลด payload มาอยู่แล้ว และต้องการให้รูปแบบการ render เหมือนตารางอื่นในระบบ admin

เลขลำดับแถวอัตโนมัติ

ใช้ data-auto-number="true" บน <th> เมื่อต้องการให้ TableManager แสดงคอลัมน์เลขลำดับแบบ reusable โดยไม่ต้องให้ backend ส่ง field อย่าง row_no, payment_no หรือชื่อใกล้เคียงมาเอง

<table data-table="payments" data-attr="data:payments">
  <thead>
    <tr>
      <th data-auto-number="true" class="center" data-cell-class="center" data-format="number">#</th>
      <th data-field="payment_method">Method</th>
      <th data-field="amount" data-format="number" class="right" data-cell-class="right">Amount</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

พฤติกรรม:

  • ใช้เลขลำดับแบบเริ่มจาก 1
  • ถ้ามี pagination จะนับต่อข้ามหน้าให้อัตโนมัติ
  • ถ้าไม่มี pagination จะนับตามลำดับ row ที่ render อยู่ในหน้านั้น

ข้อดี

ยืดหยุ่น - Columns ปรับตามข้อมูลจาก API
Clean HTML - ไม่ต้องเขียน <th> ซ้ำๆ
Dynamic - เพิ่ม/ลด columns ได้โดยไม่แก้ frontend
Type-safe - กำหนด element types ได้จาก backend

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

// PHP API Response
return [
    'translate' => [
        'columns' => [
            ['field' => 'key', 'label' => 'Key', 'cellElement' => 'text'],
            ['field' => 'th', 'label' => 'ไทย', 'cellElement' => 'textarea'],
            ['field' => 'en', 'label' => 'English', 'cellElement' => 'textarea']
        ],
        'data' => $translations
    ]
];

TableManager จะสร้าง header อัตโนมัติ:

<thead>
  <tr>
    <th data-field="key" data-cell-element="text">Key</th>
    <th data-field="th" data-cell-element="textarea">ไทย</th>
    <th data-field="en" data-cell-element="textarea">English</th>
  </tr>
</thead>

---

### การกำหนด CSS Class แยกระหว่าง Header และ Body Cells

TableManager รองรับการกำหนด CSS class แยกกันสำหรับ `<th>` (header) และ `<td>` (body cells) โดยเด็ดขาด ไม่มี fallback

#### Static HTML Table

สำหรับ table ที่กำหนด `<thead>` ใน HTML:

```html
<table data-table="myTable" data-source="/api/data">
  <thead>
    <tr>
      <!-- ใช้ class ปกติสำหรับ <th> -->
      <th data-field="id" class="header-mono">ID</th>

      <!-- ใช้ data-cell-class สำหรับ <td> -->
      <th data-field="name" class="header-bold" data-cell-class="text-bold">Name</th>

      <!-- สามารถใช้แค่ data-cell-class อย่างเดียว -->
      <th data-field="status" data-cell-class="badge center">Status</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

ผลลัพธ์:

<!-- Header -->
<th class="header-mono">ID</th>
<th class="header-bold">Name</th>
<th>Status</th>

<!-- Body -->
<td>001</td>  <!-- ไม่มี class -->
<td class="text-bold">John Doe</td>
<td class="badge center">Active</td>

Dynamic Columns (API Response)

สำหรับ table ที่ใช้ data-dynamic-columns="true":

{
  "columns": [
    {
      "field": "id",
      "label": "ID",
      "class": "header-mono",
      "cellClass": "mono small"
    },
    {
      "field": "name",
      "label": "Name",
      "class": "header-bold",
      "cellClass": "text-bold"
    },
    {
      "field": "status",
      "label": "Status",
      "cellClass": "badge center"
    }
  ],
  "data": [...]
}

ผลลัพธ์:

<!-- Header -->
<th class="header-mono">ID</th>
<th class="header-bold">Name</th>
<th>Status</th>

<!-- Body -->
<td class="mono small">001</td>
<td class="text-bold">John Doe</td>
<td class="badge center">Active</td>

สรุป

ประเภท TH Class TD Class
Static HTML class="..." (attribute ปกติ) data-cell-class="..."
Dynamic API "class": "..." (ใน JSON) "cellClass": "..." (ใน JSON)

หมายเหตุสำคัญ:

  • ⚠️ ไม่มี data-class attribute
  • แยกกันเด็ดขาด ไม่มี fallback ระหว่าง th และ td
  • สามารถใช้แค่ตัวใดตัวหนึ่ง หรือทั้งคู่ก็ได้

แนวทางที่แนะนำคือเขียน tfoot แบบ explicit ไปเลย แล้วให้แต่ละ footer cell บอกหน้าที่ของตัวเอง วิธีนี้อ่านง่ายกว่า footer แบบอิงลำดับคอลัมน์ และเหมาะเมื่อคุณต้องการข้อความคงที่อย่าง Total, ตำแหน่งที่กำหนดเอง, colspan หรือ class เฉพาะใน footer

<table data-table="orders"
       data-source="/api/orders"
       data-show-footer="true">
  <thead>
    <tr>
      <th data-field="id">ID</th>
      <th data-field="customer">Customer</th>
      <th data-field="status">Status</th>
      <th data-field="amount" data-format="number">Amount</th>
      <th data-field="created_at" data-format="date">Created</th>
    </tr>
  </thead>
  <tbody></tbody>
  <tfoot>
    <tr>
      <th data-field="id" data-aggregate="count" data-format="number" data-cell-class="center"></th>
      <th colspan="2" class="right">Total</th>
      <th data-field="amount" data-aggregate="sum" data-format="number" data-cell-class="right"></th>
      <th data-field="created_at" data-aggregate="count" data-format="number" data-cell-class="center"></th>
    </tr>
  </tfoot>
</table>
  • tfoot ใช้ได้ทั้ง <th> และ <td>
  • ข้อความคงที่ใน cell จะถูกเก็บไว้ เช่น Total จะไม่ถูกคำนวณทับ
  • ถ้า footer cell ไม่ระบุ data-field ระบบจะ fallback ไปใช้คอลัมน์ปลายทางจาก thead ตามตำแหน่ง
  • sum, avg, min, max จะทำงานเฉพาะเมื่อข้อมูลต้นทางเป็นตัวเลขทั้งหมดจริง ๆ
  • ถ้าข้อมูลเป็น text, date หรือปนกันจนไม่น่าเชื่อถือ ระบบจะข้าม cell นั้นแทนการเดาค่า
  • count จะนับค่าที่ไม่ว่าง
  • data-cell-class ใช้กับ footer ได้ด้วย จึงจัดแนว footer ให้ต่างจาก header ได้
  • colspan และ rowspan ใน tfoot ได้รับการรองรับ
Attribute Description
data-field field ต้นทางของ footer cell นี้
data-aggregate ประเภท aggregate: sum, avg, count, min, max หรือ custom
data-format รูปแบบการแสดงผลของค่า aggregate
data-formatter ชื่อ formatter function แบบกำหนดเอง
data-cell-class CSS classes ที่จะใช้กับ footer cell หลัง render
data-class ไม่รองรับ ให้ใช้ HTML class ปกติแทน
data-prefix ข้อความหน้าค่าที่ format แล้ว
data-suffix ข้อความท้ายค่าที่ format แล้ว
data-align การจัดแนวข้อความเฉพาะ footer
data-custom ชื่อ custom aggregate function เมื่อใช้ data-aggregate="custom"

Custom formatter และ custom aggregate

footer cell ใช้ formatter แบบเดียวกับ body cell ได้:

<th data-field="amount"
    data-aggregate="sum"
    data-formatter="renderFooterMoney"></th>

<th data-field="amount"
    data-aggregate="custom"
    data-custom="calculateGrandTotal"></th>
function renderFooterMoney(cell, value, rows, attrs) {
  cell.textContent = `USD ${Number(value).toLocaleString()}`;
}

function calculateGrandTotal(rows, cell, table, attrs) {
  return rows.reduce((sum, row) => sum + Number(row.amount || 0), 0);
}

data-formatter จะได้รับ (cell, value, rows, attrs) โดยในโหมด footer นั้น attrs จะมี isFooter, field, aggregateType, tableId, column, table และ aggregate

Legacy auto mode

ถ้าไม่ต้องการข้อความหรือ layout แบบ explicit ยังสามารถใช้รูปแบบเก่าได้:

<table data-table="orders"
       data-source="/api/orders"
       data-show-footer="true"
       data-footer-aggregates='{"amount":"sum","id":"count"}'>
  <thead>...</thead>
  <tbody></tbody>
  <tfoot></tfoot>
</table>

ในโหมดนี้ TableManager จะเติมค่าให้อัตโนมัติเฉพาะ footer cell ที่ว่างและกินพื้นที่เพียง 1 คอลัมน์เท่านั้น ถ้า cell มีข้อความอยู่แล้ว หรือถูกครอบด้วย colspan ระบบจะไม่แตะ

Column Attributes

Attribute Description
data-field Field name
data-sortable Enable sorting
data-type Data type (text, number, date)
data-format Display format

การตั้งค่า

TableManager.init({
  debug: false,
  urlParams: true,
  pageSizes: [10, 25, 50, 100],
  showCaption: true,
  showCheckbox: false,
  allowRowModification: false,
  rowSortable: true // แสดง drag handle เมื่อ allowRowModification เป็น true
});

API อ้างอิง

TableManager.initTable(element, options)

Initialize table

Parameter Type Description
element HTMLTableElement Table element
options object Configuration

TableManager.setData(tableId, data)

Set table data

Parameter Type Description
tableId string Table ID
data array Data array
TableManager.setData('users-table', [
  { id: 1, name: 'John', email: 'john@example.com' },
  { id: 2, name: 'Jane', email: 'jane@example.com' }
]);

TableManager.loadTableData(tableId)

Reload data from source

await TableManager.loadTableData('users-table');

TableManager.renderTable(tableId)

Re-render table

TableManager.renderTable('users-table');

TableManager.setFilters(tableId, filters)

Set filter values

TableManager.setFilters('users-table', {
  name: 'John',
  status: 'active'
});

TableManager.getSelectedRows(tableId)

Get selected row data

Returns: Array

const selected = TableManager.getSelectedRows('users-table');
console.log('Selected:', selected.length);

TableManager.clearSelection(tableId)

Clear row selection

TableManager.enableRowSort(tableId)

เปิดการลากวางเรียงแถว Drag handle จะแสดงเมื่อ data-row-sortable="true" ถูกกำหนด (ไม่จำเป็นต้องเปิด data-editable-rows)

TableManager.disableRowSort(tableId)

ปิดการลากวางและซ่อน drag handle

TableManager.toggleColumnVisibility(tableId, fieldName, visible)

ซ่อน/แสดงคอลัมน์แบบ dynamic

พารามิเตอร์:

  • tableId (string) - รหัสตาราง
  • fieldName (string) - ชื่อฟิลด์ของคอลัมน์
  • visible (boolean) - true = แสดง, false = ซ่อน

ตัวอย่าง:

// ซ่อนคอลัมน์ email
TableManager.toggleColumnVisibility('users', 'email', false);

// แสดงคอลัมน์ email กลับมา
TableManager.toggleColumnVisibility('users', 'email', true);

การลากเรียงแถว (drag & drop)

  • เปิดใช้: ใส่ data-row-sortable="true" เพื่อแสดง drag handle (⋮⋮) และเปิดการลากเรียงลำดับแถว
  • ไม่จำเป็นต้องใช้ data-editable-rows: คุณสมบัติทั้งสองแยกทำงานกันอย่างอิสระ
    • data-editable-rows="true" = เปิดปุ่มเพิ่ม/ลบแถว
    • data-row-sortable="true" = เปิดลากเรียงลำดับแถว
  • ปิดเป็นรายตาราง: เรียก TableManager.disableRowSort(tableId)
  • UX ล่าสุด: ghost ขนาดเล็กตามเคอร์เซอร์, placeholder เป็นแถวเส้นประ, การลากล็อกแนวตั้งเพื่อให้ reorder แม่นยำ
  • อีเวนต์: table:row-sorted{tableId, item, oldIndex, newIndex}
<table data-table="rows"
       data-source="/api/rows"
       data-row-sortable="true">
  <thead>
    <tr>
      <th data-field="name">Name</th>
      <th data-field="position">Position</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

<div class="controls">
  <button onclick="TableManager.disableRowSort('rows')">ปิด drag</button>
  <button onclick="TableManager.enableRowSort('rows')">เปิด drag</button>
</div>

TableManager.exportData(tableId, format, options)

ส่งออกข้อมูลตารางเป็นไฟล์

Parameter Type Description
tableId string Table ID
format string รูปแบบไฟล์: 'csv', 'json', หรือ 'excel'
options object ตัวเลือกเพิ่มเติม
Options: Option Type Description
filename string ชื่อไฟล์ที่จะบันทึก

หมายเหตุ:

  • Export ใช้ข้อมูลที่ผ่านการกรอง/เรียงลำดับแล้ว (ตรงกับที่ผู้ใช้เห็น)
  • ค่าที่ export จะถูก format แล้ว (เช่น แสดง "Engineering" แทน "1")
  • Header ใช้ชื่อคอลัมน์จาก <th> แทน field name
// Export เป็น CSV
TableManager.exportData('users-table', 'csv', {
  filename: 'employees.csv'
});

// Export เป็น JSON
TableManager.exportData('users-table', 'json', {
  filename: 'employees.json'
});

Bulk Actions (Checkbox Selection)

ระบบจัดการ bulk actions สำหรับแถวที่ถูกเลือกผ่าน checkbox พร้อม dropdown เลือก action และปุ่ม submit

การใช้งาน

<table data-table="deals"
       data-source="api/crm/deals"
       data-show-checkbox="true"
       data-actions='{"delete":"Delete","activate":"Activate","stage|lead":"Lead","stage|won":"Won"}'
       data-action-url="api/crm/deals/action"
       data-action-button="Process|btn-success">
  <!-- table content -->
</table>

Attributes

Attribute Description
data-show-checkbox แสดง checkbox สำหรับเลือกแถว
data-actions JSON object ของ actions (key: action value, value: label)
data-action-url URL สำหรับส่ง action (POST)
data-action-button ข้อความปุ่ม หรือ "label\|className"

รูปแบบ Action Key

Simple Action:

{"delete": "Delete", "activate": "Activate"}

ส่ง: {action: "delete", ids: [...]}

Compound Action (ใช้ | separator):

{"stage|lead": "Lead", "stage|won": "Won", "stage|lost": "Lost"}

ส่ง: {action: "stage", stage: "lead", ids: [...]}

รูปแบบ action|value จะแยก key เป็น:

  • action = ส่วนแรก (ก่อน |)
  • ส่วนแรกเป็นชื่อ parameter เพิ่มเติม โดยมี value = ส่วนหลัง (หลัง |)

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

<table data-table="deals"
       data-source="api/crm/deals"
       data-show-checkbox="true"
       data-actions='{
         "stage|lead":"Lead",
         "stage|qualified":"Qualified",
         "stage|proposal":"Proposal",
         "stage|negotiation":"Negotiation",
         "stage|won":"Won",
         "stage|lost":"Lost",
         "delete":"Delete"
       }'
       data-action-url="api/crm/deals/action"
       data-action-button="Process|btn-success">
  <thead>
    <tr>
      <th data-field="title">Title</th>
      <th data-field="stage">Stage</th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

Backend Handler (PHP)

public function action()
{
    $action = $this->request->post('action');
    $ids = $this->request->post('ids');

    switch ($action) {
        case 'stage':
            $stage = $this->request->post('stage'); // "lead", "won", etc.
            $this->model->updateStage($ids, $stage);
            break;
        case 'delete':
            $this->model->deleteByIds($ids);
            break;
    }

    return ['success' => true];
}

Event

EventManager.on('table:actionComplete', (e) => {
  const {tableId, action, ids, response} = e.detail;
  console.log(`Action "${action}" completed for ${ids.length} rows`);
});

Filter Actions (Table-Level Actions)

ปุ่มและลิงก์ที่ส่ง filter parameters ไปยัง action URL - สำหรับ export, report generation, หรือ bulk operations

การใช้งาน

<table data-table="my-table"
       data-source="api/users"
       data-filter-actions='{
         "export": {
           "label": "Export Filtered",
           "url": "api/export",
           "type": "button",
           "className": "btn-primary"
         },
         "report": {
           "label": "View Report",
           "url": "/reports",
           "type": "link",
           "className": "btn-info",
           "target": "_blank"
         }
       }'>
  <!-- table content -->
</table>

ตัวเลือก Filter Actions

Property Type Default Description
label string key name ข้อความแสดงบน button/link
url string required URL สำหรับ action
type string "button" "button" (POST) หรือ "link" (GET redirect)
className string "" CSS classes เพิ่มเติม
target string "_self" สำหรับ link: "_blank" เปิด tab ใหม่
confirm string - ข้อความยืนยันก่อนทำ action

Button Behavior

เมื่อกดปุ่ม จะส่ง POST request ไปยัง URL พร้อมข้อมูล:

{
  "action": "export",
  "tableId": "my-table",
  "filters": {"status": "1", "department": "5"},
  "sort": {"name": "asc"}
}

เมื่อกดลิงก์ จะ redirect ไปยัง URL พร้อม filter params ใน query string:

/reports?status=1&department=5

Event

document.addEventListener('table:filterAction', (e) => {
  const {tableId, action, type, params, response} = e.detail;
  console.log(`Filter action: ${action} (${type})`);
  console.log('Filters:', params);
});

เหตุการณ์

Event เมื่อเกิด Detail
table:loaded Data loaded {tableId, data}
table:sorted Column sorted {tableId, field, direction}
table:filtered Filter applied {tableId, filters}
table:selected Row selection changed {tableId, selected}
table:row-sorted Row drag-drop {tableId, item, oldIndex, newIndex}
table:export หลัง export เสร็จ {tableId, format, success, count}
table:filterAction หลังกด filter action {tableId, action, type, params, response}
EventManager.on('table:loaded', (e) => {
  console.log(`Loaded ${e.detail.data.length} rows`);
});

Row Template

<table data-component="table" data-source="/api/users">
  <thead>
    <tr>
      <th data-field="name">Name</th>
      <th data-field="email">Email</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody>
    <template>
      <tr data-id="{{id}}">
        <td>{{name}}</td>
        <td>{{email}}</td>
        <td>
          <button data-action="edit" data-id="{{id}}">Edit</button>
          <button data-action="delete" data-id="{{id}}">Delete</button>
        </td>
      </tr>
    </template>
  </tbody>
</table>

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

Users Table

<div class="table-container">
  <div class="table-toolbar" data-table-toolbar="users">
    <input type="search" data-filter="search" placeholder="Search...">
    <select data-filter="role">
      <option value="">All Roles</option>
      <option value="admin">Admin</option>
      <option value="user">User</option>
    </select>
  </div>

  <table id="users"
         data-component="table"
         data-source="/api/users"
         data-page-size="10"
         data-show-checkbox="true">
    <thead>
      <tr>
        <th data-checkbox></th>
        <th data-field="name" data-sortable>Name</th>
        <th data-field="email" data-sortable>Email</th>
        <th data-field="role">Role</th>
        <th data-field="created_at" data-sortable data-type="date">Created</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <div class="table-footer" data-table-pagination="users"></div>
</div>

With Actions

// Handle row actions
document.getElementById('users').addEventListener('click', (e) => {
  const action = e.target.dataset.action;
  const id = e.target.dataset.id;

  if (action === 'edit') {
    openEditModal(id);
  } else if (action === 'delete') {
    confirmDelete(id);
  }
});

// Bulk delete
document.getElementById('bulk-delete').onclick = async () => {
  const selected = TableManager.getSelectedRows('users');

  if (confirm(`Delete ${selected.length} items?`)) {
    for (const row of selected) {
      await ApiService.delete(`/api/users/${row.id}`);
    }
    TableManager.loadTableData('users');
  }
};

Server-Side Pagination

<table data-component="table"
       data-source="/api/users"
       data-server-side="true"
       data-page-size="25">
  ...
</table>

API จะได้รับ parameters:

GET /api/users?page=1&pageSize=25&sort=name&order=asc&search=john

การจัดรูปแบบ CSS

/* Table container */
.table-container {
  background: white;
  border-radius: 8px;
  overflow: hidden;
}

/* Table */
[data-component="table"] {
  width: 100%;
  border-collapse: collapse;
}

[data-component="table"] th,
[data-component="table"] td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #e5e7eb;
}

/* Sortable header */
th[data-sortable] {
  cursor: pointer;
  user-select: none;
}

th[data-sortable]:hover {
  background: #f3f4f6;
}

th[data-sortable].asc::after { content: ' ↑'; }
th[data-sortable].desc::after { content: ' ↓'; }

/* Selected row */
tr.selected {
  background: rgba(59, 130, 246, 0.1);
}

ข้อควรระวัง

⚠️ 1. อย่าใช้ business id เป็น row identity ภายใน

สำหรับตารางที่มี data-editable-rows="true" ตอนนี้ TableManager จะสร้าง row key ภายในที่คงที่เอง เพื่อให้การเพิ่ม/ลบ/คัดลอกแถวไม่พึ่งค่าจากฟิลด์ id ที่ผู้ใช้กำลังแก้ไข

// ✅ ไม่มี business id ก็ยังเพิ่ม/ลบแถวได้
{
  "userstatus": [
    {"status": 0, "color": "#000", "topic": "Member"},
    {"status": 1, "color": "#333", "topic": "Admin"}
  ]
}

// ✅ มี business id ก็ได้ และแก้ค่า id ในตารางได้โดยไม่ทำให้ row เพี้ยน
{
  "userstatus": [
    {"id": 0, "status": 0, "color": "#000", "topic": "Member"},
    {"id": 1, "status": 1, "color": "#333", "topic": "Admin"}
  ]
}

เหตุผล: ถ้าใช้ค่าจากคอลัมน์ id ที่ผู้ใช้แก้ได้โดยตรงเป็นตัวชี้แถวใน DOM พอกด +/- หลังแก้ id แล้วอาจหาแถวผิดตัวได้ ปัจจุบัน TableManager จึงใช้ row key ภายในที่คงที่แทน

ข้อแนะนำ:

  • ถ้า backend ของคุณต้องมี field id ก็ส่งมาได้ตามปกติ แต่ให้มองว่าเป็น business data ไม่ใช่ DOM identity
  • ถ้า backend ไม่ต้องใช้ id สำหรับ schema ของข้อมูล ก็ไม่จำเป็นต้องเติม id แค่เพื่อให้ปุ่ม +/- ทำงาน

กรณี backend ต้องใช้ field id:

// ส่ง id ตาม business schema ได้ตามปกติ
foreach ($data as $key => $value) {
    $result[] = [
    'id' => $key,
        'status' => $key,
        'name' => $value
    ];
}

⚠️ 2. รูปแบบ Element ID สำหรับ Error Highlighting

Element ภายในเซลล์ใช้รูปแบบ ID แบบ tableId_field_rowKey เพื่อให้ API สามารถ highlight ฟิลด์ที่มี error ได้อย่างคงที่ แม้ผู้ใช้จะแก้ค่าคอลัมน์ id ในแถวเดียวกัน:

// ✅ Element ID (predictable)
"userStatus_topic_row_2"  // tableId_field_rowKey

// ✅ API Error Response
{
  "errors": {
    "userStatus_topic_2": "Name is required"  // ตรงกับ element ID
  }
}

การทำงาน:

  • FormManager จะอ่าน error keys และ highlight element ที่ id ตรงกัน
  • รูปแบบ: {tableId}_{fieldName}_{rowId}
  • ถ้า API ส่ง key ที่ตรงกับ element ID จะเห็น error highlight ที่ฟิลด์นั้น

⚠️ 3. Caption ปิดอัตโนมัติสำหรับ Editable Tables

ตารางที่มี data-editable-rows="true" จะปิด caption อัตโนมัติ:

<!-- ✅ Caption ปิดอัตโนมัติ ไม่ต้องระบุ data-show-caption -->
<table data-table="userStatus" data-editable-rows="true">

<!-- ✅ บังคับให้แสดง caption (ถ้าต้องการ) -->
<table data-table="userStatus" data-editable-rows="true" data-show-caption="true">

เหตุผล: Editable tables แสดงข้อมูลทั้งหมดโดยไม่มี pagination ดังนั้น caption ที่บอก "page 1 of 1" จึงไม่จำเป็น

หมายเหตุ: คำเตือนนี้ปรับใหม่เนื่องจาก caption ถูกปิดอัตโนมัติแล้ว ไม่ต้องตั้งค่าเอง

⚠️ 2. Caption กับตารางแบบแก้ไขได้

ตารางที่มี data-editable-rows="true" และไม่มี pagination ควรปิด caption:

<!-- ✅ ปิด caption เพราะไม่มี pagination -->
<table data-table="userStatus"
       data-editable-rows="true"
       data-show-caption="false">

เหตุผล: Caption แสดงข้อความแบบ "All 5 entries, displayed 1 to 5, page 1 of 1 pages" ซึ่งไม่เหมาะกับตารางแก้ไขได้ที่แสดงข้อมูลทั้งหมด

⚠️ 3. Field Names ต้องตรงกับ Data

<!-- ❌ Field ไม่ตรง -->
<th data-field="userName">Name</th>
<!-- data: { name: 'John' } -->

<!-- ✅ Field ตรง -->
<th data-field="name">Name</th>

⚠️ 2. Template ใน tbody

  • ⚠️ ช่องกรอกข้อความจะใช้ maxLength เมื่อค่ามากกว่า 0 เท่านั้น; ให้ตั้งเลขบวกหรือไม่ระบุเพื่อไม่จำกัด
<!-- ❌ ไม่มี template -->
<tbody>
  <tr><td>Static row</td></tr>
</tbody>

<!-- ✅ มี template -->
<tbody>
  <template>
    <tr><td>{{field}}</td></tr>
  </template>
</tbody>

เอกสารที่เกี่ยวข้อง