他人の書いたコードをミニアプリにして魔改造した話。（複数画像投稿・編集）

#はじめに
とある事情により[こちら](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)の記事に大変お世話になったのですが、色々気になるところがあり、
手直しをしていった結果、不具合の修正やリファクタリングができたため記事にまとめてみました。

#動作環境
ruby 2.5.1
rails 5.2.4.2
carrierwave 2.1.0
mini_magick 4.9.5
jquery-rails 4.3.5

#下準備
動作の確認をするためにミニアプリを作りました。
[元記事](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)には省略されていたので、ついでにこれも載せておきます。
<details><summary>**長いので見たい人だけ展開してください**</summary><div>

```terminal.
$ rails _5.2.4_ new post_images_sample --database=mysql --skip-test --skip-bundle
$ gem install jquery-rails haml-rails carrierwave mini_magick
$ bundle install
$ rails g model item
$ rails g model image
$ rails g controller items
$ rails g uploader image
```
```asesst/javascripts/application.js
rails-ujsより上段にjqueryを追加
//= require jquery
//= require rails-ujs
```
```db/migrate/202***********_create_items.rb
class CreateItems < ActiveRecord::Migration[5.2]
  def change
    create_table :items do |t|
      t.string :name ,null: false,default:""
      t.timestamps
    end
  end
end
```
```db/migrate/202***********_create_images.rb
class CreateImages < ActiveRecord::Migration[5.2]
  def change
    create_table :images do |t|
      t.references :item ,null: false, foreign_key: true
      t.string     :image ,null: false
      t.timestamps
    end
  end
end
```
```models/item.rb
class Item < ApplicationRecord
  has_many :images, dependent: :destroy
  accepts_nested_attributes_for :images, allow_destroy: true
end
```
```models/image.rb
class Image < ApplicationRecord
  belongs_to :item
  mount_uploader :image, ImageUploader
end
```
```config/routes.rb
Rails.application.routes.draw do
  root "items#new"
  resources :items ,only: [:index,:new,:create,:edit,:update]
end
```
```app/assets/uploders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base
(中略)
  （以下を追記）
  include CarrierWave::MiniMagick
  process resize_to_fit: [200, 200] 
（後略）
end
```
```terminal.
$ rails db:create
$ rails db:migrate
```
下準備ここまで。
</div></details>

#viewについて
それでは魔改造していきましょう。
と言っても、viewはそんなにいじりませんでした。
[元記事](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)の完成度の高さが伺えますね。
※元のコードについては[元記事](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)を参照してください。

####新規投稿画面
15行目のclass名に誤字があったのと、```id: "label-box--0"```は必要なかったので削除しました。

```app/views/new.html.haml
.main
  %section.main__block
    = form_with(model:@item, local:true) do |f|
      %h2.sell__block__head
        商品の情報を入力
      .sell__block__form
        .sell__block__form__upload
          %h3.sell__block__form__upload__head
            出品画像
            %span.require 必須
          %p 最大5枚までアップロードできます
          .post__drop__box__container
            .prev-content
            .label-content
              %label{for: "item_images_attributes_0_image", class: "label-box"}
                %pre.label-box__text-visible クリックしてファイルをアップロード
            .hidden-content
              = f.fields_for :images ,multiple: true do |i|
                = i.file_field :image,class:"hidden-field"
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][1][image]", id: "item_images_attributes_1_image" }
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][2][image]", id: "item_images_attributes_2_image" }
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][3][image]", id: "item_images_attributes_3_image" }
                %input{class:"hidden-field", type: "file", name: "item[images_attributes][4][image]", id: "item_images_attributes_4_image" }
        .sell__block__form__name
          .form-group__name
            %label
              商品名
              %span.require 必須
            %div
              = f.text_field :name, placeholder:"商品名（必須 40文字まで)",class: "form__group__name"
        .sell__block__form__btn
          %div
            = f.submit "出品する",class: "btn-default__btn-red"
```

####編集画面
[元記事](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)ではedit画面に遷移した際に、jsで```delete-btn```にidを付与しているのですが、
16行目を```- @item.images.each_with_index do |image,i|```とし、view側で先にdata-delete-idをつけた方が記述が減るかなと考え、変更しました。
また、```class:'hidden-checkbox'``` は使用しない(```hidden-content```に ```display:none;``` をかけることで不要となる)ため、削除しました。

```app/assets/views/edit.html.haml
.main
  %section.main__block
    = form_with model:@item, local:true do |f|
      %h2.sell__block__head
        商品の情報を入力
      .sell__block__form
        .sell__block__form__upload
          %h3.sell__block__form__upload__head
            出品画像
            %span.require 必須
          %p 最大5枚までアップロードできます
          .post__drop__box__container
            .prev-content

              //JSで挿入したhtmlと同じ形 each文でのプレビュー表示
              - @item.images.each_with_index do |image,i|
                .preview-box
                  .upper-box
                    = image_tag image.image.url, width: "112", height: "112", alt: "preview"
                  .lower-box
                    .update-box
                      .edit-btn 編集
                    .delete-box
                      .delete-btn{data:{delete_id: i}} 削除
            .label-content

              //プレビューの数に合わせてforオプションを指定
              = f.label :"images_attributes_#{@item.images.length}_image", class: "label-box" do
                %pre.label-box__text-visible クリックしてファイルをアップロード
            .hidden-content
              = f.fields_for :images do |i|

                //プレビューが出ている分のfile_fieldとdelete用のチェックボックスを設置
                = i.file_field :image,class:"hidden-field"
                = i.check_box:_destroy

                //5つのfile_fieldを準備するに当たって、足りない分を表示
              - @item.images.length.upto(4) do |i|
                %input{name: "item[images_attributes][#{i}][image]", id: "item_images_attributes_#{i}_image", class:"hidden-field", type:"file"}
        .sell__block__form__name
          .form-group__name
            %h3.sell__block__form__upload__head
              商品名
              %span.require 必須
            %div
              = f.text_field :name, placeholder:"商品名（必須 40文字まで)",class: "form__group__name"
        .sell__block__form__btn
          %div
            = f.submit "出品する",class: "btn-default__btn-red"
```

#jsについて
[元記事](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)からかなり記述量を減らすことができました。

- **主な変更点**
 - viewで対応済みのため、編集画面に遷移した際のプレビュー画像に対して行う処理を削除。
 - function buildHTMLへの引数を増やし、URLを追加する処理を統合。
 - 処理の都度再定義されていた```var prevContent = $('.label-content').prev();``` を ```const prevContent = $('.label-content').prev();```とし、統合。
 - 所々にあった```setLabel()``` を ```setLabel(count)```とし、label-contentの表示・非表示、label-boxのid・forの操作を含ませることで統合。
 - 編集時、追加画像を削除した時にフォームの中身が削除されないバグを解消。 
 - 編集画面に```GET``` メソッドで遷移した際に```$(prevContent).css('width').replace(/[^0-9]/g, '')``` が ```No Method Error``` を 返してくるので ```parseInt($(prevContent).css('width'))``` へ記述を変更。 （turbolinksとjQueryの相性の問題）

```app/assets/javascripts/items.js
$(document).on('turbolinks:load', function(){
  //共通の定数を定義==================================================================
  const prevContent = $('.label-content').prev();

  //プレビューのhtmlを定義============================================================
  function buildHTML(id,image) {
    var html = `<div class="preview-box">
                  <div class="upper-box">
                    <img src=${image} alt="preview">
                  </div>
                  <div class="lower-box">
                    <div class="update-box">
                      <div class="edit-btn">編集</div>
                    </div>
                    <div class="delete-box">
                      <div class="delete-btn" data-delete-id= ${id}>削除</div>
                    </div>
                  </div>
                </div>`
    return html;
  }
  //ラベルのwidth・id・forの値を変更==================================================
  function setLabel(count) {
    //プレビューが5個あったらラベルを隠す
    if (count == 5) { 
      $('.label-content').hide();
    } else {
      //プレビューが4個以下の場合はラベルを表示
      $('.label-content').show();
      //プレビューボックスのwidthを取得し、maxから引くことでラベルのwidthを決定
      labelWidth = (620 - parseInt($(prevContent).css('width')));
      $('.label-content').css('width', labelWidth);
      //id・forの値を変更
      $('.label-box').attr({for: `item_images_attributes_${count}_image`});
    }
  }

  //編集ページ(items/:i/edit)へリンクした際のアクション==================================
  if (window.location.href.match(/\/items\/\d+\/edit/)){
    //プレビューの数を取得
    var count = $('.preview-box').length;
    //countに応じてラベルのwidth・id・forの値を変更
    setLabel(count) 
  }

  //プレビューの追加=================================================================
  $(document).on('change', '.hidden-field', function() {
    //hidden-fieldのidの数値のみ取得
    var id = $(this).attr('id').replace(/[^0-9]/g, '');
    //選択したfileのオブジェクトを取得
    var file = this.files[0];
    var reader = new FileReader();
    //readAsDataURLで指定したFileオブジェクトを読み込む
    reader.readAsDataURL(file);
    //読み込み時に発火するイベント
    reader.onload = function() {
      var image = this.result;
      //htmlを作成
      var html = buildHTML(id,image);
      //ラベルの直前のプレビュー群にプレビューを追加
      $(prevContent).append(html);
      //プレビュー削除したフィールドにチェックボックスがあった場合、チェックを外す
      if ($(`#item_images_attributes_${id}__destroy`)){
        $(`#item_images_attributes_${id}__destroy`).prop('checked',false);
      } 
      //プレビューの数を取得
      var count = $('.preview-box').length;
      //countに応じてラベルのwidth・id・forの値を変更
      setLabel(count);
    }
  });

  // 画像の削除=====================================================================
  $(document).on('click', '.delete-btn', function() {
    var id = $(this).attr('data-delete-id')
    //削除用チェックボックスがある場合はチェックボックスにチェックを入れる
    if ($(`#item_images_attributes_${id}__destroy`).length) {
      $(`#item_images_attributes_${id}__destroy`).prop('checked',true);
    }
    //画像を消去
    $(this).parent().parent().parent().remove();
    //フォームの中身を削除
    $(`#item_images_attributes_${id}_image`).val("");
    //プレビューの数を取得
    var count = $('.preview-box').length;
    //countに応じてラベルのwidth・id・forの値を変更
    setLabel(count);
  });
});
```

#controllerについて

**変更点**
updateメソッドで画像を全部消した上でアップデートすると、再編集の際、画像ファイルを拾ってくれないバグが存在することを確認したものの、
「画像なし」からも再編集できる方法が発見できなかったため、「画像なし」では保存できないように記述を変更し、回避しました。

```app/controllers/items_controller.rb
class ItemsController < ApplicationController

  def new
    @item = Item.new
    @images = @item.images.build
  end

  def create
    @item = Item.new(item_params)
    @item.save
    redirect_to edit_item_path(@item.id)
  end

  def edit
    @item = Item.find(params[:id])
  end

  def update
    @item = Item.find(params[:id])
    length = @item.images.length
    i = 0
    while i < length do
      if  item_update_params[:images_attributes]["#{i}"]["_destroy"] == "0"
        @item.update(item_update_params)
        redirect_to edit_item_path(@item.id)
        return
      else
        i += 1
      end
    end
    if item_update_params[:images_attributes]["#{i}"]
      @item.update(item_update_params)
    end
    redirect_to edit_item_path(@item.id)
    return
  end

  private
  def item_params
    params.require(:item).permit(
      :name,
      [images_attributes: [:image]])
  end

  def item_update_params
    params.require(:item).permit(
      :name,
      [images_attributes: [:image, :_destroy, :id]])
  end
end
```

#その他
indexページとか、showページとか、destoroyページとかも作ったのですが、元記事に関連していないため、ここでは割愛します。

#まとめ
記事にしてみると、自分では魔改造したつもりが、案外そうでもなかったですね。
[元記事](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)の作者様には改めて感謝申し上げます。
この記事をご覧になった方は是非、元記事へもご訪問下さい。

#参考にさせていただいた記事
[画像の複数枚投稿と編集とプレビューと私](https://qiita.com/gakinchoy7/items/ac1d8e64e33c3ddd377b)