3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【個人アプリ開発メモ】画像のプレビュー表示

Last updated at Posted at 2020-08-20

やりたいこと

アップロードしたい画像を選択したら、プレビュー表示できるようにしたい。

なぜ画像プレビュー機能が必要なのか?

フォームを作っただけでは、画像を選択してもどの画像が選択されたかアップロード前に確認することができない。

アップロードされてから違う画像をアップロードしてしまったと気づくようなことを防ぐために、この機能は必要。

どう進めるか

まず画像プレビュー用のブランチを立ち上げる。
次に「画像 rails プレビュー」で検索して参考記事を探す。

https://qiita.com/Masanori_N/items/71dbf648737f32dd8588
この記事を参考に進めていこう。

作業内容

jQueryで実装するため、jsファイルを作る。
javasciptディレクトリ直下に「image-preview.js」ファイルを設置した。

_main.html.haml
.new-post
      = form_with model: @post, id: 'new_post' do |f|
        .input-box
    (省略)
          .icon
            .image-upload
              %i.fa.fa-camera.fa-2x
              = f.file_field :image, class: 'image_upload'
            .btn-square
              = f.submit '投稿する', class: 'post-btn'
    .posts
      = render @posts

画像ファイル選択時にイベントを発火させる

まずはファイルを選択した時にイベントが発火するようにする。

image-preview.js
$(function() {
  $(function() {
    $(document).on('change', 'image_upload', function() {
      console.log('hoge');
    })
  });
});

これだとイベントは発火しなかった。

image-preview.js
$(document).on('change', '.image_upload', function() {
  console.log('hoge');
})

クラス名の指定で「.」が抜けているというミスが原因だった。
'image_upload''.image_upload'と修正したら無事イベントは発火した。

画像データの読みこみ

いよいよここから難しくなる。
よくわからない時はconsole.logで中身を確認するに限る。

image-preview.js
$(function() {
  $(function() {
    $(document).on('change', '.image_upload', function() {
      console.log('hoge');
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      console.log(file);
    })
  });
});

console.log(file);で選択したfileを取得できているか確認する。

検証画面で確認すると、

File {name: "image4.png", lastModified: 1596487703899, lastModifiedDate: Tue Aug 04 2020 05:48:23 GMT+0900 (日本標準時), webkitRelativePath: "", size: 110024, …}

と出てきたので、おそらくちゃんと画像fileを取得できている。

次にFileReaderを使ってFileReaderオブジェクトを生成する。
FileReaderとは何なのか?

https://developer.mozilla.org/ja/docs/Web/API/FileReader
によると、

FileReader オブジェクトを使うと、ユーザーのコンピューター内にあるファイル (もしくはバッファ上の生データ) をウェブアプリケーションから非同期的に読み込むことが出来ます。読み込むファイルやデータは File ないし Blob オブジェクトとして指定します。

とある。

なるほど、ローカルにある画像ファイルを非同期で読み込むことができるのか。

image-preview.js
//FileReaderオブジェクトの生成
var reader = new FileReader(); 

これもコンソール画面で確認してみよう。

FileReader {readyState: 0, result: null, error: null, onloadstart: null, onprogress: null, …}

次は

image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file); 

これもよくわからないのでそのままググってみよう。

https://developer.mozilla.org/ja/docs/Web/API/FileReader/readAsDataURL
ここでもMDNが参考記事として出てきた。

これもFileオブジェクトを読み込むために必要らしい。

image-preview.js
var hoge = reader.readAsDataURL(file); 
console.log(hoge);

こんな感じで変数に代入して中身を見てみた。
すると、undefinedとなった。

image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
var hoge = reader.readAsDataURL(file); 
console.log(hoge[0]);

これだとどうか?
これもダメだ。
これは中身を見ることはできないみたい。

image-preview.js
//読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
reader.onload = function() {
  ここに画像の読みこみ完了後の処理を書いていく
}

これもわからないので調べた。
https://developer.mozilla.org/ja/docs/Web/API/FileReader/onload

FileReader.onload プロパティは、readAsArrayBuffer や readAsBinaryString、 readAsDataURL、readAsText でのコンテンツ読み込みが完了して、利用可能になると発火する load イベント時に実行されるイベントハンドラを含みます

3回読んでも完全にはわからない。
readAsDataURLで画像ファイルの読みこみが完了して、利用可能になるとloadイベントが発火する。
というところまではわかった。

image-preview.js
//loadイベントが発火するかを調べる
reader.onload = function() {
  console.log('hoge');
}

コンソール画面でちゃんと「hoge」と出てきたので、loadイベントは問題なく発火してるとわかった。

その次にloadイベントの結果を取得する。

image-preview.js
//直前に実行したイベントが返した値を取得する
var image = this.result; 

プレビュー用のHTMLを設置します。
ここが結構時間かかるかな?と思っていたが、一発でいけました!!

スクリーンショット 2020-08-20 午前11.49.47.png

ここまでのコードはこんな感じです。

image-preview.js
$(function() {
  $(function() {

    // プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

    $(document).on('change', '.image_upload', function() {
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      //FileReaderオブジェクトの生成
      var reader = new FileReader(); 
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file); 
      //読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
      reader.onload = function() {
        console.log('hoge');
        //直前に実行したイベントが返した値を取得する
        var image = this.result;
        // プレビュー用のhtmlを追加
        var html = buildHTML();
        $('.text').append(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
      }
    })
  });
});

プレビュー画像のサイズ変更

image-preview.js
// プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image" height="100px" width="100px">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

<img src="" alt="preview" class="upload-image" height="100px" width="100px">
imgタグのところにheightとwidthを追加すればOK👍

どのクラスに設定するかが大事で、親クラスに設定してもサイズは変わらないので注意です

プレビュー画像の削除

「削除」を押したらプレビュー画像が消えるようにしたい。

参考記事:https://qiita.com/gakinchoy7/items/f52577d0c5f6b2edff89#2-%E3%83%97%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%81%AE%E5%89%8A%E9%99%A4

まず「削除」を押した時にイベントが発火するようにする。

image-preview.js
$(document).on("click", '.delete-box', function(){
  console.log('hogehoge');
})

これで問題なく発火した。
クラス名の指定'.delete-box'が合っていればこれで発火する。

削除はremoveメソッドを使えばOK。
久しぶりだから( )をつけるのを忘れていた😅

image-preview.js
$(document).on("click", '.delete-box', function(){
  $('.preview-box').remove();
})

これだけだと、プレビュー画像は消えたが、まださっきの画像ファイルを選択したままになってる。

image-preview.js
    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })

inputタグに入ってる画像ファイルも削除しておこう。
これでプレビュー画像を削除すると、ファイルの中身が空っぽになる。

2回連続ファイルを選択するとプレビュー画像が2つ表示されてしまう問題の解決

こんな感じで表示されてしまう。
これを選択するたびに入れ替わるように修正したい。
スクリーンショット 2020-08-20 午後0.53.32.png
どうすればこの問題を解決できるか?

考えたのは、

  • すでにプレビュー画像がある場合
  • プレビュー画像がない場合

に条件分岐すればいいのでは?

  • プレビュー画像あり → プレビュー画像を一度削除する必要あり
  • プレビュー画像なし → ここまで書いてきたコードでOK

どうやってプレビュー画像がある場合とない場合の条件分岐をするか?

image-preview.js
if($('.preview-box').length == 0){
  プレビュー画像がない場合の処理
}else{
  プレビュー画像がある場合の処理
}

こんな感じでlengthプロパティを使って条件分岐しました。

image-preview.js
        // プレビュー画像がまだ場合
        if($('.preview-box').length == 0){
          // プレビュー用のhtmlを追加
          var html = buildHTML();      
          $('.icon').before(html);
  
          //画像を追加
          $(`.upper-box img`).attr('src', `${image}`);

        // すでにプレビュー画像が存在する場合
        }else{

          // プレビュー画像を削除 ⇦ 違うのはここです
        $('.preview-box').remove();

        // プレビュー用のhtmlを追加
        var html = buildHTML();      
        $('.icon').before(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
        }

参考記事:https://www.sejuku.net/blog/34465

最終的に出来上がったコードはこちら

image-preview.js
$(function() {
  $(function() {

    // プレビューHTML生成
    function buildHTML() {
      var html = `<div class="preview-box">
                    <div class="upper-box">
                      <img src="" alt="preview" class="upload-image" height="100px" width="100px">
                    </div>
                    <div class="lower-box">
                      <div class="delete-box">
                        <span>削除</span>
                      </div>
                    </div>
                  </div>`;
      return html;
    }

    $(document).on('change', '.image_upload', function() {
      //選択したfileのオブジェクトを取得
      var file = this.files[0];
      //FileReaderオブジェクトの生成
      var reader = new FileReader(); 
      //readAsDataURLで指定したFileオブジェクトを読み込む
      reader.readAsDataURL(file); 
      //読み込み時に発火するイベント onloadメソッドは読み込みが完了したら実行する
      reader.onload = function() {
        //直前に実行したイベントが返した値を取得する
        var image = this.result;

        // プレビュー画像がまだ場合
        if($('.preview-box').length == 0){
          // プレビュー用のhtmlを追加
          var html = buildHTML();      
          $('.icon').before(html);
  
          //画像を追加
          $(`.upper-box img`).attr('src', `${image}`);

        // すでにプレビュー画像が存在する場合
        }else{
          // プレビュー画像を削除
        $('.preview-box').remove();

        // プレビュー用のhtmlを追加
        var html = buildHTML();      
        $('.icon').before(html);

        //画像を追加
        $(`.upper-box img`).attr('src', `${image}`);
        }

      }
    })

    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })

  });
});

結局何をしたのか?

  • プレビュー画像の表示ができた。
  • プレビュー画像のサイズを小さくすることができた。
  • プレビュー画像を削除できるようにした。
  • 連続でファイルを選択したら、直前に選択したファイルのプレビューが消えて、新たにプレビューが表示されるようにした。

おまけ

各投稿の画像を大きくしたい。

before

スクリーンショット 2020-08-20 午後1.41.34.png

after

スクリーンショット 2020-08-20 午後1.51.56.png

_post.html.haml
- if post.image.present?
  = image_tag post.image.url, class: 'post-image', width: '300px', height: '200px'

各投稿は部分テンプレートで上記のHTMLで設定しており、image_tagのところにwidthとheightを追加して設定しました。

ただここで問題が。。。
見てわかるとおり、画像が粗くなってしまいました。

これについては解決したらまた別の記事で書きたいと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?