Vue.js 3によるドラッグアンドドロップでのタスク管理機能

## ディレクトリ構造

```bash
app/
├── controllers/
│   ├── api/                         # API用のコントローラ
│   │   └── tasks_controller.rb      # タスク管理のAPIエンドポイント
│   ├── tasks_controller.rb          # タスク管理の通常のコントローラ
│   └── tops_controller.rb           # トップページのコントローラ
├── views/
│   ├── tasks/                       # タスク管理のビュー
│   │   ├── index.html.erb           # タスク一覧ページ
│   │   ├── show.html.erb            # タスク詳細ページ
│   │   └── ...                      # その他のタスク管理ビュー
│   └── tops/                        # トップページのビュー
│       └── index.html.erb           # トップページ
├── javascript/
│   ├── components/                  # 再利用可能なVueコンポーネント
│   │   └── DraggableCalendar.vue    # ドラッグ可能なカレンダーコンポーネント
│   └── entrypoints/                 # エントリポイントとなるJavaScriptファイル
│       └── tasks.js                 # タスク管理機能のエントリポイント
└── models/
    └── task.rb                      # タスクモデル
```

## エントリポイントの設定

エントリポイントである `tasks.js` は以下のように設定します。

```tasks.js
// app/javascript/entrypoints/tasks.js
import { createApp } from 'vue';
import DraggableCalendar from '../components/DraggableCalendar.vue';

document.addEventListener('DOMContentLoaded', () => {
  const el = document.querySelector('#task-calendar');
  if (el) {
    createApp(DraggableCalendar).mount(el);
  }
});
```

ここで、`DOMContentLoaded`イベントを使用してページが完全に読み込まれた後にVueアプリケーションをマウントすることで、マウント対象の要素がDOMに存在することを保証しています。なお、マウント対象の要素がDOMに存在することを保証しなくてもよい場合は以下の書き方も可能です。

```tasks.js
import { createApp } from 'vue';
import DraggableCalendar from '../components/DraggableCalendar.vue';

const app = createApp(DraggableCalendar);
app.mount('#task-calendar');
```


## Vueコンポーネントの作成

`DraggableCalendar.vue` コンポーネントは以下のように定義します。

```DraggableCalendar.vue
<template>
  <div class="calendar">
    <!-- カレンダーの各日に対して、ドラッグアンドドロップ可能な領域を作成 -->
    <div v-for="day in days" :key="day" class="calendar-day">
      <div class="drop-zone" @drop="drop($event, day)" @dragover.prevent @dragenter.prevent>
        <!-- この日に割り当てられたタスクを表示 -->
        <div v-for="task in tasks[day]" :key="task.id" draggable="true" @dragstart="dragStart($event, task)">
          {{ task.name }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tasks: {}, // 日付をキーとしたタスクのオブジェクト
      days: [],  // カレンダーの日付を格納する配列
    };
  },
  methods: {
    dragStart(event, task) {
      event.dataTransfer.setData('task', JSON.stringify(task));
    },
    drop(event, day) {
      const task

 = JSON.parse(event.dataTransfer.getData('task'));
      // タスクをその日に移動するロジック
    },
  },
};
</script>

<style scoped>
/* スタイリング */
.drop-zone {
  /* スタイル定義 */
}
.draggable-task {
  /* スタイル定義 */
}
</style>
```

## Railsビューの準備

タスク管理機能に関連するRailsビューは、Vueコンポーネントを含めるためのマークアップを提供します。以下は`app/views/tasks/index.html.erb`の例です。

```erb
<!-- app/views/tasks/index.html.erb -->
<p style="color: green"><%= notice %></p>

<h1>Tasks</h1>
<div id="task-calendar">
  <!-- Vueコンポーネントがここにマウントされます -->
</div>

<%= vite_javascript_tag 'entrypoints/tasks' %>

<div id="tasks">
  <% @tasks.each do |task| %>
    <%= render task %>
    <p>
      <%= link_to "Show this task", task %>
    </p>
  <% end %>
</div>

<%= link_to "New task", new_task_path %>
```

ここで、`vite_javascript_tag` ヘルパーを使用して、`tasks.js` エントリポイントを読み込んでいます。これにより、ページに `DraggableCalendar` コンポーネントがマウントされ、ドラッグアンドドロップによるタスク管理が可能になります。

## APIコントローラの設定

タスクデータをフェッチし、更新するためには、バックエンドに適切なAPIエンドポイントを設定する必要があります。以下は`app/controllers/api/tasks_controller.rb`の簡単な例です。

```ruby
# class Api::V1::TasksController < ApplicationController
# ...
# end
# 上記の書き方も可能
module Api
  module V1
    class TasksController < ApplicationController
      before_action :set_task, only: [:show, :update, :destroy]

      def index
        @tasks = Task.all
        render json: @tasks
      end

      def create
        @task = Task.new(task_params)
        if @task.save
          render json: @task, status: :created, location: api_task_url(@task)
        else
          render json: @task.errors, status: :unprocessable_entity
        end
      end

      def update
        if @task.update(task_params)
          render json: @task
        else
          render json: @task.errors, status: :unprocessable_entity
        end
      end

      def destroy
        @task.destroy
        head :no_content
      end

      private

      def set_task
        @task = Task.find(params[:id])
      end

      def task_params
        params.require(:task).permit(:name, :description, :due_date)
      end
    end
  end
end
```

このコントローラは、タスクの一覧表示、作成、更新、削除を行う基本的なAPIエンドポイントを提供します。フロントエンドのVueコンポーネントからこれらのエンドポイントにリクエストを送信することで、タスクデータを非同期に操作できます。

## ルーティングの設定
```route.rb
Rails.application.routes.draw do
  # 以下3行を追記
  namespace :api do
    resources :tasks, only: [:index]
  end
  
  devise_for :users, controllers: {
    sessions: 'users/sessions',
    registrations: 'users/registrations',
    passwords: 'users/passwords'
  }
  devise_scope :user do
    delete '/user_logout', to: 'devise/sessions#destroy', as: :logout_user
  end
  
  resources :tops, only: %i(index)
  resources :users, only: %i(index show)
  resources :tasks
  resources :blogs

  root 'tops#index'
end

```

## まとめ

この記事では、Rails 7とVue.js 3を組み合わせて、ドラッグアンドドロップによるタスク管理機能を実装する基本的な流れを説明しました。重要なポイントは、Vueコンポーネントの作成とマウント、RailsビューとAPIエンドポイントの設定です。これらのステップに従うことで、リッチなユーザーインターフェースとサーバーサイドのデータ処理を組み合わせたモダンなWebアプリケーションを構築できます。


## 参考記事
https://qiita.com/KM9973/items/31e40abe6b5f594cd7f2

https://qiita.com/KM9973/items/79dc695d269de37f45ff