1

In a Rails 2.2 project, I'm having users put together a list of projects into a portfolio object (i.e.: PortfolioHasManyProjects). On the page is a Rails form for regular text, titles etc., as well as 2 sortable lists; the lists are used for dragging projects from the global-project-list into your portfolio-project-list.

It is similar to what's done here: http://ui.jquery.com/latest/demos/functional/#ui.sortable.

I have the portfolio list (#drag_list) updating on change and submitting its serialized data through an AJAX call. This is done in the application.js file:

jQuery.ajaxSetup({ 
  'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/javascript")}
})

jQuery.fn.submitDragWithAjax = function() {
  this.submit(function() {
    $.post(this.action, $("#drag_list").sortable('serialize'), null, "script");
    return false;
  })
  return this;
};

$(document).ajaxSend(function(event, request, settings) {
  if (typeof(AUTH_TOKEN) == "undefined") return;
  // settings.data is a serialized string like "foo=bar&baz=boink" (or null)
  settings.data = settings.data || "";
  settings.data += (settings.data ? "&" : "") + "authenticity_token=" + encodeURIComponent(AUTH_TOKEN);
});


/-------------------------------------------/

$(document).ready(function(){

    $(".ajax_drag").submitDragWithAjax();

    $("#drag_list").sortable({
        placeholder: "ui-selected",
        revert: true,
        connectWith:["#add_list"],
        update : function () {
            $("#drag_list").submit();
        }
    });

    $("#add_list").sortable({ 
        placeholder: "ui-selected",
        revert: true,
        connectWith:["#drag_list"]
    });

Here is where things got tricky. I wasn't sure how to deal with the serialized data and have it submit with the form to the controller in the new.html.erb file. So what I did was have the new.js.erb insert hidden form fields into new.html.erb with the data that I would extract in the controller.

here's the new.js.erb:

$("#projects").html("");
<% r = params[:proj] %>
<% order=1 %>
<% for i in r %>
  $("#projects").append("<input type=hidden name=proj[<%=order%>] value=<%=i%> />");
  <% order=order+1 %>
<% end %>

which edits new.html.erb:

<h1>New portfolio</h1>
<h2>The List</h2>

<div class="list_box">
  <h3>All Available Projects</h3>
  <%= render :partial => "projects/add_list" %>
</div>

<div class="list_box">
  <h3>Projects currently in your new portfolio</h3>
  <%= render :partial => "projects/drag_list" %>
</div>

<div style="clear:both"></div>
<br/>
<br/>

<h2>Portfolio details</h2>
<% form_for(@portfolio) do |f| %>
  <%= f.error_messages %>
  <h3>Portfolio Name</h3>
  <p>
    <%= f.text_field :name %>
  </p>
  <h3>URL</h3>
  <p>
    <%= f.text_field :url %>
  </p>
  <h3>Details</h3>
  <p>
    <%= f.text_area :details %>
  </p>
  <p>

    <div id="projects">
    <input type="hidden" name="proj" value="" />   
    </div>

    <%= f.submit "Create" %>
  </p>
<% end %>

The form then submits to the create method in the portfolio controller:

  def new
    @projects = Project.find(:all)
    @portfolio = Portfolio.new
    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @portfolio }
      format.js
    end
  end




 def create
    @portfolio = Portfolio.new(params[:portfolio])
    proj_ids = params[:proj]
    @portfolio.projects = []
    @portfolio.save

    proj_ids.each {|key, value| puts "Key:#{key} , Value:#{value} " }
    proj_ids.each_value {|value| @portfolio.projects << Project.find_by_id(value) }

    respond_to do |format|
      if @portfolio.save
        flash[:notice] = 'Portfolio was successfully created.'
        format.html {  render :action => "index" }
        format.xml  { render :xml => @portfolio, :status => :created, :location => @portfolio }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @portfolio.errors, :status => :unprocessable_entity }
      end
    end
  end

So finally my question:

  1. Is this a proper way of doing this? For some reason I feel it isn't, mostly because doing everything else in Rails seemed so much easier and intuitive. This works, but it was hell to get it to. There has to be a more elegant way of sending serialized data to the controller through AJAX calls.

  2. How would I call for different AJAX actions on the same page? Let's say I had a sortable and an autocomplete AJAX call, could I have a sortable.js.erb and autocomplete.js.erb and call them from any file? I'm not sure how to setup the controllers to respond to this.

2
  • I don't understand the question (and unfortunately, there's a silly rep limit on commenting, so I'll post this as an answer). Why are you inserting the hidden fields? Can't you just use id's on the &lt;li&gt; items and then call 'toArray' on the sortable rather than 'serialize')? You could add that information to a single hidden form field before the form submits. That would prevent all the Ajax calls after updating the project list.
    – eelco
    Commented Jan 4, 2009 at 23:25
  • Here's my take on this: awesomeful.net/posts/47-sortable-lists-with-jquery-in-rails
    – hgmnz
    Commented Oct 8, 2009 at 13:15

2 Answers 2

2

This is a nice solution if you are using jQuery.

From the linked blog:

I just wrote some sortable code for a Rails/jQuery app and figured I would blog just how little code it takes, and also the single MySQL query I used on the backend.

0

Here's my solution that is based on the article mentioned by Silviu. I'm sorting parts that belong_to lessons, hence the inclusion of the lessonID.

This is in the view - I'm using HAML so you'll have to convert to erb.

#sorter
- @lesson.parts.each do |part|
  %div[part] <- HAML rocks - this constructs a div <div id="the_part_id" class="part">
    = part_screenshot part, :tiny
    = part.swf_asset.filename

The js looks like this:

    $('#sorter').sortable({items:'.part', containment:'parent', axis:'y', update: function() {
  $.post('/admin/lessons/' + LessonId + '/parts/sort', '_method=post&authenticity_token='+ AUTH_TOKEN+'&'+$(this).sortable('serialize'));
  $('#sorter').effect("highlight");
}});

and here is the method that is called in the PartsController:

def sort
load_lesson
part_positions = params[:part].to_a
@parts.each_with_index do |part, i|
  part.position = part_positions.index(part.id.to_s) + 1
  part.save
end
render :text => 'ok'

end

def load_lesson
@lesson = Lesson.find(params[:lesson_id])
@parts = @lesson.parts

end

It needs some work on giving feedback to the user, but does the trick for me.

Not the answer you're looking for? Browse other questions tagged or ask your own question.