Help us understand the problem. What is going on with this article?

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

はじめに

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

アプリを立ち上げる

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

hamlの導入

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

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

gem 'haml-rails'

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

routes

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

controller

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マイグレーションファイル

 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モデル

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

imageモデル

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

.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

.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の編集

-# 編集前
#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

$(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の編集

.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

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

pasha
プログラミングで学習したことをメモしていきます! 皆さんの役に立つ立てれば良いなと思います!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした