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

## やりたいこと
アップロードしたい画像を選択したら、プレビュー表示できるようにしたい。
## なぜ画像プレビュー機能が必要なのか？
フォームを作っただけでは、画像を選択してもどの画像が選択されたかアップロード前に確認することができない。

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

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

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

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

```ruby:_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
```
### 画像ファイル選択時にイベントを発火させる
まずはファイルを選択した時にイベントが発火するようにする。

```js:image-preview.js
$(function() {
  $(function() {
    $(document).on('change', 'image_upload', function() {
      console.log('hoge');
    })
  });
});
```
これだとイベントは発火しなかった。

```js:image-preview.js
$(document).on('change', '.image_upload', function() {
  console.log('hoge');
})
```
クラス名の指定で「.」が抜けているというミスが原因だった。
`'image_upload'`→`'.image_upload'`と修正したら無事イベントは発火した。

### 画像データの読みこみ
いよいよここから難しくなる。
よくわからない時は`console.log`で中身を確認するに限る。

```js: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 オブジェクトとして指定します。

とある。

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

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

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

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

次は

```js:image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
reader.readAsDataURL(file); 
```
これもよくわからないのでそのままググってみよう。

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

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

```js:image-preview.js
var hoge = reader.readAsDataURL(file); 
console.log(hoge);
```
こんな感じで変数に代入して中身を見てみた。
すると、`undefined`となった。

```js:image-preview.js
//readAsDataURLで指定したFileオブジェクトを読み込む
var hoge = reader.readAsDataURL(file); 
console.log(hoge[0]);
```
これだとどうか？
これもダメだ。
これは中身を見ることはできないみたい。

```js: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イベントが発火する。
というところまではわかった。

```js:image-preview.js
//loadイベントが発火するかを調べる
reader.onload = function() {
  console.log('hoge');
}
```
コンソール画面でちゃんと「hoge」と出てきたので、loadイベントは問題なく発火してるとわかった。

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

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

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

![スクリーンショット 2020-08-20 午前11.49.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/fcbd62f5-b0b7-e13a-3091-295853f17ed7.png)

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

```js: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}`);
      }
    })
  });
});
```
### プレビュー画像のサイズ変更

```js: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

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

```js:image-preview.js
$(document).on("click", '.delete-box', function(){
  console.log('hogehoge');
})
```
これで問題なく発火した。
クラス名の指定`'.delete-box'`が合っていればこれで発火する。

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

```js:image-preview.js
$(document).on("click", '.delete-box', function(){
  $('.preview-box').remove();
})
```
これだけだと、プレビュー画像は消えたが、まださっきの画像ファイルを選択したままになってる。

```js:image-preview.js
    // 「削除」を押すと削除イベントが発火する
    $(document).on("click", '.delete-box', function(){
      // プレビュー画像を削除
      $('.preview-box').remove();
      // inputタグに入ってる画像ファイルも削除
      $('.image_upload').val("");
    })
```
inputタグに入ってる画像ファイルも削除しておこう。
これでプレビュー画像を削除すると、ファイルの中身が空っぽになる。
### ２回連続ファイルを選択するとプレビュー画像が２つ表示されてしまう問題の解決
こんな感じで表示されてしまう。
これを選択するたびに入れ替わるように修正したい。
![スクリーンショット 2020-08-20 午後0.53.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/a8c8ee8a-6f83-506b-0133-ecc3c339e38f.png)
どうすればこの問題を解決できるか？

考えたのは、

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

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

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

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

```js:image-preview.js
if($('.preview-box').length == 0){
  プレビュー画像がない場合の処理
}else{
  プレビュー画像がある場合の処理
}
```
こんな感じでlengthプロパティを使って条件分岐しました。

```js: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

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

```js: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/3ed447a0-ef91-413e-f490-5b5cd9edf27c.png)
##### after
![スクリーンショット 2020-08-20 午後1.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547640/5bf313dd-1760-81e4-b3d5-a5bd7650e2c1.png)

```ruby:_post.html.haml
- if post.image.present?
  = image_tag post.image.url, class: 'post-image', width: '300px', height: '200px'
```
各投稿は部分テンプレートで上記のHTMLで設定しており、image_tagのところにwidthとheightを追加して設定しました。

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

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