Rails6 画像のプレビュー機能 + バリデーション失敗時での画像表示

#実装内容
**carrierwave**を使用して**画像アップロード機能**を実装し、**「よし！これで画像の投稿機能の完成だ!!」**と思っていたのですが、何か物足りなさを感じて画像投稿機能に関する記事をいろいろと調べていた所、**プレビュー機能**というワクワク機能に出会ったので今回はその紹介 + **バリデーション失敗時でも画像を表示する方法**も紹介しております。

#処理全体の流れ

1. プレビュー機能の実装
1. バリデーション失敗時の画像表示方法

#完成形はこちら

###プレビュー機能

[![Image from Gyazo](https://i.gyazo.com/223cb0d9f1dafabacf2edb961fbb1116.gif)](https://gyazo.com/223cb0d9f1dafabacf2edb961fbb1116)

###バリデーション失敗時

[![Image from Gyazo](https://i.gyazo.com/f27a90a1c130d3f0e6c6b14c5e302509.gif)](https://gyazo.com/f27a90a1c130d3f0e6c6b14c5e302509)

#環境
`macOS Big Sur 11.2.3`
`ruby: 2.7.2`
`rails: 6.1.3`
`jQuery`
`テンプレートエンジン: slim`
`レイアウト: bootstrap4`

#前提

- jquery,deviseの導入は省いております。
- carrierwaveの導入と実装は省いております。

#viewファイル

※**Rail6**ではform_forは非推奨ではありますが、上記に示した環境では問題なく動作しております。

```app/views/devise/registrations/edit.html.slim
  = form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f|
    = render 'layouts/error_messages', model: f.object
  ・
  ・
  ・
    .form-group
      .d-block
        = f.label :profile_image
      - if f.object.profile_image?
        = image_tag f.object.profile_image.to_s, id: 'profile_img_prev', class: 'user-information__img'
        .form-group.mt-2
          = f.check_box :remove_profile_image, class: 'mr-2'
          = f.label :remove_profile_image
          = f.hidden_field :profile_image_cache
      - else
        div
          = image_tag 'no_profile_img.png', id: 'profile_img_prev', class: 'user-information__img'
          = f.hidden_field :profile_image_cache
        br
      label.upload-btn
        = 'ファイルを選択'
        = f.file_field :profile_image, id: 'file_btn'
```

jQueryの操作では**id属性**を使ってセレクタの指定を行なっていきます。

今回は、画像を表示する**image_tag**に`id: 'profile_img_prev'`を付与し、**ファイル選択ボタン**に`id: 'file_btn'`を付与しています。

#プレビュー機能の実装
**webAPI**の`FileReader`というものを扱っています。
こちらを使うことで画像を**非同期**で読み取ることができます。

```app/javascript/packs/application.js
require("shared/image_preview");
```

```app/javascript/shared/image_preview.js
$(function () {
  $('#file_btn').on('change', function (e) {
    var reader = new FileReader();
    reader.onload = function (e) {
      $('#profile_img_prev').attr('src', e.target.result);
    }
    reader.readAsDataURL(e.target.files[0]);
  });
});
```

それでは順を追って説明いたします。

```app/javascript/shared/image_preview.js
$('#file_btn').on('change', function (e) {
```
上記コードは、`id:file_btn`を付与した**ファイル選択ボタン**が押され、画像が選択されるとイベントが発生し、スコープ内の処理内容を実行してくれます。
`(e)`は**イベントオブジェクト**のことで、発生したイベントに関する様々な情報（データ）が入っており、今回の場合、`#file_btn`のデータが入っています。

```app/javascript/shared/image_preview.js
var reader = new FileReader(); // FileReaderオブジェクトの生成
reader.onload = function (e) {
  $('#profile_img_prev').attr('src', e.target.result);
}
```
次に、処理内容について説明します。

上記では、**FileReaderオブジェクト**を生成し、この後に説明する**readAsDataURL**で**画像の読み込みが完了**したら`reader.onload`でイベントが発生し、スコープ内の`$('#profile_img_prev').attr('src', e.target.result);`が実行され、**該当id**を付与した`image_tag`の`src値`に先ほど取得した**画像データ**を反映させています。

```app/javascript/shared/image_preview.js
reader.readAsDataURL(e.target.files[0]);
```
最後に、先ほどの説明でも登場した**readAsDataURL**で指定した`Fileオブジェクト`を読み込んでいます。
また、ファイルを配列（複数）で受け取るようになっているため、最初のファイルのみ取得するよう`files[0]`としています。

ここで読み込むが完了すると、先ほどの`onloadメソッド`が実行され、非同期で画像が表示されるようになります。

以上で**プレビュー機能の完成**です！

#バリデーションエラー時の画像の保持方法
ここでは、**キャッシュ機能**を使用して`バリデーションの失敗時`でもアップロードした画像ファイルのデータをもとに画像を表示する方法を説明しています。

```app/views/devise/registrations/edit.html.slim
- if f.object.profile_image?
  = image_tag f.object.profile_image.to_s, id: 'profile_img_prev', class: 'user-information__img'
  .form-group.mt-2
    = f.check_box :remove_profile_image, class: 'mr-2' # 削除機能
    = f.label :remove_profile_image # ラベル
    = f.hidden_field :profile_image_cache # キャッシュ機能

```
**キャッシュ機能**を使用するためには`下記の2点を記述する`必要があります！

- `「(カラム名)_cache」`という名前の**hidden_fieldの追加**
- **ストロングパラメーター**に`「(カラム名)_cache」`の記述

そうすることで、`f.object.profile_image.to_s`でアップロードした画像ファイルのデータを取得することができ、**バリデーション失敗時**でも画像を表示することができます。

#最後に
今回プレビュー機能を実装してみての感想ですが、**「javascript、、ムズい」**です笑
でも記事にすることで再度実装内容を見直すことで多少は理解できたのかなと感じています！
今後も色んなことに挑戦しながら知識を付けていきたいと思います。

間違っている箇所や分かりづらい箇所が多々あるかと思います。
その際は、気軽にコメントいただけれると幸いです。

最後までご覧いただき、ありがとうございました!

https://yuruli.info/using-carrierwave-on-rails-6

https://qiita.com/kouichi_s/items/1858eb61790ceec843df