mosamy/sortable

A Laravel package to easily add sorting functionality to your Eloquent models.

Maintainers

Package info

bitbucket.org/mohamedsamy_10/sortable

pkg:composer/mosamy/sortable

Statistics

Installs: 35

Dependents: 0

Suggesters: 0

2.0.0 2026-04-04 20:17 UTC

This package is auto-updated.

Last update: 2026-04-04 20:17:59 UTC


README

Make your eloquent model sortable. {

Laravel Sortable

A powerful Laravel package that enables drag-and-drop sorting for Eloquent models. Supports both traditional AJAX approach and modern Livewire implementation.

Features

  • 🎯 Drag-and-drop sorting with persistent storage
  • ⚡ Two implementation patterns: AJAX (traditional) and Livewire (modern)
  • 🔌 Polymorphic relationship for any model
  • 🏗️ Computed property support with Livewire
  • 🎣 Hook system with afterSort() callback
  • 📦 Zero dependencies for Livewire implementation
  • ✅ Automatic cleanup on model deletion
  • 🔒 Database constraints ensure data integrity

Installation

1. Install via Composer

composer require mosamy/sortable

2. Publish Assets (For AJAX version only)

If you plan to use the traditional AJAX approach:

php artisan vendor:publish --provider="Mosamy\Sortable\SortableServiceProvider" --tag="assets"

3. Run Migrations

php artisan migrate

4. Publish Configuration (Optional)

Customize the package configuration:

php artisan vendor:publish --provider="Mosamy\Sortable\SortableServiceProvider" --tag="config"

This creates config/sort.php where you can adjust:

  • url: The endpoint for AJAX sort requests (default: sortable/sort)
  • middleware: Middleware applied to sort routes (default: ['web'])

Database Schema

The package creates a sortables table with a polymorphic relationship:

sortables
├── id (primary key)
├── sortable_id (the model's ID being sorted)
├── sortable_type (the fully qualified model class name)
└── Unique constraint on (sortable_id, sortable_type)

The order of records in this table determines the sort order.

Quick Start

Setup Your Model

Add the Sortable trait to any Eloquent model you want to make sortable:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Mosamy\Sortable\Traits\Sortable;

class User extends Model
{
  use Sortable;
    
  // ... rest of your model
}

Query with Sort Order

Retrieve models in their sorted order using the orderBySort() scope:

// Get all useres in sort order
User::orderBySort()->get();

// Chain with other queries
User::where('status', 'active')
  ->orderBySort()
  ->get();

Implementation Patterns

This package supports two distinct implementation patterns. Choose based on your project structure.

Pattern 1: Livewire (Recommended - Modern)

No additional JavaScript or CSS dependencies required. Uses Livewire's reactive data binding.

1. Use Traits in Component


use App\Models\User;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Mosamy\Sortable\Attributes\Sortable;
use Mosamy\Sortable\Traits\LivewireSort;

class Listing extends Component
{
  use LivewireSort;

  #[Sortable]
  #[Computed]
  public function list()
  {
    return User::orderBySort()->get();
  }
}

Key Points:

  • Use the #[Sortable] attribute on any computed or property method that returns your sortable collection
  • Use #[Computed] for reactive updates (Livewire 4)
  • The trait automatically detects and handles sort events
  • Call orderBySort() in your query to retrieve items in sort order

2. Blade Template with wire:sort

<div class="table-responsive">
  <table class="table">
    <tbody wire:sort="handleSort">
      @forelse ($this->list as $item)
        <tr wire:key="user-{{ $item->id }}" wire:sort:item="{{ $item->id }}">
          <td>{{ $item->name }}</td>
          <td>{{ $item->email }}</td>
          <td>{{ $item->phone }}</td>
        </tr>
      @empty
        <tr wire:sort:ignore>
          <td colspan="3" class="text-center">No results</td>
        </tr>
      @endforelse
    </tbody>
  </table>
</div>

Blade Directives:

  • wire:sort="handleSort" on the <tbody> - tells Livewire to call the handleSort() method when items are reordered
  • wire:sort:item="{{ $item->id }}" on each <tr> - marks the item as sortable with its ID

Features:

  • Drag and drop works immediately with no additional setup
  • No JavaScript bundle needed
  • Automatic state management through Livewire
  • Changes persist to the database instantly

Pattern 2: AJAX with Traditional Blade (Legacy)

For traditional server-rendered views with AJAX-based sorting.

1. Include Dependencies

Add these to your template (typically in <head>):

<!-- jQuery (required) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- jQuery UI (required) -->
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>

<!-- CSRF token (required) -->
<meta name="csrf-token" content="{{ csrf_token() }}" />

<!-- Setup AJAX headers -->
<script>
  $.ajaxSetup({
    headers: {
      'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
  });
</script>

2. Include the Sortable Script

Add after jQuery UI, before closing </head> or </body>:

@sortJs()

This Blade directive injects:

  • The draggable.js script from the package
  • Configuration for the sort endpoint URL

3. Build Your Table

Use these 3 placement rules:

  1. Add sortable-table class to your <table>.
  2. Put <x-sortable::item :item="$item" /> inside each <tr> in your loop.
  3. Put <x-sortable::type :list="$list" /> once after the closing </table>.
<table class="sortable-table"> <!--set the class name -->

	<tbody>

		@foreach($list as $item)
		<tr>
			<td>...</td>
			<td>...</td>

			<!--sput this line in each <tr> tag inside the loop -->
			<x-sortable::item :item="$item" />
		</tr>
		@endforeach
	</tbody>
</table>

<!--put this line after close table tag </table> -->
<x-sortable::type :list="$list" />

Component Usage:

  • <x-sortable::item :item="$item" /> - Adds hidden sortable ID for that row (use inside each looped <tr>).
  • <x-sortable::type :list="$list" /> - Adds hidden sortable type for the table (use once, after </table>).

How it works:

  1. User drags rows in the table
  2. jQuery UI sortable detects the change
  3. JavaScript collects all item IDs in new order
  4. AJAX POST to sortable/sort endpoint with IDs and model type
  5. SortController updates the database

The afterSort() Hook

Execute custom logic whenever sorting completes. Add a static method to your model:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
  use \Mosamy\Sortable\Sortable;

  public static function afterSort(){
    // add some code to execute after the sort is completed
  }

}

This method is called automatically after:

  • Livewire sort completes (in Livewire implementation)
  • AJAX sort request completes (in traditional implementation)

Advanced Usage

Conditional Sorting

// Only show active useres, sorted
$useres = User::where('status', 'active')
  ->orderBySort()
  ->get();

Multiple Sortable Collections in One Component

Add the #[Sortable] attribute to multiple methods:

class AdminDashboard extends Component
{
  use LivewireSort;

  #[Sortable]
  #[Computed]
  public function useres()
  {
    return User::orderBySort()->get();
  }

  #[Sortable]
  #[Computed]
  public function departments()
  {
    return Department::orderBySort()->get();
  }
}

The LivewireSort trait automatically handles sorting for all collections marked with #[Sortable].

Resetting Sort Order

// Clear all sort entries for a model
Sortable::where('sortable_type', User::class)->delete();

Configuration

config/sort.php

return [
  // The URL endpoint for AJAX sort requests (AJAX only)
  'url' => 'sortable/sort',
    
  // Middleware stack for the sort route (AJAX only)
  'middleware' => ['web'],
];

Route Protection

In config/sort.php, add authentication middleware to protect the sort endpoint:

'middleware' => ['web', 'auth', 'verified'],

This ensures only authenticated users can modify sort order.

How It Works

Database Storage

Sort order is stored in the sortables table using Laravel's polymorphic relationships:

Useres table          Sortables table
├── id: 1               ├── id: 1
├── id: 2               ├── sortable_id: 3
├── id: 3               ├── sortable_type: App\Models\User
└── id: 4               └── (next record has id: 2)
            ├── id: 2
            ├── sortable_id: 1
            └── sortable_type: App\Models\User
            ├── id: 3
            ├── sortable_id: 4
            └── sortable_type: App\Models\User
            ├── id: 4
            ├── sortable_id: 2
            └── sortable_type: App\Models\User

The order of records in sortables determines the display order. The orderBySort() scope uses a subquery to retrieve items in the correct sequence.

Livewire Implementation

  1. User drags a row with wire:sort:item
  2. Livewire fires the wire:sort handler (handleSort method)
  3. LivewireSort trait intercepts and:
    • Recalculates positions for all items
    • Regenerates the sortables table entries
    • Calls afterSort() hook
    • Component re-renders with new order

AJAX Implementation

  1. User drags a row (jQuery UI sortable)
  2. Old JavaScript collects IDs in new order
  3. AJAX POST to /sortable/sort with:
    {
    "sortable_ids": [3, 1, 4, 2],
    "sortable_type": "User"
    }
    
  4. SortController processes request:
    • Deletes old sort records
    • Creates new records in order
    • Calls afterSort() hook
    • Returns success response

Lifecycle & Cleanup

Automatic Cleanup

When a model with the Sortable trait is deleted, all its sort records are automatically removed:

$user = User::find(1);
$user->delete(); // Automatically deletes related sortable records

This prevents orphaned entries in the sortables table.

Accessing Sort Relationship

Retrieve the sort relationship directly:

$user = User::find(1);
$sortRecord = $user->sort; // Returns the Sortable model instance or null

Requirements

For Livewire Implementation (Recommended)

  • Laravel 12.0+
  • Livewire 4.0+
  • PHP 8.1+
  • No additional JavaScript required
  • No additional CSS required

For AJAX Implementation (Legacy)

  • Laravel 12.0+
  • PHP 8.1+
  • jQuery 3.0+ (client-side)
  • jQuery UI 1.13+ (client-side)

Best Practices

1. Always Use orderBySort() in Queries

// ✅ Good
User::orderBySort()->get();

// ❌ Avoid
User::all(); // Returns in database order, not sort order

2. Use Livewire for New Projects

The Livewire implementation is simpler, more maintainable, and has fewer dependencies.

3. Cache the Sort Order if Queried Frequently

$useres = cache()->remember('useres:sorted', 3600, function () {
  return User::orderBySort()->get();
});

Clear cache in afterSort():

public static function afterSort()
{
  cache()->forget('useres:sorted');
}

4. Validate Authorization

Use Laravel's authorization to prevent unauthorized sorting:

// In your component or controller
public function handleSort($id, $index)
{
  $this->authorize('sort', User::class);
  // Call parent implementation
  parent::handleSort($id, $index);
}

5. Use Pagination Carefully

Sorting works best with all items loaded. For paginated lists, consider:

  • Fetching all items via AJAX for sorting
  • Disabling pagination in sortable views
  • Sorting at the database level with priority weights

Troubleshooting

Items Not Appearing in Sort Order

Problem: Using User::all() or queries without orderBySort()

Solution: Always use the orderBySort() scope:

User::orderBySort()->get();

Sort Changes Not Persisting

Problem: Missing wire:sort attribute on container or incorrect handler name

Solution: Ensure your Blade template has:

<tbody wire:sort="handleSort">
  <tr wire:sort:item="{{ $item->id }}">

AJAX Requests 403/404

Problem: Misconfigured route middleware or CSRF token missing

Solution:

  • Verify config/sort.php middleware includes 'web'
  • Ensure csrf_token() meta tag is present
  • Check AJAX headers setup in your template

Changes Not Triggering afterSort()

Problem: Using the wrong component trait or missing attribute

Solution:

  • Ensure model has Sortable trait
  • Ensure component has LivewireSort trait
  • Ensure method has #[Sortable] attribute
  • Check component returns collection from marked method

Security Considerations

CSRF Protection

AJAX implementation uses CSRF tokens. Ensure:

<meta name="csrf-token" content="{{ csrf_token() }}" />

Livewire handles CSRF automatically.

Authorization

Implement authorization in your afterSort() hook or route middleware:

// In config/sort.php
'middleware' => ['web', 'auth', 'verified'],

API Reference

Traits

Sortable

  • sort() - Get the related Sortable model
  • scopeOrderBySort($query) - Query scope to retrieve models in sort order
  • bootSortable() - Boot hook that handles cleanup on deletion
  • afterSort() - Static hook called after sorting completes

LivewireSort

  • handleSort($id, $index) - Called by Livewire when items are reordered
  • getSortableMethods() - Internal: finds methods marked with #[Sortable]

Attributes

#[Sortable]

Mark a method in a Livewire component to make its returned collection sortable. Works with #[Computed] properties.

Models

Sortable (Storage Model)

  • sortable_id - The ID of the sorted model
  • sortable_type - The fully qualified class name of the sorted model
  • sortable() - Polymorphic morph relation

Controllers

SortController

Handles AJAX POST requests to /sortable/sort. Validates input and updates database.

Request Format:

{
  "sortable_ids": [3, 1, 4, 2],
  "sortable_type": "User"
}

Views/Components

sortable::components.type

Blade component that adds a hidden input with the model class name.

<x-sortable::type :list="$items" />

sortable::components.item

Blade component that adds a hidden input with the item ID.

<x-sortable::item :item="$item" />

Blade Directives

@sortJs()

Outputs the draggable.js script and sort URL configuration (AJAX only).

License

MIT License - see LICENSE file for details

Author

Mohamed Samy
Email: dev.mohamed.samy@gmail.com

Contributing

Contributions are welcome! Please submit pull requests or open issues for bugs and feature requests.

Changelog

v1.0.0 (Current)

  • Initial release
  • AJAX implementation with jQuery UI
  • Livewire 3 implementation with computed properties
  • Polymorphic relationship storage
  • AfterSort hook
  • Automatic cleanup on deletion use \Mosamy\Sortable\Sortable;

    public static function afterSort(){ // add some code to execute after the sort is completed }

}


## Config

By default, the draggable sort executes by ajax request at url **/sortable/sort** with default middleware **['web']**

If you need to change this you may publish this config file.

php artisan vendor:publish --provider="Mosamy\Sortable\SortableServiceProvider" --tag="config"

Now you can change the values of url and middleware.

return [ 'url' => 'sortable/sort', 'middleware' => ['web'] ];