AJAX forms and file uploading in Rails
September 20th, 2007 Posted in AJAX, ActionViewOne very nice thing about Ruby on Rails is the support for AJAX. I consider myself a backend guy, so I don’t generally like to touch Javascript (though I’ve written my fair share of XMLHttpRequest calls without the help of frameworks - which is probably why I dislike Javascript so much).
The other day I was working on AJAXifying some forms, because it didn’t make sense to refresh the entire browser window for these functions. Here’s my original view:
<% form_for :message, :url =>{:controller => :message,:action => :post },
:html => { :multipart => true } do |form| %>
<% fields_for :image do |image| %>
<%= image.file_field :file_data, :size => 20 %>
<% end %>
<%= form.text_area :body, :cols => 40, :rows => 2 %>
<%= submit_tag "Send", :class => :submit %>
<% end %>
Pretty straightforward stuff. The controller method wasn’t much more complex:
MessageController.rb
def send
if request.post?
@message = Message.new(params[:message])
unless params[:image].blank? or
params[:image][:file_data].blank?
@message.images << Image.new(params[:image])
end
@message.save
end
end
I won’t get too much into the Message or Image models; these can be found anywhere. A decent Image model is available inside the Rails Recipes book. As a rule of thumb, NEVER start your development with AJAX. Javascript can be a harsh mistress, and you’re better off degrading to a non-AJAX page that fully functions rather than running into Javascript problems and having to rebuild anyway.
The very first thing I did was change the form. Instead of using form_for, I changed it to form_remote_for. No problems!
<% form_remote_for :message, :url => {:controller => :message, :action => :post },
:html => { :multipart => true } do |form| %>
<% fields_for :image do |image| %>
<%= image.file_field :file_data, :size => 20 %>
<% end %>
<%= form.text_area :body, :cols => 40, :rows => 2 %>
<%= submit_tag "Send", :class => :submit %>
<% end %>
As you can see all I’ve done here is change the method name. A quick ‘tail -f development.log’ shows us that the form is indeed being sent to the server. But what is the point of AJAX without some crazy cool thing going asynchronously? Rails makes this easy too:
<% form_remote_for :message, :url =>
{:controller => :message,
:action => :post },
:loading => "Element.show('status')",
:success => "Element.hide('status')",
:html => { :multipart => true } do |form| %>
<% fields_for :image do |image| %>
<%= image.file_field :file_data, :size => 20 %>
<% end %>
<%= form.text_area :body, :cols => 40, :rows => 2 %>
<%= submit_tag "Send", :class => :submit %>
<% end %>
<div id='status' style='display:none;'>Sending ...</div>
<div id='last_sent_message' style='display:none;'>
</div>
Let’s also make the controller render an RJS template:
send.rjs
page.replace_html “last_sent_message”, :partial =>’message’, :object => @message
Again, I won’t bore you with the details of the partial or the models. For the most part, this did what I needed it to do … until I tried to upload an Image. Exception throwing time …
This is where Google comes in. The best link I could find for Rails AJAX file uploading was Dave Naffis’s Rails blog:
http://www.naffis.com/2006/12/11/ajax-uploads-image-manipulation-drag-and-drop-sorting#comments
The primary takeaway here is that pure Javascript form uploading is impossible because Javascript does not have access to a client filesystem (for security reasons), and thus, file form fields. To work around this, sites that allow “AJAX” file uploads are actually posting to an IFRAME, not sending the file via XHR.
Dave does a pretty good job, but as he says in his blog, he wrote the code in about 20 minutes. To allow AJAX like file uploads, you must do the following:
1. Install the Responds to Parent plugin. This allows any RJS rendered in an IFRAME to access the DOM of the parent document. This is not mandatory, but it allows the POST response to manipulate the main page to show that an upload completed (or whatever … ).
http://agilewebdevelopment.com/plugins/responds_to_parent
To install, type:
ruby script/plugin http://sean.treadway.info/svn/plugins/responds_to_parent/
2. Create a file called ‘remote_uploads.rb’ in your lib/ directory. Dave Naffis has a version of code on his site, but I’ve gone the extra step and modified it a bit:
module ActionView
module Helpers
module PrototypeHelper
alias :form_remote_tag_old :form_remote_tag
def form_remote_tag(options = {}, &block)
unless options[:html] && options[:html][:multipart]
form_remote_tag_old(options, &block)
else
uid = "a#{Time.now.to_f.hash}"
<<-STR
There were two problem with the original code, at least for me. First, nothing inside the form_for{} block was being rendered for some reason for a regular, non-multipart form. The method has been changed to accept &block as a second parameter, and pass this along appropriate to form_for.
The second problem was that, for whatever reason, in the original code, form_remote_tag_old just seemed to never get called. I didn’t have time to debug this, and I’m going to be accused of being incomplete, but reversing the IF and the UNLESS solved this problem for me.
3. Add
require 'remote_uploads.rb'
To environment.rb.
4. Change the controller action to render the response in a responds_to_parent block:
def send
if request.post?
@message = Message.new(params[:message])
unless params[:image].blank? or
params[:image][:file_data].blank?
@message.images << Image.new(params[:image])
end
@message.save
responds_to_parent do
render :action => 'post.rjs'
end
end
end
5. Restart your server to load the environment.rb changes.
Voila! You are done. More importantly, “AJAX” form uploads are now enabled in the future anytime you call form_remote_tag with :html => { :multipart => true }.
Now go out there and write some form_remote_tag forms with pseudo-AJAX uploads!
3 Responses to “AJAX forms and file uploading in Rails”
By Ulf on Oct 5, 2007
Great work. Thanks. Your remote_uploads.rb didn’t work for me though so I went ahead and used Dave’s. Is there maybe somthing missing in the “true})}” enctype=”multipart/form-data” target=”#{uid}…..” line?
By Ikai Lan on Oct 5, 2007
That’s a real bummer - all that’s changed is the passing of the Proc so it wouldn’t break existing functionality.
By John Devine on Oct 19, 2007
The problem Ulf is that terminating “STR” must be on a line by itself and I think if you paste the code the leading spaces (and there is also a trailing space) don’t match the <<STR. I dropped the STR to a line by itself and made sure there was no trailing space and it stopped generating errors. I have yet to test it :))