【Rails】テーブルを分けて複数の画像をアップロードする

複数枚の写真を一度に保存する機能の実装において、
【itemテーブル】と【item_imageテーブル】の二つに
分けて複数枚の写真を保存する機能を実装した際の手順をまとめたのでご紹介します。

# 完成形
○ 商品写真は最大で10枚まで投稿可能。
○ 一つのファイルフィールドに一つの写真でアップロードしていく。
○ 5枚投稿時点で下段に切り替わる。
[![Image from Gyazo](https://i.gyazo.com/c159d314e084b76e1b53bb78b2d441ae.gif)](https://gyazo.com/c159d314e084b76e1b53bb78b2d441ae)


# 1. HTMLの用意

```haml:sample.haml

~ 省略 ~

.image-container
  .image-container_box
    .form-title
      %span.box-form-explanaion
        商品画像
      %span.indispensable
        必須
    %p.image-container_box_message
      最大10枚までアップロードできます
    .image-container_box_alart-10
      ※ 1枚目は必須です
  .image-container_unit-man    【写真が順にプレビューされていく箱】
    .item-image-container__unit.preview-0  【写真一枚が入る箱　※投稿ごとに生成されていく】
      = f.fields_for :item_images do |i|
        .image-container__unit--guide
          = i.file_field :image, class: 'img-man', id:"image-label-0",type: 'file'
          .have-image
            %i.fas.fa-camera

~ 省略 ~

```

◉【写真一枚が入る箱】に2つクラスがあるのは、2枚目以降では毎回クラス名を
変えていくためです。
◉この仕様では、**1つのinputに写真は1つなので、**multiple属性は付けていません。

# 2. CSSの用意

```sass:sample.scss
  .image-container {
    padding: 40px;
    border-bottom: solid 1px rgb(235, 235, 235);
    &_box {
      &_message {
        height: 19px;
        margin: 16px 0 5px;
        font-size: 14px;
        line-height: 1.4em;
        display: block;
      }
      &_alart-10 {
        margin-bottom: 4px;
        font-size: 14px;
        font-weight: bold;
        color:red;
      }
    }
    &_unit-man {
      min-height: 152px;
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      .item-image-container__unit {
        align-content: center;
        align-items: center;
        background-color: rgb(245, 245, 245);
        display: flex;
        justify-content: flex-start;
        height: 150px;
        width: 118px;
        margin-left: 5px;
        margin-bottom: 2px;
        justify-content: center;
        position: relative;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;

        .have-image {
          position: absolute;
            left: 32px;
            top: 40px;
          z-index: 0;
          cursor: pointer;
          font-size: 16px;
          .fas.fa-camera {
            margin-left: 16px;
            font-size: 1.2rem;
          }
          .fas.fa-camera:hover {
            cursor: pointer;
            transform: scale(1.3, 1.3);
            transition: all 0.1s ease 0s;
          }
        }
        .item-image-container__unit__image {
          z-index: 1;
          height: 145px;
          width: 100%;
          margin: 0 3px;
          background-color: #ffffff;
          position: relative;
          img {
            width: 100%;
            height: auto;
          }
          .image-option__list--tag {
            width: 100%;
            background-color: lightblue;
            text-align: center;
            position: absolute;
              bottom: 0;
              left: 0;
          }
          .image-option__list--tag:hover {
            cursor: pointer;
            transform: scale(1.0, 1.0);
            transition: all 0.1s ease 0s;
            background-color: #ea352d;
            color:#ffffff;
          }

        }
      }
      .item-image-container__unit {
        input{
          display: none;
        }
      }
    }
  }
```
「.unit-man」に適用されている、[flex-direction: row;]と[flex-wrap: wrap;]により、
写真が既定の幅まで投稿されたら下段に折り返してくれます。

# 3. JSでプレビューさせる

```js:sample.js
$(function(){


~ 省略 ~


  var dataBox = new DataTransfer();  //データ用の箱を作る
  $(document).on('change', '.img-man', function(){    //inputの中身が変化したら発火する
    $.each(this.files, function(i, file){
      var fileReader = new FileReader();
      dataBox.items.add(file)    
      fileReader.readAsDataURL(file);     //ファイルの読み込み
      fileReader.onloadend = function() {     //読み込み完了すると発火
        var num = $('.item-image-container__unit').length  //写真の枚数をnumに代入
        var src = fileReader.result   //写真のデータをsrcに代入
        var html =  `<div class="item-image-container__unit__image">
                        <img src="${src}">
                      <div class="image-option__list--tag btn-${num}">削除</div>
                    </div>`

        var field = `<div class="item-image-container__unit preview-${num}">
                      <div class="image-container__unit--guide">
                        <label for="image-label-${num}">
                          <input class="img-man" id="image-label-${num}" type="file" name="item[item_images_attributes][${num}][image]">
                          <div class="have-image">
                            <i class="fas fa-camera"></i>
                          </div>
                        </label>
                      </div>
                    </div>`
        $(html).appendTo(`.preview-${num - 1}`)  //枚数で該当するクラスに写真を追加する
           　　　　　　　　　　　
        if (num < 10 ) {     //10枚分まで新しいinputの生成を行う
          $(field).appendTo('.image-container_unit-man')
        }
      };
    });
  });
  //削除機能　
  $(document).on("click", ".image-option__list--tag", function(){  //削除ボタンクリックで発火
    var num = $('.item-image-container__unit').length
    var field_2 = `<div class="item-image-container__unit preview-0">
                    <div class="image-container__unit--guide">
                      <label for="image-label">
                        <input class="img-man" id="image-label-0" type="file">
                        <div class="have-image">
                          <i class="fas fa-camera"></i>
                        </div>
                      </label>
                    </div>
                  </div>`
    $(this).parent().parent().remove();  //写真が入っている箱ごと削除
    $(".item-image-container__unit").removeClass(`preview-${num - 1}`)
    $(".item-image-container__unit").addClass(`preview-${num - 2}`)
    if(num == 1) {   //全部削除した時に新たに1つフィールドを生成する
      $(field_2).appendTo('.image-container_unit-man')
    }
  });

~ 省略 ~
```
◉変数【num】は、写真の読み込みごとに代入され、その度に横に生成される
【preview-${num}】クラスが書き換わって横に生成されるようになっています。


◉ 下部の１０枚制限の記述によって、10枚までアップされるとinputの生成が止まります。
また、multiple属性が付いていないので写真の一括選択はできなくなります。

◉削除機能に関しても、クラス名の書き換えを行う必要があります。
___________________________________
登録枚数に関しては、モデルにも別でバリーデーションは書いてあります。

```ruby:sample.rb
  def  item_images_number
    errors.add(:item_images, "は10枚までです") if item_images.size > 10
  end
```
____________________________
以上で終了です。
ご覧いただきありがとうございました。