1つの投稿に対して複数枚の画像を紐付け、同時に投稿する実装方法

# はじめに

複数枚の画像を投稿する機能の実装に、苦労したてめメモとして記事を投稿します



# アプリを立ち上げる

```
$ rails _5.2.3_ new  アプリ名 -d mysql
$ cd アプリ名
$ bin/rails db:create
```


##hamlの導入
今回実装はhamlで行うため、hamlを導入します

gemファイルに以下を追加します
忘れずにbundle installをしてください

```
gem 'haml-rails'
```

# 簡単に投稿機能を作成します

##routes

```ruby

Rails.application.routes.draw do
  root 'products#index'
  resources :products, only: [:index, :new, :create]
end

```

##controller

```ruby

class ProductsController < ApplicationController
  def index
    @products = Product.includes(:images).order('created_at DESC')
  end

  def new
    @product = Product.new
    @product.images.new
  end

  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to root_path
    else
      render :new
    end
  end
  

  private

  def product_params
    params.require(:product).permit(:name, images_attributes: [:src])
  end

end
```

## modelとマイグレーション

### productマイグレーションファイル

```
class CreateProducts < ActiveRecord::Migration[5.2]
  def change
    create_table :products do |t|
      t.string :name

      t.timestamps
    end
  end
end
```
### imageマイグレーションファイル

```ruby

 class CreateImages < ActiveRecord::Migration[5.2]
  def change
    create_table :images do |t|
      t.string :src
      t.references :product, foreign_key: true

      t.timestamps
    end
  end
end

```

### productsモデル

```ruby

class Product < ApplicationRecord
  has_many :images
  accepts_nested_attributes_for :images, allow_destroy: true
end

```



### imageモデル

```ruby

class Image < ApplicationRecord
  mount_uploader :src, ImageUploader
  belongs_to :product
end

```



## imageモデルで画像をアップロードできるようにしていきます。

Gemfileに以下を追記して$ bundle install

```
gem 'carrierwave'
gem 'mini_magick'
```
##uploaderを作成します
ターミナルで以下を実行します

```
$ rails g uploader image
```
続いて、image_uploader.rbファイルが生成されたので以下を編集します

```
include CarrierWave::MiniMagick  // この記述を探し、コメントアウトを外す

process resize_to_fit: [100, 100]  // この記述は追記
```

##最後にhamlとscssの編集

haml/new

```haml


.lead
  =link_to "/products" do
    =image_tag "http://furima.tokyo/assets/logo-d3d78326971d78b06e3d6f0ba666d025a8ad681286b4d9e00e7dbe8673bcfd23.svg", class: "lead__img"
  
= form_with model: @product, local: true do |f|
  .input-field
    .input-field__contents
      .input-field__contents-image
        .input-field__contents-image__headline
          .headlabel
            出品画像
            %span.necessary
              必須
        %p.upload
          最大5枚までアップロードできます
        
        #image-box-1 
          .item-num-0#image-box__container
            = f.fields_for :images do |i|
              .input-field__contents-image__drop__js-file
                .input-area
                  = i.file_field :src
    
      .input-field__contents-name
        .input-field__contents-image__headline
          .headlabel
            %label
              商品名
              %span.necessary
                必須
          .name-input
            = f.text_field :name, {class: "drop-input", placeholder: "40文字まで"}
    
    .input-field
      .input-field__contents
        .input-field__contents-price
          .sell
            = f.submit "出品する", class: "sellbtn", tabindex: "0"
```

new.scss

```scss


.lead {
  background-color: rgb(245, 245, 245);
  text-align: center;
  height: 128px;
  line-height: 10;
}
.input-field {
  background-color: rgb(245, 245, 245);
  width: 100%;
  .input-field__contents {
    left: 0;
    background-color: white;
    max-width: 800px;
    margin: 0 auto;
    padding: 40px;
    border-bottom: 1px solid hsl(0, 0%, 77%);
    height: 100%;
    .input-field__contents-image {
      width: 800px;
      border-bottom: rgb(204, 204, 204);
      .input-field__contents-image__headline{
        margin-top: 20px;
        margin-left: 5px;
      }
      .upload {
        margin-top: 16px;
        margin-left: 5px;
      }
      #image-box-1 {
        display: flex;
        height: 130px;
        width: 100%;
        margin-right: 0px;
        text-align: center;
        i{
          padding-top: 50px;     
        }
        .item-num-0#image-box__container  {
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
        }
      }
    }
  }
  .drop-input {
    width: 60%;
    height: 50px;
    border-color: #cccccc;
    border-radius: 4px;
    border-style: solid;
    border-width: 1px;
    margin: 10px 10px 0 0;
  }  
  .name-input{
    .drop-input{
      width: 100%;
      height: 50px;
      border-color: #cccccc;
      border-style: solid;
    }
  } 
  .sell {
    text-align: center;
    display: grid;
    width: 50%;
    margin-left: 200px;
    .sellbtn {
      background-color: #3ccace;
      color: white;
      border-color: transparent;
      font-weight: 600;
      line-height: 3;
      cursor: pointer;
    }
  }
}
```
以下の様になれば、完成です。
https://gyazo.com/a1d705516656f50c689abc7c18de5ec9

# 画像を複数枚投稿する

###jQueryの導入

```
gem 'jquery-rails'
```
続いて、bundle installをしてください

###application.jsの編集

```
//= require rails-ujs
//= require activestorage
//= require jquery
//= require_tree .
```
### new.hamlの編集

```haml

-# 編集前
#image-box-1 
 .item-num-0#image-box__container
  = f.fields_for :images do |i|
   .input-field__contents-image__drop__js-file
    .input-area
     = i.file_field :src
-#　編集後

#image-box-1 
 .item-num-0#image-box__container
  = f.fields_for :images do |i|
   .input-field__contents-image__drop__js-file
    .input-area
    = i.file_field :src, type: 'file', name: "product[images_attributes][][name]", value:"", style: "display:none", id:"img-file"
    %label{for: "img-file"}
     %i.fas.fa-camera

```

### 画像を複数枚投稿する様にする new.js作成し、編集する

new.js

```js

$(function(){
  //DataTransferオブジェクトで、データを格納する箱を作る
  var dataBox = new DataTransfer(); //ステップ②
  //querySelectorでfile_fieldを取得
  var file_field = document.querySelector('input[type=file]')
  //fileが選択された時に発火するイベント
  $('#img-file').change(function(){
    //選択したfileのオブジェクトをpropで取得
    var files = $('input[type=file]').prop('files')[0];
    $.each(this.files, function(i,file){
    //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む
    var fileReader = new FileReader();

    //DataTransferオブジェクトに対して、fileを追加
    dataBox.items.add(file) //ステップ②
    //dataTransferオブジェクトに入ったfile一覧をfile_fieldの中に代入
    file_field.files =  dataBox.files //ステップ②

    var num = $('.item-image').length + 1 + i //ステップ②
    fileReader.readAsDataURL(file); //ステップ②
     //画像が10枚になったら超えたらドロップボックスを削除する
     if (num == 5){ //ステップ②
      $('#image-box__container').css('display', 'none')
     }
    //読み込みが完了すると、srcにfileのURLを格納
    fileReader.onloadend = function() {
      var src = fileReader.result
      var html = `<div class='item-image' data-image="${file.name}">
                    <div class=' item-image__content'>
                      <div class='item-image__content--icon'>
                        <img src=${src} width="150" height="90" >
                      </div>
                    </div>
                    <div class='item-image__operetion'>
                      <div class='item-image__operetion--delete'>削除</div>
                    </div>
                  </div>`
     //image_box__container要素の前にhtmlを差し込む
      $('#image-box__container').before(html);
      };
    //   fileReader.readAsDataURL(file);
    //  });
     //image-box__containerのクラスを変更し、CSSでドロップボックスの大きさを変えてやる。
     $('#image-box__container').attr('class', `item-num-${num}`)
    });
  });
    $(document).on("click", '.item-image__operetion--delete', function(){
      //プレビュー要素を取得
      var target_image = $(this).parent().parent()
      //プレビューを削除
      target_image.remove();
      //inputタグに入ったファイルを削除
      file_field.val("")
    })
});

```
# scssの編集

```scss

.lead {
  background-color: rgb(245, 245, 245);
  text-align: center;
  height: 128px;
  line-height: 10;
}

.input-field {
  background-color: rgb(245, 245, 245);
  width: 100%;
  &__contents {
    left: 0;
    background-color: white;
    max-width: 800px;
    margin: 0 auto;
    padding: 40px;
    border-bottom: 1px solid hsl(0, 0%, 77%);
    height: 100%;
  }
  
  .input-field__contents-image {
    width: 800px;
    border-bottom: rgb(204, 204, 204);
    .input-field__contents-image__headline{
      font-weight: 600;
      margin-top: 20px;
      margin-left: 5px;
      .name-input {
        height: 54px;
        .option-input {
          display: block;
          width: 93%;
          border-color: #cccccc;
          height: 100%;
          border-radius: 4px;
          font-weight: bolder;
          padding: 0px 2px 1px;
          border-width: 1px;
        }
      }
    }
    .upload {
      margin-top: 16px;
      margin-left: 5px;
    }
    #image-box-1 {
      display: flex;
      height: 130px;
      width: 100%;
      margin-right: 0px;
      text-align: center;
      i{
        padding-top: 50px;
        cursor: pointer;
      }
      .item-num-0#image-box__container  {
      background-color: rgb(245, 245, 245);
      height: 100%;
      width: 100%;
      border-width: 1px;
      border-style: dashed;
      border-color: rgb(204, 204, 204);
      border-image: initial;
      text-align: center;
      }
      .item-num-1{
        background-color: rgb(245, 245, 245);
      height: 100%;
      width: 100%;
      border-width: 1px;
      border-style: dashed;
      border-color: rgb(204, 204, 204);
      border-image: initial;
      text-align: center;
      }
      .item-num-2{
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
      }
      .item-num-3{
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
      }
      .item-num-4{
        background-color: rgb(245, 245, 245);
      height: 100%;
      width: 100%;
      border-width: 1px;
      border-style: dashed;
      border-color: rgb(204, 204, 204);
      border-image: initial;
      text-align: center;

      }
      .item-num-5{
        background-color: rgb(245, 245, 245);
        height: 100%;
        width: 100%;
        border-width: 1px;
        border-style: dashed;
        border-color: rgb(204, 204, 204);
        border-image: initial;
        text-align: center;
      }  
    }
    //レビュー表示のCSS
    .item-image{
      height: 130px;
      width: 160px;
      border: 1px solid #eee;
      margin-right: 10px;
      .item-image__content{
        padding-top: 10px;
        .item-image__content--icon{
        }
      }
      .item-image__operetion{
        .item-image__operetion--delete{
          color: #00b0ff;
          cursor: pointer;
          padding-top: 5px;
         text-align: center;

        }
      }
    }
  }

  .text-area {
    border-radius: 4px;
    font-size: 16px;
    padding: 13px 16px;
    border-color: #cccccc;
    margin-top: 30px;
    
  }
  .drop-input {
    width: 100%;
    height: 50px;
    border-color: #cccccc;
    border-radius: 4px;
    border-style: solid;
    border-width: 1px;
    margin: 10px 10px 0 0;
    ::placeholder {
      padding: 20px;
      font-weight: inherit;
    } 
  }
 
  .headlabel {
    margin-top: 30px;
    .necessary {
      background-color: #3ccace;
      color: white;
      padding: 2px 4px;
      font-size: 14px;
      margin-left: 3px;
      cursor: pointer;
      border-radius: 2px;
     }
  }
  .sell {
    text-align: center;
    display: grid;
    width: 50%;
    margin-left: 200px;
    .sellbtn {
      background-color: #3ccace;
      color: white;
      font-size: 20px;
      min-height: 48px;
      padding: 0 24px;
      border-color: transparent;
      border-radius: 2px;
      font-weight: 600;
      line-height: 3;
    }
  }
}
```
これで一通りは完成
動作を確認
https://gyazo.com/372657130a2696e1865c02fdd6e9e303

編集や削除の機能は別記事で続きを書きます