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.
Inhaltsverzeichnis
- 1. Was x-sort in Alpine.js leistet
- 2. Installation und Setup des x-sort-Plugins
- 3. Einfache sortierbare Liste mit x-sort
- 4. Drag-Handles: Nur bestimmte Bereiche ziehbar machen
- 5. Gruppen: Elemente zwischen Listen verschieben
- 6. Callbacks: Auf Sortier-Events reagieren
- 7. Server-Synchronisation nach dem Sortieren
- 8. x-sort vs. manuelle Drag-Drop-Alternativen im Vergleich
- 9. Zusammenfassung
- 10. FAQ
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?
2Brauche ich SortableJS für x-sort?
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?
$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?
.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?
previousOrder zurücksetzen.