Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Tabledrag Laravel Package

components/tabledrag

Components/TableDrag adds drag-and-drop row reordering to Laravel tables. Sort rows with a simple handle, persist the new order to your database, and integrate with Blade or Livewire views. Lightweight, framework-friendly, and easy to drop into existing admin listings and data grids.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're installing. components/tabledrag is not a Laravel package — there is no service provider, no config file, no facade, no PHP class. It is a Composer-distributed front-end asset package: a fork of Drupal 8's core/misc/tabledrag.js plus its CSS, published under the components/* vendor namespace (the old Component installer convention). Everything you integrate lives in Blade, your asset pipeline, and a controller.

Minimal steps:

  1. composer require components/tabledrag and check where it lands — with 2018-era components/* packages this is usually components/tabledrag/ (via oomphinc/composer-installers-extender) rather than vendor/. Confirm the actual path before wiring anything.
  2. Look first at the shipped tabledrag.js / tabledrag.css and the package's README for the constructor signature. Since it's a straight fork, the authoritative reference is the Drupal 8 API docs for Drupal.tableDrag — the fork will match those semantics, not any Laravel-specific docs (there are none).
  3. Pull the files into your build (Vite resolve.alias, or copy into resources/js/vendor/).
  4. Build one table by hand with the markup Drupal's implementation expects: a table with an id, rows carrying the draggable class, a weight input per row, and a parent <select> in an indentation cell.

The canonical first use case is a self-referencing category/tree table: id, parent_id, weight. Drag to reorder siblings, drag right to indent (reparent). It's the classic Drupal taxonomy/menu UI, and it's the only thing this library is good at.

<table id="categories" class="tabledrag">
  <thead><tr><th></th><th>Name</th><th>Parent</th><th>Weight</th></tr></thead>
  <tbody>
  @foreach ($categories as $category)
    <tr class="draggable" data-id="{{ $category->id }}">
      <td class="drag"></td>
      <td>{{ $category->name }}</td>
      <td class="indentation">
        <select name="parent[{{ $category->id }}]">
          <option value="0">Root</option>
          @foreach ($categories as $candidate)
            <option value="{{ $candidate->id }}" @selected($category->parent_id === $candidate->id)>
              {{ $candidate->name }}
            </option>
          @endforeach
        </select>
      </td>
      <td><input class="tabledrag-weight" type="number" name="weight[{{ $category->id }}]" value="{{ $category->weight }}"></td>
    </tr>
  @endforeach
  </tbody>
</table>

Then, once the assets are loaded:

// after jQuery is on window
new Drupal.tableDrag(document.getElementById('categories'), {
  // settings mirror Drupal 8: indent, maxDepth, rowClass, etc.
});

Verify the exact option names against the file you actually installed — do not trust blog posts about Drupal 9/10, which diverged.

Implementation Patterns

Treat it as vendored source, not a dependency. With 1 star, 0 dependents, and a last release in January 2018, pin the version, or better, copy the JS/CSS into resources/ and commit it. That removes a dead package from your lockfile and makes future patching trivial.

Asset pipeline. jQuery must be a real global before this script evaluates — the code is written against window.jQuery/window.$, not a module import. In Vite:

// vite.config.js
resolve: { alias: { '~tabledrag': path.resolve(__dirname, 'components/tabledrag') } }
import $ from 'jquery';
window.$ = window.jQuery = $;
import '~tabledrag/tabledrag.css';
import '~tabledrag/tabledrag.js'; // expects Drupal/drupalSettings globals — see Gotchas

Persistence pattern: one endpoint, one transaction. The library only mutates the DOM. Nothing is submitted. Serialize the whole tree on drop and PATCH it:

// routes/web.php
Route::patch('/categories/tree', [CategoryTreeController::class, 'update'])->name('categories.tree');
public function update(UpdateTreeRequest $request)
{
    $rows = collect($request->validated('rows'))->keyBy('id');

    DB::transaction(function () use ($rows) {
        foreach ($rows as $id => $row) {
            // guard: no self-parent, no cycles (see Gotchas)
            Category::whereKey($id)->update([
                'parent_id' => $row['parent_id'] ?: null,
                'weight'    => $row['weight'],
            ]);
        }
    });

    return response()->noContent();
}

A FormRequest with rows.*.id => ['required','integer','exists:categories,id'], rows.*.parent_id => ['nullable','integer','exists:categories,id','not_in:rows.*.id'], and rows.*.weight => ['required','integer'] covers 90% of the garbage the client can send.

Hook the drop, don't poll. Bind your own listener on the row's change/drop (or wrap the prototype method — see the extension notes below) and debounce a fetch. Do not attempt to intercept Drupal's internal tabledrag-changed bookkeeping; it exists to manage hidden inputs, not to emit a clean public event.

Blade structure. Recursive partial (resources/views/categories/_row.blade.php) rendering children in pre-order with depth-based left padding in the indentation cell. The library keys off DOM order and the weight/parent inputs, so pre-order + correct weight values is what makes a reload look identical to the drag result.

Livewire/Inertia caveat. This is a DOM-mutating jQuery plugin. Under Livewire, wrap the table in wire:ignore and re-init on livewire:navigated; otherwise every morph wipes the drag state and the handles go dead. Under Inertia this is a non-issue, but re-init after each page visit.

When not to use it. If you only need flat reordering (no nesting, no parent select, no accessibility tree), SortableJS is smaller, maintained, framework-agnostic, and MIT. This package earns its keep only for the indentation/parent-select/maxDepth behavior, which is genuinely fiddly to reimplement.

Gotchas and Tips

  • It expects Drupal globals. The forked code path likely references Drupal.t(), Drupal.behaviors, and drupalSettings.tableDrag. The fork may or may not have stripped these. Before anything else, grep the file: grep -nE 'Drupal\.|drupalSettings' components/tabledrag/*.js. If present, shim them:

    window.Drupal = window.Drupal || {};
    window.Drupal.t = (s) => s;
    window.drupalSettings = window.drupalSettings || { tableDrag: {} };
    

    Missing shims throw on load and the failure is silent from the UI's perspective — rows just don't drag.

  • No PHP surface means no guardrails. No config/tabledrag.php, no publishable views, no artisan command. There is nothing to php artisan vendor:publish. Don't waste time looking.

  • Licensing. GPL-2.0, inherited from Drupal core. Drupal ships it as GPL because Drupal is GPL. Minifying/bundling it into a proprietary compiled asset is a legal question, not a technical one — flagging it to counsel is cheaper than a rewrite later, and MIT-licensed alternatives exist for most of the use cases.

  • Cycle prevention is server-side only. The client will happily let a user (or curl) make node 3 the parent of node 1 which is the parent of node 3. Walk ancestors before saving, or you get an infinite loop the next time you render the recursive Blade partial.

  • weight inputs must be real form inputs with names. Drupal's implementation writes weight and the parent selection into those inputs as you drag so a normal form POST round-trips. If you render a <span> for weight to save space, the drag visually works but you have nothing to serialize — you'd have to read weights from DOM order instead, which is fragile. Match the markup.

  • Indentation depends on the parent <select> being inside the row's .indentation cell. Put it in a different column and the depth clamping breaks; the drag-right gesture drives selectedIndex on that element.

  • Debugging checklist, in order: (1) is jQuery on window before the script? (2) did the shims not throw? (3) does the <tr> carry draggable after init ($('tr').hasClass('draggable'))? (4) is the table id correct? (5) is the CSS loaded — without it the handle column has zero width and is un-grabbable, which reads as "drag is broken."

  • Extension points are prototype overrides, not events. The stable hooks in Drupal's implementation are the Drupal.tableDrag.prototype methods (e.g. the row-dragging and row-dropping routines). Because this is a forked snapshot, override the prototype methods by name (with a comment noting the pinned version) rather than monkey-patching internals, and re-check after any vendor update — of which there will be none, which is both the risk and the mitigating factor.

  • Unmaintained since 2018 means jQuery-era browser assumptions. Don't expect it to be validated against modern touch/pointer events on tablets. Test mobile drag explicitly; if it fails, a "move up / move down / change parent" form fallback is the pragmatic answer and also fixes keyboard accessibility, which this library inherits from a pre-accessibility-focused Drupal release.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource