Skip to main content

24 Sortable Drag and Drop

Episode 207 · September 19, 2017

Drag and drop sorting is pretty easy to add to any Rails application using jQuery sortable

Javascript


Transcripts

What's up guys? This episode we are diving into creating a sortable list in your rails application, so we want to be able to drag and drop these quiz questions anywhere in this list and we want that to then persist server side, we're going to accomplish this with two gem, number one is access list, it's a really great gem for keeping track of the positions, and it gives you some methods in the ruby side of things to move an item up one or down one and it helps automatically assign positions. The other thing that we're going to be using is jQuery sortable, this comes from jQuery UI, and it's really really easy for us to just say: Hey, this element on the page is sortable, and whenever you're done sorting, then hit this endpoint and we'll make an AJAX request and that's it, so it's really really nice and that's what we're going to be diving into today, so let's take a look at how to get all of this set up.

As I mentioned, we're going to be using jQuery UI rails and access list, so you're going to want to save those gems to your gemfile and then run bundle install to install them, and then access list, if you're not familiar, it has a migration to add a position, so for example you want to create a migration to add position to whatever model you're going to add that to. For ours, it's a question, so we're going to generate a migration rails g migration AddPositionToQuestions position:integer. This is going to be nil by default, but our access list is going to take care of that for us, so that's going to be what we need.

The other thing we need to do for set up is go into our JavaScript application.js and then we need to add

//= require jquery-ui/widget 
//= require jquery-ui/sortable 

That will come after jQuery's set up, so that will then give us access to the sortable functionality with jQuery. Don't forget to restart your rails server after you ran bundle to make sure that the jQuery UI stuff is loadable from the gem, but we're then going to create a new JavaScript file, I'm going to call it questions.js, we're going to get rid of the questions.coffee here, so we're going to say rm app/assets/javascripts/questions.coffee and we'll create app/assets/assets/javascripts/questions.js here. We're going to have:

app/assets/javascripts/questions.js

document.addEventListener("turbolinks:load", function() {


});

This is where we're going to set up the sortable stuff, so our index for the questions needs to have some sort of id on the questions list that we're going to have to wrap each one of our questions in a div and we're going to give that div an id, so then jQuery will know this is the group of items, and every item inside of here is going to be sortable, so we'll give this <div id="questions"></div>, and

app/assets/javascripts/questions.js

document.addEventListener("turbolinks:load", function() {

    $("#questions").sortable();

});

By running that, we should be able to drag and drop these items, and you'll see that the mouse cursor changes now, that comes from jQuery's sortable and we can now drag these around and see them in their different places, but of course, if you refresh this, they all reorder themselves and go exactly right back to where they were. That is because this hasn't persisted to the database, what we're going to do here is we're also going to set a data-url=" <%=" here, and this is going to be basically a url to the path for sorting. We're going to need to go into our routes file, and go up to our questions resources block, and here we're going to do

routes.rb

Rails.application.routes.draw do 
  resources :questions do
    collection do
      patch :sort 
    end
  end 

  #Rest of code
end

app/views/questions/index.html.erb

data-url=" <%= sort_questions_path %> "

By adding that, that will add a data attribute here that whenever you finish dragging, we can grab that url, and then send that information to the server. Here you can see data-url=" /question/sort", and we will pass in all of those id's for these questions over to the server. Now, the real trick to how all of this works is that when you create these link_to's, this is just a regular old link, and jQuery knows nothing about it, there is no magic here or anything like that but if we give these an id with a DOM id of the question, that is then going to create an id on each one of these links, those links will have an id called question_(id number), that is where the magic happens from the jQuery side, so there's a serializable method that the jQuery UI sortable comes with, and what it does is it looks through all of these items inside of your group, and it looks at the name and it splits on the underscore. It's going to create an array for us called question[]=1&question[]=2, and it will put all of these in order and send them over as an actual array that rails understands. That's where the real magic comes from, that id is parsed and everything when the serialized method is called, so I'm going to show you that, and we'll print that out in the console so you can see what it does to create those DOM id's to create the params that get sent over to rails. We need to create a callback here on the update, and this is going to be a function that will take the event and the UI and we're going to need to basically call this update function every single time tat the drop finishes. Here we can call

app/assets/javascripts/questions.js

document.addEventListener("turbolinks:load", function() {

    $("#questions").sortable({
        update:function(e, ui) {
            this.sortable('serialize')
        }
        });
});

If we console.log that, the exact parameters that it's going to generate, so let's refresh our page and drag and drop this, and we'll see here that we get question[]=1&question[]=3, two, four, five and so on. What it's doing is it's giving us all of those items in the order that is listed here, but it's converting it to the format of params, so that rails will parse this, and then you will get a params question, and then it will be a params of all of these ids, so we can then take that params and then take those ids, loop through them, and save their position in the database and all of this will be wired up really really nicely. That's a really cool thing that comes with jQuery sortable that will take those items and serialize them for you, so if you ever need to build something like that from scratch in the future, you can use that as a trick using the DOM id and parse that, to send that stuff over to the server. Since we're using rails UJS in our JavaScript, we're going to do Rails.ajax instead of jQuery, and remember, this comes with three options that we need to set, we need to set url, type and data.

app/assets/javascripts/questions.js

document.addEventListener("turbolinks:load", function() {

  $("#questions").sortable({
    update: function(e, ui) {
      Rails.ajax({
        url: $(this).data("url"),
        type: "PATCH",
        data: $(this).sortable('serialize'),
      });
    }
  });

});

The last piece that we need is our sort action inside our controller. This, of course is going to:

def  index 
    @questions = Question.order(:position)
end 

def sort 
    params[:question].each_with_index do |id, index|
        Question.where(id: id).update_all(position: index + 1)
    end 
end 

head :ok

Let's try this out, we should be able to drag this one to save the very bottom, and if we see that go correctly we can go into our rails logs, we will see a patch, you see question, and we get that array of items, and then we get an update question set position where the question id is on that id, and as you might notice, I did a little bit of a weird thing here, normally you might say Question.find(id), and then update the position on that index, and that would actually issue two requests, first, it's going to load the question out of the database, and then it's going to update it, if we do the where and update all, that's actually going to do a single request for each one of those where we get only an update request to our database. If we hop back to the browser, we can refresh the page and our order is exactly the same, we can now drag and drop this to the middle, if we refresh the page, none of those have changed at all and everything is persisted and organized in the exact same way, so this is really awesome, and my favorite part of this whole solution is that sortable serialized just takes those DOM ids that rails already knows about and converts them back in a parameter format that rails can parse really easily as params question, and we don't have to do anything fancy server side like splitting a string on commas, which is kind of a common solution for somethings, and our html dictates basically how our JavaScript runs, so if you ever wanted to add maybe lessons in here, as long as your lessons had a url, data-url on it and had DOM ids for each of those items, it would be able to submit just as well, and the only thing that you would have to change is the selector, so you could add maybe lessons here, or you could change that to .sortable, and then make it apply to anything that you tagged with the class called sortable, or data-behavior="sortable" or anything you might want to do like that, so you could have data-behavior="sortable", and you could have that data attribute to any of those items in your app that you want to make sortable, so this little piece of JavaScript is kind of a little power house that you could copy and paste in other applications, and voila, you will have sortable functionality within five minutes or less if you use this sort of thing and reuse it and set up these and set up these sort methods for your models in their equivalent controllers.

That's it for this episode, I was thinking about building a Trello clone as an example where you could dive into this more in depth, maybe use a vuejs to drag and sort library as well and build more complex version of this, and if you're interested in seeing that, let me know in the comments below and we will dive into that. So, until then, I will talk to you later. Peace v

Discussion