Skip to content

Commit 2bed076

Browse files
serpentbladeclaude
andcommitted
docs(sortable): document the imperative handle + flip comparison G1 to shipped
Showcase: new '### Imperative handle' section (getInstance/toArray/sort/option + per-framework ref grab + the data-id/itemKey tip). Comparison: matrix Imperative-handle cell ❌→✅ (4-verb $expose), footnote 6 rewritten, G1 gap-table row ⏳ Deferred → ✅ SHIPPED, a new Where-Rozie-wins handle bullet, and the now-obsolete 'no handle yet' caveat removed. Docs build: check-anchors 127/0 broken. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a471a2f commit 2bed076

2 files changed

Lines changed: 34 additions & 4 deletions

File tree

docs/guide/sortable-comparison.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Per-framework column = the de-facto leader for that framework (React = `@dnd-kit
3535
| **Two-way bound data array** | ❌⁵ | ✅ (`v-model`) | ~ (consider/finalize) | ❌⁵ | ❌⁵ | hand-roll |`r-model:items` |
3636
| Custom drag handle ||||| ~ | hand-roll |`$classSelector` |
3737
| Framework-native per-row slot/render |||||| hand-roll | ✅ scoped slot |
38-
| Imperative handle | ~ (context/sensors) | ~ (instance) | ~ | ~ (`CdkDropList`) | ~ | hand-roll | ❌⁶ |
38+
| Imperative handle | ~ (context/sensors) | ~ (instance) | ~ | ~ (`CdkDropList`) | ~ | hand-roll | ✅⁶ 4-verb `$expose` |
3939
| Latest-framework support | React 19 | Vue 3 | **Svelte 5** | Angular 21 | Solid 1.x (stale) || R18+/V3.4+/Sv5/Ng19+/Solid/Lit |
4040
| Actively maintained | ✅ (~2 yr cadence) | ~ ||||||
4141
| One source → all 6 frameworks ||||||||
@@ -48,7 +48,7 @@ Per-framework column = the de-facto leader for that framework (React = `@dnd-kit
4848

4949
**No two-way data binding.** `@dnd-kit`, `@angular/cdk`, and `@thisbeyond/solid-dnd` hand you a drag-end event (`onDragEnd` / `cdkDropListDropped` / drag store) and you mutate state yourself (CDK ships a `moveItemInArray` helper, but you call it). `vuedraggable` is the exception with real `v-model`. Rozie gives every target a two-way `items` array — pass an array, get a reordered array back, no manual `onChange → setState` wiring.
5050

51-
**Rozie's `SortableList` has no `$expose` imperative handle**it predates the Phase-21 `$expose` capability that every other `@rozie-ui` port ships. See [G1](#gap-status-what-shipped-what-s-still-deferred).
51+
**Rozie's `SortableList` now ships a uniform `$expose` imperative handle**`getInstance` (raw SortableJS instance escape hatch) / `toArray` / `sort` / `option`the *same* four verbs on all six targets, grabbed with each framework's native ref mechanism. The competitors all expose *something* (the dnd-kit context, the SortableJS instance, `CdkDropList`), but each its own way and per-framework; Rozie's is one shape everywhere. See [G1](#gap-status-what-shipped-what-s-still-deferred).
5252

5353
## Where Rozie wins today
5454

@@ -57,6 +57,7 @@ Per-framework column = the de-facto leader for that framework (React = `@dnd-kit
5757
- **Two-way bound `items` array** (`r-model:items`) on all six — the thing every dnd-kit / CDK / solid-dnd consumer wires by hand. Pass an array, render rows through the scoped default slot, get the reordered array back.
5858
- **Cross-list sync + nesting from one source**`SortableListPair` (atomic transfer across two bound arrays) and `SortableListNested` / `KanbanColumn` (reorderable columns of reorderable cards), the same `.rozie` compiled to all six targets.
5959
- **Custom drag handles via `$classSelector`** — resolves on every target including React's scoped-CSS (authored class names render literally; `$classSelector` lowers to the literal `".grip"` per target and typo-checks it against your `<style>` at compile time).
60+
- **A uniform imperative handle** (`$expose`) — `getInstance` / `toArray` / `sort` / `option`, the *same* four verbs on all six targets, grabbed with each framework's native ref. `getInstance()` is the raw-SortableJS escape hatch, so the full engine API is one hop away. The competitors all expose *something* (the dnd-kit context, the SortableJS instance, `CdkDropList`) — but each its own way, per framework. See the [showcase Imperative handle section](/guide/sortable-list#imperative-handle).
6061
- **The hard part solved once** — the SortableJS-direct-DOM-mutation-vs-framework-reconciler dance (the reason these wrappers exist at all) is encapsulated in `useSortableJS()` plus the [`$reconcileAfterDomMutation()`](/guide/features#r-external-and-reconcileafterdommutation-—-dom-the-framework-doesn-t-own) sigil, hardened against SortableJS's fragile fallback-mode event shapes, across all six keyed reconcilers.
6162

6263
The ✅ cells in Rozie's row are pinned per target by the [sortable-drag VR spec](https://github.com/One-Learning-Community/rozie.js/blob/main/tests/visual-regression/specs/sortable-drag.spec.ts) — which measures *Rozie's* behavior across targets and says nothing measured about the competitors' behavior.
@@ -67,7 +68,7 @@ This page concedes where the standalone libraries are genuinely ahead — that's
6768

6869
| Gap | Who has it | Severity | Rozie status |
6970
| --- | --- | --- | --- |
70-
| **G1 — Imperative `$expose` handle** | `@dnd-kit` (sensors/context), CDK (`CdkDropList`) | **Medium** | **⏳ Deferred**`SortableList` predates the Phase-21 `$expose` capability, so there is no uniform handle to reach the SortableJS instance or trigger a programmatic sort. Every other `@rozie-ui` port ships `$expose`; this is the holdout. |
71+
| **G1 — Imperative `$expose` handle** | `@dnd-kit` (sensors/context), CDK (`CdkDropList`) | **Medium** | **✅ SHIPPED**a uniform 4-verb handle (`getInstance` / `toArray` / `sort` / `option`) on all six targets, the same shape everywhere, grabbed with each framework's native ref. `getInstance()` is the raw-SortableJS escape hatch; rows now carry `data-id` so `toArray()` / `sort()` operate on the rendered key order. See the [showcase Imperative handle section](/guide/sortable-list#imperative-handle). |
7172
| G2 — Live reconcile of construction-time knobs | (engine-level) | Low | **⏳ Deferred (by design)**`forceFallback` / `swapThreshold` / `cloneable` are construction-time-only SortableJS knobs; changing them at runtime requires re-keying the component (the [documented re-mount pattern](/guide/sortable-list#remount-on-construction-time-only-changes)). The runtime-updatable props *are* live-reconciled via `instance.option()`. |
7273
| G3 — List virtualization for large datasets | `@dnd-kit` (+ virtualizers) | Medium | **⏳ Deferred** — SortableJS renders all rows; very large lists want windowing. dnd-kit composes with `@tanstack/virtual`; Rozie has no virtualization story yet. |
7374
| G4 — Multi-select / multi-drag | `@dnd-kit`, SortableJS MultiDrag plugin | Low | **⏳ Deferred** — SortableJS's `MultiDrag` plugin is not mounted, so dragging multiple rows at once isn't wired. (Plain SortableJS options pass through via `:options`; plugins need a mount Rozie doesn't yet bridge.) |
@@ -76,7 +77,6 @@ This page concedes where the standalone libraries are genuinely ahead — that's
7677
## Honest caveats
7778

7879
- **Modern React leans dnd-kit, not SortableJS.** `@dnd-kit` (~17.0M/wk) is the React drag-and-drop standard in 2026 — sensors, virtualization, a rich ecosystem — and `react-beautiful-dnd` (~2.32M/wk), though deprecated/archived, is still everywhere. Rozie wraps **SortableJS**, a different engine with a simpler DOM-mutation model and its own tradeoffs. For a single-React app that needs virtualization or dnd-kit's sensor model, dnd-kit is the better pick. Rozie's value is cross-framework reach plus the keyboard / a11y / two-way contract from one source — not "better than dnd-kit on React."
79-
- **No imperative handle yet (G1).** This is the one place `@rozie-ui/sortable-list` is behind its own sibling ports; it's the top roadmap item for this component.
8080
- **Angular CDK and svelte-dnd-action are first-rate native toolkits.** CDK (first-party, connected lists, keyboard) and svelte-dnd-action (FLIP animations, Svelte 5, actively maintained) are excellent *single-framework* choices. The matrix scores cross-framework reach, not single-framework ergonomics.
8181
- **`@rozie-ui/sortable-list` is `0.1.0`.** The surface (17 props / 5 events / scoped row slot) is stable and VR-pinned, but younger than the established libraries — and it inherits SortableJS's engine-level limitations (touch-fallback fragility, no windowing) along with its strengths.
8282

docs/guide/sortable-list.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,36 @@ The default slot receives `{ item, index }`:
107107

108108
To rename a slot param to a more readable local name in nested-template contexts, use the slot-param rename form `{ item: column }` — see [scoped slot params](/guide/features#slots-with-scoped-params).
109109

110+
### Imperative handle
111+
112+
Beyond props and the two-way `items` array, the component exposes imperative methods declared once in the Rozie source via `$expose`. Grab a handle with your framework's native ref mechanism (React `useRef` / Vue template ref / Svelte `bind:this` / Angular `viewChild` / Solid callback ref / the Lit custom element itself) and call them directly:
113+
114+
| Method | Description |
115+
| --- | --- |
116+
| `getInstance` | Return the underlying SortableJS instance for direct API access — the raw-engine escape hatch (`save`, `closest`, … are one hop away). `null` before mount and after destroy. |
117+
| `toArray` | Return the current order as an array of `data-id` strings. Each row carries `data-id="<key>"` (the same [`itemKey`](#api)-derived key as the reconciler), so the array reflects the live key order. `[]` before mount. |
118+
| `sort` | Reorder the list by an array of `data-id` strings — `sort(order, useAnimation = true)`. |
119+
| `option` | Read or set a live SortableJS option — `option(name)` gets, `option(name, value)` sets. The runtime escape hatch for any SortableJS option beyond the curated props (and the construction-time-only ones, within SortableJS's own limits). |
120+
121+
**React example:**
122+
123+
```tsx
124+
import { useRef } from 'react';
125+
import { SortableList, type SortableListHandle } from '@rozie-ui/sortable-list-react';
126+
127+
const sl = useRef<SortableListHandle>(null);
128+
// <SortableList ref={sl} itemKey="id" ... />
129+
const order = sl.current?.toArray(); // current key order
130+
sl.current?.option('disabled', true); // disable at runtime
131+
const instance = sl.current?.getInstance(); // raw SortableJS instance
132+
```
133+
134+
The four verb names are clear of all sixteen prop names and the five events (`option` is a distinct identifier from the `options` prop), so the `$expose` collision discipline (ROZ121) passes with no renames.
135+
136+
::: tip `toArray` / `sort` rely on `data-id`
137+
Each rendered row carries `data-id="<key>"`, derived from [`itemKey`](#api) (falling back to the item value, then the index). Set `itemKey` for object lists so `toArray()` / `sort()` operate on stable keys rather than `"[object Object]"`.
138+
:::
139+
110140
## Recipes
111141

112142
### Drag handle

0 commit comments

Comments
 (0)