Rails7 stimulusを使用してドラッグアンドドロップを実装する

  • 作成日 2024.07.04
  • rails
Rails7 stimulusを使用してドラッグアンドドロップを実装する

Ruby on Rails 7で、stimulusを使用してRails7 stimulusを使用してドラッグアンドドロップを実装するサンプルコードです。

環境

  • OS Ubuntu24.04
  • rails 7.1.3

Stimulusのインストール

Rails 7では、Stimulusがデフォルトでセットアップされているため、追加のインストールは不要ですが、念のため以下のコマンドで確認します。

$ bundle exec rails stimulus:install

Scaffoldの作成

サンプルデータとして、Itemモデルを作成します。

$ rails generate scaffold Item name:string position:integer
$ rails db:migrate

db/seeds.rbにいくつかのサンプルデータを追加します。

# db/seeds.rb
Item.create(name: "Item 1", position: 1)
Item.create(name: "Item 2", position: 2)
Item.create(name: "Item 3", position: 3)

以下のコマンドを実行してサンプルデータをデータベースに追加します。

$ rails db:seed

Stimulusコントローラーの作成と設定

Stimulusコントローラーを生成します。

$ rails generate stimulus Drag

生成されたコントローラーを以下のように編集します。

// app/javascript/controllers/drag_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["item"]

  connect() {
    this.draggedElement = null;
  }

  dragstart(event) {
    this.draggedElement = event.target;
    event.target.style.opacity = 0.5;
  }

  dragend(event) {
    event.target.style.opacity = "";
  }

  dragover(event) {
    event.preventDefault();
  }

  drop(event) {
    event.preventDefault();
    if (event.target.classList.contains("dropzone")) {
      event.target.style.background = "";
      event.target.parentNode.insertBefore(this.draggedElement, event.target.nextSibling);

      // Update positions in the backend
      const itemIds = Array.from(this.itemTargets).map(item => item.dataset.id);
      this.updatePositions(itemIds);
    }
  }

  dragenter(event) {
    if (event.target.classList.contains("dropzone")) {
      event.target.style.background = "rgba(0, 0, 0, 0.1)";
    }
  }

  dragleave(event) {
    if (event.target.classList.contains("dropzone")) {
      event.target.style.background = "";
    }
  }

  updatePositions(itemIds) {
    fetch("/items/update_positions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
      },
      body: JSON.stringify({ item_ids: itemIds })
    });
  }
}

HTMLの設定

app/views/items/index.html.erbを以下のように編集します。

// app/javascript/controllers/drag_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["item"]

  connect() {
    this.draggedElement = null;
  }

  dragstart(event) {
    this.draggedElement = event.target;
    event.target.style.opacity = 0.5;
  }

  dragend(event) {
    event.target.style.opacity = "";
  }

  dragover(event) {
    event.preventDefault();
  }

  drop(event) {
    event.preventDefault();
    if (event.target.classList.contains("dropzone")) {
      event.target.style.background = "";
      event.target.parentNode.insertBefore(this.draggedElement, event.target.nextSibling);

      // Update positions in the backend
      const itemIds = Array.from(this.itemTargets).map(item => item.dataset.id);
      this.updatePositions(itemIds);
    }
  }

  dragenter(event) {
    if (event.target.classList.contains("dropzone")) {
      event.target.style.background = "rgba(0, 0, 0, 0.1)";
    }
  }

  dragleave(event) {
    if (event.target.classList.contains("dropzone")) {
      event.target.style.background = "";
    }
  }

  updatePositions(itemIds) {
    fetch("/items/update_positions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').getAttribute("content")
      },
      body: JSON.stringify({ item_ids: itemIds })
    });
  }
}

コントローラーの編集

ドラッグアンドドロップによる位置の更新をサポートするために、ItemsControllerを編集します。config/routes.rbを以下のように編集します。

Rails.application.routes.draw do
  resources :items do
    collection do
      post :update_positions
    end
  end

  root "items#index"
end

app/controllers/items_controller.rbを以下のように編集します。

# app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def index
    @items = Item.order(:position)
  end

  def update_positions
    params[:item_ids].each_with_index do |id, index|
      Item.where(id: id).update_all(position: index + 1)
    end
    head :ok
  end

  # 他のアクション
end

スタイルの追加

ドラッグアンドドロップの視覚的なフィードバックを向上させるために、スタイルを追加します。

app/assets/stylesheets/application.cssに以下のスタイルを追加します。

.dropzone {
  padding: 10px;
  border: 1px solid #ccc;
  margin-bottom: 5px;
  cursor: move;
}

Railsサーバーの起動

最後に、Railsサーバーを起動して動作を確認します。
※自分の環境は外部からアクセスできるようにして起動してます。

$ rails s -b 0.0.0.0

ブラウザでhttp://localhost:3000にアクセスすると、ドラッグアンドドロップが正しく動作することが確認できます。