The time has finally come for a follow-up to my post from a couple years ago on using jQuery, attachment_fu, and Rails 2.3 to upload an asset to my blog. I wanted to share the updated version of my attempt to determine the absolute minimal code necessary to implement AJAX uploads on Rails 3 with Carrierwave. As was the case a few years ago, the Google results still tend to suck when searching for a simple means to accomplish an AJAX upload with Rails — the most popular result I could find this evening was a Stackoverflow post that detailed 9 (ick) steps, including adding a gem to the project and creating new middleware. No thanks!
The Javascript from my previous example is essentially unchanged. It uses jQuery and the jQuery-form plugin. The main challenge in getting a AJAX uploading working is that form_for :remote 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. Following is the Rails code that goes in your html.erb. Remember that in my case I am creating an image that will be associated with a model “BlogPost” that resides in the BlogPostsController. Adapt for your models/controllers accordingly:
<%= form_for(:image_form, :url => {:controller => :blog_posts, :action => :create_asset}, :remote => true, :html => {:method => :post, :id => 'upload_form', :multipart => true}) do |f| %>
Upload a file: <%= f.file_field :uploaded_data %>
<% end %>
Here’s the associated Javascript:
$('#upload_form input').change(function(){
$(this).closest('form').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.
},
});
});
Now, chances are you’re uploading this asset from a #new action, which means that the resource (here, the BlogPost) that will be associated with the image has yet to be created. That means we’re going to need a model that we can stick the AJAX-created image in until such time that the main resource has been created. We can do this if we create a migration for a new BlogImage model like so:
def self.up
create_table :blog_images do |t|
t.string :image
end
add_column :blog_posts, :blog_image_id, :integer # once created, we'll want to reference the BlogImage we created beforehand via AJAX
end
The corresponding BlogImage model would then be:
class BlogImage < ActiveRecord::Base
mount_uploader :image, BlogImageUploader
end
Of course, if your resource already exists at the time the AJAX upload will happen, then you're on easy street. In that case, you don't have to create a separate model like BlogImage, you can just add a column to your main resource (BlogPost) and mount the uploader directly to BlogPost. In either case, the BlogImageUploader class would be setup with whatever options you want, per the Carrierwave documentation.
Continuing under the assumption that you will separate your model from the main resource (in this case, the BlogImage, which is separate from the BlogPost), we can create this image before the BlogPost exists, and stash the BlogPost id however you see fit. Thus, your controller's #create_asset method will look like:
def create_asset
blog_image = BlogImage.new
blog_image.image = params[:image_form][:uploaded_data]
blog_image.save!
# TODO: store blog_image.id in session OR pass ID back to form for storage in a hidden field
# OR if your main resource already exists, mount the uploader to it directly and go sip on a
# pina colada instead of worrying about this
render :text => blog_image.image.url
end
And that's it. No new gems, plus low-fat Javascript and controller additions.
Bonus section: How to embed this AJAX upload form in the form for its parent resource
One of the more common questions from my last post was how to display this AJAX image upload amongst the form for another resource. There are many ways to accomplish this (see comments from last post if you've got time to kill), but in keeping with the spirit of simplicity in this post, one fast hack I've used:
- After all the form fields for the main resource, close the form without a submit button
- Insert the AJAX form
- Add a link below the AJAX form that is styled to look like a button. Have this link call Javascript to submit your main form
Not going to win any beauty contests, but easy to setup and gets the job done.
This was an absolute lifesaver, even if it only covers the broad strokes. Couldn’t have done it without you.