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


実装内容

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

処理全体の流れ

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

完成形はこちら

プレビュー機能

バリデーション失敗時

環境

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_tagid: 'profile_img_prev'を付与し、ファイル選択ボタンid: 'file_btn'を付与しています。

プレビュー機能の実装

webAPIFileReaderというものを扱っています。
こちらを使うことで画像を非同期で読み取ることができます。

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_tagsrc値に先ほど取得した画像データを反映させています。

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、、ムズい」です笑
でも記事にすることで再度実装内容を見直すことで多少は理解できたのかなと感じています!
今後も色んなことに挑戦しながら知識を付けていきたいと思います。

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

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