Skip to content

Add Sortable Component (Based on sortable.js)#209

Merged
mathewtaylor merged 7 commits intoblazorblueprintui:developfrom
russkyc-forked-repos:feat/add-sortable-component
Mar 17, 2026
Merged

Add Sortable Component (Based on sortable.js)#209
mathewtaylor merged 7 commits intoblazorblueprintui:developfrom
russkyc-forked-repos:feat/add-sortable-component

Conversation

@russkyc
Copy link
Copy Markdown
Contributor

@russkyc russkyc commented Mar 5, 2026

Description

Adds a new BbSortable<TItem> component to BlazorBlueprint.Components, providing drag-and-drop sortable lists and grids powered by Sortable.js. Includes a new SortableLayout enum (List/Grid), JS interop module (wwwroot/js/sortable.js) with a bundled Sortable.js library, component styling, and a full demo page with multiple examples (basic list, drag handle, connected lists, grid layout, and Kanban-style board). Updates the demo components index and sidebar navigation, and updates the Components API surface snapshot baseline.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)

Testing Checklist

  • Blazor Server
  • Blazor WebAssembly
  • Blazor Hybrid (MAUI)
  • Keyboard navigation / accessibility
  • Dark mode

Related Issues

None

@russkyc russkyc changed the base branch from main to develop March 5, 2026 08:59
Copilot AI and others added 4 commits March 5, 2026 09:07
…layout, and comprehensive demo

- Extract BbSortable code to BbSortable.razor.cs (partial class)
- Add @namespace BlazorBlueprint.Components to BbSortable.razor
- Rename T→TItem and SortableItemTemplate→ItemTemplate
- Fix ForceFallback bug (param was defined but never passed to JS init())
- Add OnAdd EventCallback + OnAddJS [JSInvokable] for clean cross-list tracking
- Add WAI-ARIA: role=list, aria-label, sr-only live region announcements
- Add Layout (SortableLayout.List/Grid), Class, AriaLabel, AdditionalAttributes params
- Add SortableLayout enum (List, Grid)
- Update sortable.js: add onAdd handler, forceFallback now correctly wired from .NET
- Create SortableDemo.razor: basic list, handle, multiple lists, grid, kanban
- Create CodeExamples/Components/Sortable/{basic,handle,multiple-lists,grid,kanban}.txt
- Add Sortable to DemoSidebar.razor navigation (between Spinner and Split Button)
- Add Sortable card to Pages/Components/Index.razor
- Update API surface baseline to include BbSortable and SortableLayout

Co-authored-by: russkyc <32549126+russkyc@users.noreply.github.com>
Co-authored-by: russkyc <32549126+russkyc@users.noreply.github.com>
…-component-demo

feat(sortable): refactor BbSortable, add WAI-ARIA, OnAdd event, grid layout, and comprehensive demo
@russkyc russkyc marked this pull request as ready for review March 5, 2026 17:19
@russkyc
Copy link
Copy Markdown
Contributor Author

russkyc commented Mar 5, 2026

Hi @mathewtaylor , this is a basic implementation of the sortable component. This would cater to both sorting and drag and drop. Here are some previews:

compressO-basic-list.mp4
compressO-connected-lists.mp4
compressO-grid.mp4
compressO-kanban.mp4

Some quirks:

  • It seems that in server-side rendering mode, the dom updates twice when commiting the sort. First from sortable.js and then from blazor, which causes a split second flicker to the previous dom state.
  • if the sortable item tempalte is a component it needs to be wrapped in a div, something reported in other implementations of the sortable.js library in blazor.

I don't know if this is the best route because of the quirks, but it could be a starting point. I can't seem to get the quirks ironed out even with copilot.

@russkyc russkyc changed the title [In Progress] Add Sortable Component (DragDrop & Sort) Add Sortable Component (Based on sortable.js) Mar 5, 2026
@mathewtaylor
Copy link
Copy Markdown
Contributor

Hi @russkyc , thanks for putting this together and another contribution to the project! Really cool to see drag-and-drop support coming to the library. I've had a look through the changes and have some feedback before we merge this in.

  1. No destroy() call on the Sortable instance
    DisposeAsync disposes the JS module reference but never calls sortable.destroy() on the actual SortableJS instance. This will leak event listeners. The init() function should return the Sortable instance (or a wrapper), and we need a dispose/destroy export that gets called from DisposeAsync.

  2. pull default logic is off in the JS
    In sortable.js, pull: pull || true means pull can never actually be set to false — the || will always fall through to true. Should be something like pull ?? true or a proper null check.

  3. No re-initialization on parameter changes
    If Items, Group, Handle, Sort, etc. change after first render, the Sortable instance is never updated since it only initializes on firstRender. Users would need to dispose and recreate the whole component to change options, which isn't great.

  4. Missing @key on the foreach loop
    The @foreach in the razor file doesn't use @key. For a sortable list where items get reordered, this can cause Blazor's diffing to produce incorrect DOM updates, especially with more complex ItemTemplate content.

  5. Items should be IList instead of List
    This forces consumers to use List specifically. IList would be more flexible and consistent with how we do things elsewhere.

  6. Scoped CSS might not reach ghost elements
    The .razor.css uses ::deep .sortable-ghost, but SortableJS can create ghost/fallback elements outside the component's DOM tree (e.g., appended to when fallbackOnBody is true). The scoped CSS attribute won't be on those elements, so the styles may not apply.

  7. Guid.NewGuid() as default for Id and Group The issue here is that these are public [Parameter] properties with Guid.NewGuid() as the default value — meaning a new GUID is generated on every component instance, and the value could change across renders/prerendering. Other components in the codebase avoid this by using Guid.NewGuid() on private fields instead (e.g., BbCheckbox uses private string generatedId with lazy initialization, BbTabsList uses private string _componentId). Even better, the Primitives layer has IdGenerator.GenerateId() which uses a thread-safe incrementing counter for deterministic, short IDs like shadcn-1, shadcn-2 — worth using that here instead (e.g., IdGenerator.GenerateId("sortable")).

As usual, if you need my help with any of these, just let me know and I'll be happy to lend a hand.

Cheers,
Mathew

@russkyc
Copy link
Copy Markdown
Contributor Author

russkyc commented Mar 15, 2026

Hi @mathewtaylor , I was preoccupied with a few things so I wasn't able to get back on this. Please take over if possible, it would be very appreciated.

mathewtaylor added a commit that referenced this pull request Mar 16, 2026
Complete the Sortable component work from PR #209, addressing all 7
review items from mathewtaylor's feedback:

1. Add destroy() call — JS uses Map-based instance storage with explicit
   destroy export, called from DisposeAsync with exception guard.
2. Fix pull default — change `pull || true` to `pull ?? true`.
3. Re-init on parameter changes — track previous config values in
   OnParametersSet, destroy + re-init in OnAfterRenderAsync when changed.
4. Add @key on foreach — wrap each item in `<div @key="item">` for
   stable Blazor diffing.
5. IList<TItem> — change Items parameter from List<TItem> to IList<TItem>.
6. Global CSS for ghost elements — move sortable-ghost/sortable-fallback
   styles to blazorblueprint-input.css, delete scoped .razor.css.
7. IdGenerator — replace Guid.NewGuid() defaults with
   IdGenerator.GenerateId("sortable") / ("sortable-group").

Additionally:
- Split into Primitive + Component two-layer architecture. The Primitive
  (BlazorBlueprint.Primitives.Sortable) owns all JS interop, lifecycle,
  ARIA, and disposal. The Component is a thin styled wrapper adding
  SortableLayout enum and Tailwind classes.
- Move JS + SortableJS library to Primitives wwwroot.
- Use snapshot overlay pattern to prevent visible flash during DOM
  revert on drop (clone container over real element during revert→
  re-render cycle).
- Add Primitive demo page, sidebar nav entry, and index card.
- Remove wrapper div requirement from demos (automatic via @key wrapper).
- Update API surface snapshots for both Primitives and Components.
@mathewtaylor mathewtaylor merged commit af923da into blazorblueprintui:develop Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants