All threads / Drag and Drop sortable lists with Rails & Stimulus JS Discussion

Ask A Question

Notifications

You’re not receiving notifications from this thread.

Drag and Drop sortable lists with Rails & Stimulus JS Discussion

Chris, thank you so much for this! I was actually going through the old drag and drop tutorial yesterday and today to update it for Rails 6.

I seem to be having an issue both with this tutorial and the other where the Rails.ajax line in both of them is causing the error message:

Uncaught ReferenceError: Rails is not defined

In the old tutorial, I was able to get it to work by changing Rails.ajax to $.ajax but that only works with jQuery and not Stimulus. Do you know what might be causing this?

Add import Rails from "@rails/ujs"; to the top of your drag_controller.js file and it should work fine.

I believe Chris starts apps with his Jumpstart template, which includes this by default.

Tim, you may need to register Rails globally inside webpack config. Than you will not need to import it inside every controller.

// config/webpack/environment.js

const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    Rails: ['@rails/ujs']
  })
)

module.exports = environment

Thanks John and Dino! That fixed both the old and new versions. I've been trying to figure that out for awhile now. Much appreciated!

Reply

What would this look like when used with Stimulus Reflex?

Reply

I've got a question on this. What if the todo is nested within another model. Like todolist has many todos. I'm having trouble setting up the data-drag-url to be something like /todolist/:id/todos/:id/move. How do you bring the todolist :id?

hey, I have an image model. image belongs to product, and product has many images. this is how I did it.

<div data-controller="drags" data-drags-url="/products/:product_id/images/:id/move">

and in the stimulus controller, I replaced the url by this

let id = event.item.dataset.id
    let product_id = event.item.dataset.productId

    let data = new FormData()
    data.append("position", event.newIndex + 1)

    let url = this.data.get("url")
    let mapUrl = { product_id: product_id, id: id}

    url = url.replace(/product_id|id/gi, function(matched){
      return mapUrl[matched];
    })

    Rails.ajax({
      url: url,
      type: 'PATCH',
      data: data
    })

Just coming back to this now. I set it up similar to what you did. Instead of products I have compitems. Instead of images I have tasks. It can't seem to find the correct task to update the position on. I get the error

ActiveRecord::RecordNotFound (Couldn't find Task with 'id'=:3 [WHERE "tasks"."compitem_id" = $1]):
13:16:20 web.1 |

13:16:20 web.1 | app/controllers/tasks_controller.rb:81:in "set_task"

Any thoughts on why that is?

drag controller:

     import { Controller } from "stimulus"
     import Sortable from "sortablejs"

     export default class extends Controller {
      connect() {
        this.sortable = Sortable.create(this.element, {
          group: 'shared',
          animation: 150,
          onEnd: this.end.bind(this)
        })
    }
    end(event) {
      let id = event.item.dataset.id
      let compitem_id = event.item.dataset.compitemId

      let data = new FormData()
      data.append("position", event.newIndex + 1)

      let url = this.data.get("url")
      let mapUrl = { compitem_id: compitem_id, id: id}

      url = url.replace(/compitem_id|id/gi, function(matched){
        return mapUrl[matched];
      })

      Rails.ajax({
        url: url,
        type: 'PATCH',
        data: data
      })

    }
  }

Task.html.erb

    <div data-controller="drag" data-drag-url="/compitems/:compitem_id/tasks/:id/move ">
      <div class="border border-gray-400 mx-4 my-4 p-6 w-1/2" data-id="<%= task.id %>">
        <div><%= check_box_tag nil, nil, task.completed_date?, data: { reflex: "click->ExampleReflex#toggle", id: task.id } %></div>
        <div><h3 class="text-gray-900 text-sm leading-5 font-medium truncate"><%= task.description %></h3></div>
        <div><h3 class="text-gray-900 text-sm leading-5 font-medium truncate"><%= task.completed_date %></h3></div>
        </div>
    </div>

In compitems show:

<div class="mt-5 border-t border-gray-200 pt-5">
    <div id="taskitems_wrapper">
        <%= render @compitem.tasks.order(position: :asc), :locals => {:compitem_id => @compitem.id}  %>
    </div>
  </div>

tasks controller:

class TasksController < ApplicationController
  before_action :set_compitem
  before_action :set_task, except: [:create]


  # GET /tasks
  # GET /tasks.json
  def index

    @task = Task.all
  end

  # GET /tasks/1
  # GET /tasks/1.json
  def show
  end

  # GET /tasks/new
  def new
    @task = Task.new
  end

  # GET /tasks/1/edit
  def edit
  end

  # POST /tasks
  # POST /tasks.json
  def create

    @task = @compitem.tasks.create(task_params)
    @task.user_id = current_user.id
    respond_to do |format|
      if @task.save
        format.html { redirect_to @task, notice: 'Task was successfully created.' }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :new }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /tasks/1
  # PATCH/PUT /tasks/1.json
  def update
    respond_to do |format|
      if @task.update(task_params)
        format.html { redirect_to @compitem, notice: 'Task was successfully updated.' }
        format.json { render :show, status: :ok, location: @task }
      else
        format.html { render :edit }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /tasks/1
  # DELETE /tasks/1.json
  def destroy
    @task.destroy
    respond_to do |format|
      format.html { redirect_to tasks_url, notice: 'Task was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  def move
    @task.insert_at(params[:position].to_i)
    head :ok
  end

  def set_compitem
   @compitem = Compitem.find(params[:compitem_id])

  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_task
      @task = @compitem.tasks.find(params[:id])

    end

    # Only allow a list of trusted parameters through.
    def task_params
      params.require(:task).permit(:description, :position, :user_id)
    end
end

I am having the same issue. I did find out that in the event, event.item.dataset.id is empty. I am not sure why.

I was working on this today. I am rendering the partial from the compitem page in my case passing through the compitem_id as a locale. I get the error that the ID value is undefined so I'm not sure if I am passing it through correctly. It would be great to see a full working example of this.

Reply

I have an inquiry on this. Consider the possibility that the task is settled inside another model. Like todolist has numerous tasks.

Reply
Join the discussion

Want to stay up-to-date with Ruby on Rails?

Join 38,558+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.

    logo Created with Sketch.

    Ruby on Rails tutorials, guides, and screencasts for web developers learning Ruby, Rails, Javascript, Turbolinks, Stimulus.js, Vue.js, and more. Icons by Icons8

    © 2020 GoRails, LLC. All rights reserved.