Now.js Framework Documentation
Sortable - Drag and Drop Component
Sortable - Drag and Drop Component
This document explains Sortable, a component for drag-and-drop list sorting with cross-container support and automatic API integration.
📋 Table of Contents
- Overview
- Installation and Import
- Basic Usage
- HTML Attributes
- Configuration Options
- Drag Handle vs Full Area
- Cross-Container Dragging
- API Integration
- Integration with FileElementFactory
- Integration with ApiComponent
- Events
- JavaScript API
- Usage Examples
- Best Practices
Overview
Sortable is a component that enables drag-and-drop sorting for lists, supporting both HTML attributes and JavaScript API configuration.
Key Features
- ✅ Smooth Drag and Drop: Easy to use with beautiful animations
- ✅ Custom Drag Handle: Choose between drag handle or full-area dragging
- ✅ Cross-Container Dragging: Move items between groups (e.g., Kanban Board)
- ✅ Auto-Save: Connect to API for instant updates
- ✅ Touch Support: Works with both mouse and touch
- ✅ Keyboard Support: Use arrow keys and Space bar for sorting
- ✅ Event System: Detect changes and take custom actions
- ✅ FileElementFactory Integration: Easy file reordering
When to Use Sortable
✅ Use Sortable when:
- Users need to reorder items via drag and drop
- Building Kanban Boards or Task Boards
- Sorting uploaded files
- Creating prioritizable lists
- Need automatic API persistence
❌ Don't use Sortable when:
- Too many items (>1000 - use virtual scrolling instead)
- Complex drag-and-drop requirements (consider specialized libraries)
- Users shouldn't be able to reorder
Installation and Import
Sortable is included with Now.js Framework and available via window object:
// No import needed - ready to use
console.log(window.Sortable); // Sortable classDependencies
- ErrorManager – For error handling (optional)
- NotificationManager – For notifications (optional, for API integration)
Basic Usage
1. HTML-Based Usage (Recommended)
<!-- Sortable list -->
<div data-component="sortable">
<div draggable="true" data-id="1">Item 1</div>
<div draggable="true" data-id="2">Item 2</div>
<div draggable="true" data-id="3">Item 3</div>
</div>Explanation:
data-component="sortable"- Defines as Sortable componentdraggable="true"- Makes items draggabledata-id- Item ID (used for API integration)
2. JavaScript Usage
const container = document.querySelector('#my-list');
const sortable = new Sortable(container, {
draggable: '.item',
animation: 200,
onEnd: (evt) => {
console.log('Moved from', evt.oldIndex, 'to', evt.newIndex);
}
});3. Complete Basic Example
<!DOCTYPE html>
<html>
<head>
<style>
.sortable-list {
list-style: none;
padding: 0;
}
.sortable-item {
padding: 15px;
margin: 5px 0;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: move;
}
.sortable-ghost {
opacity: 0.4;
background: #c8ebfb;
}
.sortable-drag {
opacity: 0.8;
}
</style>
</head>
<body>
<ul class="sortable-list" data-component="sortable">
<li class="sortable-item" draggable="true" data-id="1">
📝 Task 1: Design UI
</li>
<li class="sortable-item" draggable="true" data-id="2">
📝 Task 2: Develop Backend
</li>
<li class="sortable-item" draggable="true" data-id="3">
📝 Task 3: Test System
</li>
</ul>
<script src="Now/Now.js"></script>
</body>
</html>HTML Attributes
Sortable supports these attributes for configuration:
Basic Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-component |
string | - | Must be "sortable" |
data-draggable |
string | '[draggable="true"]' |
Selector for draggable items |
data-handle |
string | null |
Selector for drag handle (if null, entire item is draggable) |
data-animation |
number | 150 |
Animation duration (milliseconds) |
data-ghost-class |
string | 'sortable-ghost' |
CSS class for ghost element |
data-drag-class |
string | 'sortable-drag' |
CSS class for dragging element |
Cross-Container Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-group |
string | null |
Group name for cross-container dragging |
API Integration Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
data-sortable-api |
string | - | API endpoint URL |
data-sortable-method |
string | 'PUT' |
HTTP method (GET, POST, PUT, DELETE) |
data-sortable-id-attr |
string | 'data-id' |
Attribute containing item ID |
data-sortable-stage-attr |
string | 'data-category' |
Attribute containing category/stage of container |
data-sortable-update-field |
string | 'category' |
Field name to send in API payload |
data-sortable-extra-data |
JSON | null |
Additional data to send with API requests |
Configuration Options
When creating an instance via JavaScript, all options are available:
const sortable = new Sortable(element, {
// Basic settings
draggable: '[draggable="true"]', // Selector for draggable items
handle: null, // Selector for drag handle
animation: 150, // Animation duration (ms)
ghostClass: 'sortable-ghost', // CSS class for ghost
dragClass: 'sortable-drag', // CSS class for dragging item
dataIdAttr: 'data-id', // Attribute for ID
// Behavior settings
forceFallback: false, // Force fallback mode
fallbackTolerance: 0, // Distance before drag starts (px)
scroll: true, // Enable auto-scroll
scrollSensitivity: 30, // Distance from edge to start scroll (px)
scrollSpeed: 10, // Scroll speed
rtl: false, // Right-to-Left mode
disabled: false, // Disable sorting
// Cross-container dragging
group: null, // Group name
// API Integration
apiEndpoint: '/api/items/update', // API endpoint
apiMethod: 'PUT', // HTTP method
apiIdAttr: 'data-id', // Attribute for ID
apiStageAttr: 'data-category', // Attribute for category
apiUpdateField: 'category', // Field to send in payload
apiExtraData: null, // Additional data
// Callbacks
onStart: (evt) => {
// Called when drag starts
console.log('Start dragging:', evt.item);
},
onEnd: (evt) => {
// Called when item is dropped
console.log('Dropped:', evt.item);
console.log('From:', evt.oldIndex, 'To:', evt.newIndex);
}
});Drag Handle vs Full Area
Sortable supports 2 dragging modes:
1. Full Area Dragging (Default)
<!-- Drag from anywhere on the item -->
<div data-component="sortable">
<div draggable="true" class="item">
<h3>Title</h3>
<p>Description</p>
</div>
</div>2. Drag Handle Only
<!-- Drag from handle only -->
<div data-component="sortable" data-handle=".drag-handle">
<div draggable="true" class="item">
<span class="drag-handle">⋮⋮</span>
<h3>Title</h3>
<p>Description</p>
</div>
</div>
<style>
.drag-handle {
cursor: move;
padding: 10px;
color: #999;
font-size: 20px;
}
.item {
display: flex;
align-items: center;
gap: 10px;
}
</style>When to Use Each Mode
| Mode | Pros | Cons | Best For |
|---|---|---|---|
| Full Area | Easy to use, no special button needed | May drag accidentally | Simple lists, no other buttons |
| Drag Handle | Better control, no accidental drags | Requires UI design | Cards with multiple buttons, complex items |
Cross-Container Dragging
Sortable supports dragging items between containers using group.
Example: Kanban Board
<div class="kanban-board">
<!-- To Do Column -->
<div class="kanban-column"
data-component="sortable"
data-group="tasks"
data-category="todo"
data-sortable-api="/api/tasks/update"
data-sortable-stage-attr="data-category">
<h3>To Do</h3>
<div class="task-card" draggable="true" data-id="1">
<h4>Design UI</h4>
<p>Create homepage mockup</p>
</div>
</div>
<!-- In Progress Column -->
<div class="kanban-column"
data-component="sortable"
data-group="tasks"
data-category="in-progress"
data-sortable-api="/api/tasks/update"
data-sortable-stage-attr="data-category">
<h3>In Progress</h3>
<div class="task-card" draggable="true" data-id="2">
<h4>Develop Backend</h4>
<p>Create REST API</p>
</div>
</div>
<!-- Done Column -->
<div class="kanban-column"
data-component="sortable"
data-group="tasks"
data-category="done"
data-sortable-api="/api/tasks/update"
data-sortable-stage-attr="data-category">
<h3>Done</h3>
<div class="task-card" draggable="true" data-id="3">
<h4>Setup Server</h4>
<p>Install Ubuntu + Nginx</p>
</div>
</div>
</div>Explanation:
data-group="tasks"- All 3 containers in same group, can drag between themdata-category- Each column has different category- When dragging card to another column, API request is sent automatically
API Integration
Sortable can automatically save changes via API when items are dragged to different containers.
API Integration Setup
<div data-component="sortable"
data-sortable-api="/api/items/update"
data-sortable-method="PUT"
data-sortable-id-attr="data-id"
data-sortable-stage-attr="data-status"
data-sortable-update-field="status">
<!-- Items -->
</div>Request Sent
When dragging an item to another container, Sortable sends:
// PUT /api/items/update
{
"id": "123", // From data-id of item
"status": "in-progress", // From data-status of target container
"update_stage_only": true // Flag for stage-only update
}Expected Response
{
"success": true,
"message": "Updated successfully",
"data": {
"id": "123",
"status": "in-progress"
}
}Example API Endpoint (PHP)
<?php
// api/items/update.php
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? null;
$status = $data['status'] ?? null;
if (!$id || !$status) {
echo json_encode([
'success' => false,
'message' => 'Missing required data'
]);
exit;
}
// Update database
$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$stmt = $db->prepare('UPDATE items SET status = ? WHERE id = ?');
$stmt->execute([$status, $id]);
echo json_encode([
'success' => true,
'message' => 'Updated successfully',
'data' => [
'id' => $id,
'status' => $status
]
]);Integration with FileElementFactory
Sortable works seamlessly with FileElementFactory for file reordering.
Usage Example
<form>
<div class="form-group">
<label>Upload Images</label>
<input type="file"
id="product-images"
data-element="file"
data-preview="true"
data-sortable="true"
data-action-url="/api/products/images"
data-file-reference="id"
data-files='[
{"id": 1, "url": "/uploads/image1.jpg", "name": "image1.jpg"},
{"id": 2, "url": "/uploads/image2.jpg", "name": "image2.jpg"}
]'
multiple>
</div>
</form>Explanation:
data-sortable="true"- Enable Sortable for filesdata-action-url- API endpoint for saving orderdata-file-reference="id"- Useidfield as reference- FileElementFactory creates Sortable instance automatically
API Request When Reordering Files
// POST /api/products/images
{
"action": "sort",
"order": [
{"id": 2, "position": 0},
{"id": 1, "position": 1}
]
}Integration with ApiComponent
Sortable can work with ApiComponent for loading data and managing updates.
Example: Task List Loaded from API
<!-- Load list from API -->
<div data-component="api"
data-endpoint="/api/tasks"
data-params='{"status": "active"}'>
<template>
<!-- Sortable container -->
<div data-component="sortable"
data-group="tasks"
data-sortable-api="/api/tasks/update"
data-sortable-id-attr="data-task-id"
data-sortable-stage-attr="data-priority"
data-sortable-update-field="priority">
<!-- Loop items -->
<div data-for="task in data"
class="task-item"
draggable="true"
data-bind:data-task-id="task.id"
data-bind:data-priority="task.priority">
<span class="drag-handle">⋮⋮</span>
<span data-bind="task.title"></span>
</div>
</div>
</template>
</div>Events
Sortable dispatches events when changes occur.
Event Types
| Event | When | Details |
|---|---|---|
sortable:start |
Drag starts | {item, startIndex, event} |
sortable:end |
Item dropped | {item, newIndex, oldIndex, to, from} |
sortable:change |
Position changes | {item, newIndex, oldIndex} or {item, to, from} |
sortable:select |
Item selected with Space | {item, selected} |
sortable:api-success |
API success | {item, response, payload} |
sortable:api-error |
API failure | {item, error} |
Listening to Events
const container = document.querySelector('#my-sortable');
// Drag start
container.addEventListener('sortable:start', (e) => {
console.log('Start dragging:', e.detail.item);
console.log('From position:', e.detail.startIndex);
});
// Item dropped
container.addEventListener('sortable:end', (e) => {
console.log('Dropped:', e.detail.item);
console.log('From:', e.detail.oldIndex, 'To:', e.detail.newIndex);
console.log('Source container:', e.detail.from);
console.log('Target container:', e.detail.to);
});
// Position changed
container.addEventListener('sortable:change', (e) => {
console.log('Position changed:', e.detail);
});
// API success
container.addEventListener('sortable:api-success', (e) => {
console.log('Saved successfully:', e.detail.response);
NotificationManager.success('Order saved');
});
// API failure
container.addEventListener('sortable:api-error', (e) => {
console.error('Save failed:', e.detail.error);
NotificationManager.error('Failed to save order');
});JavaScript API
Sortable provides methods for JavaScript control.
Create Instance
const sortable = new Sortable(element, options);Access Instance
// From element directly
const sortable = element._sortableInstance;
// Or from ComponentManager
const sortable = ComponentManager.getInstance(element, 'sortable');Methods
enable()
Enable Sortable
sortable.enable();disable()
Disable Sortable
sortable.disable();destroy()
Destroy instance and remove event listeners
sortable.destroy();Usage Examples
1. Simple List
<ul data-component="sortable" class="simple-list">
<li draggable="true" data-id="1">🍎 Apple</li>
<li draggable="true" data-id="2">🍌 Banana</li>
<li draggable="true" data-id="3">🍊 Orange</li>
<li draggable="true" data-id="4">🍇 Grape</li>
</ul>
<style>
.simple-list {
list-style: none;
padding: 0;
}
.simple-list li {
padding: 10px;
margin: 5px 0;
background: #f0f0f0;
border-radius: 4px;
cursor: move;
}
.simple-list li.sortable-ghost {
opacity: 0.4;
}
</style>2. Cards with Drag Handle
<div data-component="sortable"
data-handle=".drag-handle"
class="card-list">
<div class="card" draggable="true" data-id="1">
<span class="drag-handle">⋮⋮</span>
<div class="card-content">
<h3>Article 1</h3>
<p>Article content...</p>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
<div class="card" draggable="true" data-id="2">
<span class="drag-handle">⋮⋮</span>
<div class="card-content">
<h3>Article 2</h3>
<p>Article content...</p>
<button>Edit</button>
<button>Delete</button>
</div>
</div>
</div>
<style>
.card {
display: flex;
gap: 10px;
padding: 15px;
margin: 10px 0;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
}
.drag-handle {
cursor: move;
color: #999;
font-size: 20px;
padding: 5px;
}
.card-content {
flex: 1;
}
.card button {
margin-right: 5px;
}
</style>Best Practices
✅ Do
-
Always use data-id
<!-- Good --> <div draggable="true" data-id="123">Item</div> -
Use drag handle for cards with other buttons
<!-- Good - prevents accidental dragging --> <div data-component="sortable" data-handle=".drag-handle"> <div draggable="true"> <span class="drag-handle">⋮⋮</span> <button>Edit</button> <button>Delete</button> </div> </div> -
Use CSS classes for different states
.sortable-ghost { opacity: 0.4; background: #e3f2fd; } .sortable-drag { opacity: 0.8; transform: rotate(2deg); } -
Handle API errors
container.addEventListener('sortable:api-error', (e) => { console.error('API Error:', e.detail.error); NotificationManager.error('Failed to save, please try again'); }); -
Use groups for Kanban Boards
<div data-component="sortable" data-group="tasks">...</div> <div data-component="sortable" data-group="tasks">...</div>
❌ Don't
-
Don't omit data-id
<!-- Bad - API integration won't work --> <div draggable="true">Item</div> -
Don't use too many items
<!-- Bad - slow, use virtual scrolling instead --> <div data-component="sortable"> <!-- 1000+ items --> </div> -
Don't forget loading state
// Bad - no loading indicator container.addEventListener('sortable:end', async (e) => { await saveToAPI(e.detail); }); // Good - show loading state container.addEventListener('sortable:end', async (e) => { LoadingManager.show(); try { await saveToAPI(e.detail); NotificationManager.success('Saved successfully'); } catch (error) { NotificationManager.error('Save failed'); } finally { LoadingManager.hide(); } }); -
Don't forget group for cross-container
<!-- Bad - can't drag between containers --> <div data-component="sortable">...</div> <div data-component="sortable">...</div> <!-- Good --> <div data-component="sortable" data-group="shared">...</div> <div data-component="sortable" data-group="shared">...</div>
💡 Tips
-
Use transitions for smooth animations
.sortable-item { transition: transform 0.2s ease; } -
Add visual feedback
.sortable-chosen { box-shadow: 0 4px 12px rgba(0,0,0,0.2); transform: scale(1.05); } -
Use debounce for API calls
let saveTimeout; container.addEventListener('sortable:end', (e) => { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { saveToAPI(e.detail); }, 500); }); -
Add accessibility
<div draggable="true" role="button" tabindex="0" aria-label="Drag to reorder"> Item </div>
Summary
Sortable is a powerful and flexible component perfect for creating drag-and-drop UIs, from simple lists to complex Kanban Boards. Integration with FileElementFactory and ApiComponent makes it easy to build complete data management systems.
Highlights
- 🎯 Easy to use via HTML attributes
- 🔄 Cross-container dragging support
- 💾 Automatic API persistence
- 📱 Touch and keyboard support
- 🎨 Easy CSS customization
- 🔌 Works well with other components
Additional Resources
- ApiComponent - API integration
- FileElementFactory - File management
- ComponentManager - Component system