Drag and drop in rails
As I said in my earlier post, the product I'm developing has lots of self referential links between articles. Now that I'm comfortable modeling them with has_many through relationships, I need to start thinking about how to build these links. I need to have a nice graphical back end, in which I can drag thumbnail images representing articles and drop onto another article to make a link.
Enter jquery draggable! There are some great tutorials on the web on jquery, particularly this one, but no really good ones on using jquery draggable with rails. It took me quite a long time to get this sorted out, especially as I was learning so many new things at once.
Continuing from the earlier article, I've added images to my articles - here's my current schema:
db/schema.rb
ActiveRecord::Schema.define(:version => 20120304112520) do
create_table "articles", :force => true do |t|
t.string "name"
t.string "body"
t.string "image"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "links", :force => true do |t|
t.integer "parent_id"
t.integer "child_id"
t.integer "weight"
t.datetime "created_at"
t.datetime "updated_at"
end
end
On the show page, I’ve added graphical links to each of the article’s children:
The edit page is very similar, with a blank placeholder image to drag new links into and a list of all the articles that can be linked to:
Here’s the code for the edit page:
app/views/articles/edit.html.erb
<h1>Editing article</h1>
<%= render 'form' %>
<%= render 'edit_child_articles' %>
<div id="all-articles">
<%= render partial: @articles, locals: {in_edit: true} %>
</div>
<%= link_to 'Show', @article %> |
<%= link_to 'Back', articles_path %>
The form is the same as normal, so here’s the edit_child_articles partial:
app/views/articles/_edit_child_articles.html.erb
<div id="edit-article-children">
<h2>You might also like...</h2>
<ul id="child-links" >
<%= render @article.child_links if @child_links %>
<li id="droppable-li">
<%= image_tag "placeholder.png", class: "this_is_droppable", id: @article.id, size: "80x80" %>
</li>
</ul>
</div>
And the articles partial:
app/views/articles/_article.html.erb
<div id="all-articles">
<%= link_to (image_tag article.image), article unless in_edit %>
<%= image_tag article.image, class: "this_is_draggable", id: article.id if in_edit %>
</div>
Now the clever bit is in the javascript. First of all as jquery draggable and droppable are in jquery ui, you must include that in the application:
app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs
//= require jquery-ui
//= require_tree .
Then in articles.js, I make the the article images in the list of all articles draggable, the empty box droppable and add a function to run when something draggable is dropped into the droppable box.
app/assets/javascripts/articles.js
$(function() {
$(make_draggable_and_droppable());
function make_draggable_and_droppable() {
$(".this_is_draggable").draggable({
helper: "clone",
snap: ".this_is_droppable",
cursor: "move"
});
$(".this_is_droppable").droppable({
drop: drop_article
});
}
function drop_article(event, ui) {
$.ajax({
type: "POST",
url: "/links/",
data: {
link: {
parent_id: this.id,
child_id: ui.draggable.attr("id")
}
},
dataType: "script",
remote: "true",
success: function() {}
});
}
});
What does this do?
At the start; the make_draggable_and_droppable function makes the article images draggable and the empty placeholder image droppable and adds the drop_article function as the function to run when an article is dropped into the droppable space. The drop_article function creates a new link by doing a jquery ajax call to the links controller.
Now... When the drop_article function is run, the create.js.erb links view is called
app/assets/views/links/create.js.erb
$("#link_<%= @link.id %>").hide();
$("#droppable-li").before("<%= escape_javascript(render(@link)) %>");
$("#droppable-li").show("slide", { direction: "left" }, 1000);
$("#link_<%= @link.id %>").show("drop", { direction: "left" }, 1000);
The new link is hidden first, then slides into view.
If the delete link is clicked, the destroy.js.erb link view is called which hides the appropriate image.
The app is on github here and there's a demo app on heroku here. For some reason it doesn't always seem to work in google chrome, but does in any other browser.