Now.js Framework Documentation

Now.js Framework Documentation

Sortable

TH 15 Dec 2025 08:52

Sortable

ภาพรวม

Sortable คือ component สำหรับ drag-and-drop sorting ใน Now.js Framework รองรับ lists, grids และ API integration

ใช้เมื่อ:

  • ต้องการ reorder items ด้วย drag-drop
  • ต้องการ Kanban boards
  • ต้องการ sortable tables
  • ต้องการ auto-save position

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

  • ✅ Drag and drop reordering
  • ✅ Cross-container moving
  • ✅ Keyboard support (accessibility)
  • ✅ Touch device support
  • ✅ Auto-save API integration
  • ✅ Ghost element preview
  • ✅ Placeholder visualization

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

HTML Declarative

<div data-component="sortable" data-sortable-draggable=".item">
  <div class="item" draggable="true">Item 1</div>
  <div class="item" draggable="true">Item 2</div>
  <div class="item" draggable="true">Item 3</div>
</div>

With Handle

<div data-component="sortable"
     data-sortable-draggable=".item"
     data-sortable-handle=".handle">
  <div class="item" draggable="true">
    <span class="handle">☰</span>
    <span>Item 1</span>
  </div>
  <div class="item" draggable="true">
    <span class="handle">☰</span>
    <span>Item 2</span>
  </div>
</div>

JavaScript API

const sortable = Sortable.create(container, {
  draggable: '.item',
  handle: '.handle',
  onEnd: (evt) => {
    console.log('Moved from', evt.oldIndex, 'to', evt.newIndex);
  }
});

Data Attributes

Attribute Description
data-sortable-draggable Selector for draggable items
data-sortable-handle Selector for drag handle
data-sortable-group Group name for cross-container
data-sortable-api API endpoint for auto-save
data-sortable-id-attr Attribute for item ID
data-sortable-update-field Field to update on move

Options

Sortable.create(element, {
  // Selector for draggable items
  draggable: '.item',

  // Selector for drag handle (optional)
  handle: '.drag-handle',

  // Group name for cross-container sorting
  group: 'shared',

  // Ghost element class
  ghostClass: 'sortable-ghost',

  // Placeholder class
  placeholderClass: 'sortable-placeholder',

  // Animation duration (ms)
  animation: 150,

  // Callbacks
  onStart: (evt) => {},
  onEnd: (evt) => {},
  onChange: (evt) => {}
});

API อ้างอิง

Sortable.create(element, options)

สร้าง sortable instance

Parameter Type Description
element HTMLElement Container element
options object Configuration options

Returns: Sortable - Instance

const sortable = Sortable.create(container, {
  draggable: '.card'
});

Sortable.getInstance(element)

รับ instance จาก element

Parameter Type Description
element HTMLElement Container element

Returns: Sortable|null

instance.enable()

เปิดใช้งาน sorting

instance.disable()

ปิดใช้งาน sorting

instance.destroy()

ทำลาย instance

เหตุการณ์

Event เมื่อเกิด Detail
sortable:start เริ่ม drag {item, oldIndex}
sortable:end วาง item {item, oldIndex, newIndex, oldContainer, newContainer}
sortable:change ลำดับเปลี่ยน {items}
element.addEventListener('sortable:end', (e) => {
  console.log(`Moved from ${e.detail.oldIndex} to ${e.detail.newIndex}`);
});

Auto-Save API

บันทึกตำแหน่งอัตโนมัติเมื่อ drop:

<div data-component="sortable"
     data-sortable-draggable=".task"
     data-sortable-api="/api/tasks/{id}/position"
     data-sortable-id-attr="data-id"
     data-sortable-update-field="position">
  <div class="task" data-id="1" draggable="true">Task 1</div>
  <div class="task" data-id="2" draggable="true">Task 2</div>
</div>

API Request ที่ส่ง:

PATCH /api/tasks/1/position
{ "position": 2 }

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

/* Draggable items */
.item[draggable="true"] {
  cursor: grab;
}

.item[draggable="true"]:active {
  cursor: grabbing;
}

/* Ghost element (follows cursor) */
.sortable-ghost {
  opacity: 0.7;
  transform: scale(1.02);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2);
}

/* Placeholder (where item will drop) */
.sortable-placeholder {
  background: rgba(59, 130, 246, 0.1);
  border: 2px dashed #3b82f6;
  border-radius: 8px;
}

/* Drag handle */
.handle {
  cursor: grab;
  padding: 8px;
  color: #9ca3af;
}

.handle:hover {
  color: #374151;
}

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

Kanban Board

<div class="kanban-board">
  <!-- Todo Column -->
  <div data-component="sortable"
       data-sortable-group="kanban"
       data-sortable-draggable=".card"
       data-sortable-api="/api/tasks/{id}"
       data-sortable-id-attr="data-id"
       data-sortable-stage-attr="data-status"
       data-sortable-update-field="status"
       data-status="todo">
    <h3>To Do</h3>
    <div class="card" data-id="1" draggable="true">Task 1</div>
    <div class="card" data-id="2" draggable="true">Task 2</div>
  </div>

  <!-- In Progress Column -->
  <div data-component="sortable"
       data-sortable-group="kanban"
       data-sortable-draggable=".card"
       data-sortable-api="/api/tasks/{id}"
       data-sortable-id-attr="data-id"
       data-sortable-stage-attr="data-status"
       data-sortable-update-field="status"
       data-status="in-progress">
    <h3>In Progress</h3>
    <div class="card" data-id="3" draggable="true">Task 3</div>
  </div>

  <!-- Done Column -->
  <div data-component="sortable"
       data-sortable-group="kanban"
       data-sortable-draggable=".card"
       data-sortable-api="/api/tasks/{id}"
       data-sortable-id-attr="data-id"
       data-sortable-stage-attr="data-status"
       data-sortable-update-field="status"
       data-status="done">
    <h3>Done</h3>
  </div>
</div>

Todo List

<ul data-component="sortable" data-sortable-draggable="li">
  <li draggable="true">Buy groceries</li>
  <li draggable="true">Call mom</li>
  <li draggable="true">Finish report</li>
</ul>

Table Rows

<table>
  <tbody data-component="sortable" data-sortable-draggable="tr">
    <tr draggable="true"><td>Row 1</td></tr>
    <tr draggable="true"><td>Row 2</td></tr>
    <tr draggable="true"><td>Row 3</td></tr>
  </tbody>
</table>

Keyboard Navigation

Key Action
Space เริ่ม/หยุด drag mode
/ ย้าย item ขึ้น/ลง
Enter Confirm position
Escape Cancel drag

ข้อควรระวัง

⚠️ 1. draggable="true" บน items

<!-- ❌ ลืม draggable -->
<div class="item">Item</div>

<!-- ✅ เพิ่ม draggable -->
<div class="item" draggable="true">Item</div>

⚠️ 2. Group Name ต้องตรงกัน

<!-- ❌ ต่าง group - ย้ายข้ามไม่ได้ -->
<div data-sortable-group="a">...</div>
<div data-sortable-group="b">...</div>

<!-- ✅ group เดียวกัน -->
<div data-sortable-group="cards">...</div>
<div data-sortable-group="cards">...</div>

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