🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

BlazorでDrag and Dropを実装する

Blazorで簡単なDrag and Dropを実装してみます。

デモ

こんな感じです。

f:id:piyo_esq:20191023070159g:plain
デモ

コード

作ったコードはこちらにUpしてあります。

github.com

概要

以下の3つのコンポーネント・サービスを作成します

  1. Drag可能なオブジェクトを指定するためのDraggableコンポーネント
  2. Drop可能な領域を指定するためのDroppableコンポーネント
  3. DraggableコンポーネントDroppable間でデータを受け渡すためのDragAndDropService

Draggable.razor

Drag可能なコンポーネントです。

  • draggable="true"を指定することでドラッグ可能なdiv要素になります。詳細はこちら

  • ドラッグの開始時に@OnDragStart関数を実行します。
    関数では[Parameter]で受け取ったデータをDragAndDropServiceに追加しています。
    ここで追加したデータがDrop先のコンポーネントに受け渡されるデータです。

@inject DragAndDropService DragAndDropService

<div draggable="true" @ondragstart="@OnDragStart" style="cursor: move;">
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">@Data.Title</h5>
            <p class="card-text">@Data.Content</p>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public Todo Data { get; set;}

    public void OnDragStart(DragEventArgs e)
    {
        DragAndDropService.StartDrag(Data);
    }
}

Droppable.razor

Drop可能なコンポーネントです。

  • Dropされたときに@OnDrop関数を実行します。
    Draggableで受け渡したDragAndDropService.Dataを引数にAction<Todo> Dropを実行します。

  • コンポーネントDrop可能な領域を囲む形で使用するためRenderFragmentを使用します。
    ※詳細はIndex.razorを参照

@inject DragAndDropService DragAndDropService

<div class="border border-info" ondragover="event.preventDefault()" @ondrop="@OnDrop" style="margin: 1rem;">
    @ChildContent
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public Action<Todo> Drop { get; set; }

    public void OnDrop(DragEventArgs e)
    {
        Drop?.Invoke((Todo)DragAndDropService.Data);
    }
}

Index.razor

  • 3つのカラムがあり、カラム内にはTodoのItemを表示しています。TodoのItemはカラム間をDrag and Dropで移動可能です。

  • Droppableな領域にDropされた際は@OnDrop***Row関数が実行されるようになっています。 上記関数で画面の更新処理を行っています。

@page "/"

<div class="container">
    <div>
        <h1>Todo List.</h1>
    </div>

    <div class="row">
        <Droppable Drop="@OnDropTodoRow">
            <div class="col">
                <h2>Todo</h2>
                @{
                    foreach(var todo in Todos.Where(w => w.Category == TodoCategory.Todo))
                    {
                        <Draggable Data="todo"/>
                    }
                }
            </div>
        </Droppable>

        <Droppable Drop="@OnDropTestingRow">
            <div class="col">
                <h2>Testing</h2>
                @{
                    foreach(var todo in Todos.Where(w => w.Category == TodoCategory.Testing))
                    {
                        <Draggable Data="todo" />
                    }
                }
            </div>
        </Droppable>

        <Droppable Drop="@OnDropDoneRow">
            <div class="col">
                <h2>Done</h2>
                @{
                    foreach(var todo in Todos.Where(w => w.Category == TodoCategory.Done))
                    {
                        <Draggable Data="todo" />
                    }
                }
            </div>
        </Droppable>
    </div>
</div>

@code {
    List<Todo> Todos = new List<Todo>();

    protected override void OnInitialized()
    {
        Todos.Add(new Todo() { Id = 0, Title = "トリッシュを護衛する", Content = "守る👩<200d>🦰", Category = TodoCategory.Todo});
        Todos.Add(new Todo() { Id = 1, Title = "部下を守る", Content = "🐢含む", Category = TodoCategory.Todo});
        Todos.Add(new Todo() { Id = 2, Title = "任務を遂行する", Content = "ペッシ🍍とプロシュート🕺を倒す", Category = TodoCategory.Todo});
        Todos.Add(new Todo() { Id = 3, Title = "列車から飛び降りる", Content = "覚悟を決める🚂💨", Category = TodoCategory.Testing});
        Todos.Add(new Todo() { Id = 4, Title = "駅に辿り着く", Content = "ボス👤の指令", Category = TodoCategory.Done});
        Todos.Add(new Todo() { Id = 5, Title = "カプリ島に辿り着く", Content = "ペリーコロさん👨<200d>🦳に献金💰", Category = TodoCategory.Done});
    }

    void OnDropTodoRow(Todo todo)
    {
        todo.Category = TodoCategory.Todo;
        StateHasChanged();
    }

    void OnDropTestingRow(Todo todo)
    {
        todo.Category = TodoCategory.Testing;
        StateHasChanged();
    }

    void OnDropDoneRow(Todo todo)
    {
        todo.Category = TodoCategory.Done;
        StateHasChanged();
    }
}

DragAndDropService.cs

データを受け渡すためのサービスです。

namespace BlazorDragSample.Data
{
    public class DragAndDropService
    {
        public object Data { get; set; }
        public void StartDrag(object data)
        {
            this.Data = data;
        }
    }
}

備考:Draggable, Droppableコンポーネントジェネリック化について

現状のDraggableDroppableTodoクラス専用のコンポーネントになってしまっています。
@typeparamを使用することでジェネリックコンポーネントにすることができます。

docs.microsoft.com

呼び出し時にTの型を指定しなくてもプリミティブな型であれば推論してくれるっぽいのですが、
Todoみたいな自作クラスを渡すと、型推論がうまく働かないため<Droppable T="@nameof(Todo)">みたいにTの型を指定する必要がある感じの挙動でした。うーんダサい。

これについてはまた別の記事で触れたいと思います(たぶん

おわり

  • ServiceにTodoの参照型を渡してアレコレするのは、なんとなくリークとか発生しそうな気がして怖いですね(未検証)
    実際にはIdとかプリミティブな型でやり取りした方がいいのかな。

  • アリーヴェデルチ!(さよならだ)