mosamy / sortable
A Laravel package to easily add sorting functionality to your Eloquent models.
Requires
- php: >=8.1
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 thehandleSort()method when items are reorderedwire: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:
- Add
sortable-tableclass to your<table>. - Put
<x-sortable::item :item="$item" />inside each<tr>in your loop. - 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:
- User drags rows in the table
- jQuery UI sortable detects the change
- JavaScript collects all item IDs in new order
- AJAX POST to
sortable/sortendpoint with IDs and model type SortControllerupdates 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
- User drags a row with
wire:sort:item - Livewire fires the
wire:sorthandler (handleSortmethod) LivewireSorttrait intercepts and:- Recalculates positions for all items
- Regenerates the
sortablestable entries - Calls
afterSort()hook - Component re-renders with new order
AJAX Implementation
- User drags a row (jQuery UI sortable)
- Old JavaScript collects IDs in new order
- AJAX POST to
/sortable/sortwith:{ "sortable_ids": [3, 1, 4, 2], "sortable_type": "User" } SortControllerprocesses 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.phpmiddleware 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
Sortabletrait - Ensure component has
LivewireSorttrait - 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 relatedSortablemodelscopeOrderBySort($query)- Query scope to retrieve models in sort orderbootSortable()- Boot hook that handles cleanup on deletionafterSort()- Static hook called after sorting completes
LivewireSort
handleSort($id, $index)- Called by Livewire when items are reorderedgetSortableMethods()- 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 modelsortable_type- The fully qualified class name of the sorted modelsortable()- 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'] ];