Now.js Framework Documentation

Now.js Framework Documentation

Sortable - Drag and Drop Component

EN 01 Dec 2025 01:05

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

  1. Overview
  2. Installation and Import
  3. Basic Usage
  4. HTML Attributes
  5. Configuration Options
  6. Drag Handle vs Full Area
  7. Cross-Container Dragging
  8. API Integration
  9. Integration with FileElementFactory
  10. Integration with ApiComponent
  11. Events
  12. JavaScript API
  13. Usage Examples
  14. 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 class

Dependencies

  • ErrorManager – For error handling (optional)
  • NotificationManager – For notifications (optional, for API integration)

Basic Usage

<!-- 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 component
  • draggable="true" - Makes items draggable
  • data-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 them
  • data-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 files
  • data-action-url - API endpoint for saving order
  • data-file-reference="id" - Use id field 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

  1. Always use data-id

    <!-- Good -->
    <div draggable="true" data-id="123">Item</div>
  2. 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>
  3. Use CSS classes for different states

    .sortable-ghost {
     opacity: 0.4;
     background: #e3f2fd;
    }
    
    .sortable-drag {
     opacity: 0.8;
     transform: rotate(2deg);
    }
  4. Handle API errors

    container.addEventListener('sortable:api-error', (e) => {
     console.error('API Error:', e.detail.error);
     NotificationManager.error('Failed to save, please try again');
    });
  5. Use groups for Kanban Boards

    <div data-component="sortable" data-group="tasks">...</div>
    <div data-component="sortable" data-group="tasks">...</div>

❌ Don't

  1. Don't omit data-id

    <!-- Bad - API integration won't work -->
    <div draggable="true">Item</div>
  2. Don't use too many items

    <!-- Bad - slow, use virtual scrolling instead -->
    <div data-component="sortable">
     <!-- 1000+ items -->
    </div>
  3. 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();
     }
    });
  4. 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

  1. Use transitions for smooth animations

    .sortable-item {
     transition: transform 0.2s ease;
    }
  2. Add visual feedback

    .sortable-chosen {
     box-shadow: 0 4px 12px rgba(0,0,0,0.2);
     transform: scale(1.05);
    }
  3. Use debounce for API calls

    let saveTimeout;
    container.addEventListener('sortable:end', (e) => {
     clearTimeout(saveTimeout);
     saveTimeout = setTimeout(() => {
       saveToAPI(e.detail);
     }, 500);
    });
  4. 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