shiningthrough

Dynamic multiple image uploads with Ruby on Rails

23 February 2010

This tutorial explains how to create a simple web application that allows a user to upload one, or multiple images. It makes use of the paperclip plugin, Ajax using RJS and new multi-model form functionality that has arrived with Rails 2.3.3 in the form of accepts_nested_attributes_for.

Make sure your up to date with Rails, you will need at least 2.3.3

Lets start by making a new rails application

rails shop

Lets add some scaffolding to our new application ...

script/generate scaffold product name:string price:decimal

Next we create a model class that will hold our photos:

script/generate model photo product_id:integer

Now we need to install the paperclip plugin, you need to have git installed for this:

script/plugin install git://github.com/thoughtbot/paperclip.git

Run the Paperclip generator, this will create the appropriate migrations.

script/generate paperclip photo data

Then we just run the migrations:

rake db:migrate

Right, thats everything setup, now its time to delve into the code. First thing to do is setup the models. The photo model should look like this:

class Photo < ActiveRecord::Base
  belongs_to :product
  has_attached_file :data  
end

The first line sets up half the relationship between Photo and Product. The second line adds the paperclip attachment to our Photo model, there are a lot of optional settings that can be made here, but are beyond the scope of this tutorial, see the tutorial in the references for more information on paperclip. For now lets keep things simple.

The product model should look like this:

class Product < ActiveRecord::Base
  has_many :photos
  accepts_nested_attributes_for :photos, :allow_destroy => true
end

The first line specifies that we want one or more photos to be associated with our product. The second line is where the magic happens, this is new in rails 2.3.3, this specifies that photos is a nested attribute, this allows you to save attributes on associated 'Photo' records through the parent, 'Product'.

The View Code

All we have to do now is write the view code. In most scenarios, the number of images you want to add will be variable, therefore we will allow the user to dynamically add as many images as required. This kind of functionality means javascript, so lets add in the prototype libraries in products.html.erb.

<head>
  ....
  <%= javascript_include_tag :defaults %>
</head>

Firstly lets add the view code necessary to create a new photo, add the following to your new.html.erb file, within the form_for:

<%= add_photo(f) %>

And also add file upload support to the form, by adding :multipart => true to the form_for, as shown here:

<% form_for(@product, :html => {:multipart => true}) do |f| %>

Next we will add the implementation for the add_photo function. Go to products_helper.rb, and add the following funtion:

def add_photo(form_builder)
  link_to_function "add", :id  => "add_photo" do |page|
    form_builder.fields_for :photos, Photo.new, :child_index => 'NEW_RECORD' do |photo_form|
      html = render(:partial => 'photo', :locals => { :f => photo_form })
      page << "$('add_photo').insert({ before: '#{escape_javascript(html)}'.replace(/NEW_RECORD/g, new Date().getTime()) });"
    end
  end
end

This is a little bit complex, so lets go through it.

link_to_funtion is a helper found in the ActionView library, you provide it with some link text, and some JavaScript to execute when the link is clicked, it is important for this example that the link is given an id, you will see why shortly. We must build up a string to be executed within our link_to_funtion block, so we create a model scope for our new photo using form_for. Within this new scope we evaluate a partial(we have not written this yet) and store it locally, we then pass this evaluated partial into some prototype javascript that will insert the rendered HTML into the page. The point at which it is inserted is specified above as inserting 'before' the element with id add_photo, that is why the link was given an id, so that the HTML could be inserted before it.

Next we need to add the partial that was mentioned above. Create a new file in your app/views/products directory called _photo.html.erb (the underscore prefix is a Rails convention that specifies it as a partial), and paste in the following code:

<fieldset>
    <% if !f.object.new_record? %>
        <%= image_tag f.object.data.url %>
    <% end %>
    
    <%= f.file_field :data %>
    <%= delete_photo(f) %>
</fieldset>

We use a fieldset tag so we can group related items together, in this case its an image, a file upload input tag, and a delete link that runs some javascript. The fieldset allows that delete link to target the element that encompasses it. Next, we check if the object we are displaying is a new record, if its not, then we have an existing photo, so lets display it with an image_tag. The final two lines here provide a file upload input tag, and a call to a helper method that will create the delete link, lets have a look at that helper now.

def delete_photo(form_builder)
  if form_builder.object.new_record?
    link_to_function("Remove this Photo", "this.up('fieldset').remove()")
  else
    form_builder.hidden_field(:_delete) +
    link_to_function("Remove this Photo", "this.up('fieldset').hide(); $(this).previous().value = '1'")
  end
end

The first thing we do in delete_photo is to check if we are dealing with a new record or not, if it is, then we can simply delete the fieldset element we marked up previously. If, however, we have an actual photo, then it needs to be deleted from the database. Because we set :allow_destroy => true for accepts_nested_attributes_for, we can use the virtual attribute _delete in order to delete the child record.

update

In order to display all the photos for your particular product, add the following code to your show.html.erb file.

<% @product.photos.each do |photo| %>
  <%= image_tag photo.data.url %>
<% end %>

And that wraps it up, try it out, you are now able to add and remove multiple images dynamically!

References:

Comments

Devon

Cracking stuff, cheers mate.

Rustin Jessen

Great write-up. Thank you. I just completed making this work with attachment_fu. All is working perfectly. I ran across your write up while searching for an way to display thumbnails of the existing images that have been uploaded. The only bit I can't seem to make work is <%= image_tag f.object.data.url %> I'm sure it's a difference between Paperclip and Attachment_fu, but I'm afraid I don't know enough to sort it out. I've attempted to format that expression to match my other thumbnail calls, but can't seem to make it work. Do you have any experience with Attachment_fu? Any idea how to display those existing thumbnails?

shiningthrough

I don't have any recent experience with attachment_fu, sorry, but the line you quote just displays an existing image, and should be the same code as you use elsewhere to display images. If that's not working, only thing i can think of is you may have omitted the ! in front of f.object.new_record?

hilikuz

Great! It's working. What should I put in show.html.erb to show the images?

shiningthrough

@hilikuz - sorry, I should have put that in the original tutorial, I have made an update above.

Max

Hey, thanks for the tutorial -- what about an edit page? That's the last part of the puzzle for me. I'd like to be able to add/delete images from the parent resource's edit page.

hilikuz

@shiningthrough, Thank you for the update. I was able to add the code in show.html.erb. however database is looking for photos.product_id. I am a newbie here, please correct me if the code is wrong. I was able to fix the error. I edited _create_photos.rb migration file: class CreatePhotos < ActiveRecord::Migration def self.up create_table :photos do |t| t.references :product t.timestamps end add_index :photos, :product_id end def self.down drop_table :photos end end

Marcelo

Great tutorial, thanks!

Narain

Nice tutorial. If this would be only with Jquery, that would've made my day.

Ariel De La Rosa

I am using rails 2.2.2 and accepts_nested_attributes_for is not a defined method for it. Is there an alternative to that method ? Thanks in advance!

shiningthrough

@Ariel De La Roasa - You could try something like this http://railscasts.com/episodes/73-complex-forms-part-1, but it's no where near as nice as accepts_nested_attributes_for why don't you upgrade?

RakeBates

Great Tutorials, Thanks

xain

Thank you for the great tutorial, but I need to add a column called photo_id to photos table to make it work too. And I am also waiting for the editing page part.

pop

Keep in mind Paperclip has an ImageMajick dependency.

Nick

Great tutorial, far and away the best help on this subject I've seen. PS: You must change "script/generate model photo" to "script/generate model photo product_id:integer"

shiningthrough

Thanks for the kind words Nick, and thanks for reporting the mistake, I have updated the article.

rudf0rd

So for those that found this and wanted to see what goes in an edit page, this is what I came up with (note that i'm using formtastic, so modify accordingly for yourself). Also if you're on rails 3, I believe you will need to change _delete to _destroy in the delete_photo helper. In my form partial: <%= form.semantic_fields_for :photos do |p| %> <fieldset class="photo_display"> <%= p.input :caption %> <%= image_tag p.object.photo.url %> <%= delete_photo(p) %> </fieldset> <% end %>

IT Contractor Mortgages

Awesome, cheers for the walkthrough. Worked first time which is not always the case with plugin tutorials. Richie.

Jon

Might be a silly question, but how might I add just one of the images for a given product, instead of all of them? I am trying to display one full-sized image and multiple thumbnails. I can't seem to figure out how to display just one. After that I'd like to have the large image change with AJAX when the user selects a different thumbnail. Thanks! Jon