A common feature for web apps is sortable lists. SortableJS is one of my favorite JavaScript libraries and I missed it when developing with Blazor. To remedy this, I decided to wrap SortableJS to make it a Blazor component, named Bazor Sortable, that I have made open source on GitHub that I think you will love. In this post I will walk you through how to add it into your own Blazor web apps.
Note: Blazor Sortable is an open-source community component and not an official component from Microsoft. The Fluent UI for Blazor team has integrated a sortable component for Fluent UI for Blazor. You can try the Fluent UI Sortable Demo today.
Check the demo out here: https://blazorsortable.theurlist.com
Every Friday, Jon Galloway (you’ve never heard of him but he’s cool trust) and I work on rebuilding a real app called theurlist.com in Blazor. The stream is called “Burke Learns Blazor” on Twitch and .NET YouTube (Like and Subscribe!). And we’d love for you to join us. Mostly because we need all the help we can get with this thing because I have no idea what I’m doing.
We ended up needing a sortable list component for this rebuild, and while there are a few “Blazor Sortable” examples out there, I kinda had my heart set on SortableJS. SortableJS is a brilliant library for building sortable lists of items with virtually every feature you could need – sorting, sorting between lists, cloning items, filtering, custom animation easing, lumbar support. OK – not that last one, but that’s, like, that’s the only thing it doesn’t have.
So with a little help from Steve Sanderson, we built a simple abstraction on SortableJS that you can drop in and use in your own apps. Let’s take a look at how to use and customize Blazor Sortable for your own Blazor apps.
Using Blazor Sortable
The GitHub repo for Blazor Sortable contains the source code for the sortable list as well as demos. For your own project, all you need is the Shared/SortableList.razor
, Shared/SortableList.razor.css
and Shared/SortableList.razor.js
files.
The SortableList
component is a generic component that takes a list of items and SortableItemTemplate
that defines how to render each item in the sortable list. For instance, let’s say that you have a list of books that looks like this…
public class Book
{
public string Title { get; set; } = "";
public string Author { get; set; } = "";
public int Year { get; set; }
}
public List<Book> books = new List<Book>
{
new Book { Title = "The Very Hungry Caterpillar", Author = "Eric Carle", Year = 1969 },
new Book { Title = "Where the Wild Things Are", Author = "Maurice Sendak", Year = 1963 },
new Book { Title = "Goodnight Moon", Author = "Margaret Wise Brown", Year = 1947 },
new Book { Title = "The Cat in the Hat", Author = "Dr. Seuss", Year = 1957 },
new Book { Title = "Charlotte's Web", Author = "E.B. White", Year = 1952 },
new Book { Title = "Harry Potter and the Sorcerer's Stone", Author = "J.K. Rowling", Year = 1997 },
new Book { Title = "The Lion, the Witch and the Wardrobe", Author = "C.S. Lewis", Year = 1950 },
new Book { Title = "Matilda", Author = "Roald Dahl", Year = 1988 },
new Book { Title = "The Giving Tree", Author = "Shel Silverstein", Year = 1964 },
new Book { Title = "Oh, the Places You'll Go!", Author = "Dr. Seuss", Year = 1990 }
};
You can render this list in a SortableList
like this…
<div>
<SortableList Items="books" Context="book">
<SortableItemTemplate>
<div class="book">
<p>@book.Title</p>
</div>
</SortableItemTemplate>
</SortableList>
</div>
The SortableList
component will render the list of items using the SortableItemTemplate
and then make the list sortable using SortableJS. The Context
parameter is used to define the name of the variable that will be used to represent each item in the list. In this case, the Context
is book
and so each item in the list will be represented by a variable called book
.
However, if you were to try and drag and drop items around at this point, you would notice that whenever you drop one, it just goes back to where it was before. That’s because we haven’t told the SortableList
what to do when the list is sorted. We do that by handling the OnUpdate
event and doing the sorting ourselves.
<div>
<SortableList Items="books" Context="book" OnUpdate="@SortList">
<SortableItemTemplate>
<div class="book">
<p>@book.Title</p>
</div>
</SortableItemTemplate>
</SortableList>
</div>
...
public void SortList((int oldIndex, int newIndex) indices)
{
// deconstruct the tuple
var (oldIndex, newIndex) = indices;
var items = this.books;
var itemToMove = items[oldIndex];
items.RemoveAt(oldIndex);
if (newIndex < items.Count)
{{
items.Insert(newIndex, itemToMove);
}}
else
{{
items.Add(itemToMove);
}}
}
The OnUpdate
event handler will be called whenever the list is sorted. It will pass a tuple containing the old index and the new index of the item that was moved. In the SortList
method, we deconstruct the tuple into two variables and then use those to move the item in the list.
It’s SUPER important that you never ever mutate DOM that Blazor controls. Blazor keeps an internal copy of the DOM and if you change it with something like JavaScript, you will get bizarre results since the page state will be out of sync with Blazor’s internal state. So what we do behind the scenes here is cancel the JavaScript move so that the item doesn’t actually move on the page. Then we move the item in the list and Blazor will re-render the list with the new order.
A More Complex Example
SortableJS is a very powerful library and it can do a lot more than just sort lists. It can also sort between lists, clone items, filter items, and more. The SortableList
component supports many of these features. Let’s take a look at a more complex example – sorting between two lists…
<div>
<div class="container">
<div class="columns">
<div class="column">
<h3>Books</h3>
<SortableList Items="books" Context="book" OnRemove="@AddToFavoriteList" Group="favorites">
<SortableItemTemplate>
<div class="book">
<p>@book.Title</p>
</div>
</SortableItemTemplate>
</SortableList>
</div>
<div class="column">
<h3>Favorite Books</h3>
<SortableList Items="favoriteBooks" Context="book" OnRemove="@RemoveFromFavoriteList" Group="favorites">
<SortableItemTemplate>
<div class="book">
<p>@book.Title</p>
</div>
</SortableItemTemplate>
</SortableList>
</div>
</div>
</div>
</div>
In this example, we have two lists – a list of all books and a list of favorite books. They are linked together via the Group
property.
We want to be able to drag and drop books from the list of all books to the list of favorite books. To do that, we need to handle the OnRemove
event for both lists.
public void AddToFavoriteList((int oldIndex, int newIndex) indices)
{
var (oldIndex, newIndex) = indices;
var book = books[oldIndex];
favoriteBooks.Insert(newIndex, book);
books.RemoveAt(oldIndex);
}
public void RemoveFromFavoriteList((int oldIndex, int newIndex) indices)
{
var (oldIndex, newIndex) = indices;
var book = favoriteBooks[oldIndex];
books.Insert(newIndex, book);
favoriteBooks.RemoveAt(oldIndex);
}
Styling the SortableList
By default, the SortableList
contains some default styling that hides the “ghost” element while dragging. This will give you a gap between items as you are dragging. Without this style change, the item itself is shown as the drop target. This is a little weird because it means that the item you are dragging is also the item you are dropping on. But if that’s your jam, you can just override the styles in the SortableList.razor.css
file or just don’t include it at all.
Since all of the content rendered inside of a SortableList
is rendered inside of a SortableItemTemplate
child, you always have to use the “::deep” modifier for any changes to take effect.
If you style the SortableList
from a parent page/component (i.e. Index.razor.css) you MUST wrap the SortableList
in a container element and use the “::deep” modifier as well. If you don’t do this, your styles won’t take effect and you’ll be sad and confused and mad at me for making this component. This is a Blazor thing, not a SortableJS thing. You can read more about scope styles in the ASP.NET Core docs.
I feel like nobody is going to read that last paragraph and there will be much wailing and gnashing of teeth. But I tried. I’m sorry in advance.
Why not HTML5 Drag and Drop?
Fair question and one that I certainly looked into before going to a JavaScript solution. The long and short of it is that the native HTML5 support for drag and drop simply isn’t robust enough for a decent sortable. For instance, there is no way to style much of the behaviour of the drag and drop. It looks…goofy…and there isn’t anything you can really do about it. It also has pretty flaky support across browsers. There are some essential properties that only work in Chrome.
All of that said, SortableJS actually will try and use HTML5 drag and drop and fallback to a JavaScript solution on platforms like iOS. However, you still lose control over the styling and you get the goofy looking drag and drop. So I’ve got HTML5 turned off on the SortableList
. If you want it back on, go into the SortableList.razor.razor.js
file and remove the forceFallback: true
attribute. I should probably make this a setting at some point.
Get Blazor Sortable
Check out Blazor Sortable and let us know what you think! You can do a lot with it, including cloning items, disabling sorting on certain items, specifying drag handles and more. We haven’t implemented every single feature of SortableJS. Yet. Pull requests are welcome! 😉
Blazor Sortable is an open-source community project.
Nice work, thanks.
I tried to use it in my application, but the sorting doesn’t work correctly when you have a custom Blazor component inside the SortableItemTemplate instead of straight HTML. I posted an issue with a sample on the Github site for that.
Really nice, just simple. Thanks for sharing.
same kind of examples can also be found here.
https://preview.keenthemes.com/html/metronic/docs/general/draggable/swappable
https://shopify.github.io/draggable/
https://github.com/Shopify/draggable
Thank you for this, really cool! Do you think having the list rendered horizontally in a flexbox would disrupt the behavior somehow? In my OnUpdate method the correct indices are found but the item in the list which the oldIndex is pointing to is sometimes not the item being dragged, very confusing.
Maybe same issue as mine (see below)?
very nice, Thank you x)
Great work ! But...
... it seems that this lib does not support changing content or non uniform height items. If I change the content of a element to add a paragraph tag, then it do not work anymore. It seems that it work for limited, simple and basic use cases. I've struggle to make that kind of lib working in my real-world production développements (LOB). I forget it, and develop my own HTML5 lib that...
I need this! Thank you
Does this work with a Blazor Web application without interactivity? It seems `OnAfterRenderAsync` only works for interactive pages?
I don’t think this would work without interactivity since Blazor needs to re-render this component on every change
In the first paragraph the Blazor Sortable link is missing an l “Bazor Sortable”
Thanks for the component.
Got it! Ty!