Now.js Framework Documentation

Now.js Framework Documentation

TableManager

TH 30 Dec 2025 05:05

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="create_date 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="create_date" data-sort="create_date" 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-page-size จำนวนรายการต่อหน้า
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 เปิด/ปิดการลากแถว (ค่าเริ่มต้น: true เมื่อเปิดแก้ไขแถว)
data-sortable-rows เปิดใช้ลากวางเรียงลำดับ
data-url-params บันทึกสถานะใน URL

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 รูปแบบการแสดงผล
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

Data Binding (data-attr)

ใช้ data-attr เพื่อ bind ข้อมูลจาก nested object ใน API response:

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

ตัวอย่าง:

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

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

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

ข้อดี

ยืดหยุ่น - 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
  • สามารถใช้แค่ตัวใดตัวหนึ่ง หรือทั้งคู่ก็ได้

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 จะแสดงเมื่อ allowRowModification เป็น true ค่าเริ่มต้นเปิดอยู่ เว้นแต่กำหนด rowSortable เป็น false หรือใช้ data-row-sortable="false"

TableManager.disableRowSort(tableId)

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

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

  • เปิดใช้: ใส่ data-editable-rows="true" (alias ของ allowRowModification: true) เพื่อแสดงปุ่มเพิ่ม/ลบและ drag handle เมื่อ rowSortable ไม่ได้ตั้งเป็น false.
  • ปิดเป็นรายตาราง: ใช้ data-row-sortable="false" หรือเรียก TableManager.disableRowSort(tableId).
  • UX ล่าสุด: ghost ขนาดเล็กตามเคอร์เซอร์, placeholder เป็นแถวเส้นประ, การลากล็อกแนวตั้งเพื่อให้ reorder แม่นยำ
  • อีเวนต์: table:row-sorted{tableId, item, oldIndex, newIndex}
<table data-table="rows"
       data-source="/api/rows"
       data-editable-rows="true"
       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. Row Data ต้องมี id Field (สำคัญมาก!)

สำหรับตารางที่มี data-editable-rows="true" ข้อมูลแต่ละแถวต้องมี field id เสมอ มิฉะนั้นการเพิ่ม/ลบแถวจะไม่ทำงาน

// ❌ ไม่มี id - ปุ่ม +/- จะทำงานผิดพลาด
{
  "userstatus": [
    {"status": 0, "color": "#000", "topic": "Member"},
    {"status": 1, "color": "#333", "topic": "Admin"}
  ]
}

// ✅ มี id - ทำงานถูกต้อง
{
  "userstatus": [
    {"id": 0, "status": 0, "color": "#000", "topic": "Member"},
    {"id": 1, "status": 1, "color": "#333", "topic": "Admin"}
  ]
}

เหตุผล: TableManager ใช้ id เพื่อ:

  • ติดตาม row ใน DOM (tr[data-id="${item.id}"])
  • ค้นหา row ที่จะ copy/delete
  • อัปเดตข้อมูลแถวที่ถูกต้อง

วิธีแก้ใน Backend:

// ใช้ field ที่มีอยู่แล้วเป็น id (ถ้ามันเป็น unique)
foreach ($data as $key => $value) {
    $result[] = [
        'id' => $key,  // ← เพิ่มบรรทัดนี้
        'status' => $key,
        'name' => $value
    ];
}

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

Element ภายในเซลล์ใช้รูปแบบ ID แบบ tableId_field_rowId เพื่อให้ API สามารถ highlight ฟิลด์ที่มี error ได้:

// ✅ Element ID (predictable)
"userStatus_topic_2"  // tableId_field_rowId

// ✅ 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>

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