5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

商品出品画面を作る②ActiveStorage(複数画像投稿)

Last updated at Posted at 2020-03-24

ActiveStorageを使用して複数枚の画像を投稿する

先ずはここを見てくださいRailsガイド、導入方法とか書いてあります。
S3に接続するときは注意が必要ですね。

商品出品画面を作る①へ

そもそもActiveStorageは何がいいのか?

ActiveStorageの特徴としては画像をitemモデルとして扱えることでしょう。
どう言う事かと言うと、itemとimageが1:多の関係だとすると、テーブルを2個用意しないといけません。
そしてアソシエーションを組んで〜と言う処理をします。
削除や変更の時に処理が若干複雑になってしまいます。
そんな時、ActiveStorageならitemテーブルのみを対象に処理をすれば良いわけです。
ついでに、フォームの作成が比較的簡単になります。

今回の要件

  • 複数同時選択による複数枚の画像投稿
  • 複数回選択による複数枚の画像投稿
  • プレビュー表示
  • 投稿枚数管理
  • プレビューの削除

###モデル

imet.rb
has_many_attached :images

ActiveStorageで複数画像を扱う為にモデルに記述

###コントローラー

items_controller.rb
def new
    @item = Item.new
    @category_parent =  Category.where("ancestry is null")
  end

  def create
    @item = Item.new(item_params)
    if @item.save
      redirect_to root_path 
    else
      render :new
    end
  end
 private

  def item_params
    params.require(:item).permit(:name, :text, :category_id, :condition_id, :deliverycost_id, :pref_id, :delivery_days_id, :price, images: []).merge(user_id: current_user.id, boughtflg_id:"1")
  end

def item_paramsの images:[] がポイントです。
こうする事で、複数画像を受け取ることが出来ます。

###HAML

new.html.haml
-# 画像部分
.sell-container__content
  .sell-title
    %h3.sell-title__text
      出品画像
      %span.sell-title__require
        必須
  .sell-container__content__max-sheet 最大10枚までアップロードできます
  .sell-container__content__upload
    .sell-container__content__upload__items
      .sell-container__content__upload__items__box
        %ul#output-box
     ここにプレビューが入ります
          %div#image-input{tabindex:"0"}
            = f.label :images, for: "item_images0", class: 'sell-container__content__upload__items__box__label', data: {label_id: 0 } do 
              = f.file_field :images, multiple: true, class: "sell-container__content__upload__items__box__input", id: "item_images0", style: 'display: none;'
            ここに新しinputが入ります
              %pre
                %i.fas.fa-camera.fa-lg
                ドラッグアンドドロップ
                またはクリックしてファイルをアップロード
  .error-messages#error-image
    ここにエラーメッセージが入ります

= f.file_fieldの multiple:true がポイントです
name属性がname="item[images][]"になっていると思います。
これにより、選択した画像を配列とし保持できるようになり、複数枚を同時に選択できるようになります。
image.png

###JS

item_new.js
$(document).on('turbolinks:load', function(){
  $('#image-input').on('change', function(e){
  // 画像が選択された時プレビュー表示、inputの親要素のdivをイベント元に指定
    
    //ファイルオブジェクトを取得する
    let files = e.target.files;
    $.each(files, function(index, file) {
      let reader = new FileReader();

      //画像でない場合は処理終了
      if(file.type.indexOf("image") < 0){
        alert("画像ファイルを指定してください。");
        return false;
      }
      //アップロードした画像を設定する
      reader.onload = (function(file){
        return function(e){
          let imageLength = $('#output-box').children('li').length;
          // 表示されているプレビューの数を数える
          
          let labelLength = $("#image-input>label").eq(-1).data('label-id');
          // #image-inputの子要素labelの中から最後の要素のカスタムデータidを取得

          // プレビュー表示
          $('#image-input').before(`<li class="preview-image" id="upload-image${labelLength}" data-image-id="${labelLength}">
                                      <figure class="preview-image__figure">
                                        <img src='${e.target.result}' title='${file.name}' >
                                      </figure>
                                      <div class="preview-image__button">
                                        <a class="preview-image__button__edit">編集</a>
                                        <a class="preview-image__button__delete" data-image-id="${labelLength}">削除</a>
                                      </div>
                                    </li>`);
          $("#image-input>label").eq(-1).css('display','none');
          // 入力されたlabelを見えなくする

          if (imageLength < 9) {
            // 表示されているプレビューが9以下なら、新たにinputを生成する
            $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                        <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                        <i class="fas fa-camera fa-lg"></i>
                                      </label>`);
          };
        };
      })(file);
      reader.readAsDataURL(file);
    });
  });

今回の1番の要所です
流れとしては

  1. 画像を選択し、inputに保持させる
  2. 保持された画像からファイルオブジェクトを取得する
  3. プレビューを表示する(イベント元のイベント元のカスタムデータIDの番号を付加する)
  4. 表示されているプレビューの数を数え、9以下なら次のinputを生成する(イベント元のカスタムデータIDの次の番号を付加する)

ポイント

  • プレビューとinputはカスタムデータIDの番号で紐づけます。
    **data-image-id="${labelLength}"data-label-id="0"**の部分です。

  • input要素はidで区別します。
    id="item_images${labelLength+1}" となっている部分です。
    これは#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得指定ます。
    つまり、イベント元のカスタムデータIDの番号に+1しています。

  • labelとinputは、labelのfor属性inputのidで紐づける。
    **for="item_images${labelLength+1}"id="item_images${labelLength+1}"**の部分です。
    image.png
    ul要素の下のli要素がプレビューです。
    その下のdiv要素の下がinputが囲われたlabelです。

プレビューを削除する

item_new.js

  //削除ボタンが押された時
  $(document).on('click', '.preview-image__button__delete', function(){
    let targetImageId = $(this).data('image-id');
    // イベント元のカスタムデータ属性の値を取得
    $(`#upload-image${targetImageId}`).remove();
    //プレビューを削除
    $(`[for=item_images${targetImageId}]`).remove();
    //削除したプレビューに関連したinputを削除

    let imageLength = $('#output-box').children('li').length;
    // 表示されているプレビューの数を数える
    if (imageLength ==9) {
      let labelLength = $("#image-input>label").eq(-1).data('label-id');
      // 表示されているプレビューが9枚なら,#image-inputの子要素labelの中から最後の要素のカスタムデータidを取得
      $("#image-input").append(`<label for="item_images${labelLength+1}" class="sell-container__content__upload__items__box__label" data-label-id="${labelLength+1}">
                                  <input multiple="multiple" class="sell-container__content__upload__items__box__input" id="item_images${labelLength+1}" style="display: none;" type="file" name="item[images][]">
                                  <i class="fas fa-camera fa-lg"></i>
                                </label>`);
    };
  });

バリデーション

item.rb(モデル)
  validate :images_presence
  #バリデーションを呼び出す
  
  def images_presence
    if images.attached?
      # inputに保持されているimagesがあるかを確認
      if images.length > 10
        errors.add(:image, '10枚まで投稿できます')
      end
    else
      errors.add(:image, '画像がありません')
    end
  end

ActiveStorageは基本のバリデーションが使えません。
なので自分で作りました。

最後に

後はCSSでデコレーションすればOKですね。

ただし、一つ問題があります。
複数同時選択を行い、プレビューを削除した時に不具合が発生する事です。
と言うのも、同時選択をすると一つのinputに複数の画像の情報が保持されるます。
そしてinputから情報を全て抜き取る事は出来ません。
つまり、あるinputを削除すると複数画像のデータが消えてしまう・・・と言う事です。

対策としては

  • 同時選択を出来ないようにする
    この場合、name属性にも手を加える必要があります。

  • 先にDBに一旦保存してしまう
    ちょっと難しそう

それらは今後試すことにします。

今回は以上です

5
6
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?