[Rails]画像の複数投稿とプレビュー機能 ~ fields_for ~

# はじめに
以前に最大1枚の画像プレビュー機能について書きましたが、
この記事では、その応用として画像の複数投稿を実装するまでを書いていきます。
[[Rails]画像選択時にプレビュー表示](https://qiita.com/Masanori_N/items/71dbf648737f32dd8588)
albumモデル(親)
photoモデル(子)
今回はアルバム名をalbumsテーブルのnameカラムに、
写真をphotosテーブルのimageカラムに保存していきます。

- Ruby 2.5.1
- Ruby on Rails 5.0.1.7
- Carrierwave Gem
- 画像投稿は最大5枚
- 今回はalbumモデルを渡した1つのフォームの中でfields_forを用いてphtosテーブルへの保存も行う

なお、Carrierwaveの設定は今回の記事では割愛させていただきます。

## アソシエーションを組む

今回はfields_forを使用するため以下の記述となります。
fields_forについては、また別の記事に書こうと思います。

````album.rb
has_many :photos
accepts_nested_attributes_for :photos, allow_destroy: true
````

````photo.rb
belongs_to :album
````
#### allow_destroy: true
accepts_nested_attributes_forメソッドのオプションとして、引数に書くことができる記述です。このオプションをつけることで、親のレコードが削除された場合に、関連付いている子のレコードも一緒に削除してくれます。

## コントローラーの編集

#### new, createアクション
フォームで使用するインスタンスを作成します。
今回は保存の結果次第でフラッシュメッセージを出してみます。

````albums_controller.rb
def new
  @album = Album.new
  @album.photos.new # Albumクラスのインスタンスに関連づけられたPhotoクラスのインスタンスが作成されます。
end

def create
    @album = Album.new(album_params)
    unless @album.valid?
      flash[:alert] = "入力情報を確認してください"
      redirect_to new_album_path
    end
    @album.save
    flash[:notice] = "保存が完了しました"
    redirect_to root_path
  end
````


#### ストロングパラメータ

````album_controller.rb
def album_params
  params.require(:album).permit(:name, photos_attributes: [:image])
end
````

#### photos_attributes: [:image]
fields_forを利用して作成されたフォームから来る値は、○○s_attributes: [:××]という形でparamsに入ります。○○は関連付く側のモデルの名前、××にはフォームに対応するカラムの名前が入ります。

## フォームの編集
次に、hamlを用いてフォームを作成していきます。
なお、フォーム本体は隠してます。
.label-boxをクリックする度に随時、.label-boxが対応するファイルを切り替えていきます。
そのため投稿枚数に関わらず、ユーザーは同じボタンのみを押せばいいことになります。

````views/albums/new.html.haml
= form_for @album do |f|
      .names
        .names__inner
          .teams
            %h3.team
              アルバム名
          = f.text_field :name, placeholder: "アルバム名"
      .image
        .image__inner
          .teams
            %h3.team
              写真
          .image__inner__container
            .prev-content
            / ここにプレビュー画像を表示していきます
            .label-content
              / ユーザーはここのみをクリックします
              %label{for: "album_photos_attributes_0_image", class: "label-box", id: "label-box--0"}
                %pre.label-box__text-visible クリックしてファイルをアップロード
                %p 最大5枚までアップロードできます
            .hidden-content
              = f.fields_for :photos do |i|
                = i.file_field :image, class:"hidden-field"
                %input{class:"hidden-field", type: "file", name: "album[photos_attributes][1][image]", id: "album_photos_attributes_1_image" }
                %input{class:"hidden-field", type: "file", name: "album[photos_attributes][2][image]", id: "album_photos_attributes_2_image" }
                %input{class:"hidden-field", type: "file", name: "album[photos_attributes][3][image]", id: "album_photos_attributes_3_image" }
                %input{class:"hidden-field", type: "file", name: "album[photos_attributes][4][image]", id: "album_photos_attributes_4_image" }
````

最大5枚までの投稿ができるため、最初から5つのフォームを用意して隠しておきます。

#### %input{class:"hidden-field", type: "file", name: "album[photos_attributes][0][image]", id: "album_photos_attributes_0_image" }
fields_forで生成されるフォームの1つ目は、このようにidやclassに0という添え字が当てられます。2つ目のフォーム以降は、この添え字の部分を1,2...といった具合に増やしていけば大丈夫です。
idやclassの数字に応じて.label-boxを対応させるようにJSファイルを編集していきます。


## jsファイルの編集

````album.js
$(document).on('turbolinks:load', function () {
  $(function () {

    function buildHTML(count) {
      var html = `<div class="preview-box" id="preview-box__${count}">
                    <div class="upper-box">
                      <img src="" alt="preview">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box" id="delete_btn_${count}">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`
      return html;
    }

     // ファイルに画像が選択された瞬間に発火します
    $(document).on('change', '.hidden-field', function () {
      var id = $(this).attr('id').replace(/[^0-9]/g, '');
      $('.label-box').attr({ id: `label-box--${id}`, for: `album_photos_attributes_${id}_image` });
      var file = this.files[0];
      var reader = new FileReader();
      $('.hidden-btn').show();
      reader.readAsDataURL(file);
      reader.onload = function () {
        var image = this.result;
        if ($(`#preview-box__${id}`).length == 0) {
          var count = $('.preview-box').length;
          var html = buildHTML(id);
          var prevContent = $('.label-content').prev();
          $(prevContent).append(html);
        }
        $(`#preview-box__${id} img`).attr('src', `${image}`);
        var count = $('.preview-box').length;
        // 画像が5枚選択された場合、投稿ボタン(.label-content)を隠します
        if (count == 5) {
          $('.label-content').hide();
        }

        if (count < 5) {
          $('.label-box').attr({ id: `label-box--${count}`, for: `album_photos_attributes_${count}_image` });
        }
      }
    });

    $(document).on('click', '.delete-box', function () {
      var count = $('.preview-box').length;
      var id = $(this).attr('id').replace(/[^0-9]/g, '');
      $(`#preview-box__${id}`).remove();
    });
  });
});
````

# おわり
今回は新規投稿でしたが、次回はそのデータの編集パターンも書ければと思います。
最後まで見ていただき、ありがとうございました。