Rails Ajax Image Uploading Made Simple with jQuery

Last week, as part of getting Bloggity rolling with the key features of WordPress, I realized that we needed to allow the user to upload images without doing a page reload. Expecting a task as ordinary as this would be well covered by Google, I dutifully set out in search of “rails ajax uploading” and found a bunch of pages that either provided code that simply didn’t work, or claims that it couldn’t be done without a Rails plugin.

Not so. If you use jQuery and the jQuery-form plugin.

The main challenge in getting a AJAX uploading working is that the standard remote_form_for doesn’t understand multipart form submission, so it’s not going to send the file data Rails seeks back with the AJAX request. That’s where the jQuery form plugin comes into play. Here’s the Rails code for it:

<% remote_form_for(:image_form, :url => { :controller => "blogs", :action => :create_asset }, :html => { :method => :post, :id => 'uploadForm', :multipart => true }) do |f| %>
 Upload a file: <%= f.file_field :uploaded_data %>
<% end %>

Here’s the associated Javascript:

$('#uploadForm input').change(function(){
 $(this).parent().ajaxSubmit({
  beforeSubmit: function(a,f,o) {
   o.dataType = 'json';
  },
  complete: function(XMLHttpRequest, textStatus) {
   // XMLHttpRequest.responseText will contain the URL of the uploaded image.
   // Put it in an image element you create, or do with it what you will.
   // For example, if you have an image elemtn with id "my_image", then
   //  $('#my_image').attr('src', XMLHttpRequest.responseText);
   // Will set that image tag to display the uploaded image.
  },
 });
});

And here’s the Rails controller action, pretty vanilla:

 @image = Image.new(params[:image_form])
 @image.save
 render :text => @image.public_filename

As you can see, all quite straightforward with the help of jQuery. I’ve been using this for the past few weeks with Bloggity, and it’s worked like a champ.

38 Replies to “Rails Ajax Image Uploading Made Simple with jQuery”

  1. Really cool and simple, Thank you for sharing this.
    Just a word to anyone wanting to make use fo this
    1) The code won’t work if javascript is not enabled in whatever browser is being used

    2) You can’t include forms inside forms so if you want to make use of the view code inside an existing table maintenance form you will need to do some jigging around.

    Nice work though and it’s about time someone posted a simple solution.

  2. Cool use of the plugin. I think you are missing a key piece of code that might confuse jquery newbies (like myself), the ending braces for the change function.

  3. I really like this solution, I currently use it to upload images and then adding hidden fields to another form to link to populate my Product#image_ids on save. I am trying to style the page to make it appear as thought the 2nd form appears within the first so that the flow of the form isn’t broken but its a lot more fiddly that I first thought.

    Anybody seen any working examples of how this could fit together?

  4. We do something similar for our new item form on Bonanzle. We have a separate image upload form that is declare after the main form body, but you can mouse over an icon in the main form, and it will absolutely position (again, using jQuery) the image form over the main form.

    Visit http://www.bonanzle.com, click “Start selling with one click” on home page, and click “Add an Item” to see how it works (and the code). Our solution isn’t that straightforward from a code standpoint, but it’s one pretty functional solution to what is not an easy problem.

  5. After the upload of an image, how can I display the image ?
    In your example you say

    XMLHttpRequest.responseText = URL to uploaded image. Put it in an img you create or something.

    Thanks

  6. Forget my previous post, I found the answer, however (using
    Jquery 1.2.6) when doing the render :text => ‘public path of image’
    from the controller to send the image back to the browser
    the url is wrapped between a

     tag.
    By removing the 
     tag in the javascript it works.
  7. “XMLHttpRequest.responseText = URL to uploaded image. Put it in an img you create or something. ”

    Yeah I’m baffled by this too… is this a part of the javascript meant to be completed in situ, as in “XMLHttpRequest.responseText = ‘[path/whatever]’ ” or is “XMLHttpRequest.responseText” used elsewhere?

    Be nice to see a more complete example to crib from… Anyone?

  8. Dvaid: the “XMLHttpRequest.responseText” variable will contain the path to the image you created. If you put that path in an image tag, that image tag will show the image that was uploaded.

  9. I like your very terse tutorial. But I am wondering. What exactly is the Image class?

    @image = Image.new(params[:image_form])

    Is this Image class your own Model class? Or is it coming from some other plugin? Could you please post the whole source code of the example somewhere?

  10. I am trying upload an image with the same technique. I am trying to save the image to a file rather than to the database (for some initial prototyping). But when I save it to a file, I end up saving a file with contents like “#”. It’s really not saving the image itself

    Here’s my controller code:

    def save_img
    path = “public/images/img1.jpg”
    File.open(path, ‘wb’) {|f| f.write(params[:image_form]) }
    render :text => path
    end

    I end up with a file inside RAILS_ROOT/public/images/img1.jpg. But I can open it with a text editor and see the contents as “#”. Any idea how I can get the binary contents?

  11. Hi:
    I tried this in my rails app with remote_form_for and form_remote_tag and the multipart still does not get to the controller. It looks like the javascript is not getting executed for me. Do I need document.ready for the jquery?

  12. I am trying to use this technique, but the code would just not work for me. The javascript doesn’t seem to get executed as the only parameter that I see in the action is the authenticity token (nothing from the form). Everything works once I put in form_for, but I loose the ajax. I have jrails, jquery, jquery_form, facebox and the form is showing inside a facebox. Any help would be appreciated. Thanks.

  13. Hi there,

    isn’t it so that by using the remote_form_for your actually using prototype, since it’s a prototype-helper. So your not doing it all in jQuery.

  14. Bryan,

    Good point. We use the jrails plugin, which automagically translates all of the Rails form helpers (and Rails helpers of any sort) from Prototype to jQuery. If we weren’t using that, then yes, the form would be Prototype. But given the size of both libraries, it doesn’t make much sense to mix jQuery and prototype, so if you’re using jQuery with RAils, chances are you’re using jrails.

    I’m pretty sure that even without the jrails plugin this method would still work.

    B

  15. Hey Bolade, hard to figure out what might be going wrong from the limited details. It’d probably make sense to debug in pieces, i.e., is the form being submitted when the input box changes? If not, look at the jquery for how you’re invoking the form submit. If the form gets submitted but is missing the multipart data, then make sure you’ve included jquery-form (which does that particular magic) and jquery. You might read through some of the jquery-form documentation on its particulars to get more data about circumstances under which it might not submit the multipart data.

  16. I have used this approach with rails file upload plugin FlexImage with great success. Couple of points to note here:

    – You do not have to use a remote_form_for necessarily. A call to form_for is sufficient.

    – Do not call ajaxForm to ajaxify your form. That is not going to submit your form using XHR in this case. You must use ajaxSubmit when you have a file input element in your form.

    – You can set dataType to “script” if you are expecting a javascript to be returned from server.

    Regards
    Indra

  17. If I set the data type to ‘script’, do I need to wrap the resulting Javascript in a textarea? If so, how do I do that? Everything I have tried stuffs the textarea tag inside the try/catch block or omits it.

  18. WN :
    Forget my previous post, I found the answer, however (using
    Jquery 1.2.6) when doing the render :text => €˜public path of image €™
    from the controller to send the image back to the browser
    the url is wrapped between a

     tag.
    By removing the 
     tag in the javascript it works.

    Can you please re-post, how you removed the head and body tags around the image url?

    thanks

  19. @Waqas

    I am facing the same problem as you were. The XMLHttpRequest.responseText returns image path with body and head tags wrapped around, which is causing problem to display image.

  20. Waqas :
    @Waqas
    I am facing the same problem as you were. The XMLHttpRequest.responseText returns image path with body and head tags wrapped around, which is causing problem to display image.

    Thanks I figured it out. Following code helped me in getting the clean image src:

    var regex = /(]+)>)/ig;
    var body = XMLHttpRequest.responseText;
    var result = body.replace(regex, “”);
    $(‘#preview_image’).attr(‘src’, result);

    thanks Bill for this pretty post.

  21. This works even easier with Paperclip, since you can do

    render :text => @model_instance.attachment_name.url

    and not have to worry about parsing it with the above.

    One thing I’ve found: this method will not work if you are not using Prototype or jRails (required for remote_form_for). It also will not work with a simple form_for like somebody above suggested. This caused me much grief tonight. Also: if you put any sort of formatting around the input tag (like a paragraph tag) you need to change the js to point to the correct parent – the form – accordingly.

  22. Alternatively, you can specify the id of the form directly instead of calling this.parent-
    jQuery(“#uploadForm”).ajaxSubmit({
    beforeSubmit: function(a,f,o) {

  23. Hi Thanks for your tutorial.
    I got it working but have some strange behavior.
    The image uploads and it replaces the src of an image on the page.
    The first time I upload an image it works.
    The second time the image uploads but does not replace the image on the page unless I reload the page. I have been working for days trying to figure out what gives.

    Any help would be appreciated.

    Thanks

  24. Hi
    Thank you very much for your tutorial. I am using it and it works very well. Except for one strange thing.
    The image upload works the first time. However when I want to upload an image again on the same page it looks like it works but won’t change the image unless I reload the page.

    Any ideas why?

  25. Hi Bill, pretty informative tutorial… I had a thing to confirm….
    Aren’t we supposed to add something like in our /app/views/layouts/application.html.erb file….? . Here the file_name_having_js_code would be the file code for javascript as shown above.. I had seen this when basically we make use of jquery.. as per the railscast by Ryan B(http://railscasts.com/episodes/136-jquery).. Just wanted to confirm its addition in the above case also and whether I am actually doing it correctly..

    Thanks for the blog..

  26. thanks for this great post. i’ve been sweating on this for too long. and its the most simple solution of the lot.
    no flash, no iframes, no bulky plugins
    instead of a text response i threw in a partial and fed the object to it and ran append on the complete option and it works a treat.
    thanks again

  27. Thanks for this clear tutorial!

    The article needs an update though: ‘remote_form_for’ was removed from recent Rails versions, use ‘form_for’ with ‘:remote => true’ option instead.

    Also, ‘:url => { :controller => “blogs”, :action => :create_asset }’ generated an URL starting with ‘/assets?’ in my case.
    To avoid this, I used a simple route name as a parameter, which gives ‘:url => some_route_path’.

    Cheers,
    Samy

Leave a Reply to Bill Cancel reply

Your email address will not be published. Required fields are marked *