#はじめに
とある事情によりこちらの記事に大変お世話になったのですが、色々気になるところがあり、
手直しをしていった結果、不具合の修正やリファクタリングができたため記事にまとめてみました。
#動作環境
ruby 2.5.1
rails 5.2.4.2
carrierwave 2.1.0
mini_magick 4.9.5
jquery-rails 4.3.5
#下準備
動作の確認をするためにミニアプリを作りました。
元記事には省略されていたので、ついでにこれも載せておきます。
**長いので見たい人だけ展開してください**
$ rails _5.2.4_ new post_images_sample --database=mysql --skip-test --skip-bundle
$ gem install jquery-rails haml-rails carrierwave mini_magick
$ bundle install
$ rails g model item
$ rails g model image
$ rails g controller items
$ rails g uploader image
rails-ujsより上段にjqueryを追加
//= require jquery
//= require rails-ujs
class CreateItems < ActiveRecord::Migration[5.2]
def change
create_table :items do |t|
t.string :name ,null: false,default:""
t.timestamps
end
end
end
class CreateImages < ActiveRecord::Migration[5.2]
def change
create_table :images do |t|
t.references :item ,null: false, foreign_key: true
t.string :image ,null: false
t.timestamps
end
end
end
class Item < ApplicationRecord
has_many :images, dependent: :destroy
accepts_nested_attributes_for :images, allow_destroy: true
end
class Image < ApplicationRecord
belongs_to :item
mount_uploader :image, ImageUploader
end
Rails.application.routes.draw do
root "items#new"
resources :items ,only: [:index,:new,:create,:edit,:update]
end
class ImageUploader < CarrierWave::Uploader::Base
(中略)
(以下を追記)
include CarrierWave::MiniMagick
process resize_to_fit: [200, 200]
(後略)
end
$ rails db:create
$ rails db:migrate
下準備ここまで。
#viewについて
それでは魔改造していきましょう。
と言っても、viewはそんなにいじりませんでした。
元記事の完成度の高さが伺えますね。
※元のコードについては元記事を参照してください。
####新規投稿画面
15行目のclass名に誤字があったのと、id: "label-box--0"
は必要なかったので削除しました。
.main
%section.main__block
= form_with(model:@item, local:true) do |f|
%h2.sell__block__head
商品の情報を入力
.sell__block__form
.sell__block__form__upload
%h3.sell__block__form__upload__head
出品画像
%span.require 必須
%p 最大5枚までアップロードできます
.post__drop__box__container
.prev-content
.label-content
%label{for: "item_images_attributes_0_image", class: "label-box"}
%pre.label-box__text-visible クリックしてファイルをアップロード
.hidden-content
= f.fields_for :images ,multiple: true do |i|
= i.file_field :image,class:"hidden-field"
%input{class:"hidden-field", type: "file", name: "item[images_attributes][1][image]", id: "item_images_attributes_1_image" }
%input{class:"hidden-field", type: "file", name: "item[images_attributes][2][image]", id: "item_images_attributes_2_image" }
%input{class:"hidden-field", type: "file", name: "item[images_attributes][3][image]", id: "item_images_attributes_3_image" }
%input{class:"hidden-field", type: "file", name: "item[images_attributes][4][image]", id: "item_images_attributes_4_image" }
.sell__block__form__name
.form-group__name
%label
商品名
%span.require 必須
%div
= f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name"
.sell__block__form__btn
%div
= f.submit "出品する",class: "btn-default__btn-red"
####編集画面
元記事ではedit画面に遷移した際に、jsでdelete-btn
にidを付与しているのですが、
16行目を- @item.images.each_with_index do |image,i|
とし、view側で先にdata-delete-idをつけた方が記述が減るかなと考え、変更しました。
また、class:'hidden-checkbox'
は使用しない(hidden-content
に display:none;
をかけることで不要となる)ため、削除しました。
.main
%section.main__block
= form_with model:@item, local:true do |f|
%h2.sell__block__head
商品の情報を入力
.sell__block__form
.sell__block__form__upload
%h3.sell__block__form__upload__head
出品画像
%span.require 必須
%p 最大5枚までアップロードできます
.post__drop__box__container
.prev-content
//JSで挿入したhtmlと同じ形 each文でのプレビュー表示
- @item.images.each_with_index do |image,i|
.preview-box
.upper-box
= image_tag image.image.url, width: "112", height: "112", alt: "preview"
.lower-box
.update-box
.edit-btn 編集
.delete-box
.delete-btn{data:{delete_id: i}} 削除
.label-content
//プレビューの数に合わせてforオプションを指定
= f.label :"images_attributes_#{@item.images.length}_image", class: "label-box" do
%pre.label-box__text-visible クリックしてファイルをアップロード
.hidden-content
= f.fields_for :images do |i|
//プレビューが出ている分のfile_fieldとdelete用のチェックボックスを設置
= i.file_field :image,class:"hidden-field"
= i.check_box:_destroy
//5つのfile_fieldを準備するに当たって、足りない分を表示
- @item.images.length.upto(4) do |i|
%input{name: "item[images_attributes][#{i}][image]", id: "item_images_attributes_#{i}_image", class:"hidden-field", type:"file"}
.sell__block__form__name
.form-group__name
%h3.sell__block__form__upload__head
商品名
%span.require 必須
%div
= f.text_field :name, placeholder:"商品名(必須 40文字まで)",class: "form__group__name"
.sell__block__form__btn
%div
= f.submit "出品する",class: "btn-default__btn-red"
#jsについて
元記事からかなり記述量を減らすことができました。
- 主な変更点
- viewで対応済みのため、編集画面に遷移した際のプレビュー画像に対して行う処理を削除。
- function buildHTMLへの引数を増やし、URLを追加する処理を統合。
- 処理の都度再定義されていた
var prevContent = $('.label-content').prev();
をconst prevContent = $('.label-content').prev();
とし、統合。 - 所々にあった
setLabel()
をsetLabel(count)
とし、label-contentの表示・非表示、label-boxのid・forの操作を含ませることで統合。 - 編集時、追加画像を削除した時にフォームの中身が削除されないバグを解消。
- 編集画面に
GET
メソッドで遷移した際に$(prevContent).css('width').replace(/[^0-9]/g, '')
がNo Method Error
を 返してくるのでparseInt($(prevContent).css('width'))
へ記述を変更。 (turbolinksとjQueryの相性の問題)
$(document).on('turbolinks:load', function(){
//共通の定数を定義==================================================================
const prevContent = $('.label-content').prev();
//プレビューのhtmlを定義============================================================
function buildHTML(id,image) {
var html = `<div class="preview-box">
<div class="upper-box">
<img src=${image} alt="preview">
</div>
<div class="lower-box">
<div class="update-box">
<div class="edit-btn">編集</div>
</div>
<div class="delete-box">
<div class="delete-btn" data-delete-id= ${id}>削除</div>
</div>
</div>
</div>`
return html;
}
//ラベルのwidth・id・forの値を変更==================================================
function setLabel(count) {
//プレビューが5個あったらラベルを隠す
if (count == 5) {
$('.label-content').hide();
} else {
//プレビューが4個以下の場合はラベルを表示
$('.label-content').show();
//プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定
labelWidth = (620 - parseInt($(prevContent).css('width')));
$('.label-content').css('width', labelWidth);
//id・forの値を変更
$('.label-box').attr({for: `item_images_attributes_${count}_image`});
}
}
//編集ページ(items/:i/edit)へリンクした際のアクション==================================
if (window.location.href.match(/\/items\/\d+\/edit/)){
//プレビューの数を取得
var count = $('.preview-box').length;
//countに応じてラベルのwidth・id・forの値を変更
setLabel(count)
}
//プレビューの追加=================================================================
$(document).on('change', '.hidden-field', function() {
//hidden-fieldのidの数値のみ取得
var id = $(this).attr('id').replace(/[^0-9]/g, '');
//選択したfileのオブジェクトを取得
var file = this.files[0];
var reader = new FileReader();
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file);
//読み込み時に発火するイベント
reader.onload = function() {
var image = this.result;
//htmlを作成
var html = buildHTML(id,image);
//ラベルの直前のプレビュー群にプレビューを追加
$(prevContent).append(html);
//プレビュー削除したフィールドにチェックボックスがあった場合、チェックを外す
if ($(`#item_images_attributes_${id}__destroy`)){
$(`#item_images_attributes_${id}__destroy`).prop('checked',false);
}
//プレビューの数を取得
var count = $('.preview-box').length;
//countに応じてラベルのwidth・id・forの値を変更
setLabel(count);
}
});
// 画像の削除=====================================================================
$(document).on('click', '.delete-btn', function() {
var id = $(this).attr('data-delete-id')
//削除用チェックボックスがある場合はチェックボックスにチェックを入れる
if ($(`#item_images_attributes_${id}__destroy`).length) {
$(`#item_images_attributes_${id}__destroy`).prop('checked',true);
}
//画像を消去
$(this).parent().parent().parent().remove();
//フォームの中身を削除
$(`#item_images_attributes_${id}_image`).val("");
//プレビューの数を取得
var count = $('.preview-box').length;
//countに応じてラベルのwidth・id・forの値を変更
setLabel(count);
});
});
#controllerについて
変更点
updateメソッドで画像を全部消した上でアップデートすると、再編集の際、画像ファイルを拾ってくれないバグが存在することを確認したものの、
「画像なし」からも再編集できる方法が発見できなかったため、「画像なし」では保存できないように記述を変更し、回避しました。
class ItemsController < ApplicationController
def new
@item = Item.new
@images = @item.images.build
end
def create
@item = Item.new(item_params)
@item.save
redirect_to edit_item_path(@item.id)
end
def edit
@item = Item.find(params[:id])
end
def update
@item = Item.find(params[:id])
length = @item.images.length
i = 0
while i < length do
if item_update_params[:images_attributes]["#{i}"]["_destroy"] == "0"
@item.update(item_update_params)
redirect_to edit_item_path(@item.id)
return
else
i += 1
end
end
if item_update_params[:images_attributes]["#{i}"]
@item.update(item_update_params)
end
redirect_to edit_item_path(@item.id)
return
end
private
def item_params
params.require(:item).permit(
:name,
[images_attributes: [:image]])
end
def item_update_params
params.require(:item).permit(
:name,
[images_attributes: [:image, :_destroy, :id]])
end
end
#その他
indexページとか、showページとか、destoroyページとかも作ったのですが、元記事に関連していないため、ここでは割愛します。
#まとめ
記事にしてみると、自分では魔改造したつもりが、案外そうでもなかったですね。
元記事の作者様には改めて感謝申し上げます。
この記事をご覧になった方は是非、元記事へもご訪問下さい。
#参考にさせていただいた記事
画像の複数枚投稿と編集とプレビューと私