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

# ActiveStorageを使用して複数枚の画像を投稿する
先ずはここを見てください[Railsガイド](https://railsguides.jp/active_storage_overview.html)、導入方法とか書いてあります。
S3に接続するときは注意が必要ですね。

[商品出品画面を作る①へ](https://qiita.com/m-kawakami/items/8617ac3d9b3528ad0326)

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

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

###モデル

```ruby:imet.rb
has_many_attached :images
```
**ActiveStorageで複数画像を扱う為にモデルに記述**

###コントローラー

```ruby: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

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565886/e7dda397-bc82-1987-c26f-9004d3d1033c.png)

###JS

```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) {
            // 表示されているプレビューが９以下なら、新たに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. 画像を選択し、inputに保持させる
2. 保持された画像からファイルオブジェクトを取得する
3. プレビューを表示する（イベント元のイベント元のカスタムデータIDの番号を付加する）
4. 表示されているプレビューの数を数え、９以下なら次のinputを生成する（イベント元のカスタムデータIDの次の番号を付加する）

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

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

- labelとinputは、**labelのfor属性**と**inputのid**で紐づける。
**for="item_images${labelLength+1}"**と**id="item_images${labelLength+1}"**の部分です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565886/9f86b486-16db-5bd4-b7f5-fc925e133e88.png)
ul要素の下のli要素がプレビューです。
その下のdiv要素の下がinputが囲われたlabelです。

## プレビューを削除する

```js: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');
      // 表示されているプレビューが９枚なら,#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>`);
    };
  });
```

## バリデーション
```ruby: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に一旦保存してしまう
ちょっと難しそう

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

今回は以上です