Drag and drop has become a popular interface solution in modern applications. It's common to find it in productivity tools, great examples of this are Trello, JIRA and Notion. As well as being an intuitive interface for the user, it can definitely add a bit of "eye-candy" to an application.

We've been thinking about incorporating drag and drop into some screens of the product my team are building at work. This has given me a great opportunity to see how drag and drop can be accomplished with Blazor.

This post is going to cover what I've found while I've been experimenting and a walk through of a simple prototype app I built to test things out. Below is a sneak peak of the finished prototype.

The code for this post is available on GitHub.

The drag and drop API - A brief introduction

The drag and drop API is part of the HTML5 spec and has been around for a long time now. The API defines a set of events and interfaces which can be used to build a drag and drop interface.

Events

  • drag
    Fires when a dragged item (element or text selection) is dragged.
  • dragend
    Fires when a drag operation ends, such as releasing a mouse button or hitting the Esc key.
  • dragenter
    Fires when a dragged item enters a valid drop target.
  • dragexit
    Fires when an element is no longer the drag operation's immediate selection target.
  • dragleave
    Fires when a dragged item leaves a valid drop target.
  • dragover
    Fires when a dragged item is being dragged over a valid drop target, every few hundred milliseconds.
  • dragstart
    Fires when the user starts dragging an item.
  • drop
    Fires when an item is dropped on a valid drop target.

Certain events will only fire once during a drag-and-drop interaction such as dragstart and dragend. However, others will fire repeatedly such as drag and dragover.

Interfaces

There are a few interfaces for drag and drop interactions but the key ones are the DragEvent interface and the DataTransfer interface.

The DragEvent interface is a DOM event which represents a drag and drop interaction. It contains a single property, dataTransfer, which is a DataTransfer object.

The DataTransfer interface has several properties and methods available. It contains information about the data being transferred by the interaction as well as methods to add or remove data from it.

Properties

  • dropEffect
    Gets the type of drag-and-drop operation currently selected or sets the operation to a new type. The value must be none, copy, link or move.
  • effectAllowed
    Provides all of the types of operations that are possible. Must be one of none, copy, copyLink, copyMove, link, linkMove, move, all or uninitialized.
  • files
    Contains a list of all the local files available on the data transfer. If the drag operation doesn't involve dragging files, this property is an empty list.
  • items
    Gives a DataTransferItemList object which is a list of all of the drag data.
  • types
    An array of strings giving the formats that were set in the dragstart event.

Methods

  • DataTransfer.clearData()
    Remove the data associated with a given type. The type argument is optional. If the type is empty or not specified, the data associated with all types is removed. If data for the specified type does not exist, or the data transfer contains no data, this method will have no effect.
  • DataTransfer.getData()
    Retrieves the data for a given type, or an empty string if data for that type does not exist or the data transfer contains no data.
  • DataTransfer.setData()
    Set the data for a given type. If data for the type does not exist, it is added at the end, such that the last item in the types list will be the new format. If data for the type already exists, the existing data is replaced in the same position.
  • DataTransfer.setDragImage()
    Set the image to be used for dragging if a custom one is desired.

Drag and drop API in Blazor

As with most UI events, Blazor has C# representations for the drag and drop API. Below is the UIDragEventArgs and DataTransfer classes which represents the DragEvent and DataTransfer interfaces I mentioned earlier.

/// <summary>
/// Supplies information about an drag event that is being raised.
/// </summary>
public class UIDragEventArgs : UIMouseEventArgs
{
    /// <summary>
    /// The data that underlies a drag-and-drop operation, known as the drag data store.
    /// See <see cref="DataTransfer"/>.
    /// </summary>
    public DataTransfer DataTransfer { get; set; }
}
/// <summary>
/// The <see cref="DataTransfer"/> object is used to hold the data that is being dragged during a drag and drop operation.
/// It may hold one or more <see cref="UIDataTransferItem"/>, each of one or more data types.
/// For more information about drag and drop, see HTML Drag and Drop API.
/// </summary>
public class DataTransfer
{
    /// <summary>
    /// Gets the type of drag-and-drop operation currently selected or sets the operation to a new type.
    /// The value must be none, copy, link or move.
    /// </summary>
    public string DropEffect { get; set; }

    /// <summary>
    /// Provides all of the types of operations that are possible.
    /// Must be one of none, copy, copyLink, copyMove, link, linkMove, move, all or uninitialized.
    /// </summary>
    public string EffectAllowed { get; set; }

    /// <summary>
    /// Contains a list of all the local files available on the data transfer.
    /// If the drag operation doesn't involve dragging files, this property is an empty list.
    /// </summary>
    public string[] Files { get; set; }

    /// <summary>
    /// Gives a <see cref="UIDataTransferItem"/> array which is a list of all of the drag data.
    /// </summary>
    public UIDataTransferItem[] Items { get; set; }

    /// <summary>
    /// An array of <see cref="string"/> giving the formats that were set in the dragstart event.
    /// </summary>
    public string[] Types { get; set; }
}

This was a great start to my investigation, however, it was short lived. After a quick bit of experimenting, it seems at this point in time there isn't a way to populate these values and pass data around using them. At least from C#, which is my goal at the moment. What is available though are the various events of the drag and drop API, I just needed to come up with a way of tracking the data as it moved about.

Building the prototype - A todo list

As you have seen from the gif at the start of this post, the prototype is a highly original todo list. I set myself some goals I wanted to achieve from the exercise, they were:

  • Be able to track an item being dragged
  • Control where items could be dropped
  • Give a visual indicator to the user where items could be dropped or not dropped
  • Update an item on drop
  • Feedback when an item has been updated

Overview

My solution ended up with three components, JobsContainer, JobList and Job which are used to manipulate a list of JobModels.

public class JobModel
{
    public int Id { get; set; }
    public JobStatuses Status { get; set; }
    public string Description { get; set; }
    public DateTime LastUpdated { get; set; }
}

public enum JobStatuses
{
    Todo,
    Started,
    Completed
}

The JobsContainer is responsible for overall list of jobs, keeping track of the job being dragged and raising an event whenever a job is updated.

The JobsList component represents a single job status, it creates a drop-zone where jobs can be dropped and renders any jobs which have its status.

The Job component renders a JobModel instance. If the instance is dragged then it lets the JobsContainer know so it can be tracked.

JobsContainer Component

<div class="jobs-container">
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
</div>

@code {
    [Parameter] public List<JobModel> Jobs { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }
    [Parameter] public EventCallback<JobModel> OnStatusUpdated { get; set; }

    public JobModel Payload { get; set; }

    public async Task UpdateJobAsync(JobStatuses newStatus)
    {
        var task = Jobs.SingleOrDefault(x => x.Id == Payload.Id);

        if (task != null)
        {
            task.Status = newStatus;
            task.LastUpdated = DateTime.Now;
            await OnStatusUpdated.InvokeAsync(Payload);
        }
    }
}

Its main job (no pun intended!) is to coordinate updates to jobs as they are moved about the various statuses. It takes a list of JobModel as a parameter as well as exposing an event which consuming components can handle to know when a job gets updated.

It passes itself as a CascadingValue to the various JobsList components, which are child components. This allows them access to the list of jobs as well as the UpdateJobAsync method, which is called when a job is dropped onto a new status.

JobsList Component

<div class="job-status">
    <h3>@ListStatus (@Jobs.Count())</h3>

    <ul class="dropzone @dropClass" ondragover="event.preventDefault();"
        @ondrop="HandleDrop"
        @ondragenter="HandleDragEnter"
        @ondragleave="HandleDragLeave">

        @foreach (var job in Jobs)
        {
            <Job JobModel="job" />
        }

    </ul>
</div>

@code {

    [CascadingParameter] JobsContainer Container { get; set; }
    [Parameter] public JobStatuses ListStatus { get; set; }
    [Parameter] public JobStatuses[] AllowedStatuses { get; set; }

    List<JobModel> Jobs = new List<JobModel>();
    string dropClass = "";

    protected override void OnParametersSet()
    {
        Jobs.Clear();
        Jobs.AddRange(Container.Jobs.Where(x => x.Status == ListStatus));
    }

    private void HandleDragEnter()
    {
        if (ListStatus == Container.Payload.Status) return;

        if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status))
        {
            dropClass = "no-drop";
        }
        else
        {
            dropClass = "can-drop";
        }
    }

    private void HandleDragLeave()
    {
        dropClass = "";
    }

    private async Task HandleDrop()
    {
        dropClass = "";

        if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status)) return;

        await Container.UpdateJobAsync(ListStatus);
    }
}

There is quite a bit of code so let's break it down.

[Parameter] JobStatuses ListStatus { get; set; }
[Parameter] JobStatuses[] AllowedStatuses { get; set; }

The component takes a ListStatus and array of AllowedStatuses. The AllowedStatuses are used by the HandleDrop method to decide if a job can be dropped or not.

The ListStatus is the job status that the component instance is responsible for. It's used to fetch the jobs from the JobsContainer component which match that status so the component can render them in its list.

This is performed using the OnParametersSet lifecycle method, making sure to clear out the list each time to avoid duplicates.

protected override void OnParametersSet()
{
    Jobs.Clear();
    Jobs.AddRange(Container.Jobs.Where(x => x.Status == ListStatus));
}

I'm using an unordered list to display the jobs. The list is also a drop-zone for jobs, meaning you can drop other elements onto it. This is achieved by defining the ondragover event, but note there's no @ symbol in-front of it. This isn't a typo.

<ul class="dropzone @dropClass" ondragover="event.preventDefault();"
    @ondrop="HandleDrop"
    @ondragenter="HandleDragEnter"
    @ondragleave="HandleDragLeave">

    @foreach (var job in Jobs)
    {
        <Job JobModel="job" />
    }

</ul>

The event is just a normal JavaScript event, not a Blazor version, calling preventDefault. The reason for this is that by default you can't drop elements onto each other. By calling preventDefault it stops this default behaviour from occurring.

The rest of the events are all Blazor versions. OnDragEnter and OnDragLeave are both used to set the CSS of for the drop-zone.

private void HandleDragEnter()
{
    if (ListStatus == Container.Payload.Status) return;

    if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status))
    {
        dropClass = "no-drop";
    }
    else
    {
        dropClass = "can-drop";
    }
}

private void HandleDragLeave()
{
    dropClass = "";
}

HandleDragEnter manages the border of the drop-zone to give the user visual feedback if a job can be dropped.

If the job being dragged has the same status as the drop-zone it's over then nothing happens. If a job is dragged over the drop-zone, and it's a valid target, then a green border is added via the can-drop CSS class. If it's not a valid target then a red border is added via the no-drop CSS class.

The HandleDragLeave method just resets the class once the job has been dragged away.

private async Task HandleDrop()
{
    dropClass = "";

    if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status)) return;

    await Container.UpdateJobAsync(ListStatus);
}

Finally, HandleDrop is responsible for making sure a job is allowed to be dropped, and if so, updating its status via the JobsContainer.

Job Component

<li class="draggable" draggable="true" title="@JobModel.Description" @ondragstart="@(() => HandleDragStart(JobModel))">
    <p class="description">@JobModel.Description</p>
    <p class="last-updated"><small>Last Updated</small> @JobModel.LastUpdated.ToString("HH:mm.ss tt")</p>
</li>

@code {
    [CascadingParameter] JobsContainer Container { get; set; }
    [Parameter] public JobModel JobModel { get; set; }

    private void HandleDragStart(JobModel selectedJob)
    {
        Container.Payload = selectedJob;
    }
}

It's responsible for displaying a JobModel and for making it draggable. Elements are made draggable by adding the draggable="true" attribute. The component is also responsible for handling the ondragstart event.

When ondragstart fires the component assigns the job to the JobsContainers Payload property. This keeps track of the job being dragged which is used when handling drop events, as we saw in the JobsList component.

Usage

Now we've gone through each component let's see what it looks like all together.

<JobsContainer Jobs="Jobs" OnStatusUpdated="HandleStatusUpdated">
    <JobList ListStatus="JobStatuses.Todo" AllowedStatuses="@(new JobStatuses[] { JobStatuses.Started})" />
    <JobList ListStatus="JobStatuses.Started" AllowedStatuses="@(new JobStatuses[] { JobStatuses.Todo})" />
    <JobList ListStatus="JobStatuses.Completed" AllowedStatuses="@(new JobStatuses[] { JobStatuses.Started })" />
</JobsContainer>

@code {
    List<JobModel> Jobs = new List<JobModel>();

    protected override void OnInitialized()
    {
        Jobs.Add(new JobModel { Id = 1, Description = "Mow the lawn", Status = JobStatuses.Todo, LastUpdated = DateTime.Now });
        Jobs.Add(new JobModel { Id = 2, Description = "Go to the gym", Status = JobStatuses.Todo, LastUpdated = DateTime.Now });
        Jobs.Add(new JobModel { Id = 3, Description = "Call Ollie", Status = JobStatuses.Todo, LastUpdated = DateTime.Now });
        Jobs.Add(new JobModel { Id = 4, Description = "Fix bike tyre", Status = JobStatuses.Todo, LastUpdated = DateTime.Now });
        Jobs.Add(new JobModel { Id = 5, Description = "Finish blog post", Status = JobStatuses.Todo, LastUpdated = DateTime.Now });
    }

    void HandleStatusUpdated(JobModel updatedJob)
    {
        Console.WriteLine(updatedJob.Description);
    }
}

Looking back at the goals I set for this exercise:

  • Be able to track an item being dragged
  • Control where items could be dropped
  • Give a visual indicator to the user where items could be dropped or not dropped
  • Update an item on drop
  • Feedback when an item has been updated

I'm feel pretty happy that each one of those has been achieved with the above solution. Please keep in mind this was just a fact finding exercise and the code above is just a prototype. There are probably quite a few bits which could use a tweak or a re-factor before actually using it.

One thing which I thought about after I started was the ability to re-order using dragging and dropping. But that isn't something I could make work in a way I would've been happy with. In traditional JavaScript applications, this is achieved by manipulating the DOM directly. This is something which isn't possible right now with Blazor. I have a few ideas about ways to achieve this using C# but I'm leaving them for another time.

Summary

I had a lot of fun experimenting with drag and drop with Blazor. As usual, I found that getting something up and working was pretty quick and easy. I would definitely want to iterate on this code a bit before I started using it in a real app but I hope it will give people a good starting point.

In this post, I've given an overview of the HTML drag and drop API as well as showing what parts are available to us in Blazor. I then walked through a prototype for a drag and drop interface using a todo list as the example.