x-data
Alpine
Alpine.js · x-sort · Drag and Drop · Sortierbare Listen
Alpine.js x-sort
Drag-and-Drop Sortieren ohne externe Bibliothek

x-sort bringt natives Drag-and-Drop-Sorting in Alpine.js – ohne SortableJS, ohne jQuery UI und ohne externe Abhängigkeiten. Sortierbare Listen, gruppenübergreifendes Verschieben, Drag-Handles und Server-Synchronisation sind mit wenigen HTML-Attributen umsetzbar.

14 Min. Lesezeit x-sort · Gruppen · Handles · Callbacks · Server-Sync Alpine.js 3.x · Moderne Browser

1. Was x-sort in Alpine.js leistet

x-sort ist ein offizielles Alpine.js-Plugin, das Drag-and-Drop-Sortierung in eine deklarative Direktive kapselt. Intern nutzt x-sort die Browser-native HTML Drag and Drop API, bietet aber eine deutlich höhere Abstraktionsebene als das direkte Arbeiten mit dragstart-, dragover-, drop- und dragend-Events. Wer ohne x-sort sortierbare Listen bauen will, schreibt schnell 50 bis 100 Zeilen Event-Handler-Code für eine einzige Liste – mit x-sort schrumpft das auf zwei HTML-Attribute.

Der Unterschied zu externen Bibliotheken wie SortableJS oder jQuery UI Sortable: x-sort bringt keine eigene Abhängigkeit mit und ist vollständig in das Alpine.js-Reaktivitätssystem integriert. Das bedeutet, dass Alpine-State-Änderungen nach dem Sortieren sofort reaktiv sind – kein manuelles Auslesen des DOM-Zustands, kein Abgleich zwischen DOM und Datenmodell. x-sort nutzt Alpine's Reactivity-System direkt und hält das Datenmodell automatisch synchron mit der visuellen Reihenfolge im DOM.


// Installation via npm
import Alpine from 'alpinejs';
import sort from '@alpinejs/sort';

Alpine.plugin(sort);
Alpine.start();

// Alternativ via CDN:
// <script src="https://cdn.jsdelivr.net/npm/@alpinejs/sort@3.x.x/dist/cdn.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

// Grundstruktur: x-sort auf dem Container, x-sort:item auf den Kindelementen
// <ul x-data x-sort>
//   <li x-sort:item>Element 1</li>
//   <li x-sort:item>Element 2</li>
// </ul>

Ein häufiges Missverständnis: x-sort ist keine vollständige Drag-Drop-Bibliothek für beliebige Drag-Szenarien. Es ist speziell auf das Sortieren von Listen optimiert. Für Drag-Drop zwischen völlig unterschiedlichen Komponenten – z.B. Dateien auf ein Upload-Feld ziehen – ist die native Drag-and-Drop-API oder eine spezialisierte Bibliothek besser geeignet. Für das typische Admin-Panel-Szenario (Reihenfolge ändern, Elemente zwischen Spalten verschieben) ist x-sort die direkteste und wartbarste Lösung im Alpine-Ökosystem.

2. Installation und Setup des x-sort-Plugins

x-sort muss wie alle Alpine.js-Plugins vor dem Start von Alpine registriert werden. Der kritische Punkt: Die Reihenfolge der Script-Tags oder Imports ist entscheidend. Das Plugin muss vor Alpine.start() geladen sein, sonst erkennt Alpine die x-sort-Direktive nicht und gibt einen Fehler in der Konsole aus. Bei der CDN-Variante bedeutet das: Plugin-Script-Tag vor dem Alpine-Script-Tag.

Für Magento 2 Hyvä Themes empfiehlt sich die npm-Variante über den Build-Prozess, da Hyvä das Alpine-Bundle über Tailwind CSS und den eigenen Build-Stack zusammenstellt. Das Plugin wird in der Alpine-Initialisierungsdatei des Themes registriert, die Hyvä bereitstellt. So bleibt x-sort im Bundle enthalten und wird nicht als separates Script nachgeladen, was die CSP-Compliance und die Performance sicherstellt.

3. Einfache sortierbare Liste mit x-sort

Die einfachste sortierbare Liste braucht nur zwei Attribute: x-sort auf dem Container und x-sort:item auf jedem sortierbaren Kindelement. Alpine registriert dann automatisch alle notwendigen Event-Listener und erledigt das visuelle Feedback beim Ziehen. Kein weiterer JavaScript-Code ist nötig – das ist der Kernvorteil von x-sort gegenüber manueller Drag-Drop-Implementierung.

Wichtig für das Datenmodell: x-sort manipuliert standardmäßig nur den DOM, nicht das Alpine-Datenmodell. Um nach dem Sortieren das Alpine-Array in der richtigen Reihenfolge zu halten, übergibt man einen Callback an x-sort, der die neue Reihenfolge zurück in das Alpine-Datenobjekt schreibt. Das ist der einzige Schritt, der über das reine HTML-Setup hinausgeht, und er ist in wenigen Zeilen erledigt.


<!-- Einfache sortierbare To-Do-Liste -->
<div x-data="{
  tasks: [
    { id: 1, title: 'Design-Review durchführen', done: false },
    { id: 2, title: 'Staging deployen', done: false },
    { id: 3, title: 'Tests schreiben', done: true },
    { id: 4, title: 'Code-Review anfordern', done: false },
  ],
  reorder(newOrder) {
    // newOrder ist ein Array von { item, position }-Objekten
    this.tasks = newOrder.map(({ item }) => this.tasks.find(t => t.id === parseInt(item)));
  }
}">
  <ul
    x-sort="reorder($item, $position)"
    class="space-y-2"
  >
    <template x-for="task in tasks" :key="task.id">
      <li
        x-sort:item.string="task.id"
        class="flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-3 cursor-grab active:cursor-grabbing shadow-sm hover:shadow-md transition-shadow"
      >
        <input type="checkbox" :checked="task.done" class="rounded">
        <span :class="task.done ? 'line-through text-slate-400' : 'text-slate-800'" x-text="task.title"></span>
      </li>
    </template>
  </ul>
</div>

4. Drag-Handles: Nur bestimmte Bereiche ziehbar machen

In den meisten UIs sollte nicht das gesamte Listenelement ziehbar sein, sondern nur ein dediziertes Handle – ein Icon oder ein Bereich, das dem Nutzer klar signalisiert, dass hier gezogen werden kann. Das verhindert unbeabsichtigte Drag-Aktionen beim Klicken auf Buttons, Links oder Formularfelder innerhalb des Listenelements. Mit x-sort:handle auf einem Kindelement wird genau dieses Verhalten konfiguriert: Nur das Handle-Element startet einen Drag, der Rest des Elements verhält sich normal.

Das Handle-Icon sollte visuell eindeutig sein – das klassische Sechs-Punkte-Drag-Icon ( oder ein SVG-Grid) ist in Admin-Interfaces etabliert. Dazu empfiehlt sich der CSS-Cursor grab auf dem Handle und grabbing im aktiven Zustand. Das Handle selbst benötigt kein JavaScript – es ist ein reines HTML-Attribut auf dem Element. Wichtig: Das Handle-Element darf keine eigenständigen Klick-Handler haben, da der Browser Drag-Aktionen und Klick-Events unterschiedlich behandelt.


<!-- Sortierbare Liste mit Drag-Handle -->
<div x-data="{
  items: [
    { id: 1, name: 'Kategorie: Elektronik', count: 42 },
    { id: 2, name: 'Kategorie: Kleidung', count: 128 },
    { id: 3, name: 'Kategorie: Bücher', count: 67 },
    { id: 4, name: 'Kategorie: Sport', count: 35 },
  ],
  saveOrder(item, position) {
    // ID und neue Position für Server-Sync merken
    console.log('Verschoben:', item, '→ Position:', position);
  }
}">
  <ul x-sort="saveOrder($item, $position)" class="space-y-2">
    <template x-for="row in items" :key="row.id">
      <li
        x-sort:item="row.id"
        class="flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-3 shadow-sm"
      >
        <!-- Nur dieses Element ist der Drag-Trigger -->
        <span
          x-sort:handle
          class="cursor-grab active:cursor-grabbing text-slate-300 hover:text-slate-500 transition-colors flex-shrink-0"
          title="Zum Sortieren ziehen"
        >
          <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
            <path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"/>
          </svg>
        </span>
        <span class="flex-1 font-medium text-slate-800" x-text="row.name"></span>
        <span class="text-xs text-slate-400 bg-slate-100 rounded px-2 py-0.5" x-text="row.count + ' Produkte'"></span>
      </li>
    </template>
  </ul>
</div>

5. Gruppen: Elemente zwischen Listen verschieben

Eines der mächtigsten Features von x-sort ist das gruppenübergreifende Sortieren: Elemente können zwischen mehreren Container-Listen verschoben werden, wenn beide Container denselben Gruppen-Namen tragen. Das ist das klassische Kanban-Board-Muster – Aufgaben zwischen "Offen", "In Arbeit" und "Erledigt" verschieben. Mit x-sort und dem .group-Modifier ist das ohne zusätzlichen JavaScript-Code möglich.

Der Callback, der an x-sort übergeben wird, erhält beim gruppenübergreifenden Verschieben die neue Position und den Ziel-Container. Das Alpine-Datenmodell muss dann entsprechend aktualisiert werden – das Element aus der Quellliste entfernen und in die Zielliste einfügen. Für einfache Kanban-Boards reicht dafür ein zentraler Alpine-Store, der beide Listen hält und den Callback-Handler bereitstellt.


<!-- Kanban-Board: Drag zwischen drei Spalten -->
<div x-data="{
  columns: {
    todo:  [{ id:1, text:'Feature A' }, { id:2, text:'Bug B' }],
    doing: [{ id:3, text:'Refactoring C' }],
    done:  [{ id:4, text:'Deploy D' }],
  },
  move(fromCol, toCol, itemId, newIndex) {
    const item = this.columns[fromCol].find(i => i.id === itemId);
    this.columns[fromCol] = this.columns[fromCol].filter(i => i.id !== itemId);
    this.columns[toCol].splice(newIndex, 0, item);
  }
}">
  <div class="grid grid-cols-3 gap-4">
    <template x-for="[colKey, colItems] in Object.entries(columns)" :key="colKey">
      <div class="bg-slate-50 rounded-xl p-4 min-h-48">
        <h3 class="font-bold text-sm uppercase tracking-wider text-slate-500 mb-3" x-text="colKey"></h3>
        <ul
          x-sort.group.kanban="(item, pos) => move(colKey, $el.dataset.col, parseInt(item), pos)"
          :data-col="colKey"
          class="space-y-2 min-h-8"
        >
          <template x-for="card in colItems" :key="card.id">
            <li
              x-sort:item="card.id"
              class="bg-white border border-slate-200 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 cursor-grab shadow-sm"
              x-text="card.text"
            ></li>
          </template>
        </ul>
      </div>
    </template>
  </div>
</div>

6. Callbacks: Auf Sortier-Events reagieren

Der Callback, der an die x-sort-Direktive übergeben wird, ist der zentrale Integrationspunkt zwischen der visuellen Drag-Drop-Interaktion und dem Alpine-Datenmodell. x-sort stellt zwei magische Variablen bereit: $item enthält den Wert, der mit x-sort:item gesetzt wurde (typischerweise eine ID), und $position enthält die neue nullbasierte Index-Position nach dem Loslassen. Diese beiden Werte sind alles, was nötig ist, um das Datenmodell zu aktualisieren und eine Server-Anfrage abzuschicken.

Für komplexere Szenarien – z.B. wenn das Sortieren durch eine Validierung genehmigt werden muss oder wenn eine asynchrone Server-Anfrage fehlschlägt und die alte Reihenfolge wiederhergestellt werden soll – empfiehlt sich ein optimistic update-Muster: Alpine speichert die alte Reihenfolge vor dem Sortieren, sendet die Anfrage und stellt die alte Reihenfolge im Fehlerfall wieder her. Das ist dasselbe Muster, das auch bei Formularen und Mutation-Requests verwendet wird, und passt sich natürlich in Alpine's Fetch-Integration ein.

7. Server-Synchronisation nach dem Sortieren

Reine clientseitige Sortierung ist in vielen Admin-Panel-Szenarien nicht ausreichend: Die neue Reihenfolge muss persistent auf dem Server gespeichert werden, damit sie nach einem Reload erhalten bleibt. Das Muster dafür ist klar: Im x-sort-Callback wird nach dem Aktualisieren des Alpine-Datenmodells ein Fetch-Request abgeschickt, der die neue Reihenfolge (als Array von IDs oder als ID-Position-Mapping) an den Server sendet.

Wichtig ist die Fehlerbehandlung: Wenn der Server-Request fehlschlägt, muss die alte Reihenfolge wiederhergestellt werden. Das optimistic update-Pattern sieht vor, die Reihenfolge sofort im Frontend zu aktualisieren (für sofortiges Nutzer-Feedback), den Request asynchron zu senden und bei einem Fehler das Frontend auf den alten Stand zurückzusetzen. Eine kleine Toast-Benachrichtigung informiert den Nutzer über den Fehler, ohne den gesamten Workflow zu blockieren.


<!-- Sortierbare Liste mit Server-Synchronisation und optimistic update -->
<div x-data="{
  items: [], // Wird vom Server geladen
  saving: false,
  error: null,
  previousOrder: [],

  async init() {
    const r = await fetch('/api/categories/order');
    this.items = await r.json();
  },

  async reorder(itemId, newPosition) {
    // Optimistic: update Alpine state immediately
    this.previousOrder = [...this.items];
    const idx = this.items.findIndex(i => i.id === parseInt(itemId));
    const [moved] = this.items.splice(idx, 1);
    this.items.splice(newPosition, 0, moved);

    // Persist to server
    this.saving = true;
    this.error = null;
    try {
      const response = await fetch('/api/categories/order', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ order: this.items.map((item, i) => ({ id: item.id, position: i })) }),
      });
      if (!response.ok) throw new Error('Server-Fehler ' + response.status);
    } catch (e) {
      // Rollback on failure
      this.items = this.previousOrder;
      this.error = 'Reihenfolge konnte nicht gespeichert werden.';
    } finally {
      this.saving = false;
    }
  }
}">
  <div class="flex items-center gap-3 mb-4">
    <h2 class="font-bold text-slate-800">Kategorien sortieren</h2>
    <span x-show="saving" class="text-xs text-teal-600 animate-pulse">Speichern…</span>
    <span x-show="error" x-text="error" class="text-xs text-red-600"></span>
  </div>

  <ul x-sort="reorder($item, $position)" class="space-y-2">
    <template x-for="item in items" :key="item.id">
      <li x-sort:item="item.id"
          class="flex items-center gap-3 bg-white border border-slate-200 rounded-xl px-4 py-3 cursor-grab shadow-sm">
        <span x-sort:handle class="text-slate-300 hover:text-slate-500 cursor-grab">⠿</span>
        <span x-text="item.name" class="flex-1 font-medium text-slate-800"></span>
      </li>
    </template>
  </ul>
</div>

8. x-sort vs. manuelle Drag-Drop-Alternativen im Vergleich

Für die Entscheidung, ob x-sort oder eine externe Bibliothek die richtige Wahl ist, helfen konkrete Vergleichspunkte: Bundle-Größe, Feature-Umfang, Integration in Alpine und Wartungsaufwand.

Kriterium x-sort (Alpine Plugin) SortableJS Natives HTML Drag & Drop
Bundle-Größe ~3 KB gzipped ~15 KB gzipped 0 KB (nativ)
Alpine-Integration Nativ, reaktiv Manueller DOM-Abgleich nötig Vollständig manuell
Drag-Handles x-sort:handle handle-Option Manuell implementieren
Gruppen/Kanban .group-Modifier group-Option Sehr komplexe Eigenimpl.
Touch-Support Browser-abhängig Eingebaut Kein nativer Touch-Support
Wartungsaufwand Minimal (HTML-Attribute) Moderate JS-Konfiguration Hoch (viele Event-Handler)

Touch-Support ist der einzige signifikante Vorteil von SortableJS gegenüber x-sort: SortableJS nutzt einen eigenen Touch-Event-Polyfill, der auf Mobilgeräten funktioniert, während die native HTML Drag and Drop API auf Touch-Geräten eingeschränkt ist. Für Admin-Panels, die überwiegend auf Desktop genutzt werden, ist x-sort die bessere Wahl. Für mobile-first Drag-Drop-Szenarien – z.B. Bild-Reihenfolge auf einem mobilen Theme-Editor – sollte SortableJS oder eine spezialisierte Touch-Bibliothek evaluiert werden.

Mironsoft

Alpine.js Admin-Panel Komponenten für Magento 2

Sortierbare Admin-Panels und Kanban-Boards für Magento 2?

Wir bauen sortierbare Produkt-, Kategorie- und Content-Listen mit x-sort für Magento 2, implementieren Server-Synchronisation und integrieren die Komponenten sauber in das Hyvä-Admin-Ökosystem.

Drag-and-Drop-Listen

x-sort mit Handles, Gruppen und Server-Sync für Magento-Admin-Panels

Kanban-Boards

Spaltenübergreifendes Verschieben mit optimistic updates und Rollback

Persistenz

REST-API- oder GraphQL-Integration für dauerhafte Reihenfolge-Speicherung

9. Zusammenfassung

x-sort löst das Drag-and-Drop-Sortier-Problem in Alpine.js deklarativ und ohne externe Abhängigkeiten. Die Kombination aus x-sort auf dem Container, x-sort:item auf den Elementen, x-sort:handle für dedizierte Drag-Griffe und dem .group-Modifier für spaltenübergreifendes Verschieben deckt alle gängigen Sortier-Szenarien in Admin-Panels ab. Der Callback-Parameter liefert $item und $position für die Synchronisation mit dem Alpine-Datenmodell und dem Server.

Der einzige signifikante Vorbehalt gegenüber SortableJS ist Touch-Support auf Mobilgeräten. Für Desktop-orientierte Admin-Panels – das typische Magento 2-Backend oder ein Hyvä-basiertes CMS – ist x-sort die sauberere, schlankere und wartbarere Lösung. Die direkte Integration in Alpine's Reaktivitätssystem eliminiert den manuellen DOM-State-Abgleich, der bei externen Bibliotheken immer ein potenzielle Quelle von Bugs ist.

x-sort in Alpine.js — Das Wichtigste auf einen Blick

Grundsetup

x-sort auf dem Container + x-sort:item auf den Kindelementen. Plugin via npm oder CDN installieren und vor Alpine.start() registrieren.

Drag-Handles

x-sort:handle auf einem Kindelement macht nur dieses zum Drag-Trigger. Verhindert unbeabsichtigte Drags beim Klicken auf Buttons und Links.

Gruppen / Kanban

x-sort.group.NAME auf mehreren Containern erlaubt spaltenübergreifendes Verschieben. $item und $position im Callback für Datenmodell-Sync.

Server-Sync

Optimistic update: Alpine-State sofort aktualisieren, fetch-Request senden, bei Fehler auf previousOrder zurücksetzen. Toast-Benachrichtigung für Nutzer-Feedback.

10. FAQ: Alpine.js x-sort

1Was ist x-sort in Alpine.js?
Offizielles Alpine.js-Plugin für deklarative Drag-and-Drop-Sortierung. Nutzt die native HTML Drag and Drop API und ist reaktiv in Alpine integriert – kein manuelles DOM-Auslesen nötig.
2Brauche ich SortableJS für x-sort?
Nein. x-sort ist eigenständig, ohne Abhängigkeit zu SortableJS. Installation: npm install @alpinejs/sort oder CDN-Script-Tag vor dem Alpine-Script.
3Wie registriere ich x-sort?
Alpine.plugin(sort) vor Alpine.start() aufrufen. Beim CDN: Plugin-Script-Tag vor Alpine-Script-Tag platzieren.
4Wie funktioniert x-sort:item?
Markiert ein Element als sortierbar und weist ihm einen Wert zu (typisch ID). Dieser Wert steht im Callback als $item zur Verfügung.
5Drag-Handle mit x-sort bauen?
x-sort:handle auf einem Kindelement des Items. Nur dieses Element startet dann den Drag. Cursor grab/grabbing und ein Drag-Icon empfehlenswert.
6Kanban: Zwischen Listen verschieben?
Alle Container bekommen denselben .group.NAME-Modifier: x-sort.group.kanban. Callback aktualisiert das Alpine-Datenmodell (aus Quellliste entfernen, in Zielliste einfügen).
7Was sind $item und $position?
$item ist der mit x-sort:item gesetzte Wert (typisch ID). $position ist der nullbasierte Index der neuen Position nach dem Loslassen.
8Neue Reihenfolge auf dem Server speichern?
Im Callback Alpine-State aktualisieren und fetch-Request senden. Optimistic update: sofort im Frontend anzeigen, bei Fehler auf previousOrder zurücksetzen.
9Touch-Support bei x-sort?
Eingeschränkt. Native HTML Drag and Drop API hat keine vollständige Touch-Unterstützung. Für mobile Sortier-Szenarien SortableJS mit Touch-Polyfill evaluieren.
10Vorteil von x-sort vs. manuelle drag-Events?
x-sort ersetzt 50-100 Zeilen Event-Handler-Code durch 2 HTML-Attribute. Reaktive Alpine-Integration eliminiert DOM-State-Abgleich. Gruppen und Handles ohne zusätzlichen JS-Code.