はじめに
初投稿になります。syomaと申します。よろしくお願いいたします。
某プログラミングスクールの最終課題で某フリマアプリのクローンにて、商品出品における画像の複数投稿が大変だった件について投稿いたします。
参考記事が幾つかございましたので、そちらをを参考にさせていただきました。この場をお借りして感謝申し上げます。ただ、どの記事にもscssの記載が無く、クラスに当たるscssの記述に苦戦したので画像複数(10枚まで)投稿の記述とscssをセットで記載してみます。
実現したいこと
1.画像をアップロードし、結果を送信する前にプレビューを表示する。(ドラッグ&ドロップは非対応です。すみません。)
2.そのほかのフォームの結果と一緒に画像をコントローラに送信する。
3.画像のプレビュー時に、削除ボタンで個別にアップロードする画像を変更する。
環境
Rails 5.2.4.1
ruby 2.5.1
注意点
- Rails初学者のため、間違っている部分が多々ある可能性がありますので、参考程度にご覧ください。
- gemのCarrierwaveで1枚画像投稿ができている。
導入がまだの方→https://qiita.com/syoma/items/183e37ea973f569673b9 - モデル側については、解説ありません。コントローラ、ビュー、javascript部分になります。モデル側は、good(商品)に対して複数のphotosテーブルのレコードを登録できるような中間テーブルを作成しています。
テーブル構造
モデル同士の関係
userテーブル(誰が)、goodテーブル(何を)、categoryテーブル(どんな→Gemのancestryを使用)、photo(画像)と分けておりますが、今回はgood,photoテーブルのみ記載しておきます。
class Good < ApplicationRecord
belongs_to :user
belongs_to :category
has_many :photos, dependent: :destroy
accepts_nested_attributes_for :photos, allow_destroy: true
end
class Photo < ApplicationRecord
belongs_to :good , optional: true
mount_uploader :image, ImageUploader
end
controller
class GoodsController < ApplicationController
def new
@good = Good.new
@good.photos.build()
end
def create
@good = Good.new(good_params)
respond_to do |format|
if @good.save!
params[:good_photos][:image].each do |image|
@good.photos.create(image: image, good_id: @good.id)
end
format.html{redirect_to root_path}
else
@good.photos.build
format.html{render action: 'new'}
end
end
end
def good_params
params.require(:good).permit(:category_id, :brand, :name, :condition, :discription, :size, :delivery_type, :prefecture, :day, :fee, photos_attributes: [:image]).merge(user_id: current_user.id)
end
end
.merge(user_id: current_user.id)は誰が出品したかをgoodデーブルに保存してくれる記述です。
current_userはdeviseが提供しているメソッドでログインしているユーザーの情報を取得してくれます。
もし、user登録機能または出品を単体でしたい場合はこの一文を消してくださいまし。
haml
出品なのでnewになります。
= form_for @good , html: {id: "item-dropzone"} do |f|
.upload-box
.upload-box__head
%h3.bigger 出品画像
%span 必須
%p.discription 最大10枚までアップロードできます
-# ここからが複数画像出品の部分です
= f.fields_for :photos do |image|
.dropzone-container
#preview
.dropzone-area
= image.label :image, class: "dropzone-box", for: "upload-image" do
.input_area
= image.file_field :image, multiple: true, name: 'good_photos[image][]', id: "upload-image", class: "upload-image", 'data-image': 0
%p ここをクリックしてください
.dropzone-container
#preview2
.dropzone-area2
= image.label :image, class: "dropzone-box", for: "upload-image" do
%p ここをクリックしてください
-# 複数画像出品終わり、以下は様々なフォームがある想定
10枚の画像を取り込むためにinputタグを10個用意すんのかい、せんのかいと不安でしたが、どうやら
= image.file_field :image, multiple: true
の一文で対応してくれそう!やったね!
クラスについてですが.dropzone-areaに画像が放り込まれると#previewに表示されるイメージです。
おいおいsyomaさん。消去・編集ボタンないけど!?(キレ気味)
ご安心を!javascript側で画像が投稿された瞬間に消去・編集ボタンを#previewに差し込むという記述にしております。
scss
//image投稿欄のCSS
.dropzone-container{
display: block;
margin: 16px auto 0;
display: flex;
//プレビュー表示欄のCSS
#preview , #preview2{
display: flex;
.img_view {
height: 162px;
width: 112px;
margin: 0 15px 10px 0;
img{
width: 112px;
height: 112px;
}
}
.btn_wrapper {
display: flex;
text-align: center;
.btn.edit {
color: #00b0ff;
width: 50%;
height: 50px;
line-height: 50px;
border: 1px solid #eee;
background: #f5f5f5;
cursor: pointer;
}
.btn.delete {
color: #00b0ff;
width: 50%;
height: 50px;
line-height: 50px;
border: 1px solid #eee;
background: #f5f5f5;
cursor: pointer;
}
}
}
//投稿クリックエリアのCSS
.dropzone-area {
margin-bottom: 10px;
width: 620px;
.dropzone-box {
display: block;
border: 1px dashed #ccc;
position: relative;
background: #f5f5f5;
width: 100%;
height: 162px;
cursor: pointer;
p {
position: absolute;
top: 50%;
left: 16px;
right: 16px;
text-align: center;
font-size: 14px;
line-height: 1.5;
font-weight: bold;
-webkit-transform: translate(0, -50%);
transform: translate(0, -50%);
pointer-events: none;
white-space: pre-wrap;
word-wrap: break-word;
}
.input_area {
display: none;
}
}
}
//投稿クリックエリアのCSS
.dropzone-area2{
display: none;
margin-bottom: 10px;
width: 620px;
.dropzone-box {
display: block;
border: 1px dashed #ccc;
position: relative;
background: #f5f5f5;
width: 100%;
height: 162px;
cursor: pointer;
p {
position: absolute;
top: 50%;
left: 16px;
right: 16px;
text-align: center;
font-size: 14px;
line-height: 1.5;
font-weight: bold;
-webkit-transform: translate(0, -50%);
transform: translate(0, -50%);
pointer-events: none;
white-space: pre-wrap;
word-wrap: break-word;
}
.input_area {
display: none;
}
}
}
}
.dropzone-area2はdisplay: none;にしております。javascript側で5枚目が選択された時点で表示される仕組みにするためです。
javascript
$(document).on('turbolinks:load', function(){
var dropzone = $('.dropzone-area');
var dropzone2 = $('.dropzone-area2');
var dropzone_box = $('.dropzone-box');
var images = [];
var inputs =[];
var input_area = $('.input_area');
var preview = $('#preview');
var preview2 = $('#preview2');
$(document).on('change', 'input[type= "file"].upload-image',function(event) {
var file = $(this).prop('files')[0];
var reader = new FileReader();
inputs.push($(this));
var img = $(`<div class= "img_view"><img></div>`);
reader.onload = function(e) {
var btn_wrapper = $('<div class="btn_wrapper"><div class="btn edit">編集</div><div class="btn delete">削除</div></div>');
img.append(btn_wrapper);
img.find('img').attr({
src: e.target.result
})
}
reader.readAsDataURL(file);
images.push(img);
if (images.length <= 4) {
$('#preview').empty();
$.each(images, function(index, image) {
image.data('image', index);
preview.append(image);
})
dropzone.css({
'width': `calc(100% - (20% * ${images.length}))`
})
// 画像が5枚のとき1段目の枠を消し、2段目の枠を出す
} else if (images.length == 5) {
$("#preview").empty();
$.each(images, function(index, image) {
image.data("image", index);
preview.append(image);
});
dropzone2.css({
display: "block"
});
dropzone.css({
display: "none"
});
preview2.empty();
// 画像が6枚以上のとき
} else if (images.length >= 6) {
// 1〜5枚目の画像を抽出
var pickup_images1 = images.slice(0, 5);
// 1〜5枚目を1段目に表示
$('#preview').empty();
$.each(pickup_images1, function(index, image) {
image.data('image', index);
preview.append(image);
})
// 6枚目以降の画像を抽出
var pickup_images2 = images.slice(5);
// 6枚目以降を2段目に表示
$.each(pickup_images2, function(index, image) {
image.data('image', index + 5);
preview2.append(image);
})
dropzone.css({
'display': 'none'
})
dropzone2.css({
'display': 'block',
'width': `calc(100% - (20% * ${images.length - 5}))`
})
// 画像が10枚になったら枠を消す
if (images.length == 10) {
dropzone2.css({
display: "none"
});
}
}
var new_image = $(`<input multiple= "multiple" name="good_photos[image][]" class="upload-image" data-image= ${images.length} type="file" id="upload-image">`);
input_area.prepend(new_image);
});
$(document).on('click', '.delete', function() {
var target_image = $(this).parent().parent();
$.each(inputs, function(index, input){
if ($(this).data('image') == target_image.data('image')){
$(this).remove();
target_image.remove();
var num = $(this).data('image');
images.splice(num, 1);
inputs.splice(num, 1);
if(inputs.length == 0) {
$('input[type= "file"].upload-image').attr({
'data-image': 0
})
}
}
})
$('input[type= "file"].upload-image:first').attr({
'data-image': inputs.length
})
$.each(inputs, function(index, input) {
var input = $(this)
input.attr({
'data-image': index
})
$('input[type= "file"].upload-image:first').after(input)
})
if (images.length >= 5) {
dropzone2.css({
'display': 'block'
})
$.each(images, function(index, image) {
image.attr('data-image', index);
preview2.append(image);
})
dropzone2.css({
'width': `calc(100% - (135px * ${images.length - 5}))`
})
if(images.length == 9) {
dropzone2.find('p').replaceWith('<i class="fa fa-camera"></i>')
}
if(images.length == 8) {
dropzone2.find('i').replaceWith('<p>ココをクリックしてください</p>')
}
} else {
dropzone.css({
'display': 'block'
})
$.each(images, function(index, image) {
image.attr('data-image', index);
preview.append(image);
})
dropzone.css({
'width': `calc(100% - (135px * ${images.length}))`
})
}
if(images.length == 4) {
dropzone2.css({
'display': 'none'
})
}
if(images.length == 3) {
dropzone.find('i').replaceWith('<p>ココをクリックしてください</p>')
}
})
});
画像の枚数によってif文で条件分岐をしております。ドチャクソ長いですがご理解ください。。。
参考文献
https://qiita.com/shinnosuke960801/items/66f2a511803d7dac53a3
https://qiita.com/yamayu_504/items/bdde3eeb9ae06a3876bc
https://kolosek.com/carrierwave-upload-multiple-images/
まとめ
carrierwaveの導入は
【Rails】複数画像を保存したい時のcarrierwaveとMiniMagickの導入
https://qiita.com/syoma/items/183e37ea973f569673b9
にてご案内しております。
twitterもやっておりますので是非フォローよろしくお願いいたします。
https://twitter.com/syomabusiness
youtubeも始める予定です。
ご指摘がございましたら編集リクエストをよろしくお願いいたします!
このままeditも書きたいところですが、時間との兼ね合いもあり少し先になりそうです(すまんの)。。。
参考になった方は是非「いいね」していただけると幸いです!
最後までご覧いただきありがとうございました。