はじめに
某フリマアプリの模倣アプリを開発中、出品機能実装で複数画像の登録に死ぬほど手を焼いたので、備忘録として掲載します。
誤った記述などあればご指摘いただけると幸いです。
開発環境・前提
Ruby 2.5.1p57
Ruby on rails 5.2.3
jquery-rails 4.3.5
haml-rails 2.0.1
sass-rails 5.1.0
CarrierWave 2.1.0
完成コード
先に完成コードを載せておく。
belongs_to :item, optional: true
validates_presence_of :item
validates :content, presence: true
mount_uploader :content, ImageUploader
belongs_to :brand, optional: true
belongs_to :user, optional: true
belongs_to :category, optional: true
has_many :images, dependent: :destroy
accepts_nested_attributes_for :images, allow_destroy: true
def new
@item = Item.new
@brands = Brand.all
@category_parent_array = ["指定なし"]
Category.where(ancestry: nil).each do |parent|
@category_parent_array << parent.name
end
@item.images.build
end
def create
@item = Item.new(item_params)
if @item.save!
@image = @item.images.create
redirect_to :root
else
render :new
end
end
private
def item_params
params.require(:item).permit(
:name, :description, :condition, :price,
:fee, :brand_id, :area, :shipping_days,
images_attributes: [:content, :id, :_destroy]
).merge(user_id: current_user.id, category_id: params[:category_id], brand_id: params[:item][:brand_id])
end
# 画像投稿フォームの記述部分
.main-items
= form_with model: @item, local: true do |f|
.wrapper.image-wrapper
#image-box.image-wrapper__image-box
= f.fields_for :images do |i|
.image-wrapper__image-box__js.js-file_group{data:{index: "#{i.index}"}}
= i.label :content, class: "image-wrapper__image-box__js__label" do
.image-wrapper__image-box__js__label__image.img_field{id: "img_field--#{i.index}", onClick: "$('#file').click()"}
- if @item.images[i.index][:content].present?
= image_tag(f.image.content)
- else
= image_tag 'icon_camera.png', class: "image-wrapper__image-box__js__label__image__url"
= i.file_field :content, class: "image-wrapper__image-box__js__label__file js-file", id: "item_images_attributes_#{i.index}_content"
.js-remove
%span.js-remove__text
削除
モデルへのmout_uploaderの記述
before
mount_uploaders :content, ImageUploader
after
mount_uploader :content, ImageUploader
mount_uploaderとするかmount_uploadersか。
NoMethodError (undefined method `map' for #<ActionDispatch::Http::UploadedFile......> #省略
Did you mean? tap):
app/controllers/items_controller.rb:68:in `create'
mount_uploadersにすると、デフォルトでmapメソッドが使われてしまう。
つまり、1つのfile_fieldに複数の画像データが入っている配列である必要があるのだ。
そういう時は、file_fieldにmultiple: trueを記載する必要がある。
multiple: true
multiple: true を記述すると、一つのfile_fieldに複数画像をアップロードしようとする。
修正前の記述
= form_with model: @item, local: true do |f|
.wrapper.image-wrapper
#image-box.image-wrapper__image-box
= f.fields_for :images do |i|
.image-wrapper__image-box__js.js-file_group{data:{index: "#{i.index}"}}
= i.label :content, class: "image-wrapper__image-box__js__label" do
.image-wrapper__image-box__js__label__image.img_field{id: "img_field--#{i.index}", onClick: "$('#file').click()"}
- if @item.images[i.index][:content].present?
= image_tag(f.image.content)
- else
= image_tag 'icon_camera.png', class: "image-wrapper__image-box__js__label__image__url"
= i.file_field :content, multiple: true, class: "image-wrapper__image-box__js__label__file js-file", id: "item_images_attributes_#{i.index}_content", required: "required"
.js-remove
%span.js-remove__text
削除
inputタグのtype[file]部分のHTML(検証)
multiple有り
<input multiple="multiple" class="image-wrapper__image-box__js__label__file js-file"
id="item_images_attributes_0_content" type="file"
name="item[images_attributes][0][content][]">
multiple無し
<input class="image-wrapper__image-box__js__label__file js-file"
id="item_images_attributes_0_content" type="file"
name="item[images_attributes][0][content]">
デフォルトで設定されるname属性が変わる
私の場合は、各file_fieldに一つずつ保存させるようなコードを書いていたのにもかかわらず、multipleの記述をしてしまっていて、エラーが起きた。
labelタグのfor属性
labelタグのfor属性は、連動させたい子要素のidの値を記述する必要がある。
修正前のlabel部分の記述
%label.image-wrapper__image-box__js__label
.image-wrapper__image-box__js__label__image.img_field{id: "img_field--#{i.index}", onClick: "$('#file').click()"}
- if @item.images[i.index][:content].present?
= image_tag(f.image.content)
- else
= image_tag 'icon_camera.png', class: "image-wrapper__image-box__js__label__image__url"
= i.file_field :content, multiple: true, class: "image-wrapper__image-box__js__label__file js-file", id: "item_images_attributes_#{i.index}_content", required: "required"
.js-remove
%span.js-remove__text
削除
修正後
= i.label :content, class: "image-wrapper__image-box__js__label" do
.image-wrapper__image-box__js__label__image.img_field{id: "img_field--#{i.index}", onClick: "$('#file').click()"}
- if @item.images[i.index][:content].present?
= image_tag(f.image.content)
- else
= image_tag 'icon_camera.png', class: "image-wrapper__image-box__js__label__image__url"
= i.file_field :content, multiple: true, class: "image-wrapper__image-box__js__label__file js-file", id: "item_images_attributes_#{i.index}_content", required: "required"
.js-remove
%span.js-remove__text
削除
labelタグの性質をよく理解せずに使っていた。
修正前の記述だと、検証でみてみるとわかるが、labelタグにfor属性が付与されておらず、inputタグのid属性(ここでは、item_images_attributes_0_content)に対応しておらず、不具合がおきた。
後からわかったことだが、、hidden_fieldの記述を消してやれば、%labelのままでも支障はないことがわかった。
hidden_fieldの記述
修正前の記述
= form_with model: @item, local: true do |f|
.wrapper.image-wrapper
#image-box.image-wrapper__image-box
= f.fields_for :images do |i|
.image-wrapper__image-box__js.js-file_group{data:{index: "#{i.index}"}}
= i.label :content, class: "image-wrapper__image-box__js__label" do
.image-wrapper__image-box__js__label__image.img_field{id: "img_field--#{i.index}", onClick: "$('#file').click()"}
- if @item.images[i.index][:content].present?
= image_tag(f.image.content)
- else
= image_tag 'icon_camera.png', class: "image-wrapper__image-box__js__label__image__url"
= i.file_field :content, multiple: true, class: "image-wrapper__image-box__js__label__file js-file", id: "item_images_attributes_#{i.index}_content", required: "required"
= i.hidden_field :item_id, value: @item.id
.js-remove
%span.js-remove__text
削除
imagesのitem_idをparamsに送るための記述をしていたが、idや外部キーはデフォルトで送られるようになっているため、必要なかった。逆に、これがあることによって、:contentが入っていない空のfile_fieldがhidden_fieldと共にparamsに送られてしまうため、validationに引っかかってしまう。
最後に
チーム開発で商品出品機能を担当したことにより、HTML&CSS, jQuery, Rubyについての知識がかなり深まった。欲張りな性格なので、いろんな記事のいいとこ取りをしようとした結果、こんなにもの何重もの罠を自分で仕掛けて自分でハマるということになってしまった。次からは是非とも一つ一つの用法や性質を理解した上で、実装していきたい。
でも、何かしらの初学者ってこういう風に泥臭く成長していくのかなぁとも思った。
諦めたら、そこで試合終了だよ。