Railsで登録した商品画像を差し替える機能

##前提
- ruby on rails 6.0.0 を使用。
- ユーザー機能はdeviseにより導入されているものとする。
- viewファイルは全てhaml形式とする。
- ちなみに使っているのはMacBook Air(Retina, 13-inch, 2020)です。

##はじめに
<a href="https://qiita.com/silvercrow222/items/40ff5cdc28f4d7b7a0d2">前回(part3)</a>のあらすじです。jQueryを使用して新規登録画面にあんなことやこんなことを実装しました。

現在の状態としては、画像を選択すると新しいフォームが出現する、削除を押したら消える、という感じです。一見出来上がったように見えますが、これらは全てjsによるアクションなのでリロードされた編集画面では消えてしまいます。

なので今回のpartでは、編集画面にも新しいフォームを出現させる、といった作業になります。

ちなみに前置きや手順などは <a href="https://qiita.com/silvercrow222/items/55aeb5d13ec2bde305db"">part1</a> に詳しく記載してあるので気になったら是非見てやってください。

##やってみよう
やることとしては主に以下の3つです。

- 編集画面に新しいフォームを表示させる。
- 削除ボタンで、すでに登録された情報も削除できるようにする。
- 画像フォームがもつ固有のインデックスが被らないようにする。

こんな感じですね。さっそくやっていきますが、ビューファイルをいじる前にコントローラに少し追記をする必要があるので、そちらからいきましょう。

```ruby:app/controllers/products_controller.rb
#~省略~
  private
  def product_params
    params.require(:product).permit(:name, images_attributes: [:src, :_destroy, :id]).merge(user_id: current_user.id)
  end
#~省略~
end
```
ストロングパラメータに少し記述を増やしました。この_destroyというのは、関連づいた子モデルの情報を削除してくれるキーです。見慣れない形ですがちゃんとしたキーなので安心しましょう。

それではビューファイルを記述していきます。

```haml:app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    // ~省略~
    - if @product.persisted?
      .group{ data: { index: @product.images.count } }
        = file_field_tag :src, name: "product[images_attributes][#{@product.images.count}][src]", class: 'file'
        .remove
          削除
  = f.submit 'SEND'
```
追記したのは@product.persisted?の部分です。難しく見えますが、やっていることとしては前回jQueryで追加したフォーム部分をhaml形式にしただけです。新しいフォームを表示させようってやつですね。

このpersisted?ですが、こいつは利用したインスタンスが保存済みか否かを判断してくれます。要は新規なのかすでに登録された情報なのかってことです。とても便利なので覚えておきましょう。

表示はできましたが、現在の削除ボタンではすでにデータベースに登録された情報を削除することができません。なので先ほど追加した_destroyキーを使って削除できるようにする必要があります。

```haml:app/views/products/_form.html.haml
= form_with model: @product, local: true do |f|
  = f.text_field :name, placeholder: 'name'
  #image-box
    = f.fields_for :images do |i|
      // ~省略~
      - if @product.persisted?
        = i.check_box :_destroy, data: { index: i.index }, class: 'hidden'
  - if @product.persisted?
    // ~省略~
  = f.submit 'SEND'
```
先ほどとは別に、image-boxの内部にもpersisted?で文を追加しました。_destroyキーを持ったチェックボックスにチェックを入れると、データベース上から対応するレコードが削除される、といった記述です。なぜそうなるのかは詳しく書きませんが(自分が理解しきれていないので...)、こういう書き方があるのか程度に覚えておくといいと思います。

さて、これで仕組み自体はできました。ですが削除ボタンと別にチェックボックスがあるのはよろしくないのでこいつらを連動させていきます。

```javascript:app/assets/javascripts/product.js
$(function() {
  // ~省略~
  $('.hidden').hide();
  $('#image-box').on('click', '.remove', function() {
    // フォームに割り振られた固有のインデックスを取得。
    const targetIndex = $(this).parent().data('index')
    // 取得したインデックスに対応するチェックボックスを取得。
    const hiddenCheck = $(`input[data-index="${targetIndex}"].hidden`);
    // チェックボックスが存在する場合チェックを入れる。
    if (hiddenCheck) hiddenCheck.prop('checked', true);
    ~省略~
  });
});
```
image-boxのクリックイベントに処理を追加しました。一行ずつの解説も書いておきました。やっていること自体は簡単なので、jQueryの基礎が分かっていれば問題はないと思います。

削除ボタンを押せばチェックもできるようになったのでチェックボックスは非表示にしておきましょう。今回はjsの.hide()を使用してやりましたが、cssで display: none にしていただいても構いません。

それでは最後にインデックス被りを防止して実装は完了となります。

```javascript:app/assets/javascripts/product.js
$(function() {
  // ~省略~
  let fileIndex = [1,2,3,4,5]
  lastIndex = $('.group:last').data('index');
  fileIndex.splice(0, lastIndex);
  // ~省略~
});
```
fileIndexの定義部分に記述を追加しましょう。考え方としては、現在使われている最後のインデックスを取得し、その値でfileindexの値を入れ替える、といった感じです。

ちなみに.spliceについてですが、こいつは指定した要素から数えて好きな分だけ取り除き、ついでに要素の追加もできてしまう優れものです。

今回は第一引数で指定した数以降の値を全て取り除き、第二引数で指定した値を挿入してくれています。やれることが多い分、書き方に多少複雑な部分があるので是非詳しく調べてみてください。

さて、以上で編集機能自体の実装は終了となります！

##最後に

とうとう出来上がりましたね。いやぁ長かった。

仕様書でいう「画像をの差し替えを一枚ごとにできる。」は達成できたとしていいと思います。あとはわかりやすいように画像のプレビューをつければ完成です。

アプリで画像をプレビューする前に記事に画像をプレビューしろよって天の声が聞こえますが気にしないことにします。読みづらくてすみません。

それではまた次のpartで！！

<a href="https://qiita.com/silvercrow222/items/c7dfa43dcfcc9d78e63c">商品情報編集機能を実装したい~part5~</a>