Rails 4 multiple image or file upload using carrierwave
Solution 1
This is solution to upload multiple images using carrierwave in rails 4 from scratch
Or you can find working demo : Multiple Attachment Rails 4
To do just follow these steps.
rails new multiple_image_upload_carrierwave
In gem file
gem 'carrierwave'
bundle install
rails generate uploader Avatar
Create post scaffold
rails generate scaffold post title:string
Create post_attachment scaffold
rails generate scaffold post_attachment post_id:integer avatar:string
rake db:migrate
In post.rb
class Post < ActiveRecord::Base
has_many :post_attachments
accepts_nested_attributes_for :post_attachments
end
In post_attachment.rb
class PostAttachment < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
belongs_to :post
end
In post_controller.rb
def show
@post_attachments = @post.post_attachments.all
end
def new
@post = Post.new
@post_attachment = @post.post_attachments.build
end
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
params[:post_attachments]['avatar'].each do |a|
@post_attachment = @post.post_attachments.create!(:avatar => a)
end
format.html { redirect_to @post, notice: 'Post was successfully created.' }
else
format.html { render action: 'new' }
end
end
end
private
def post_params
params.require(:post).permit(:title, post_attachments_attributes: [:id, :post_id, :avatar])
end
In views/posts/_form.html.erb
<%= form_for(@post, :html => { :multipart => true }) do |f| %>
<div class="field">
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<%= f.fields_for :post_attachments do |p| %>
<div class="field">
<%= p.label :avatar %><br>
<%= p.file_field :avatar, :multiple => true, name: "post_attachments[avatar][]" %>
</div>
<% end %>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
To edit an attachment and list of attachment for any post. In views/posts/show.html.erb
<p id="notice"><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @post.title %>
</p>
<% @post_attachments.each do |p| %>
<%= image_tag p.avatar_url %>
<%= link_to "Edit Attachment", edit_post_attachment_path(p) %>
<% end %>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
Update form to edit an attachment views/post_attachments/_form.html.erb
<%= image_tag @post_attachment.avatar %>
<%= form_for(@post_attachment) do |f| %>
<div class="field">
<%= f.label :avatar %><br>
<%= f.file_field :avatar %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
Modify update method in post_attachment_controller.rb
def update
respond_to do |format|
if @post_attachment.update(post_attachment_params)
format.html { redirect_to @post_attachment.post, notice: 'Post attachment was successfully updated.' }
end
end
end
In rails 3 no need to define strong parameters and as you can define attribute_accessible in both the model and accept_nested_attribute to post model because attribute accessible is deprecated in rails 4.
For edit an attachment we cant modify all the attachments at a time. so we will replace attachment one by one, or you can modify as per your rule, Here I just show you how to update any attachment.
Solution 2
If we take a look at CarrierWave's documentation, this is actually very easy now.
https://github.com/carrierwaveuploader/carrierwave/blob/master/README.md#multiple-file-uploads
I will use Product as the model I want to add the pictures, as an example.
-
Get the master branch Carrierwave and add it to your Gemfile:
gem 'carrierwave', github:'carrierwaveuploader/carrierwave'
-
Create a column in the intended model to host an array of images:
rails generate migration AddPicturesToProducts pictures:json
-
Run the migration
bundle exec rake db:migrate
-
Add pictures to model Product
app/models/product.rb class Product < ActiveRecord::Base validates :name, presence: true mount_uploaders :pictures, PictureUploader end
-
Add pictures to strong params in ProductsController
app/controllers/products_controller.rb def product_params params.require(:product).permit(:name, pictures: []) end
-
Allow your form to accept multiple pictures
app/views/products/new.html.erb # notice 'html: { multipart: true }' <%= form_for @product, html: { multipart: true } do |f| %> <%= f.label :name %> <%= f.text_field :name %> # notice 'multiple: true' <%= f.label :pictures %> <%= f.file_field :pictures, multiple: true, accept: "image/jpeg, image/jpg, image/gif, image/png" %> <%= f.submit "Submit" %> <% end %>
-
In your views, you can reference the images parsing the pictures array:
@product.pictures[1].url
If you choose several images from a folder, the order will be the exact order you are taking them from top to bottom.
Solution 3
Some minor additions to the SSR answer:
accepts_nested_attributes_for does not require you to change the parent object's controller. So if to correct
name: "post_attachments[avatar][]"
to
name: "post[post_attachments_attributes][][avatar]"
then all these controller changes like these become redundant:
params[:post_attachments]['avatar'].each do |a|
@post_attachment = @post.post_attachments.create!(:avatar => a)
end
Also you should add PostAttachment.new
to the parent object form:
In views/posts/_form.html.erb
<%= f.fields_for :post_attachments, PostAttachment.new do |ff| %>
<div class="field">
<%= ff.label :avatar %><br>
<%= ff.file_field :avatar, :multiple => true, name: "post[post_attachments_attributes][][avatar]" %>
</div>
<% end %>
This would make redundant this change in the parent's controller:
@post_attachment = @post.post_attachments.build
For more info see Rails fields_for form not showing up, nested form
If you use Rails 5, then change Rails.application.config.active_record.belongs_to_required_by_default
value from true
to false
(in config/initializers/new_framework_defaults.rb) due to a bug inside accepts_nested_attributes_for (otherwise accepts_nested_attributes_for won't generally work under Rails 5).
EDIT 1:
To add about destroy:
In models/post.rb
class Post < ApplicationRecord
...
accepts_nested_attributes_for :post_attachments, allow_destroy: true
end
In views/posts/_form.html.erb
<% f.object.post_attachments.each do |post_attachment| %>
<% if post_attachment.id %>
<%
post_attachments_delete_params =
{
post:
{
post_attachments_attributes: { id: post_attachment.id, _destroy: true }
}
}
%>
<%= link_to "Delete", post_path(f.object.id, post_attachments_delete_params), method: :patch, data: { confirm: 'Are you sure?' } %>
<br><br>
<% end %>
<% end %>
This way you simply do not need to have a child object's controller at all! I mean no any PostAttachmentsController
is needed anymore. As for parent object's controller (PostController
), you also almost don't change it - the only thing you change in there is the list of the whitelisted params (to include the child object-related params) like this:
def post_params
params.require(:post).permit(:title, :text,
post_attachments_attributes: ["avatar", "@original_filename", "@content_type", "@headers", "_destroy", "id"])
end
That's why the accepts_nested_attributes_for
is so amazing.
Solution 4
Also I figured out how to update the multiple file upload and I also refactored it a bit. This code is mine but you get the drift.
def create
@motherboard = Motherboard.new(motherboard_params)
if @motherboard.save
save_attachments if params[:motherboard_attachments]
redirect_to @motherboard, notice: 'Motherboard was successfully created.'
else
render :new
end
end
def update
update_attachments if params[:motherboard_attachments]
if @motherboard.update(motherboard_params)
redirect_to @motherboard, notice: 'Motherboard was successfully updated.'
else
render :edit
end
end
private
def save_attachments
params[:motherboard_attachments]['photo'].each do |photo|
@motherboard_attachment = @motherboard.motherboard_attachments.create!(:photo => photo)
end
end
def update_attachments
@motherboard.motherboard_attachments.each(&:destroy) if @motherboard.motherboard_attachments.present?
params[:motherboard_attachments]['photo'].each do |photo|
@motherboard_attachment = @motherboard.motherboard_attachments.create!(:photo => photo)
end
end
Solution 5
Here is my second refactor into the model:
- Move private methods to model.
- Replace @motherboard with self.
Controller:
def create
@motherboard = Motherboard.new(motherboard_params)
if @motherboard.save
@motherboard.save_attachments(params) if params[:motherboard_attachments]
redirect_to @motherboard, notice: 'Motherboard was successfully created.'
else
render :new
end
end
def update
@motherboard.update_attachments(params) if params[:motherboard_attachments]
if @motherboard.update(motherboard_params)
redirect_to @motherboard, notice: 'Motherboard was successfully updated.'
else
render :edit
end
end
In motherboard model:
def save_attachments(params)
params[:motherboard_attachments]['photo'].each do |photo|
self.motherboard_attachments.create!(:photo => photo)
end
end
def update_attachments(params)
self.motherboard_attachments.each(&:destroy) if self.motherboard_attachments.present?
params[:motherboard_attachments]['photo'].each do |photo|
self.motherboard_attachments.create!(:photo => photo)
end
end
Related videos on Youtube
SSR
Ping me on Skype : savan.raval4 Drop me an Email : [email protected]
Updated on May 07, 2020Comments
-
SSR almost 4 years
How can I upload multiple images from a file selection window using Rails 4 and CarrierWave? I have a
post_controller
andpost_attachments
model. How can I do this?Can someone provide an example? Is there a simple approach to this?
-
wael about 10 yearsin the show action of the post controller i think you've forgot @post =Post.find(params[:id])
-
SSR about 10 yearsYes thanks, but In rails 4 by default there is one method called "set_post" when you create scaffold. it finds parameter for show, edit, update method.
-
hawk almost 10 years@SSR Why you looping through each post attachments in
create
action? Rails and carrierwave are smart enough to save collections automatically. -
Chris almost 10 yearsIt's not possible to create a Post then go back and change the image? @SSR
-
SSR almost 10 years@Chris: Means you want to create a post first and then attach a Image?
-
SSR almost 10 years@hawk : Yea we can do so. but when you cant find nested attributes params, then you can do so.
-
user1876128 over 9 years@SSR :I have used the code, in my index method in post_controller
code
@posts = Posts.all @post_attachment = @posts.post_attachments.allcode
I have undefined method post_attachments for <Post::ActiveRecord_Relation:0x007fa6087482a8> -
Jepzen over 9 yearsThis works great on a new post but does not look very good on an edit. Anyway to avoid showing all attachments in the edit form?
-
SSR over 9 years@Jepzen : Will update my ans with edit view very soon. Actually I missed that part. I didn't work on that yet but I will do now.
-
Tun over 9 yearsWould love to see edit (especially handling
:_destroy
part) -
Deepti Kakade over 9 yearsI am not getting the above answer you have posted for edit, how we will edit multiple files if we are using accepts_nested_attributes_for?
-
SSR over 9 years@DeeptiKakade : You can add multiple items at a time but can't change or update multiple items at time. because how you define that which item will replace with existing one. so you can only edit a single item.
-
Ji Mun about 9 yearsThis has been very helpful! Do you know how to use cache to sort-of save the files to the uploaded? I've been having trouble using the cache feature for multiple file uploads.
-
raj_on_rails about 9 years@SSR - Your answer is very helpful. Could you please update your answer with edit action too.
-
SSR almost 9 yearsThanks for sharing your code. when you get time please update code at my gihub repo and do not forget to comment for each method so everyone can easily understand code.
-
Chris Habgood almost 9 yearsI cloned the repos, will you give me permission to do a PR?
-
SSR almost 9 yearsYes sure. What is your github username
-
Chris Habgood almost 9 yearsHave you had a chance to give me access?
-
Dercni over 8 yearsWhy do you need to include the post_attachments in the post_params as these are not being mass assigned as a normal nested attribute is? These are being manually added within the create action referencing the params hash directly. I removed the post_attachments from the strong params and it still works.
-
Dercni over 8 yearsUpon further testing it doesn't work with the nested item removed from the strong params.
-
Loi Huynh over 8 yearsHi @SSR, if I want just ONE (maybe the first) image to show on the index page while all images showing on the show, what do I need to do on the index page to just have the first image showing (not every image)
-
Dennis over 8 yearsDoes anyone know how to make the images persist in a form that has been submitted and failed due to validation errors? There's documentation for this in the carrierwave gem but only for individual files, and I can't get it working for this scenario. Thanks!
-
user4584963 about 8 yearsIn the create controller shouldn't it be
params[:post][:post_attachments]['avatar'].each
?params[:post_attachments]
is nil -
Toby 1 Kenobi about 8 yearsCarrierWave's solution to this problem makes me cringe. It involves putting all the references to the files into one field in an array! It certainly wouldn't be considered the "rails way". What if you then want to remove some, or add extra files to the post? I'm not saying it wouldn't be possible, I'm just saying it would be ugly. A join table is a much better idea.
-
epicrato about 8 yearsI couldn't agree more Toby. Would you be so kind to provide that solution?
-
Toby 1 Kenobi about 8 yearsThat solutions is already provided by SSR. Another model is put in place to hold the uploaded file, then the thing that needs many files uploaded relates in a one-to-many or many-to-many relationship with that other model. (the join table I mentioned in my earlier comment would be in the case of a many-to-many relationship)
-
dchess almost 8 yearsWhen I add validations to the post_attachment model, they do not prevent the post model from saving. Instead the post is saved, and then the ActiveRecord invalid error is thrown for the attachment model only. I think this is because of the create! method. but using create instead just fails silently. Any idea how to have the validation happen on the post reach into the attachments?
-
chaostheory over 7 yearsSince this is making a gallery, is there a way to make this work with carrierwave versions i.e. generating and using thumbnails
-
chaostheory over 7 yearsThanks @Toby1Kenobi, I was wondering how the column array method would account for image versions (I don't see how it can). Your strategy is doable.
-
Mansi Shah almost 7 yearsI have implemented this feature of Carrierwave with Rails 5.x.x, github.com/carrierwaveuploader/carrierwave/blob/master/… But I am not able to run it successfully, and it is generating error,
UndefinedConversionError ("\x89" from ASCII-8BIT to UTF-8)
For SSR solution, it works fine with Rails 4.x.x, but I am facing challenges (with Rails 5.x.x.) i.e. its storingActionDispatch::Http::UploadedFile
in database instead filename. Its also not storing files in public folders for given path in uploader. -
Luis Fernando Alen almost 7 yearsThose are actually major additions to @SSR answer, not minor :) accept_nested_attributes_for is quite something. Indeed there's no need for a child controller at all. By following your approach, the only thing I'm unable to do is to display form error messages for the child when something goes wrong with the upload.
-
W.M. over 6 yearsPutting multiple images of an entity inside one JSON/text field makes it also impossible to assign
title
andalt
attributes for those images, unless it is possible to do so using nested JSON. Any idea? -
SEJU over 4 yearsThanks for your input. I got the upload working, but I am wondering how I could add additional attributes to the post_attachments form field in views/posts/_form.html.erb?
<%= d.text_field :copyright, name: "album[diapos_attributes][][copyright]", class: 'form-field' %>
writes the copyright only to the last record and not to all of them. -
Nitin Jain over 3 yearsMultiple file upload has several issues with carrierwave with mount_uploaders :pictures, PictureUploader